【问题标题】:stack pointer changes through function call堆栈指针通过函数调用改变
【发布时间】:2021-07-26 07:24:11
【问题描述】:

我试图通过函数调用了解更多关于堆栈指针的行为,但我不确定我是否理解当我们调用函数并从函数返回时会发生什么。 假设我有这个主程序:

int main()
{
    demo();
    return 0;
}

而demo是这样定义的:

void demo()
{

}

我使用的是 VS2019,当我调试时,我会通过时间检查以下 SP 值与汇编代码相关的内容(调试会话的值示例):

  1. 在输入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) 
  1. 进入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)  
  1. 一步入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. 进入 (1),更改 - EAX = 00DDD006 EBX = 00608000 ECX = 00DDD006 EDX = 00000001 ESI = 00DD1023 EDI = 008FFBA8 EIP = 00DD1A31 ESP = 008FFAB0 EBP = 008FFBA8 EFL = 00000246
  2. 进入 (2),更改 -
    AX = 00DDD006 EBX = 00608000 ECX = 00DDD006 EDX = 00000001 ESI = 00DD1023 EDI = 008FFBA8 EIP = 00DD1A33 ESP = 008FFAB0 EBP = 008FFAB0 EFL = 00000246
  3. 进入 (3),更改 - EAX = 00DDD006 EBX = 00608000 ECX = 00DDD006 EDX = 00000001 ESI = 00DD1023 EDI = 008FFBA8 EIP = 00DD1A39 ESP = 008FF9F0 EBP = 008FFAB0 EFL = 00000206
  4. 进入 (4),更改 - EAX = 00DDD006 EBX = 00608000 ECX = 00DDD006 EDX = 00000001 ESI = 00DD1023 EDI = 008FFBA8 EIP = 00DD1A3A ESP = 008FF9EC EBP = 008FFAB0 EFL = 00000206
  5. 进入 (5),更改 - EAX = 00DDD006 EBX = 00608000 ECX = 00DDD006 EDX = 00000001 ESI = 00DD1023 EDI = 008FFBA8 EIP = 00DD1A3B ESP = 008FF9E8 EBP = 008FFAB0 EFL = 00000206
  6. 进入 (6),更改 - EAX = 00DDD006 EBX = 00608000 ECX = 00DDD006 EDX = 00000001 ESI = 00DD1023 EDI = 008FFBA8 EIP = 00DD1A3C ESP = 008FF9E4 EBP = 008FFAB0 EFL = 00000206
  7. 进入 (7),更改 - EAX = 00DDD006 EBX = 00608000 ECX = 00DDD006 EDX = 00000001 ESI = 00DD1023 EDI = 008FF9F0 EIP = 00DD1A42 ESP = 008FF9E4 EBP = 008FFAB0 EFL = 00000206
  8. 进入 (8),更改 - EAX = 00DDD006 EBX = 00608000 ECX = 00000030 EDX = 00000001 ESI = 00DD1023 EDI = 008FF9F0 EIP = 00DD1A47 ESP = 008FF9E4 EBP = 008FFAB0 EFL = 00000206
  9. 进入 (9),更改 - EAX = CCCCCCCC EBX = 00608000 ECX = 00000030 EDX = 00000001 ESI = 00DD1023 EDI = 008FF9F0 EIP = 00DD1A4C ESP = 008FF9E4 EBP = 008FFAB0 EFL = 00000206
  10. 跨步进入 (10),更改- EAX = CCCCCCCC EBX = 00608000 ECX = 00000000 EDX = 00000001 ESI = 00DD1023 EDI = 008FFAB0 EIP = 00DD1A4E ESP = 008FF9E4 EBP = 008FFAB0 EFL = 00000206
  11. 跨过 (11),改变—— EAX = CCCCCCCC EBX = 00608000 ECX = 00DDD00C EDX = 00000001 ESI = 00DD1023 EDI = 008FFAB0 EIP = 00DD1A53 ESP = 008FF9E4 EBP = 008FFAB0 EFL = 00000206
  12. 进入(12),改变—— EAX = CCCCCCCC EBX = 00608000 ECX = 00DDD00C EDX = 00000001 ESI = 00DD1023 EDI = 008FFAB0 EIP = 00DD134D ESP = 008FF9E0 EBP = 008FFAB0 EFL = 00000206

我的问题是:

  1. 在进入演示之前,ESP = 008FFAB8,然后将ESP 更改为ESP = 008FFAB4。在这 4 个字节中插入了什么导致堆栈指针增加(向下)?
  2. EBP 本质上是“帧指针”吗?在它下面的 4 个字节中,我们可以假设返回地址驻留?然后是函数的参数?
  3. 差异EBP - ESP 是否导致为函数的本地分配的内存?
  4. 是否每次我们在demo 范围内推送某些内容(如 (1) 中所做的那样),堆栈指针会增加吗?
  5. 希望能解释一下步骤 (1)-(9) 中堆栈中究竟发生了什么。

【问题讨论】:

  • 您也可以使用 VS2019 单步执行汇编代码。使用 Debug>Windows>Disassembly 命令。
  • 我怀疑这个问题的上下文是this previous question
  • 在“2. 进入demo,就在开头”和“3. 进入demo 函数”之后,不清楚您在代码中的哪个位置。以及究竟是什么意思“4.从demo返回到main”。第 4 步实际上可能是为局部变量释放内存并返回调用函数的两个步骤。 (函数调用的实现很大程度上取决于处理器架构。)
  • 我已经详细说明了我的问题,可以重新打开吗?

