在 x86 32 位架构(使用 cdecl
约定)中,入栈顺序 由多个因素决定,包括:
- 函数调用时的参数压栈
- 函数执行时的局部变量分配
- 函数返回时的栈恢复
- 函数嵌套调用的栈变化
下面我们分情况详细讲解。
1. 基本的函数调用入栈顺序
在 cdecl
约定下,函数参数按从右到左的顺序入栈,调用时会发生:
- 右侧参数先入栈,左侧参数后入栈。
call
指令会把返回地址压入栈中。- 被调用函数
func
进入后,可能会保存寄存器(如ebp
),并分配局部变量的空间。
示例代码
void func(int a, int b, int c) {
int x = a + b + c;
}
void caller() {
func(1, 2, 3);
}
2. caller()
调用 func()
时的栈变化
当 caller()
执行 func(1, 2, 3);
,汇编层面的大致执行流程如下:
push 3 ; 参数 c(最右边的参数)
push 2 ; 参数 b
push 1 ; 参数 a
call func ; 调用函数 func,自动 push 返回地址
此时的栈布局
地址 栈内容
------------------------
0x0FFC | 3 | <-- c 参数
0x0FF8 | 2 | <-- b 参数
0x0FF4 | 1 | <-- a 参数
0x0FF0 | ret_addr | <-- 返回地址
3. func()
执行时的栈布局
当 func()
开始执行时:
call
指令会将返回地址ret_addr
压入栈。- 进入
func()
后,func
可能会保存ebp
作为栈基址。 func
可能会分配局部变量的空间。
func:
push %ebp ; 保存旧的栈基址
mov %esp, %ebp ; 设置新的栈基址
sub $4, %esp ; 为局部变量 x 预留空间
执行后栈布局
地址 栈内容
------------------------
0x0FFC | 3 | <-- c 参数
0x0FF8 | 2 | <-- b 参数
0x0FF4 | 1 | <-- a 参数
0x0FF0 | ret_addr | <-- 返回地址
0x0FEC | 旧 ebp | <-- 进入函数时 push 的 ebp
0x0FE8 | 局部变量 x(未初始化)
注意:局部变量通常存放在
ebp
之下(即更低的地址),而参数存放在ebp
之上。
4. func()
结束时的栈恢复
函数返回时:
- 释放局部变量的栈空间。
- 恢复
ebp
(恢复调用者的栈基址)。 ret
指令会弹出返回地址,并跳转回调用者的caller()
。
add $4, %esp ; 释放局部变量(如果有的话)
pop %ebp ; 恢复旧的 ebp
ret ; 弹出返回地址,并跳转回 caller
执行后的栈布局
地址 栈内容
------------------------
0x0FFC | 3 | <-- c 参数
0x0FF8 | 2 | <-- b 参数
0x0FF4 | 1 | <-- a 参数
0x0FF0 | ret_addr | <-- 返回地址(ret 弹出后,栈回到 caller)
5. 如果 func()
调用了另一个函数呢?
如果 func()
内部调用了 sub_func()
,整个过程会继续重复:
func()
先保存自己的寄存器,分配局部变量。func()
调用sub_func()
,再次压入新的参数和返回地址。- 进入
sub_func()
,保存寄存器、分配局部变量。
例如:
void sub_func(int d) {
int y = d * 2;
}
void func(int a, int b, int c) {
int x = a + b + c;
sub_func(x);
}
完整的栈布局变化
caller()
调用func(1,2,3)
:
地址 栈内容
------------------------
0x0FFC | 3 | <-- c
0x0FF8 | 2 | <-- b
0x0FF4 | 1 | <-- a
0x0FF0 | ret_addr | <-- 返回到 caller
func()
进入,保存ebp
并分配x
:
0x0FEC | 旧 ebp | <-- 进入 func 时 push 的 ebp
0x0FE8 | x | <-- 局部变量 x
func()
调用sub_func(x)
:
0x0FE4 | x | <-- 作为 sub_func 的参数 d
0x0FE0 | ret_addr | <-- 返回到 func
0x0FDC | 旧 ebp | <-- 进入 sub_func 时 push 的 ebp
0x0FD8 | y | <-- sub_func 的局部变量
当 sub_func()
结束后:
pop %ebp
恢复func
的ebp
。ret
让func
继续执行。
最终 func()
也返回,恢复 caller
的栈状态。
6. 总结
函数调用时的入栈顺序
- 参数从右到左依次压栈(符合
cdecl
约定)。 call
指令自动压入返回地址。- 进入函数后,
push %ebp
保护ebp
,mov %esp, %ebp
设定新栈基址。 - 局部变量通过
sub $N, %esp
预留栈空间。
函数返回时的出栈顺序
- 释放局部变量空间(
add $N, %esp
)。 pop %ebp
恢复调用者的ebp
。ret
弹出返回地址并跳转回调用者。
嵌套函数的调用
- 每个函数都会创建新的栈帧(
ebp
标记),形成 栈帧链,逐层调用、逐层返回。 - 栈帧的结构 由参数、返回地址、旧
ebp
和局部变量组成。
如果你有更深入的问题,比如:
- 如何处理递归函数?
- 如何用
esp
操作参数、返回值? - 如何用
ebp
回溯调用栈?
欢迎继续讨论!😊