初始EBP和ESP的设置
下面用一个示意图和详细解释说明为什么在函数栈帧中函数参数通常放在基址指针(EBP)的正偏移位置,以及为什么是用+4、+8等偏移量来访问参数。如果有多个参数,它们又是如何排列的。
栈帧的基本结构
在经典的 32 位 x86 调用约定(比如 cdecl)中,一个函数进入时通常会执行如下指令建立栈帧:
push ebp ; 保存上一个函数的基址指针
mov ebp, esp ; 将当前 ESP 的值复制到 EBP,建立新的栈帧基准
sub esp, N ; 为局部变量分配 N 字节的空间(N 根据局部变量的多少设定)
这时栈内的内存布局一般如下(地址从高到低):
高地址
+-----------------------+
| 参数 k | <- [EBP + (4 + 4*k)] (k从0开始计数)
+-----------------------+
| ... |
+-----------------------+
| 参数2 | <- [EBP+12]
+-----------------------+
| 参数1 | <- [EBP+8]
+-----------------------+
| 返回地址(Call 指令压入) | <- [EBP+4]
+-----------------------+
| 保存的旧EBP | <- [EBP](由 push ebp 保存)
+-----------------------+
| 局部变量区域 | <- [EBP-4], [EBP-8], ...(由 sub esp, N 分配)
+-----------------------+
低地址
为什么参数放在“正偏移”位置?
-
EBP 的固定作用
当程序执行完以下指令后,push ebp mov ebp, esp
此时 EBP 被固定下来,作为当前函数栈帧的基准地址。
- 旧 EBP 被保存于
[EBP]
。 - 返回地址 则在调用
call
指令时压入栈中,位于[EBP+4]
。 - 而后调用者在调用函数时已经将所有参数压入栈中,这些参数就位于返回地址之上,也就是在
EBP+8
及更高的位置。
- 旧 EBP 被保存于
-
局部变量与参数的区隔
- 局部变量:为了便于访问,编译器将其分配在栈帧中距离 EBP 较近、低地址的负偏移区(比如
[EBP-4]
、[EBP-8]
等)。 - 函数参数:由于在调用时参数是先被压入栈中,再由
call
压入返回地址,所以它们位于 EBP 以上(正偏移区域)。
例如,函数的第一个参数就位于[EBP+8]
,第二个参数在[EBP+12]
,依此类推。
- 局部变量:为了便于访问,编译器将其分配在栈帧中距离 EBP 较近、低地址的负偏移区(比如
-
为什么用 +4 和 +8 等偏移?
[EBP+0]
:实际上就是 EBP 自身,存放的是保存的旧 EBP 值。[EBP+4]
:存放着调用call
指令时推入的返回地址。[EBP+8]
:即第一个参数,因为在函数调用时,参数在返回地址之上依次压入,所以离 EBP 的距离是 8 字节。- 如果有多个参数,依次排列为
[EBP+8]
(第一个)、[EBP+12]
(第二个)、[EBP+16]
(第三个)等等。
也就是说,参数的偏移量依赖于保存的返回地址和每个参数的大小(通常 32 位系统中每个参数 4 字节)。
举例说明
考虑下面的 C 函数:
int add(int a, int b) {
return a + b;
}
调用时的栈帧布局
当调用 add
函数时,调用者会按从右到左的顺序将参数压入栈,然后执行 call add
。假设参数压入栈后的情况如下:
高地址
+--------------+
| b | <- 实际在栈中的位置(稍后在函数中为 [EBP+12])
+--------------+
| a | <- 实际在栈中的位置(稍后在函数中为 [EBP+8])
+--------------+
| 返回地址 | <- 由 call 指令压入,存于 [EBP+4]
+--------------+
| 保存的旧EBP | <- push ebp 后保存,存于 [EBP]
+--------------+
| 局部变量区域 | <- 如果有局部变量,位于 [EBP-4] 等
+--------------+
低地址
在 add
函数内部,通过以下指令可以访问参数:
- 第一参数
a
:mov eax, [ebp+8]
- 第二参数
b
:mov edx, [ebp+12]
函数体内就可以使用这些参数进行计算返回 a+b
。
总结
-
“正偏移”:
指的是相对于固定的基址指针(EBP)向高地址方向计算的偏移。由于栈帧内局部变量采用负偏移(例如[EBP-4]
),而返回地址和参数则在 EBP 之上,所以参数访问时使用正偏移。 -
为什么 [EBP+4] 和 [EBP+8]?
[EBP+4]
存放着返回地址。[EBP+8]
是第一个函数参数;如果有多个参数,则依次使用[EBP+12]
、[EBP+16]
等来访问后续参数。
-
多个参数:
参数依次按从右到左被压入栈中(在 cdecl 中),因此在调用函数后,第一参数总是位于[EBP+8]
,第二参数位于[EBP+12]
,第三参数位于[EBP+16]
,等等。
这种布局设计使得在函数内部可以通过固定(且不随栈操作变化)的 EBP 来方便、准确地访问所有参数和局部变量,从而简化了编译器生成代码及调试时对栈帧的分析。