跳到主要内容

过程调用

过程(函数)调用是程序模块化的基础。本章介绍 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 个RDIXMM0
第 2 个RSIXMM1
第 3 个RDXXMM2
第 4 个RCXXMM3
第 5 个R8XMM4
第 6 个R9XMM5
更多

返回值

  • 整数/指针返回值: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 个RCXXMM0
第 2 个RDXXMM1
第 3 个R8XMM2
第 4 个R9XMM3
更多

寄存器保存

寄存器保存者
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 字节对齐要求
  • 叶子函数:简化优化的函数形式

下一章将学习字符串处理指令。