---
# System prepended metadata

title: 匯編語言
tags: [嵌入式]

---

---
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_Ｂ` 函數　(尚未跳轉)**：所以會 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: `嵌入式`
