在 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回溯调用栈?
欢迎继续讨论!😊
1万+

被折叠的 条评论
为什么被折叠?