标签: c stack-frame stack-pointer


【解决方案1】:

函数调用指令使 CPU 将返回地址压入堆栈,在本例中为 4 个字节。函数本身也可以分配更多的堆栈空间。

在 Visual C++ 中,您看到的是 { 行表示函数中的堆栈设置代码(序言)。当下一条指令是{ 时,调用已执行,但函数序言尚未执行。因此,已经占用了 4 个字节来存储返回地址,但尚未分配函数想要使用的任何额外字节。当您越过{ 时,这就是为函数设置其余堆栈帧的函数序言。

Jabberwocky 的回答提供了函数的汇编代码,但并没有真正解释为什么您看到堆栈指针分两部分递减。

【讨论】:

    【解决方案2】:

    这是编译器为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 生成的汇编代码(cmets 是我的):

    void demo()
    {
    003743C0  push        ebp  
    003743C1  mov         ebp,esp  
    003743C3  sub         esp,0F4h  
    003743C9  push        ebx  
    003743CA  push        esi  
    003743CB  push        edi  
    003743CC  lea         edi,[ebp-34h]  
    003743CF  mov         ecx,0Dh  
    003743D4  mov         eax,0CCCCCCCCh  
    003743D9  rep stos    dword ptr es:[edi]  
    003743DB  mov         ecx,offset _4C554807_foo@c (037C000h)  
    003743E0  call        @__CheckForDebuggerJustMyCode@4 (037130Ch)  
      char test[20];
    
      for (int i = 0; i < 30; i++)
    003743E5  mov         dword ptr [ebp-24h],0  
    003743EC  jmp         __$EncStackInitStart+2Bh (03743F7h)  
    
    // start of for loop
    003743EE  mov         eax,dword ptr [ebp-24h]         // [ebp-24h] is i
    003743F1  add         eax,1                           // i++
    003743F4  mov         dword ptr [ebp-24h],eax  
    
    003743F7  cmp         dword ptr [ebp-24h],1Eh        // i > 30 (0x1e)
    003743FB  jge         _scanf+3h (0374423h)           //  yes -> go to end of for loop
    
    // check for local local buffer overflow
    003743FD  mov         eax,dword ptr [ebp-24h]  
    00374400  mov         dword ptr [ebp-0F0h],eax  
    00374406  cmp         dword ptr [ebp-0F0h],14h  
    0037440D  jae         __$EncStackInitStart+45h (0374411h)  
    0037440F  jmp         __$EncStackInitStart+4Ah (0374416h)  
    00374411  call        ___report_rangecheckfailure (0371046h)  
    00374416  mov         ecx,dword ptr [ebp-0F0h]  
    // end check for local local buffer overflow
    
    0037441C  mov         byte ptr test[ecx],0          // test[i] = 0
    00374421  jmp         __$EncStackInitStart+22h (03743EEh)  
    }
    

    【讨论】:

      【解决方案3】:

      堆栈指针当然是一个实现细节。 C 中没有明确的方法来查看堆栈指针是什么,或者知道它是如何工作的——事实上,甚至无法保证 是一个常规堆栈或堆栈指针。

      堆栈中通常有两个(或更多)活动指针。通常有一个“堆栈指针”来跟踪有多少数据被压入堆栈,还有一个“帧指针”始终指向当前函数堆栈帧的基址。

      通常,每次将单词压入堆栈时,堆栈指针都会增加sizeof(int)。例如,如果您调用了f(1, 2, 3),您可能会看到堆栈指针在三个参数被压入时更改了3*sizeof(int),并且在f 被调用之前。 (实际上,它比这更复杂,因为现在一些参数通常在寄存器中传递给函数,而不是在堆栈中。)

      通常,调用函数时堆栈指针会增长很多,因为必须创建一个全新的堆栈帧。除其他事项外,还必须保存旧的堆栈指针和返回地址,还可能是指向调用函数堆栈帧的链接指针。

      堆栈指针也可能因其他原因而改变。局部变量存储在堆栈中,因此当调用新函数时,它可能会调整堆栈指针以为它们留出空间。您的函数也有可能在函数的中途做一些事情,这些事情可能会导致在堆栈上分配额外的内存 - 因此堆栈指针需要进一步调整。如果你通过写来声明一个“变长数组”(VLA)

      int a[n];
      

      其中n 是一个直到运行时才知道的变量,堆栈指针将由n*sizeof(int) 调整。如果您调用旧的 alloca 函数,或多或少会发生同样的事情。如果您在循环或条件语句的内部块中声明额外的局部变量,则在进入该块时可能会进一步调整堆栈指针。

      我提到了“帧指针”,它通常只在调用函数时更改一次,然后只要该函数处于活动状态就保持不变。帧指针用于访问局部变量——每个局部变量都存储在帧指针的某个已知的固定偏移处。

      当函数返回时,堆栈和帧指针以及程序计数器都会根据堆栈帧中保存的值恢复到函数被调用之前的状态。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-08-19
        • 2020-01-03
        • 1970-01-01
        • 1970-01-01
        • 2018-05-03
        • 2020-05-08
        • 2012-06-22
        • 2013-01-14
        相关资源
        最近更新 更多