背景
嵌入式项目中,在处理linux崩溃问题时,有时候需要反汇编可执行文件或者动态库定位崩溃函数(见<Linux崩溃问题定位办法总结一>),这时就需要掌握一些基础的汇编知识,才可以大概弄明白汇编代码和源码的对应关系,方便定位问题,而且懂汇编也方便我们后续优化代码性能。
概述
汇编语言是一种低级编程语言,与机器指令直接对应,常用于嵌入式系统、操作系统内核开发等场景。理解汇编需要掌握寄存器、指令集、内存访问等核心概念。
注意
由于嵌入式领域架构繁多(ARM, RISC-V, AVR, MSP430等),这里以最常见的ARM Cortex-M系列为例,讲解核心概念。其汇编语法通常为UAL(统一汇编语言)。
寄存器介绍
寄存器是CPU内部的高速存储单元,用于存储指令、数据或地址。常见的寄存器包括:
- PC(Program Counter):程序计数器,存储下一条要执行的指令地址。每执行一条指令,PC会自动递增(或根据跳转指令修改)。
- LR(Link Register):链接寄存器,用于存储函数调用时的返回地址(如ARM架构中
BL
指令会将返回地址存入LR)。 - SP(Stack Pointer):栈指针,指向当前栈顶地址。Cortex-M有两个SP:MSP(主堆栈指针,用于内核和异常)和PSP(进程堆栈指针,用于任务)。
- 通用寄存器:如
AX
、BX
(x86),R0-R12
(ARM),用于数据操作。
常用汇编指令分类
数据传送指令
- MOV:将数据从源操作数复制到目标操作数。
MOV R0, #5 ; 将立即数5存入R0 MOV R1, R0 ; 将R0的值复制到R1
- LDR/STR(ARM):加载/存储指令。
LDR R0, [R1] ; 从R1指向的内存地址加载数据到R0 STR R0, [R1] ; 将R0的值存储到R1指向的内存地址
算术运算指令
- ADD/SUB:加法/减法。
ADD R0, R1, R2 ; R0 = R1 + R2 SUB R0, R1, #3 ; R0 = R1 - 3
- MUL/DIV:乘法/除法(部分架构需特殊支持)。
逻辑运算指令
- AND/ORR/EOR:按位与、或、异或。
AND R0, R1, R2 ; R0 = R1 & R2 EOR R0, R0, #0xFF ; R0 = R0 ^ 0xFF
控制流指令
- B/BL(ARM):跳转/带链接跳转。
B label ; 无条件跳转到label BL func ; 调用函数func,并将返回地址存入LR
- CMP + Bcond:条件跳转。
CMP R0, R1 ; 比较R0和R1 BGT label ; 若R0 > R1则跳转
栈操作指令
- PUSH/POP:压栈/出栈。
PUSH {R0, LR} ; 将R0和LR压入栈 POP {R0, PC} ; 出栈并恢复R0和PC(用于函数返回)
PC和LR的详细作用
-
PC:
程序计数器指向当前执行指令的下一条指令地址。执行顺序指令时,PC自动增加;跳转指令(如B
)会直接修改PC的值。 -
LR:
在函数调用时(如BL
指令),LR会自动保存返回地址。函数结束时可通过MOV PC, LR
或POP {PC}
返回到调用点。
注意:若函数内嵌套调用,需提前保存LR到栈中,否则会被覆盖。
示例:函数调用与返回
main:
BL func ; 调用func,LR=下条指令地址
...
func:
PUSH {LR} ; 保存LR到栈
... ; 函数体
POP {PC} ; 恢复LR到PC,实现返回
寻址模式介绍
寻址模式是汇编语言中指令获取操作数的方式,它决定了程序如何访问数据,直接影响代码的效率和灵活性,下面这个表格汇总了主要的寻址模式及其特点。
寻址模式 |
操作数位置 |
特点与应用 |
示例指令 (ARM) |
---|---|---|---|
立即寻址 |
指令本身 |
用于加载常数,执行速度最快 |
|
寄存器寻址 |
CPU内部寄存器 |
寄存器间操作,无需访问内存,速度快 |
|
直接内存寻址 |
内存地址 |
通过变量名或直接地址访问内存数据 |
|
寄存器间接寻址 |
寄存器指向的内存 |
寄存器作为指针访问内存,适用于动态地址 |
|
寄存器相对寻址 |
基址+固定偏移 |
访问数组元素或结构体成员 |
|
基址变址寻址 |
基址+变址寄存器 |
适用于二维数组或复杂数据结构 |
|
相对基址变址寻址 |
基址+变址+偏移 |
最灵活的寻址方式,用于复杂数据结构 |
|
多寄存器寻址 |
连续内存块 |
批量加载/存储寄存器,高效处理数据块 |
|
堆栈寻址 |
堆栈区 |
专门用于堆栈操作,如函数调用保存现场 |
|
相对寻址 |
PC + 偏移量 |
用于跳转指令(B, BL)和位置无关代码 |
|
下面具体介绍下每个寻址方式:
1.立即数寻址
特点:操作数直接包含在指令中(即常数)。
用途:用于加载常量或进行快速算术运算。
示例:
MOV R0, #0x55 ; R0 = 0x55(将立即数0x55存入R0)
ADD R1, R2, #10 ; R1 = R2 + 10(R2加立即数10,结果存入R1)
说明:
-
#
表示立即数。 -
在C语言中类似:
int a = 10;
。
2.寄存器寻址
特点:操作数是CPU寄存器。
用途:用于寄存器之间的数据传递或运算。
示例:
MOV R0, R1 ; R0 = R1(将R1的值复制到R0)
ADD R2, R3, R4 ; R2 = R3 + R4(R3和R4相加,结果存入R2)
说明:
-
这是最快的操作方式,因为寄存器在CPU内部,无需访问内存。
-
C语言类比:
int a = b;
(b
是变量,可能存储在寄存器中)。
3.寄存器间接寻址
特点:操作数的地址存储在寄存器中,指令通过该寄存器访问内存。
用途:用于访问数组、指针操作、外设寄存器等。
示例:
LDR R0, [R1] ; R0 = *R1(从R1指向的内存地址加载数据到R0)
STR R2, [R3] ; *R3 = R2(将R2的值存储到R3指向的内存地址)
说明:
-
[ ]
表示间接寻址,类似于C语言的指针解引用(*ptr
)。 -
在嵌入式开发中,常用于访问外设寄存器:
-
LDR R0, =0x40021000 ; 假设这是GPIOA的基地址 LDR R1, [R0] ; 读取GPIOA的输入数据寄存器
4.基址变址寻址
特点:操作数的地址 = 基址寄存器 + 偏移量(立即数或寄存器)。
用途:用于访问数组、结构体成员、外设寄存器组等。
示例:
LDR R0, [R1, #4] ; R0 = *(R1 + 4)(基址R1 + 偏移4)
STR R2, [R3, R4] ; *(R3 + R4) = R2(基址R3 + 偏移R4)
在C语言中类似:
int arr[10];
int val = arr[2]; // 相当于基址arr + 偏移2*sizeof(int)
5.前变址寻址
特点:先计算地址(基址 + 偏移),再访问内存,并且可以更新基址寄存器。
用途:用于循环遍历数组或缓冲区。
示例:
LDR R0, [R1, #4]! ; R0 = *(R1 + 4),然后 R1 = R1 + 4
说明:
-
!
表示更新基址寄存器(R1 += 4)。 -
C语言类比:
int *ptr = &arr[0];
int val = *(ptr += 2); // ptr移动2个位置后取值
6. 后变址寻址
特点:先使用基址寄存器访问内存,然后再更新基址寄存器(基址 += 偏移)。
用途:适用于读取数据后移动指针的场景(如FIFO缓冲区)。
示例:
LDR R0, [R1], #4 ; R0 = *R1,然后 R1 = R1 + 4
C语言类比:
int val = *ptr++; // 先取值,再移动指针
7.相对寻址
特点:操作数的地址 = PC(程序计数器) + 偏移量。
用途:用于跳转指令(B
, BL
)和加载常量(LDR
)。
示例:
B label ; 跳转到label(PC + 偏移量)
LDR R0, =0x12345678 ; 编译器会转换为PC相对寻址加载
在C语言中类比:
goto label; // 编译器计算相对偏移
8. 多寄存器寻址
特点:一条指令可以加载/存储多个寄存器。
用途:用于函数调用时的寄存器保存(压栈/出栈)。
示例:
STMIA R0!, {R1-R4} ; 将R1-R4的值存储到R0指向的地址,并递增R0
LDMDB R1!, {R2-R5} ; 从R1指向的地址加载数据到R2-R5,并递减R1
说明:
-
STM
(Store Multiple)和LDM
(Load Multiple)常用于堆栈操作:PUSH {R0-R3, LR} ; 保存寄存器到堆栈(STMDB SP!, {R0-R3, LR}) POP {R0-R3, PC} ; 从堆栈恢复寄存器(LDMIA SP!, {R0-R3, PC})
总结:不同寻址模式的应用场景
寻址模式 |
典型用途 |
C语言类比 |
---|---|---|
立即数寻址 |
加载常量 |
|
寄存器寻址 |
寄存器间运算 |
|
寄存器间接寻址 |
指针操作、访问外设 |
|
基址变址寻址 |
数组访问、结构体成员访问 |
|
前变址寻址 |
遍历数组(先计算地址再访问) |
|
后变址寻址 |
FIFO缓冲区(先访问再移动指针) |
|
PC相对寻址 |
跳转指令、常量池访问 |
|
多寄存器寻址 |
函数调用时的寄存器保存(压栈/出栈) |
|