--- title: '匯編語言' disqus: kyleAlien --- 匯編語言 === ## OverView of Content 匯編語言 跟 MCU 晶片有密切的關係,**不同 MCU 晶片會有不同的匯編語言 (指令)**,以下使用 `ARM Cotrex M4F` 內核,所以以下匯編指令都與 M4F 有關 :::info `ARM Cortex M4F` 屬於 `ARMv7E-M` 架構,與 `ARM Cortex M3` 相同架構,但 **比起 M3 多了 DSP 和浮點數運算指令** ::: [TOC] ## Register 暫存器 匯編語言直接跟硬體打交道,而 **MCU 中所有的硬體操作都計基於 Register** (包括內核) ### MCU 暫存器 * 元老級別的暫存器有 16 個,這些暫存器都是 32 bit 大小,基本上可以依照功能分為 1. **通用 `R0` ~ `R12`**:通用暫存器,保存資料、地址... 等等 2. **程式計數器 `R15`**:也就是 Program counter,PC 在內核中用 R15 形式存在,一般可以透過 PC 這個關鍵字來訪問 > 在涉及控制轉移指令時,會將 PC 的數值更新,讓 CPU 轉移執行位置 3. **鏈結 `R14`**:**也稱為 LR**,功能是保存準備退回的函數的位置 ```c= // 偽程式 #include <studio.h> int main(int argc, char** argv) { printHello(); // 位置為 1111,進入前先存下返回的位置到 LR (R14) } void printHello() { printf("Hello\n"); } // 從 LR (R14) 取返回位置為 1111,跳回 1111 ``` :::warning * R14 一次只能存一個值,深度為 1,不可嵌套 相對應的解決方案是使用 Stack ::: 4. **棧指針 `R13`**:**又稱為 `SP`**,該暫存器有 **兩個分身** * MSP (`Main Stack Process`) * PSP (`Process Stack Pointer`) > ![](https://i.imgur.com/qn7Zb6k.png) ### 其他 Register - 分類 * ARM Cortex M4F 的匯編指令按照功能有分為以下幾種 1. **匯編偽指令**:並非真正的指令,為了方便程式而設計的 > **標號**:這就是一個偽指令,任何一個英文字符串、字符串,在一行開頭頂格,後面緊跟著括號 > > **標號代表了一個地址**,類似於 C 語言的函數名、宏定義 2. **內核 Register 之間的數據傳輸**:內核中特定 Register 對 Register Read、Write 操作 3. **Register 儲存 RAM 資料**:Register 對 RAM Read、Write 操作 4. **運算、邏輯、Byte 操作**:常見的 加、減、乘、除 操作,其中也包含浮點數、AND、OR、NOT... 操作 5. **程式流程**:類似於程式中的 `if/else`、`switch/case`,可以實現程式中的判斷 6. **特殊指令**:中斷屏蔽、MCU 休眠、異常處理... 等等 ### 其他 Register - 內核狀態 * **代表內核狀態的 3 個 Register:^1.^ APSR 正常運行的狀態、^2.^ 中斷狀態、^3.^ 分支狀態**,這三個狀態可以 **使用 `MRS` 指令** 一次讀出 :::warning * 為何需要分開 3 個 Register ? 主要原因是內核有三種運行模式: 1. 特權模式:CPU 可以運行任何指令 2. 非特權模式:使用者模式下,只能運行部份指令 3. 中斷、異常模式 ::: * 內核狀態 Register 表示 | Register\Bit | 31 | 30 | 29 | 28 | 27 | 26:25 | 24 | 23:20 | 19:16 | 15:10 | 9 | 8:0 | | - | - | - | - | - | - | - | - | - | - | - | - | - | | APSR (正常) | N | Z | C | V | Q | | | | GE | | | | | IPSR (中斷) | | | | | | | | | | | | 異常 | | EPSR (分支) | | | | | | ICI/IT | T | | | ICI/IT | | | 符號描述 | Bit | 說明 | | - | - | | N | 負號標誌 | | Z | 零標誌 | | C | 進位 (OR、NOT) 標誌 | | V | 溢位標誌 | | Q | 飽和標誌 | | GE[3:0] | 大於、等於 | | ICI/IT | ICI 中斷繼續指令、IT(IF-THEN) 狀態位,用來執行條件 | | T | Thumb 狀態位,Always 1 | | 異常 | 處理器正在處理的異常編號 | ### 其他 Register - 中斷控制 * 有 3 個特殊 Register 用於中斷控制 | Register\Bit | 31:8 | 7:1 | 0 | | - | - | - | - | | `PRIMASK` | | | 0 | | `FAULTMASK` | | | 0 | | `BASEPRI` | | 8:3 | | ### 其他 Register - 控制 CONTROL * 專門用來控制、紀錄內核的運行 | Register\Bit | 31:3 | 2 | 1 | 0 | | - | - | - | - | - | | CONTROL | | FPCA | SPSEL | nPRIV | * 內核狀態控制 Register | Bit | 說明 | 補充 | | - | - | - | | FPCA | 浮點數 | | | SPSEL | **SP 棧指針的選擇**,也就是我們上面說到的 SP 有兩種狀態 MSP (0)、PSP(1) | 異常發生時 SPSEL 始終為 0 | | nPRIV | **Thread 的特權等級** | 莫認為 0 特權模式,1 則是非特權模式 | * SPSEL & nPRIV 有 4 總組合 | nPRIV | SPSEL | 使用 | | -------- | -------- | -------- | | 0 (特) | 0 (MSP) | 簡單應用,主程序 & 中斷 只會使用一個棧,也就是 MSP | | 0 (特) | 1 (PSP) | 嵌入式系統,當前執行的任務在特權模式,當前任務選擇使用 PSP,而 MSP 留給內核 & 異常使用 | | 1 (普) | 1 (PSP) | 嵌入式系統,當前執行的任務在普通模式(非特權),選擇使用 PSP,而 MSP 留給內核 & 異常使用 | | 1 (普) | 0 (MSP) | Thread 模式運行在普通 (非特權) 的等級,並使用 MSP。在 **處理模式中** 可見,一般用戶無法使用該模式 | ## Stack 棧 :::success Stack 數據結構簡單來說就是 **先進後出 FILO**,這裡不詳細說明 ::: * 在單晶片重新接上電源後,暫存器 `LR`、`SP` 都會被置為 0,這時 `LR` 指向 main 函數位置 ,那 `SP` 就會指向 main 的地址。 :::info `SP` 暫存器運作的基本行為是入棧時 + 1,出棧時 - 1 ::: 以下為我們要分析的程式,它與 `LR`、`SP` 這兩個 Register 的關係 ```c= // 偽程式 int main(int argc, char** argv) { // 地址 0x10 sub_A(); // 地址 0x11 } // 地址 0x12 void sub_A() { // 地址 0x20 sub_B(); // 地址 0x21 } // 地址 0x22 void sub_B() { // 地址 0x30 } // 地址 0x31 ``` 1. 執行 `main` 函數:**SP 被初始化為 main 地址 `0x10` (SP 本身數值為 `0xA0`)** ```c= // 偽程式 int main(int argc, char** argv) { // 地址 0x10 !!這裡!! sub_A(); // 地址 0x11 } // 地址 0x12 ``` | Register | 數值 | 補充 | | - | - | - | | PC | 0x10 | | | LR | 0x00 | | | SP | 0xA0(儲存器地址) | 0x10(內容) | 2. **準備執行 `sub_A` 函數 (尚未跳轉)**:**PC 指到地址 `0x11`,LR 保存為 `0x12`** ```c= // 偽程式 int main(int argc, char** argv) { // 地址 0x10 sub_A(); // 地址 0x11 !!這裡!! } // 地址 0x12 ``` | Register | 數值 | 補充 | | - | - | - | | PC | 0x11 | | | LR | 0x12 | | | SP | 0xA0(儲存器地址) | `0x10`(內容) | 3. **執行 `sub_A` 函數**:PC 指向 `0x20` ```c= // 偽程式 int main(int argc, char** argv) { // 地址 0x10 sub_A(); // 地址 0x11 } // 地址 0x12 void sub_A() { // 地址 0x20 !!這裡!! sub_B(); // 地址 0x21 } // 地址 0x22 ``` | Register | 數值 | 補充 | | - | - | - | | PC | 0x20 | | | LR | 0x12 | | | SP | 0xA0(儲存器地址) | `0x10`(內容) | 4. **準備跳轉到 `sub_B` 函數 (尚未跳轉)**:所以會 LR 更新保存 `0x22` 地址,**SP 指針 +1,並指向下一個空間 `0x12`** :::warning * 前面有說過 LR 一次只能保存一個值,怎麼辦 ? **這時就要使用到 SP 暫存器,透過匯編指令 `PUSH LR` 將 LR 原來的地址 `0x12` 壓入 Stack 中!** ::: ```c= // 偽程式 int main(int argc, char** argv) { // 地址 0x10 sub_A(); // 地址 0x11 } // 地址 0x12 入棧 void sub_A() { // 地址 0x20 sub_B(); // 地址 0x21 !!這裡!! } // 地址 0x22 ``` | Register | 數值 | 補充 | | - | - | - | | PC | 0x21 | | | LR | **0x22** | **使用 `PUSH LR` 指令,將原來的 `0x12` 內容存到 SP** | | SP | **0xA1**(儲存器地址) | `0x10` 入棧,而更新 **指向內容 `0x12`**(內容) | 5. **進入 `sub_B`**:更新 PC 數值到 `0x30`,後續的 `0x31` 就不再繼續說明 ```c= // 偽程式 int main(int argc, char** argv) { // 地址 0x10 sub_A(); // 地址 0x11 } // 地址 0x12 void sub_A() { // 地址 0x20 sub_B(); // 地址 0x21 } // 地址 0x22 void sub_B() { // 地址 0x30 1. !!這裡!! } // 地址 0x31 2. !!這裡!! ``` | Register | 數值 | 補充 | | - | - | - | | PC | ^1.^ 0x30、^2.^ 0x31 | | | LR | **0x22** | | | SP | **0xA1**(儲存器地址) | 更新指向內容 `0x12`(內容),`0x10` 在 Stack 底 | 6. `sub_B` 結束後會使用 `POP PC`,從 SP 中取出一個數據,並將數據設定給 PC,這樣 PC 就可以指回到正確位置 ```c= // 偽程式 int main(int argc, char** argv) { // 地址 0x10 sub_A(); // 地址 0x11 } // 地址 0x12 void sub_A() { // 地址 0x20 sub_B(); // 地址 0x21 !!這裡!! } // 地址 0x22 ``` | Register | 數值 | 補充 | | - | - | - | | PC | 0x21 | 返回上一個函數地址 | | LR | **0x22** | | | SP | **0xA0**(儲存器地址) | 收到 POP PC 指令,將 `0x12` 設定給 PC,數值更新成 `0x10` | 這樣 SP & PC 就產生關係 ### Stack 原則 1. Stack 必須在系統初始化的時候就定義好,Stack 一般位於 RAM 中 2. Stack 有限制空間,操作不好可能會導致系統崩潰 stack over flow,這也就是為何函數內部要聲明過大的 Array,也不要傳遞過大的結構的原因 :::info 如果需要應該要使用動態分配 malloc、free 方案 ::: 3. 匯編的 `POP`、`PUSH` 必須成對使用 ## 常用匯編指令 * 以下舉幾個常見的匯編指令 | 指令 | 是否是匯編指令 | 說明 | 舉例 | | -------- | -------- | - | - | | MOV | O | Register 之間傳輸 | `MOV R4, R0;` 將 R0 值賦予給 R4 | | MRS | O | RAM 賦值到 Register | `MRS R7, PRIMASK;` 將數據從 PRIMASK 複製到 R7 | | MSR | O | Register 賦值到 RAM | `MSR CONTROL, R2;` 將數據從 R2 複製到 CONTROL | | LDR | X(偽指令) | 將一個數賦予給 Register | `LDR R0, =0x11223344;` 將 0x11223344 賦予 R0 | | BL | O | 轉跳到特定標號 | `BL A;` 跳轉到 A 的地址處,返回地址保存到 LR Register | | CBZ | O | 若 Register Val 為 0 轉跳 | `CBZ R0, exit;` 若 R0 的數值等於 0,則跳轉到 exit 標號處 | | PUSH | O | 將 Register 壓入 Stack | `PUSH {R0, R3, LR}` 將 R0, R3, LR 的內容入棧 | | POP | O | 從 Stack 中彈出 | `POP {R0, R3, PC}`,將棧中內容按照 FILO 保存到 R0, R3, PC Register 中 (觸發跳轉) | ## Appendix & FAQ :::info ::: ###### tags: `嵌入式`