---
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`)
> 
### 其他 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: `嵌入式`