函数入栈调用顺序

在 x86 32 位架构(使用 cdecl 约定)中,入栈顺序 由多个因素决定,包括:

  1. 函数调用时的参数压栈
  2. 函数执行时的局部变量分配
  3. 函数返回时的栈恢复
  4. 函数嵌套调用的栈变化

下面我们分情况详细讲解。


1. 基本的函数调用入栈顺序

cdecl 约定下,函数参数按从右到左的顺序入栈,调用时会发生:

  1. 右侧参数先入栈,左侧参数后入栈
  2. call 指令会把返回地址压入栈中。
  3. 被调用函数 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() 开始执行时:

  1. call 指令会将返回地址 ret_addr 压入栈。
  2. 进入 func() 后,func 可能会保存 ebp 作为栈基址
  3. 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() 结束时的栈恢复

函数返回时:

  1. 释放局部变量的栈空间。
  2. 恢复 ebp(恢复调用者的栈基址)。
  3. 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(),整个过程会继续重复:

  1. func() 先保存自己的寄存器,分配局部变量。
  2. func() 调用 sub_func(),再次压入新的参数和返回地址。
  3. 进入 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);
}

完整的栈布局变化

  1. caller() 调用 func(1,2,3)
地址       栈内容
------------------------
0x0FFC  |  3        |  <-- c
0x0FF8  |  2        |  <-- b
0x0FF4  |  1        |  <-- a
0x0FF0  |  ret_addr |  <-- 返回到 caller
  1. func() 进入,保存 ebp 并分配 x
0x0FEC  |  旧 ebp   |  <-- 进入 func 时 push 的 ebp
0x0FE8  |  x        |  <-- 局部变量 x
  1. 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 恢复 funcebp
  • retfunc 继续执行。

最终 func() 也返回,恢复 caller 的栈状态。


6. 总结

函数调用时的入栈顺序

  1. 参数从右到左依次压栈(符合 cdecl 约定)。
  2. call 指令自动压入返回地址
  3. 进入函数后,push %ebp 保护 ebpmov %esp, %ebp 设定新栈基址
  4. 局部变量通过 sub $N, %esp 预留栈空间

函数返回时的出栈顺序

  1. 释放局部变量空间add $N, %esp)。
  2. pop %ebp 恢复调用者的 ebp
  3. ret 弹出返回地址并跳转回调用者

嵌套函数的调用

  • 每个函数都会创建新的栈帧(ebp 标记),形成 栈帧链,逐层调用、逐层返回。
  • 栈帧的结构 由参数、返回地址、旧 ebp 和局部变量组成。

如果你有更深入的问题,比如:

  • 如何处理递归函数?
  • 如何用 esp 操作参数、返回值?
  • 如何用 ebp 回溯调用栈?

欢迎继续讨论!😊

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值