过程调用
过程(函数)调用是程序模块化的基础。本章介绍 x86 的过程调用机制、栈帧管理和调用约定。
CALL 和 RET 指令
CALL 指令
CALL 指令调用过程,将返回地址压栈并跳转:
; 直接调用
call function_name
; 间接调用(通过寄存器)
call eax ; 32 位模式
call rax ; 64 位模式
; 间接调用(通过内存)
call [function_ptr]
call [jump_table + eax*4]
CALL 指令等价于:
push return_address
jmp function_name
RET 指令
RET 指令从过程返回,弹出返回地址并跳转:
ret ; 返回到调用者
ret n ; 返回并清理 n 字节的参数(stdcall 约定)
RET 指令等价于:
pop eip ; 伪代码,实际不能直接操作 EIP
栈帧结构
栈帧布局
每次函数调用都会在栈上创建一个栈帧:
高地址
┌─────────────────┐
│ 参数 n │
│ ... │
│ 参数 2 │
│ 参数 1 │
├─────────────────┤
│ 返回地址 │ ← CALL 指令压入
├─────────────────┤
│ 保存的 RBP │ ← PUSH RBP
├─────────────────┤
│ 局部变量 │
│ ... │
│ 临时空间 │
├─────────────────┤
│ 保存的寄存器 │
├─────────────────┤
│ 参数构建区 │ ← 调用其他函数时的参数
└─────────────────┘ ← RSP(栈指针)
低地址
函数序言和尾声
标准序言
function:
push rbp ; 保存旧的栈帧指针
mov rbp, rsp ; 设置新的栈帧基址
sub rsp, 32 ; 分配局部变量空间
标准尾声
mov rsp, rbp ; 恢复栈指针
pop rbp ; 恢复旧的栈帧指针
ret ; 返回
简化尾声(使用 LEAVE)
leave ; 等价于 mov rsp, rbp; pop rbp
ret
调用约定
调用约定定义了函数调用时参数传递、返回值处理和寄存器保存的规则。
System V AMD64 ABI(Linux/macOS)
参数传递
前 6 个整数/指针参数通过寄存器传递:
| 参数 | 整数/指针 | 浮点 |
|---|---|---|
| 第 1 个 | RDI | XMM0 |
| 第 2 个 | RSI | XMM1 |
| 第 3 个 | RDX | XMM2 |
| 第 4 个 | RCX | XMM3 |
| 第 5 个 | R8 | XMM4 |
| 第 6 个 | R9 | XMM5 |
| 更多 | 栈 | 栈 |
返回值
- 整数/指针返回值:RAX
- 浮点返回值:XMM0
- 128 位返回值:RAX + RDX
寄存器保存
| 寄存器 | 保存者 |
|---|---|
| RAX | 调用者(返回值) |
| RBX | 被调用者 |
| RCX | 调用者(第 4 个参数) |
| RDX | 调用者(第 3 个参数) |
| RSI | 调用者(第 2 个参数) |
| RDI | 调用者(第 1 个参数) |
| RBP | 被调用者 |
| RSP | 被调用者 |
| R8-R11 | 调用者 |
| R12-R15 | 被调用者 |
Microsoft x64 调用约定(Windows)
参数传递
前 4 个参数通过寄存器传递:
| 参数 | 整数/指针 | 浮点 |
|---|---|---|
| 第 1 个 | RCX | XMM0 |
| 第 2 个 | RDX | XMM1 |
| 第 3 个 | R8 | XMM2 |
| 第 4 个 | R9 | XMM3 |
| 更多 | 栈 | 栈 |
寄存器保存
| 寄存器 | 保存者 |
|---|---|
| RAX | 调用者(返回值) |
| RBX, RBP, RDI, RSI, R12-R15 | 被调用者 |
| RCX, RDX, R8, R9, R10, R11 | 调用者 |
调用示例
Linux 调用约定示例
; int add(int a, int b, int c)
; 参数:EDI = a, ESI = b, EDX = c
; 返回:EAX
add_three:
mov eax, edi ; EAX = a
add eax, esi ; EAX += b
add eax, edx ; EAX += c
ret
; 调用示例
mov edi, 10
mov esi, 20
mov edx, 30
call add_three ; EAX = 60
Windows 调用约定示例
; int add(int a, int b, int c)
; 参数:ECX = a, EDX = b, R8d = c
; 返回:EAX
add_three:
mov eax, ecx ; EAX = a
add eax, edx ; EAX += b
add eax, r8d ; EAX += c
ret
; 调用示例
mov ecx, 10
mov edx, 20
mov r8d, 30
call add_three ; EAX = 60
局部变量访问
通过 RBP 访问
function:
push rbp
mov rbp, rsp
sub rsp, 16 ; 分配 16 字节局部变量
; 访问局部变量
mov dword [rbp - 4], 10 ; 第一个局部变量
mov dword [rbp - 8], 20 ; 第二个局部变量
mov dword [rbp - 12], 30 ; 第三个局部变量
; 访问参数(假设有参数)
mov eax, [rbp + 16] ; 第一个参数(64 位模式)
leave
ret
通过 RSP 访问(优化方式)
现代编译器常使用 RSP 直接访问局部变量,节省 RBP:
function:
sub rsp, 24 ; 分配局部变量空间
; 访问局部变量
mov dword [rsp], 10
mov dword [rsp + 4], 20
mov dword [rsp + 8], 30
add rsp, 24
ret
递归函数
阶乘函数
; int factorial(int n)
; 参数:EDI = n
; 返回:EAX = n!
factorial:
push rbp
mov rbp, rsp
push rbx ; 保存 RBX(被调用者保存)
mov ebx, edi ; 保存 n
cmp ebx, 1
jle .base_case
; 递归调用 factorial(n-1)
mov edi, ebx
dec edi
call factorial
; EAX = factorial(n-1)
imul eax, ebx ; EAX = n * factorial(n-1)
jmp .done
.base_case:
mov eax, 1
.done:
pop rbx
pop rbp
ret
斐波那契函数
; int fibonacci(int n)
; 参数:EDI = n
; 返回:EAX = fib(n)
fibonacci:
push rbp
mov rbp, rsp
push rbx
push r12
mov ebx, edi ; 保存 n
cmp ebx, 1
jle .base_case
; fibonacci(n-1)
mov edi, ebx
dec edi
call fibonacci
mov r12d, eax ; 保存 fibonacci(n-1)
; fibonacci(n-2)
mov edi, ebx
sub edi, 2
call fibonacci
; EAX = fibonacci(n-1) + fibonacci(n-2)
add eax, r12d
jmp .done
.base_case:
mov eax, ebx ; 返回 n(当 n <= 1)
.done:
pop r12
pop rbx
pop rbp
ret
可变参数函数
可变参数函数(如 printf)需要特殊处理:
; int sum(int count, ...)
; 参数:EDI = 参数个数,后续参数在栈上或寄存器中
sum:
push rbp
mov rbp, rsp
xor eax, eax ; 累加器
test edi, edi
jz .done
; 前 6 个参数在寄存器中
cmp edi, 1
jb .done
add eax, esi ; 第 2 个参数
cmp edi, 2
jb .done
add eax, edx ; 第 3 个参数
cmp edi, 3
jb .done
add eax, ecx ; 第 4 个参数
cmp edi, 4
jb .done
add eax, r8d ; 第 5 个参数
cmp edi, 5
jb .done
add eax, r9d ; 第 6 个参数
; 更多参数在栈上
mov ecx, edi
sub ecx, 6
jle .done
mov rsi, rbp
add rsi, 16 ; 指向第 7 个参数
.stack_loop:
add eax, [rsi]
add rsi, 8
dec ecx
jnz .stack_loop
.done:
pop rbp
ret
栈对齐
对齐要求
x86-64 要求在执行 CALL 指令时,栈指针 RSP 必须 16 字节对齐:
; 错误:栈未对齐
push rbp ; RSP 未对齐到 16 字节边界
call function ; 可能导致 SSE 指令出错
; 正确:确保栈对齐
push rbp
push rbp ; 或分配额外的栈空间
call function
函数调用中的对齐
caller:
push rbp
mov rbp, rsp
sub rsp, 32 ; 分配 32 字节(保持 16 字节对齐)
; 调用前确保 RSP 16 字节对齐
; 如果当前 RSP 已对齐,需要分配 16 的倍数 + 8(因为 CALL 压入 8 字节返回地址)
call function
add rsp, 32
pop rbp
ret
叶子函数
叶子函数不调用其他函数,可以简化栈帧:
; 简单的叶子函数
; int add(int a, int b)
add:
mov eax, edi
add eax, esi
ret
; 需要局部变量的叶子函数
; int compute(int x)
compute:
sub rsp, 8 ; 只分配需要的空间
mov [rsp], edi ; 保存参数
; ... 计算 ...
add rsp, 8
ret
小结
本章介绍了 x86 的过程调用机制:
- CALL/RET 指令:函数调用和返回
- 栈帧结构:局部变量和参数的存储
- 调用约定:System V AMD64 和 Microsoft x64
- 递归函数:正确保存和恢复寄存器
- 可变参数:处理不定数量的参数
- 栈对齐:16 字节对齐要求
- 叶子函数:简化优化的函数形式
下一章将学习字符串处理指令。