函数调用过程

本文最后更新于 2025年7月29日 晚上

; =================================================================
; 函数调用的完整汇编执行过程示例
; 演示: int result = add_numbers(10, 20);
; =================================================================

.text
.global _start

_start:
; ===== 程序开始执行,栈初始状态 =====
; ESP = 0x7FFF0000 (假设的栈顶地址)
; EBP = 0x7FFF0000 (帧指针初始化)

main:
; ===== 建立main函数的栈帧 =====
push %ebp ; 保存调用者的帧指针
; ESP = 0x7FFFEFFC, [ESP] = old_EBP
mov %esp, %ebp ; 建立当前函数的帧指针
; EBP = 0x7FFFEFFC
sub $16, %esp ; 为局部变量分配空间(16字节)
; ESP = 0x7FFFEFEC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; ===== 准备调用add_numbers(10, 20) =====
; 按C调用约定,参数从右到左压栈

; 压入第二个参数 (20)
push $20 ; ESP -= 4, [ESP] = 20
; ESP = 0x7FFFEFE8, 栈顶现在是20

; 压入第一个参数 (10)
push $10 ; ESP -= 4, [ESP] = 10
; ESP = 0x7FFFEFE4, 栈顶现在是10

; ===== 执行函数调用 =====
call add_numbers ; call指令执行两个动作:
; 1) push 返回地址: ESP -= 4, [ESP] = 返回地址
; ESP = 0x7FFFEFE0
; 2) jmp add_numbers: EIP = add_numbers地址

; ===== 此时控制权转移到add_numbers函数 =====

add_numbers:
; ===== 函数序言 - 建立新的栈帧 =====
push %ebp ; 保存调用者(main)的帧指针
; ESP = 0x7FFFEFD C, [ESP] = main的EBP
mov %esp, %ebp ; 建立add_numbers的帧指针
; EBP = 0x7FFFEFDC
sub $8, %esp ; 为局部变量分配空间
; ESP = 0x7FFFEFD4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
; ===== 当前栈布局 (从高地址到低地址) =====
; [EBP+12] = 20 (第二个参数)
; [EBP+8] = 10 (第一个参数)
; [EBP+4] = 返回地址 (call指令保存的)
; [EBP] = main的EBP (push %ebp保存的)
; [EBP-4] = 局部变量1 (可用空间)
; [EBP-8] = 局部变量2 (可用空间) ← ESP指向这里

; ===== 函数体 - 执行实际计算 =====
mov 8(%ebp), %eax ; 将第一个参数(10)加载到EAX
; EAX = 10
add 12(%ebp), %eax ; 将第二个参数(20)加到EAX
; EAX = 10 + 20 = 30
mov %eax, -4(%ebp) ; 将结果存储到局部变量
; [EBP-4] = 30

; ===== 准备返回值 =====
mov -4(%ebp), %eax ; 将结果放入EAX寄存器(返回值约定)
; EAX = 30

; ===== 函数尾声 - 清理栈帧并返回 =====
mov %ebp, %esp ; 恢复栈指针,释放局部变量空间
; ESP = 0x7FFFEFDC
pop %ebp ; 恢复调用者的帧指针
; EBP = main的EBP, ESP = 0x7FFFEFE0
ret ; 返回指令执行两个动作:
; 1) pop 返回地址到EIP: EIP = [ESP], ESP += 4
; 2) 跳转回调用点: ESP = 0x7FFFEFE4

; ===== 控制权返回到main函数 =====

back_to_main:
; ===== 清理参数 (C调用约定: 调用者负责清理) =====
add $8, %esp ; 清理两个参数 (2 × 4字节)
; ESP = 0x7FFFEFEC (回到调用前状态)

1
2
3
4
5
6
7
8
9
10
11
12
13
; ===== 保存返回值 =====
mov %eax, -4(%ebp) ; 将返回值(30)保存到局部变量result
; [EBP-4] = 30

; ===== main函数结束 =====
mov %ebp, %esp ; 恢复栈指针
pop %ebp ; 恢复帧指针

; ===== 程序退出 =====
mov $1, %eax ; 系统调用号: exit
mov $0, %ebx ; 退出状态码
int $0x80 ; 调用内核

; =================================================================
; 栈的变化过程总结:
;
; 1. 初始状态: ESP = 0x7FFF0000
; 2. main序言后: ESP = 0x7FFFEFEC

; 3. push 20后: ESP = 0x7FFFEFE8 (参数2)
; 4. push 10后: ESP = 0x7FFFEFE4 (参数1)
; 5. call后: ESP = 0x7FFFEFE0 (返回地址)
; 6. func序言后: ESP = 0x7FFFEFD4 (新栈帧)
; 7. ret后: ESP = 0x7FFFEFE4 (清理栈帧)
; 8. add $8后: ESP = 0x7FFFEFEC (清理参数)
;
; 关键点:
; - push指令: 修改ESP并存储数据
; - call指令: 保存返回地址并跳转
; - 函数序言: 建立新栈帧
; - 函数尾声: 清理栈帧
; - ret指令: 恢复返回地址并跳转
; - 参数清理: 调用者负责(C约定)
; =================================================================


函数调用过程
http://gadoid.io/2025/07/29/函数调用过程/
作者
Codfish
发布于
2025年7月29日
许可协议