问题描述
我试图通过函数调用来更多地了解堆栈指针的行为,但我不确定我是否理解当我们从函数调用和返回时会发生什么。 假设我有这个主程序:
int main()
{
demo();
return 0;
}
demo 是这样定义的:
void demo()
{
}
我使用的是 VS2019,当我调试时,我会随着时间的推移检查以下与汇编代码相关的 SP 值(调试会话的值示例):
- 在输入
demo
之前 -
EAX = 00DDD006 EBX = 00608000 ECX = 00DDD006 EDX = 00000001 ESI = 00DD1023 EDI = 008FFBA8 EIP = 00DD1D4F ESP = 008FFAB8 EBP = 008FFBA8 EFL = 00000246
demo();
00DD1D4F call _bar (0DD1410h)
- 走进
call
-EAX = 00DDD006 EBX = 00608000 ECX = 00DDD006 EDX = 00000001 ESI = 00DD1023 EDI = 008FFBA8 EIP = 00DD1410 ESP = 008FFAB4 EBP = 008FFBA8 EFL = 00000246
在汇编中的位置是:
00DD1410 jmp demo (0DD1A30h)
- 一步进入
jmp
函数 -EAX = 00DDD006 EBX = 00608000 ECX = 00DDD006 EDX = 00000001 ESI = 00DD1023 EDI = 008FFBA8 EIP = 00DD1A30 ESP = 008FFAB4 EBP = 008FFBA8 EFL = 00000246
汇编中的函数流程:
void demo()
{
(1) 00DD1A30 push ebp
(2) 00DD1A31 mov ebp,esp
(3) 00DD1A33 sub esp,0C0h
(4) 00DD1A39 push ebx
(5) 00DD1A3A push esi
(6) 00DD1A3B push edi
(7) 00DD1A3C lea edi,[ebp-0C0h]
(8) 00DD1A42 mov ecx,30h
(9) 00DD1A47 mov eax,0CCCCCCCCh
(10)00DD1A4C rep stos dword ptr es:[edi]
(11)00DD1A4E mov ecx,offset _2D317A6C_scratch_pad@c (0DDD00Ch)
(12)00DD1A53 call @__CheckForDebuggerJustMyCode@4 (0DD134Dh)
}
- 步入(1),变化——
EAX = 00DDD006 EBX = 00608000 ECX = 00DDD006 EDX = 00000001 ESI = 00DD1023 EDI = 008FFBA8 EIP = 00DD1A31 ESP = 008FFAB0 EBP = 008FFBA8 EFL = 00000246
- 步入(2),变化-
AX = 00DDD006 EBX = 00608000 ECX = 00DDD006 EDX = 00000001 ESI = 00DD1023 EDI = 008FFBA8 EIP = 00DD1A33 ESP = 008FFAB0 EBP = 008FFAB0 EFL = 00000246
- 步入(3),变化——
EAX = 00DDD006 EBX = 00608000 ECX = 00DDD006 EDX = 00000001 ESI = 00DD1023 EDI = 008FFBA8 EIP = 00DD1A39 ESP = 008FF9F0 EBP = 008FFAB0 EFL = 00000206
- 步入(4),变化——
EAX = 00DDD006 EBX = 00608000 ECX = 00DDD006 EDX = 00000001 ESI = 00DD1023 EDI = 008FFBA8 EIP = 00DD1A3A ESP = 008FF9EC EBP = 008FFAB0 EFL = 00000206
- 步入(5),变化——
EAX = 00DDD006 EBX = 00608000 ECX = 00DDD006 EDX = 00000001 ESI = 00DD1023 EDI = 008FFBA8 EIP = 00DD1A3B ESP = 008FF9E8 EBP = 008FFAB0 EFL = 00000206
- 进入(6),变化——
EAX = 00DDD006 EBX = 00608000 ECX = 00DDD006 EDX = 00000001 ESI = 00DD1023 EDI = 008FFBA8 EIP = 00DD1A3C ESP = 008FF9E4 EBP = 008FFAB0 EFL = 00000206
- 步入(7),变化——
EAX = 00DDD006 EBX = 00608000 ECX = 00DDD006 EDX = 00000001 ESI = 00DD1023 EDI = 008FF9F0 EIP = 00DD1A42 ESP = 008FF9E4 EBP = 008FFAB0 EFL = 00000206
- 步入(8),变化——
EAX = 00DDD006 EBX = 00608000 ECX = 00000030 EDX = 00000001 ESI = 00DD1023 EDI = 008FF9F0 EIP = 00DD1A47 ESP = 008FF9E4 EBP = 008FFAB0 EFL = 00000206
- 进入(9),变化——
EAX = CCCCCCCC EBX = 00608000 ECX = 00000030 EDX = 00000001 ESI = 00DD1023 EDI = 008FF9F0 EIP = 00DD1A4C ESP = 008FF9E4 EBP = 008FFAB0 EFL = 00000206
- 进入(10),改变-
EAX = CCCCCCCC EBX = 00608000 ECX = 00000000 EDX = 00000001 ESI = 00DD1023 EDI = 008FFAB0 EIP = 00DD1A4E ESP = 008FF9E4 EBP = 008FFAB0 EFL = 00000206
- 跨过 (11),更改-
EAX = CCCCCCCC EBX = 00608000 ECX = 00DDD00C EDX = 00000001 ESI = 00DD1023 EDI = 008FFAB0 EIP = 00DD1A53 ESP = 008FF9E4 EBP = 008FFAB0 EFL = 00000206
- 进入(12),变化——
EAX = CCCCCCCC EBX = 00608000 ECX = 00DDD00C EDX = 00000001 ESI = 00DD1023 EDI = 008FFAB0 EIP = 00DD134D ESP = 008FF9E0 EBP = 008FFAB0 EFL = 00000206
我的问题是:
- 在进入演示之前,
ESP = 008FFAB8
,然后是ESP
-ESP = 008FFAB4
。在这 4 个字节中插入了什么导致堆栈指针增加(向下)? - EBP 本质上是“帧指针”吗?在它下面的 4 个字节中,我们可以假设返回地址驻留?然后是函数的参数?
- 差异
EBP - ESP
是否会导致为函数的局部变量分配内存? - 是不是每次我们在
demo
范围内推入一些东西(如 (1) 中所做的那样),那么堆栈指针会增加吗? - 希望能解释步骤 (1)-(9) 中堆栈中究竟发生了什么。
解决方法
函数调用指令使 CPU 将返回地址压入堆栈,在本例中为 4 个字节。函数本身也可以分配更多的堆栈空间。
在 Visual C++ 中,您看到的是 {
行表示函数中的堆栈设置代码(序言)。当下一条指令是 {
时,调用已执行,但函数序言尚未执行。因此,已使用 4 个字节来存储返回地址,但尚未分配函数想要使用的任何其他字节。当您跨过 {
时,即为函数设置其余堆栈帧的函数序言。
Jabberwocky 的回答提供了该函数的汇编代码,但并没有真正解释为什么您看到堆栈指针分为两部分递减。
,这是编译器为 mm<-diag(5)
函数生成的代码:
demo
对于一个什么都不做的函数来说,这似乎很多。所有这些代码仅添加到调试版本中,其目的是让调试运行时能够检测本地缓冲区溢出:
我们来看看这个小程序,我们在本地缓冲区void demo()
{
00331E10 push ebp
00331E11 mov ebp,esp
00331E13 sub esp,0C0h // <<<< you are probably refering to this
00331E19 push ebx
00331E1A push esi
00331E1B push edi
00331E1C mov edi,ebp
00331E1E xor ecx,ecx
00331E20 mov eax,0CCCCCCCCh
00331E25 rep stos dword ptr es:[edi]
00331E27 mov ecx,offset _4C554807_foo@c (033C000h)
00331E2C call @__CheckForDebuggerJustMyCode@4 (033130Ch)
}
的末尾之外写入:
test
这是为 void demo()
{
char test[20];
for (int i = 0; i < 30; i++)
test[i] = 0;
}
int main()
{
demo();
}
生成的汇编代码(评论是我的):
demo
,
栈指针当然是一个实现细节。在 C 中没有定义的方法来查看堆栈指针是什么,或者知道它是如何工作的——事实上,甚至不能保证 是传统的堆栈或堆栈指针。
通常有两个(或更多)活动指针进入堆栈。通常有一个“堆栈指针”,用于跟踪有多少数据被推入堆栈,还有一个“帧指针”,它始终指向当前函数堆栈帧的基址。
通常,每次将单词压入堆栈时,堆栈指针都会增加 sizeof(int)
。例如,如果您调用了 f(1,2,3)
,您可能会看到堆栈指针随着三个参数被压入 3*sizeof(int)
而改变,并且在 f
被调用之前。 (实际上,它比这更复杂,因为现在一些参数通常在寄存器中传递给函数,而不是在堆栈中。)
通常在调用函数时堆栈指针会增长很多,因为必须创建一个全新的堆栈帧。除其他外,旧的堆栈指针和返回地址必须被保存,可能还有一个指向调用函数堆栈帧的链接指针。
堆栈指针也可能因其他原因而改变。局部变量存储在堆栈中,因此当调用新函数时,它可能会调整堆栈指针为它们留出空间。您的函数也有可能在函数中途执行可能导致在堆栈上分配额外内存的事情——因此需要进一步调整堆栈指针。如果您通过写入声明“可变长度数组”(VLA)
int a[n];
其中 n
是一个直到运行时才知道的变量,堆栈指针将通过 n*sizeof(int)
进行调整。如果您调用旧的 alloca
函数,或多或少会发生相同的事情。如果在循环或条件语句的内部块中声明额外的局部变量,则在进入该块时可能会进一步调整堆栈指针。
我提到了“帧指针”,它通常只在调用函数时更改一次,然后只要该函数处于活动状态就保持不变。帧指针用于访问局部变量——每个局部变量都存储在与帧指针相距某个已知的固定偏移量处。
当函数返回时,堆栈和帧指针以及程序计数器都根据保存在堆栈帧中的值恢复到函数被调用之前的状态。