mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1131 字
3 分钟
x86 汇编语言入门
2020-11-13

你写了一段 C 代码,编译器优化后性能反而变差了。打开 -S 输出看汇编,满屏的 movlleaqcallq 完全读不懂。或者你在调试段错误,GDB 停在了一个没有源码的系统调用里,只能看汇编指令。无论做性能优化、安全逆向还是内核调试,x86 汇编都是绕不开的基础。本文从寄存器和内存模型讲起,带你建立对 x86 汇编的系统理解。

一、x86 架构概述#

1.1 x86 演进历史#

年份架构位宽寄存器数量特性
1978808616位14实模式
198538632位16保护模式、分页
2003AMD64/x86-6464位18+长模式、更多寄存器
2011AVX256位16 XMM向量运算
2017AVX-512512位32 ZMM更宽向量

1.2 两种语法风格#

特性AT&TIntel (NASM)
操作数顺序源, 目的目的, 源
寄存器前缀%eaxeax
立即数前缀$4242
内存操作offset(%base,%index,scale)[base + index*scale + offset]
指令后缀movl, movqmov dword, mov qword
注释#;

本文统一使用 AT&T 语法(GCC/GDB 默认),关键示例同时给出 Intel 语法对照。

二、寄存器#

2.1 通用寄存器#

64 位模式下,通用寄存器及其子寄存器关系:

| 63 ... 32 | 31 ... 16 | 15 ... 8 | 7 ... 0 |
|-----------|-----------|----------|---------|
| RAX | EAX | AX | AH | AL |
| RBX | EBX | BX | BH | BL |
| RCX | ECX | CX | CH | CL |
| RDX | EDX | DX | DH | DL |
| RSI | ESI | SI | SIL |
| RDI | EDI | DI | DIL |
| RBP | EBP | BP | BPL |
| RSP | ESP | SP | SPL |
| R8-R15 | R8D-R15D | R8W-R15W | R8B-R15B|

访问规则:

  • 64 位:%rax, %r8
  • 32 位:%eax, %r8d
  • 16 位:%ax, %r8w
  • 8 位低:%al, %r8b
  • 8 位高:%ah(仅 AX/BX/CX/DX 有)

2.2 特殊用途寄存器#

寄存器约定用途
%rax函数返回值
%rsp栈指针
%rbp帧指针(调试用)
%rip指令指针(程序计数器)
%rdi第一个参数
%rsi第二个参数
%rdx第三个参数 / 乘除法扩展
%rcx第四个参数 / 循环计数
%r8第五个参数
%r9第六个参数
%r10Caller-saved / syscall 编号
%r11Caller-saved
%rbxCallee-saved(必须保存)
%r12-r15Callee-saved(必须保存)

2.3 标志寄存器 RFLAGS#

常用标志位:

标志含义
CF0进位标志
ZF6零标志
SF7符号标志
OF11溢出标志
DF10方向标志(字符串操作)

三、内存寻址#

3.1 寻址模式#

AT&T 语法通用格式:offset(%base, %index, scale)

计算地址 = %base + %index * scale + offset

其中 scale 只能是 1、2、4、8。

# AT&T
movl $42, %eax # 立即数
movl %eax, %ebx # 寄存器
movl (%rsp), %eax # 直接寻址
movl 8(%rsp), %eax # 基址 + 偏移
movl (%rsi, %rdi, 4), %eax # 基址 + 索引 * 比例
movl 0x10(%rsi, %rdi, 4), %eax # 完整格式
; Intel 等价
mov eax, 42
mov ebx, eax
mov eax, [rsp]
mov eax, [rsp + 8]
mov eax, [rsi + rdi*4]
mov eax, [rsi + rdi*4 + 0x10]

3.2 大端与小端#

x86 采用小端序(Little-Endian):低字节存放在低地址。

# 存储 0x12345678 到内存 0x100
地址: 0x100 0x101 0x102 0x103
值: 0x78 0x56 0x34 0x12

四、常用指令#

4.1 数据传送#

