如何看懂 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。
关联知识
格外需要注意栈是头朝下(从高地址向低地址增长)的
栈帧布局:
(上图注释者 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