Assembly Intro
目录
汇编基础
基本指令格式
操作码 目标寄存器, 源寄存器1, 源寄存器2/立即数
1. 数据移动指令
MOV R0, R1 @ 把R1的值复制到R0
MOV R0, #100 @ 把数字100放到R0
2. 算术运算指令
ADD R0, R1, R2 @ R0 = R1 + R2
SUB R0, R1, R2 @ R0 = R1 - R2
MUL R0, R1, R2 @ R0 = R1 × R2
3. 内存访问指令
寄存器地址结构
[基址寄存器, 偏移量]
- 固定的偏移量(单位:4字节)
LDR R0, [R1,#4] @ 从R1+4指向的内存地址加载4个字节的数据到R0;Load Register
LDRH R0, [R1,#4] @ 从R1+4指向的内存地址的加载2个字节(half)的数据到R0;
LDRD R0, [R1,#4] @ 从R1+4指向的内存地址的加载8个字节(double)的数据到R0;
LDRB R0, [R1,#4] @ 从R1+4指向的内存地址的加载1个字节的数据到R0;
STR R0, [R1] @ 把R0的数据存储到R1指向的内存地址,Store Register
4. 比较和跳转指令
CMP R0, R1 @ 比较R0和R1,设置标志位
B label @ 无条件跳转到label
BL label @ Branch and Link,先把返回地址保存在LR寄存器再跳转
BEQ label @ 如果相等则跳转
BNE label @ 如果不相等则跳转
简单汇编示例
@ 计算 10 + 20
MOV R0, #10 @ R0 = 10
MOV R1, #20 @ R1 = 20
ADD R2, R0, R1 @ R2 = R0 + R1 = 30
## 更多指令及详解 - PUSH - - POP - - B and BX
C语言与反汇编
先了解
- Mem_Mgmt
- label
简单的C程序
// simple.c
int add_numbers(int a, int b) {
int result = a + b;
return result;
}
int main() {
int x = 5;
int y = 3;
int sum = add_numbers(x, y);
return 0;
}
编译为汇编
使用GCC编译器可以查看生成的汇编代码:
arm-none-eabi-gcc -S simple.c -o simple.s
C代码与汇编代码分析
C源代码
int add_numbers(int a, int b) {
int result = a + b;
return result;
}
int main() {
int x = 5;
int y = 3;
int sum = add_numbers(x, y);
return 0;
}
对应的ARM汇编代码及分析
需要先了解:
add_numbers函数汇编(简单版本)
add_numbers:
@ 简单函数 - 直接计算并返回
ADD R0, R0, R1 @ result = a + b (参数a在R0, b在R1, 结果直接放回R0)
BX LR @ 返回调用者
add_numbers函数汇编(保存寄存器版本)
add_numbers:
@ 函数开始 - 保存寄存器
PUSH {R4, R5, LR} @ 保存需要保护的寄存器(R4,R5)和返回地址(LR)到栈中
@ 函数体
MOV R4, R0 @ 将参数a保存到R4
MOV R5, R1 @ 将参数b保存到R5
ADD R0, R4, R5 @ result = a + b,结果放在R0
@ 函数结束
POP {R4, R5, PC} @ 恢复寄存器并用PC替代LR实现返回
main函数汇编
main:
@ 函数开始 - 分配栈空间
PUSH {R4, R5, LR} @ 保存需要保护的寄存器和返回地址到栈中
@ 这里R5其实用不到,这里仅作示例给出
SUB SP, SP, #12 @ 为局部变量x,y,sum分配栈空间
@ 初始化局部变量
MOV R4, #5 @ x = 5
STR R4, [SP, #8] @ 将x存储到栈中[SP+8]
MOV R4, #3 @ y = 3
STR R4, [SP, #4] @ 将y存储到栈中[SP+4]
@ 函数调用,局部变量传入
LDR R0, [SP, #8] @ 准备第一个参数x到R0
LDR R1, [SP, #4] @ 准备第二个参数y到R1
BL add_numbers @ 调用add_numbers函数
STR R0, [SP, #0] @ 将返回值存储到sum [SP+0]
@ 函数返回
MOV R0, #0 @ 设置返回值0
ADD SP, SP, #12 @ 释放局部变量空间
POP {R4, R5, PC} @ 恢复寄存器并返回
疑问:为什么需要借助寄存器来访问内存?因为 ARM采用Load-Store架构,这意味着: - 只有LOAD/STORE指令可以访问内存 - 所有数据处理指令只能在寄存器间操作 - 没有"直接内存到内存"的操作
栈帧布局分析
main函数栈帧
高地址 → ┌─────────────┐ ← 旧SP
│ 旧LR │
├─────────────┤
│ R5 │
├─────────────┤
│ R4 │
├─────────────┤ ← 当前SP (函数开始后)
│ x (5) │ [SP+8]
├─────────────┤
│ y (3) │ [SP+4]
├─────────────┤
│ sum(8) │ [SP+0]
低地址 → └─────────────┘ ← 新SP (SP-12后)
关键点分析
1. 参数传递规则
- 前4个参数:通过R0-R3传递
- 返回值:通过R0返回
2. 寄存器使用约定
- R0-R3:调用者保存,可用于参数传递
- R4-R11:被调用者保存,使用前必须压栈保存
- LR (R14):链接寄存器,存储返回地址
- SP (R13):栈指针
3. 函数调用过程
- 参数准备:将参数放入R0-R3
- BL指令:跳转到函数,同时将返回地址保存到LR
- 函数执行:被调用函数工作
- BX LR:返回到调用点
4. 栈平衡
- PUSH/POP 必须成对出现
- SUB/ADD SP 必须成对出现
- 确保函数返回时SP恢复到原始值
反汇编代码结构解析
反汇编代码通常显示为以下格式:
内存地址 机器码 ASCII显示 指令 操作数
具体示例分析
0x08002F34 B503 .. PUSH {R0, R1, LR}
逐段解释:
| 部分 | 内容 | 含义 |
|---|---|---|
0x08002F34 |
内存地址 | 这条指令在内存中的位置 |
B503 |
机器码 | CPU实际执行的二进制代码(16进制显示) |
.. |
ASCII显示 | 机器码对应的ASCII字符(不可打印字符显示为点) |
PUSH |
指令助记符 | 人类可读的指令名称 |
{R0, R1, LR} |
操作数 | 指令要操作的数据或寄存器 |
各部分详细说明
1. 内存地址
- 指令在内存中的存储位置
- 16进制格式,如
0x08002F34 - 代码执行时,PC(程序计数器)就指向这些地址
2. 机器码
- CPU真正理解和执行的二进制指令
- 以16进制显示,如
B503 - ARM架构中通常是2字节或4字节
3. ASCII显示列
- 用途:将机器码字节当作ASCII字符显示
- 规则:可打印字符显示原字符,不可打印字符显示为
.
常见情况:
0x08002F36 4408 .D ADD R0, R1, R0 ; 44='D', 08=不可打印→'.'
0x08002F38 4142 AB MOV R2, R1 ; 41='A', 42='B'→"AB"
0x08002F3C 0000 .. MOVS R0, R0 ; 00,00都不可打印→".."
4. 指令和操作数
- 指令:人类可读的操作名称,如
ADD,PUSH,LDR - 操作数:指令操作的对象,如寄存器、内存地址、立即数
更多完整示例
; 示例1:函数开场
0x08002F00 B570 p. PUSH {R4-R6,LR} ; 保存寄存器到栈
; ↑地址 ↑机器码 ↑ASCII ↑指令 ↑操作数
; 示例2:算术运算
0x08002F36 4408 .D ADD R0, R1, R0 ; R0 = R1 + R0
; 44='D', 08=不可打印→".D"
; 示例3:内存访问
0x08002F38 6801 .h LDR R1, [R0, #0] ; 从R0地址加载数据到R1
; 68='h', 01=不可打印→".h"
; 示例4:条件跳转
0x08002F3A D101 .. BNE 0x08002F40 ; 如果不相等则跳转
; D1,01都不可打印→".."
; 示例5:长指令(4字节)
0x08002F3C F8DFE004 .... LDR LR, [PC, #4] ; 长指令,4字节机器码
ASCII显示列的实用价值
1. 识别区域类型
- 代码区域:多为
..或规律的模式 - 数据区域:可能出现可读字符串
- 混合区域:指令中偶然出现的可打印字符
2. 实际应用示例
; 代码段 - 多为".."
0x08002F40 B401 .. PUSH {R0}
0x08002F42 4602 .F MOV R2, R0
0x08002F44 E002 .. B 0x08002F4C
; 数据段 - 可能包含文本
0x08002F50 6C6C6548 Hell ; 字符串"Hello"的开始
0x08002F54 006F6F72 roo. ; 可能的数据区域
如何阅读反汇编
1. 关注指令流
0x08002F34 B503 PUSH {R0,R1,LR} ; 保存寄存器到栈
0x08002F36 4408 ADD R0, R1, R0 ; R0 = R1 + R0
0x08002F38 6801 LDR R1, [R0, #0] ; 从内存加载数据
0x08002F3A D101 BNE 0x08002F40 ; 条件跳转
2. 理解数据流向
-
寄存器间:
ADD R0, R1, R2→ 数据从R1,R2流向R0 -
内存到寄存器:
LDR R0, [R1]→ 数据从内存流向寄存器 -
寄存器到内存:
STR R0, [R1]→ 数据从寄存器流向内存
3. 识别函数结构
; 函数开始
0x08002F00 B570 PUSH {R4-R6,LR} ; 保存现场
0x08002F02 4604 MOV R4, R0 ; 处理参数
; ... 函数体 ...
0x08002F10 BD70 POP {R4-R6,PC} ; 恢复现场并返回
快速识别常见模式
| 模式 | 示例 | 含义 |
|---|---|---|
| 函数开头 | PUSH {..., LR} |
保存返回地址,开始新函数 |
| 函数结尾 | POP {..., PC} |
恢复寄存器并返回 |
| 函数调用 | BL 0x0800xxxx |
调用子函数 |
| 条件判断 | CMP R0, #0 + BEQ/BNE |
if-else逻辑 |
| 循环 | 标签 + 比较 + 条件跳转 | for/while循环 |
| ## 快速阅读技巧 |
- 重点看指令列:理解程序逻辑的主要依据
- ASCII列为辅助:帮助识别区域类型和数据内容
- 关注数据流:观察寄存器间的数据传递
- 识别模式:函数调用、循环、条件判断的固定模式