movq %rax, %rbx # rbx = rax
movq $42, %rax # rax = 42
movq (%rsp), %rax # rax = *rsp
movq %rax, (%rsp) # *rsp = rax
leaq 8(%rsp), %rax # rax = rsp + 8(取地址,不读内存)
xchgq %rax, %rbx # 交换 rax 和 rbx

4.2 算术运算#

addq %rbx, %rax # rax += rbx
subq $1, %rax # rax -= 1
imulq %rbx, %rax # rax *= rbx
incq %rax # rax++
decq %rax # rax--
negq %rax # rax = -rax

4.3 逻辑与位运算#

andq $0xFF, %rax # rax &= 0xFF
orq %rbx, %rax # rax |= rbx
xorq %rax, %rax # rax = 0(常用清零)
shlq $4, %rax # rax <<= 4
shrq $8, %rax # rax >>= 8(逻辑右移)
sarq $1, %rax # rax >>= 1(算术右移)
notq %rax # rax = ~rax

4.4 比较与测试#

cmpq %rbx, %rax # 计算 rax - rbx,设置标志位
testq %rax, %rax # 计算 rax & rax,检测是否为 0

4.5 条件跳转#

je label # 相等 (ZF=1)
jne label # 不等 (ZF=0)
jl label # 有符号小于 (SF!=OF)
jg label # 有符号大于 (ZF=0 且 SF=OF)
jb label # 无符号低于 (CF=1)
ja label # 无符号高于 (CF=0 且 ZF=0)
jle label # 有符号小于等于
jge label # 有符号大于等于
jbe label # 无符号低于等于
jae label # 无符号高于等于
js label # 负数 (SF=1)
jz label # 为零 (同 je)
jnz label # 非零 (同 jne)

4.6 条件传送#

条件传送比条件跳转更高效(避免分支预测失败):

cmovle %rbx, %rax # 如果 rax <= rbx,rax = rbx
cmovne %rcx, %rdx # 如果不等,rdx = rcx

4.7 栈操作#

pushq %rax # rsp -= 8; *rsp = rax
popq %rax # rax = *rsp; rsp += 8

五、函数调用约定#

5.1 System V AMD64 ABI#

Linux/macOS 使用的调用约定:

参数传递

参数序号整数/指针浮点数
1%rdi%xmm0
2%rsi%xmm1
3%rdx%xmm2
4%rcx%xmm3
5%r8%xmm4
6%r9%xmm5
7+

寄存器保存规则

  • Caller-saved(调用者保存):%rax, %rcx, %rdx, %rsi, %rdi, %r8, %r9, %r10, %r11
  • Callee-saved(被调用者保存):%rbx, %rbp, %r12, %r13, %r14, %r15

5.2 函数栈帧#

# 典型函数序言/结语
pushq %rbp # 保存旧帧指针
movq %rsp, %rbp # 设置新帧指针
subq $16, %rsp # 分配局部变量空间
# ... 函数体 ...
movq %rbp, %rsp # 恢复栈指针
popq %rbp # 恢复帧指针
ret # 返回

栈帧布局(从高地址到低地址):

| 返回地址 | <- call 指令压入
| 旧 %rbp | <- push %rbp
| 局部变量 1 | <- -8(%rbp)
| 局部变量 2 | <- -16(%rbp)

5.3 函数调用示例#

C 代码:

int add(int a, int b) {
return a + b;
}
int main() {
int result = add(3, 4);
return result;
}

对应汇编:

add:
leal (%rdi, %rsi), %eax # eax = edi + esi
ret
main:
subq $8, %rsp # 栈对齐
movl $3, %edi # 第一个参数
movl $4, %esi # 第二个参数
call add # 调用 add
addq $8, %rsp # 恢复栈
ret

六、系统调用#

6.1 syscall 约定#

Linux x86-64 系统调用约定:

项目约定
指令syscall
编号%rax
参数 1-6%rdi, %rsi, %rdx, %r10, %r8, %r9
返回值%rax
错误返回值在 -4095 到 -1 之间
Info

注意:系统调用使用 %r10 而非 %rcx 传递第 4 个参数,因为 syscall 指令会覆盖 %rcx%r11

