【问题标题】:Disecting x86 stack frame set up Visual C++ (Visual Studio 2010)剖析 x86 堆栈框架设置 Visual C++ (Visual Studio 2010)
【发布时间】:2014-06-05 20:56:03
【问题描述】:

关于这个将两个数字相加的非常简单的函数,只是几个好奇的问题。这是带注释的反汇编(问题在标有???的代码中)

这里是C函数

int Add(int x , int y)
{
    int additionAnswer = 0 ;
    additionAnswer = x + y;
    return additionAnswer;
}

这是带有我的注释和问题的反汇编(DEBUG BUILD)

int Add(int x , int y)
{
  push        ebp        ; preserve base pointer 
  mov         ebp,esp    ; move base pointer to start of stack frame for this function
  sub         esp,0CCh   ; ??? is this preserving  space on the stack for local
                         ;  variable..204bytes seems execessive!!!
  push        ebx  //??? Why is this preserving ebx, esi and edi
  push        esi  //??? when clearly this function does not use it!
  push        edi  
  lea         edi,[ebp-0CCh]   ; ??? Why is it loading the address of top of stack into edi?
  mov         ecx,33h          ; ??? What is that doing
  mov         eax,0CCCCCCCCh   ; ??? What is that doing 
  rep stos    dword ptr es:[edi]   ; ??? What is that doing         
  mov         dword ptr [additionAnswer],0  ;int additionAnswer = 0 
  mov         eax,dword ptr [x]  ;eax = x
  add         eax,dword ptr [y]  ;eax = eax + y
  mov         dword ptr [additionAnswer],eax   ;answer = eax
  mov         eax,dword ptr [additionAnswer]   ; return addition in eax
  pop         edi  ;restore edi even though I didn't use it !!!!
  pop         esi  ;restore esi even though I didn't use it !!!!
  pop         ebx  ;restore ebx even though I didn't use it !!!!
  mov         esp,ebp  ; clean up stack frame and restore sp to 
                        ;4 bytes above it's original pre-frame value
  pop         ebp  ;restore base pointer back to it's original value 
                   ;and at same time this will add 4 to sp hence restoring it 
                   ;back to its former pre-frame value, and pointing to return address on stack
  ret  

我挖了一下,显然在 Win32 中,必须保留以下内容:edi、esi、ebp 和 ebx。 我可以理解调用函数可能会使用源/目标索引寄存器 (esi /edi),但为什么 calling 函数不保留 EBX 本身而不是我的函数做不必要的驴工作,调用者肯定知道它需要保留什么,不需要保留什么! 最后为什么要保留 EBX 而不是 ECX,为什么我的函数有责任保留这些寄存器(甚至是任何寄存器!)。

最后一件事我在发布模式下构建时看不到任何这些寄存器保存代码(没有优化)......这是仅调试概念吗????

【问题讨论】:

  • 你用什么选项编译?看起来像调试模式,其中未使用的内存用 0xCC 填充,以帮助您捕获未初始化内存的使用。如果您要评估指令生成的效率,您应该在关闭所有调试选项并打开所有优化的情况下进行编译。 (发布模式)
  • 这看起来像是使用调试选项编译的。在发布版本下尝试。
  • 是的,线索在最后一句中,我确实说明了在发布模式下不存在寄存器保存代码,因此上面的代码包含寄存器保存,因此不能是发布代码。我将编辑问题。
  • @Dark Falcon。感谢您的回答。我不是试图衡量效率,而是试图理解这里的意图。当您说“未使用的内存被 0xCC 填充以帮助您捕获未初始化内存的使用”时,您的意思是这行代码“mov eax,0CCCCCCCCh”。如果是这样,我不确定将常量移入 eax 是如何做到你所说的,你能给我任何指向网站的指针,以阅读更多关于这个的内容.....
  • 哈哈找到了!这个问题的答案之一stackoverflow.com/questions/370195/… 指出“操作码 0xCC 是 int3 操作码,它是软件断点中断。因此,如果您尝试在已填充该填充值的未初始​​化内存中执行代码,您将立即命中断点,操作系统将允许您附加调试器(或终止进程)。”....这一切都很好地结合在一起

标签: c assembly x86 stack masm


【解决方案1】:

回答你的问题

为什么调用函数不保存它自己而不是我的函数做不必要的驴工作,调用者肯定知道它需要保存什么,它不需要保存什么!

因为,调用约定指定函数可以假定它调用的函数不会修改 edi、esi、ebp 和 ebx。很多时候,调用者也会关心其他寄存器(eax、ecx 和 edx)中的值,但它知道它们可能会被修改,因此如果调用者关心,它们需要被调用者保存。

有时让调用者保存东西更简单,有时让被调用者做这件事更简单。由于调用约定需要被不同语言的许多不同编译器理解(并遵循)(以允许这些编译器一起工作),标准调用约定为您提供调用者保存和被调用者保存的混合,任何编译器都可以它自己(独立)选择为各种值使用哪种寄存器,以最大限度地减少完成的工作量。

【讨论】:

  • 理解意图对我来说是关键。我理解这是惯例,但我同意我的函数保留基址和堆栈指针以保持堆栈帧结构的一致性、状态和完整性是有意义的,但通用寄存器 EAx、EBX、ECX 不能这样说、EDI 和 ESI。当然,调用函数知道这些寄存器的用法,它应该保留它需要的东西并保留它不需要的东西,这是一个更简洁的模型,并优化了不必要的寄存器保存。因此我的问题是,如果不需要,为什么要保留 EBX ESI 和 EDI。
  • @Ted 您必须在这里采取整体观点:考虑一个函数调用链,而不仅仅是两个。函数本质上是不同的:如果你有一个叶函数f(a) {return a*2;},你要从使用许多寄存器的更复杂的函数中调用它怎么办?如果您将调用约定设置为“调用者保存所有内容”,则每次从复杂函数调用 f 时都必须推送当前正在使用的许多寄存器,然后将它们弹出回来,即使 f 不会覆盖它们,所以你在调用者中做了很多不必要的工作……
  • ...这里有一个权衡:让调用者保存所有内容的缺点是它会增加调用小函数的开销,有效地使一对指令函数不切实际,并增加代码调用者中的大小(这可能导致指令缓存未命中)。在调用约定中为编译器/程序员提供一些灵活性是有意义的。
  • @Leaky 说得通!因此,如果我想象(出于争论)存在一个约定,即调用函数必须保留所有寄存器(EBP 和 ESP 除外),那么您在说什么,让我们假设一个场景,例如调用函数(出于某种愚蠢的原因)将调用我的简单函数 5 次,然后它显然不需要保存寄存器 5 次!因此,作为平衡,每个人都同意 my 函数将保留 EBX、ESI 和 EDI。谢谢你,现在有意义了!
  • P.S 我不知道为什么我认为 EBX 可能有一些特殊用途,就像 EAX 用于返回值一样,但也许我错了......
【解决方案2】:

上面的 cmets 中有很多好的答案,经过又一夜的搜索,我发现了以下代码的作用

  lea         edi,[ebp-0CCh]         ;set edi to point to top of stack
  mov         ecx,33h                ;  set ecx  =  51 
  mov         eax,0CCCCCCCCh   
  rep stos    dword ptr es:[edi]   ; like memset - this line fills a specified amount (ecx, 
                                    which this case is 51 (33h) bytes) of memory (at [edi]) 
                                    with a given value (in eax which in  this case is 
                                    0CCCCCCCCh).  

这里是rep stos What does the "rep stos" x86 assembly instruction sequence do?的解释

0xCCCCCCCC 被微软的 C++ 调试运行时库和许多 DOS 环境用来标记未初始化的堆栈内存。 使用 0xCCCCCCCC 是因为它类似于(但不一样!)0xCC,它是 x86 处理器上 INT 3 调试断点中断的操作码。 因此,如果您尝试执行代码 0xCC,您将立即遇到断点(请参阅How do debuggers guarantee correctness when using INT 3 (0xCC) software breakpoint even though an instruction was patched?)。但是,如果您尝试执行 0xCCCCCCCC,它很可能只会让您的程序崩溃!

这是汉斯的一个很好的解释...... Why is the stack filled with 0xCCCCCCCC

所以唯一没有解释的行是下面的

sub         esp,0CCh ;why is is it preserving 204 bytes (0CCh) on the stack? For edit/continue?

感谢您的帮助!

【讨论】:

    猜你喜欢
    • 2011-07-21
    • 2011-11-22
    • 2010-12-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-11
    • 1970-01-01
    相关资源
    最近更新 更多