如何看懂 x86 汇编(ATT 版)

上次学 CSAPP 的时候学过 AT&T 格式的汇编,当时觉得简单就没记笔记,结果现在基本忘完了。

我决定分为这几步来介绍:基本语法、关联知识、常见套路、相关工具。

基本语法

操作数修饰

寄存器使用 % 前缀。

立即数使用 $ 前缀。

指令格式

opcode src, dest

指令后缀

C声明 Intel数据类型 后缀 大小
char 字节 b 1
short w 2
int 双字 l 4
long 四字 q 8
char* 四字 q 8
float 单精度 s 4
double 双精度 l 8

如果省略后缀,GAS 会从 oprand 的大小来决定。

GNU汇编器(GNU Assembler),简称为 GAS,是由GNU计划所使用的汇编器

寄存器

寄存器的命名规则

寄存器还有修饰。

  • r- 表示 64 位的寄存器,例如 %rax

  • e- 表示 32 位的寄存器,例如 %eax

  • 没有,表示 16 位的寄存器,例如 %ax

  • x 换成 l 表示低位。例如 %al 表示 %ax 的低 8 位。

  • x 换成 h 表示高位。例如 %ah 表示 %ax 的高 8 位。

寄存器使用约定

x86 调用约定(不同编译器可能不遵守):

  • %rax:用作返回值寄存器

  • %rip:指令指针寄存器,存放下条指令的地址

函数调用时

如果参数数量小于等于 6,就通过 %rdi%rsi%rdx%rcx%r8%r9 的寄存器来传递参数,分别对应函数的第 1 ~ 6 个参数。

如果参数数量大于 6,就需要通过栈来传递。传递时对齐 8 字节。

调用者保存寄存器和被调用者保存寄存器

Caller saved: %rdi, %rsi, %rdx, %rcx, %r8, %r9, %r10, %r11 Callee saved: %rbx, %rbp, %rsp, %r12, %r13, %r14

寻址

通用形式

Imm(rb,ri,s) 

其中Imm为立即数,表示一个偏移量,rb则为基址寄存器,ri为变址寄存器,s为比例因子。

有效地址被计算为:

Imm + rb + ri * s 

重要的指令

call

  • 负责将被调指令地址写入 rip

  • 还要负责将返回地址压入栈中

push / pop

push把数据压入栈。等价于:先减去栈指针的值,然后将压入数据的值写入栈顶。

1pushl $0x1234
2// 等价于
3sub $0x4 %rsp
4movl $0x1234 (%rsp)

pop 把数据弹出栈,是 push 的逆变换。等价于:把栈顶数据写入操作数,然后增加栈指针的值。

1popq %rax
2// 等价于
3movq (%rsp), %rax
4addq $8, %rsp

跳转

指令 描述
jmp Label 直接跳转
jmp *Operand 间接跳转
je 相等
jne 不相等
js 负数
jns 非负数
jg 大于
jge 大于或等于
jl 小于
jle 小于或等于
ja 超过
jae 超过或等于
jb 低于
jbe 低于或相等

位拓展

当我们将较小的值传送到较大的值时,如将16位的值传送到32位中,我们便需要对高位进行处理。

  • movz 将剩余字节填充为0

  • movs 将设关于字节填充为最高位

注意没有将4字节0拓展为8字节的指令,但根据我们的特殊规则,生成4字节的指令会把高4字节置为0。

关联知识

格外需要注意栈是头朝下(从高地址向低地址增长)的

栈帧布局:

image-20220714124417372

(上图注释者 xiehongfeng100)

CPU 并不能超然地“看”到整个栈。%rsp%rbp 都可以被称为栈指针(stack pointer)。

  • %rsp 是栈顶指针,它指示栈顶的位置。

  • %rbp 是栈基址指针,它指向当前栈底的位置,它是历史栈顶的快照,因此也充当各栈帧的分隔符。

通过 %rbp,内存中串起一个栈帧链表。只要不断对其解引用就可以回溯到最前面的栈帧。

常见套路

subq $16, %rsp

常见于调用另一个函数前。作用:预留栈空间,从而存入参数。

后面通常是跟 movq $val, (%rsp) 这样的指令,用于将参数压栈。并且从 16 可以看出要压入两个参数。调用完之后,会通过 addq $16, %rsp 来释放栈空间。

局部变量需要字节对齐

实例分析

手把手带你看汇编

1int32_t square(int32_t num) {
2    int k = 10;
3    return 5 * num + 6 - k;
4}
 1square:
 2        // 注意,此时 create_obj 的栈帧还未构建完毕
 3        // 因此 rsp 虽然是栈顶指针,但起始指向的是上一个汉奸的栈顶
 4        // 同时 rbp 也是指向上一个函数的栈底
 5        pushq   %rbp                // 保存前一个栈帧栈底到当前栈顶。
 6                                    // 这是为了让上一个函数调用后能恢复它栈底的值到 %rbp。
 7        movq    %rsp, %rbp          // 保存当前栈顶的值到 `%rbp`,作为当前函数的栈底指针的值。
 8        movl    %edi, -20(%rbp)     // 将函数的第一个参数 num 保存到当前函数的栈底 -20 处。
 9        movl    $10, -4(%rbp)       // 将函数的第一个局部变量 k 保存到当前函数的栈底 -4 处。
10        movl    -20(%rbp), %edx     // 将变量 num 保存到 %edx 中。
11        movl    %edx, %eax          // 将变量 num 保存到 %eax 中。
12        sall    $2, %eax            // 将变量 num 乘以 4。(左移 2 相当于乘以 4) 得到 %eax = num * 4。
13        addl    %edx, %eax          // 将变量 %eax 加上 %edx ,即 %eax = num * 4 + num = num * 5。
14        addl    $6, %eax            // 将变量 %eax 加上 6,即 %eax = num * 5 + 6;
15        subl    -4(%rbp), %eax      // 将变量 %eax 减去 k ,即 %eax = num * 5 + 6 - k;        
16        popq    %rbp                // 将栈顶保存到 %rbp 中,并释放栈帧。
17        ret