6.2 常用系统调用#

编号名称用途
0read读取
1write写入
2open打开文件
3close关闭文件
60exit退出
39getpid获取 PID

6.3 Hello World 示例#

.data
msg:
.ascii "Hello, World!\n"
len = . - msg
.text
.global _start
_start:
# write(1, msg, len)
movq $1, %rax # syscall: write
movq $1, %rdi # fd: stdout
leaq msg(%rip), %rsi # buf: 字符串地址
movq $len, %rdx # count: 长度
syscall
# exit(0)
movq $60, %rax # syscall: exit
xorq %rdi, %rdi # status: 0
syscall

七、浮点运算#

7.1 SSE/AVX 寄存器#

寄存器宽度用途
%xmm0 - %xmm15128 位SSE 浮点运算
%ymm0 - %ymm15256 位AVX 浮点运算
%zmm0 - %zmm31512 位AVX-512 运算

7.2 浮点指令#

# 标量浮点运算(SSE)
addss %xmm0, %xmm1 # 单精度加法
addsd %xmm0, %xmm1 # 双精度加法
mulss %xmm0, %xmm1 # 单精度乘法
divsd %xmm0, %xmm1 # 双精度除法
# 浮点比较
ucomiss %xmm0, %xmm1 # 单精度比较
ucomisd %xmm0, %xmm1 # 双精度比较
# 数据传送
movss (%rsp), %xmm0 # 加载单精度
movsd (%rsp), %xmm0 # 加载双精度

八、实战技巧#

8.1 使用 GDB 查看汇编#

# 反汇编当前函数
(gdb) disas
# 反汇编指定函数
(gdb) disas main
# Intel 语法
(gdb) set disassembly-flavor intel
# 查看寄存器
(gdb) info registers
# 查看标志位
(gdb) info registers eflags
# 单步执行(汇编级)
(gdb) si # step instruction
(gdb) ni # next instruction

8.2 使用 objdump#

# 反汇编整个文件
objdump -d program
# Intel 语法
objdump -d -M intel program
# 只看某个函数
objdump -d program | grep -A 20 '<main>'

8.3 编译器输出汇编#

# 生成汇编文件
gcc -S -masm=att program.c # AT&T 语法
gcc -S -masm=intel program.c # Intel 语法
# 不优化
gcc -O0 -S program.c
# 优化并查看
gcc -O2 -S program.c
# 包含 C 源码对照
gcc -g -S program.c

8.4 常见优化模式#

模式优化前优化后说明
清零mov $0, %raxxor %rax, %rax更短,更快
乘以 2 的幂imul $8, %raxshl $3, %rax移位代替乘法
取模 2 的幂idiv $16and $15, %rax位与代替除法
条件传送je/jmpcmov避免分支预测失败
循环展开循环 4 次4 次顺序操作减少循环开销

九、内联汇编#

9.1 GCC 内联汇编格式#

asm ( "汇编模板"
: 输出操作数 /* 可选 */
: 输入操作数 /* 可选 */
: 修改的寄存器 /* 可选 */
);

9.2 基本示例#

// 读取时间戳计数器
static inline unsigned long rdtsc(void) {
unsigned int lo, hi;
asm volatile (
"rdtsc"
: "=a" (lo), "=d" (hi)
);
return ((unsigned long)hi << 32) | lo;
}
// 原子比较并交换
bool cas(long *ptr, long oldval, long newval) {
unsigned char result;
asm volatile (
"lock; cmpxchgq %2, %1"
: "=a" (result), "+m" (*ptr)
: "r" (newval), "0" (oldval)
: "memory"
);
return result;
}

9.3 约束字符#

约束含义
"r"任意通用寄存器
"a"%rax/%eax/%al
"b"%rbx/%ebx/%bl
"c"%rcx/%ecx/%cl
"d"%rdx/%edx/%dl
"m"内存操作数
"i"立即数(编译时常量)
"0"与第 0 个输出操作数相同

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

x86 汇编语言入门
https://blog.souloss.com/posts/language/language-x86-assembly/
作者
Souloss
发布于
2020-11-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时