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. 函数调用过程

  1. 参数准备:将参数放入R0-R3
  2. BL指令:跳转到函数,同时将返回地址保存到LR
  3. 函数执行:被调用函数工作
  4. 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循环
## 快速阅读技巧
  1. 重点看指令列:理解程序逻辑的主要依据
  2. ASCII列为辅助:帮助识别区域类型和数据内容
  3. 关注数据流:观察寄存器间的数据传递
  4. 识别模式:函数调用、循环、条件判断的固定模式