【问题标题】:Examining code generated by the Visual Studio C++ compiler, part 1 [duplicate]检查 Visual Studio C++ 编译器生成的代码,第 1 部分 [重复]
【发布时间】:2009-11-01 20:06:56
【问题描述】:

可能重复:
Why is such complex code emitted for dividing a signed integer by a power of two?

背景

我只是通过检查编译器生成的二进制代码来学习 x86 asm。

使用 Visual Studio 2010 beta 2 中的 C++ 编译器编译的代码。

Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.21003.01 for 80x86

C 代码(沙盒.c)

int mainCRTStartup()
{
    int x=5;int y=1024;
    while(x) { x--; y/=2; }
    return x+y;
}

使用 Visual Studio 命令提示符编译它

cl /c /O2 /Oy- /MD sandbox.c
link /NODEFAULTLIB /MANIFEST:NO /SUBSYSTEM:CONSOLE sandbox.obj

在 OllyDgb 中删除 sandbox.exe

以下从入口点开始。

00401000 >/$ B9 05000000    MOV ECX,5
00401005  |. B8 00040000    MOV EAX,400
0040100A  |. 8D9B 00000000  LEA EBX,DWORD PTR DS:[EBX]
00401010  |> 99             /CDQ
00401011  |. 2BC2           |SUB EAX,EDX
00401013  |. D1F8           |SAR EAX,1
00401015  |. 49             |DEC ECX
00401016  |.^75 F8          \JNZ SHORT sandbox.00401010
00401018  \. C3             RETN

考试

MOV ECX, 5          int x=5;
MOV EAX, 400        int y=1024;
LEA  ...            // no idea what LEA does here. seems like ebx=ebx. elaborate please.
                    // in fact, NOPing it does nothing to the original procedure and the values.

CQD                 // sign extends EAX into EDX:EAX, which here: edx = 0. no idea why.
SUB EAX, EDX        // eax=eax-edx, here: eax=eax-0. no idea, pretty redundant. 
SAR EAX,1           // okay, y/= 2
DEC ECX             // okay, x--, sets the zero flag when reaches 0.
JNZ ...             // okay, jump back to CQD if the zero flag is not set.

这部分让我很困扰:

0040100A  |. 8D9B 00000000  LEA EBX,DWORD PTR DS:[EBX]
00401010  |> 99             /CDQ
00401011  |. 2BC2           |SUB EAX,EDX

您可以全部nop,最后EAX 和ECX 的值将保持不变。那么,这些说明的意义何在?

【问题讨论】:

    标签: c assembly x86


    【解决方案1】:

    整件事

    00401010  |> 99             /CDQ
    00401011  |. 2BC2           |SUB EAX,EDX
    00401013  |. D1F8           |SAR EAX,1
    

    代表y /= 2。你看,独立的SAR 不会按照编译器作者的意图执行有符号整数除法。有符号整数除法的 C++98 标准建议将结果向 0 舍入,而单独的 SAR 将向负无穷大舍入。 (允许向负无穷舍入,选择留给实现)。为了实现对负操作数的舍入到 0,使用了上述技巧。如果你使用无符号类型而不是有符号类型,那么编译器将只生成一个移位指令,因为负除的问题不会发生。

    诀窍很简单:对于负的y 符号扩展将在EDX 中放置一个11111...1 的模式,这实际上是-1 在2 的补码表示中。如果原始y 值为负,则以下SUB 将有效地将1 加到EAX。如果原始y 为正数(或0),则EDX 在符号扩展后将保持0EAX 将保持不变。

    换句话说,当您使用签名的y 编写y /= 2 时,编译器会生成类似于以下内容的代码

    y = (y < 0 ? y + 1 : y) >> 1;
    

    或者,更好

    y = (y + (y < 0)) >> 1;
    

    请注意,C++ 标准不要求除法结果向零舍入,因此编译器有权仅对有符号类型进行一次移位。但是,通常编译器会遵循建议向零舍入(或提供控制行为的选项)。

    P.S.我不确定LEA 指令的目的是什么。这确实是一个无操作。但是,我怀疑这可能只是插入代码中以进行进一步修补的占位符指令。如果我没记错的话,MS 编译器有一个选项可以强制在每个函数的开头和结尾插入占位符指令。将来,补丁程序可以使用将执行补丁代码的CALLJMP 指令覆盖此指令。选择这个特定的LEA 只是因为它产生了正确长度的空操作占位符指令。当然,也可能完全不同。

    【讨论】:

    • 不是-2/2 + 1,而是(-2 + 1) &gt;&gt; 1。这评估为-1
    【解决方案2】:

    lea ebx,[ebx] 只是一个 NOP 操作。它的目的是在内存中对齐循环的开头,这将使其更快。正如您在此处看到的,循环的开头从地址 0x00401010 开始,由于这条指令,该地址可以被 16 整除。

    CDQSUB EAX,EDX 运算确保除法会将负数舍入为零 - 否则 SAR 会将其舍入,从而给出不正确的负数结果。

    【讨论】:

    • 错误:__tmainCRTStartu 使用 EBX 来检查这是否是托管应用程序,因此它不是 NOP,并且与对齐没有任何关系
    • 你自己错了。 lea ebx,[ebx] 等价于 mov ebx,ebx - 它什么也不做。不检查 ebx 寄存器的值。
    • 它似乎没有做任何事情,因为在调用 main 之前,EBX 在 __tmainCRTStartup 中以零(xor ebx,ebx)初始化。只需反汇编 __tmainCRTStartup 中的代码,您就会看到。一旦 main 返回 ebx 就会被检查。如果你例如在 main 返回之前显式分配给 EBX,编译器将保留 EBX 的值(原生应用为零)并在 main 返回时恢复它。
    • jn:EBX 持有一个值这一事实是无关紧要的,因为该指令不会将它与任何东西进行比较或改变它。它可以访问任何其他寄存器。如果您查看 Visual Studio 创建的代码,您会在大多数内部循环之前看到这种代码 - 循环的开头将对齐到 16(或可能 8)字节。
    • interjay:当您说此操作用于对齐循环时,您是对的。在开头插入 NOP 会使 VS 插入 lea ecx, [ecx+0]。另一方面,运行时实际上使用 EBX 来检查这是否是托管应用程序。运行时关闭的处理方式不同
    【解决方案3】:

    编译器发出这个的原因:

    LEA EBX,DWORD PTR DS:[EBX]
    

    而不是语义上等价的:

    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    

    ..是处理器执行一条 6 字节指令比执行六条 1 字节指令更快。就是这样。

    【讨论】:

      【解决方案4】:

      这并不能真正回答问题,而是一个有用的提示。您可以让 Visual Studio 为您生成 asm 文件,而不是到处乱搞 OllyDbg.exe,它还有一个额外的好处是它可以将原始源代码作为 cmets 放入。这对您当前的小项目来说没什么大不了的,但是随着项目的发展,您最终可能会花费大量时间来确定哪些汇编代码与哪些源代码匹配。

      在命令行中,您需要 /FAs 和 /Fa 选项 (MSDN)。

      这是您的示例代码的部分输出(我编译了调试代码,因此 .asm 更长,但您可以对优化后的代码执行相同的操作):

      _wmain  PROC                        ; COMDAT
      
      ; 8    : {
      
          push    ebp
          mov ebp, esp
          sub esp, 216                ; 000000d8H
          push    ebx
          push    esi
          push    edi
          lea edi, DWORD PTR [ebp-216]
          mov ecx, 54                 ; 00000036H
          mov eax, -858993460             ; ccccccccH
          rep stosd
      
      ; 9    :     int x=5; int y=1024;
      
          mov DWORD PTR _x$[ebp], 5
          mov DWORD PTR _y$[ebp], 1024        ; 00000400H
      $LN2@wmain:
      
      ; 10   :     while(x) { x--; y/=2; }
      
          cmp DWORD PTR _x$[ebp], 0
          je  SHORT $LN1@wmain
          mov eax, DWORD PTR _x$[ebp]
          sub eax, 1
          mov DWORD PTR _x$[ebp], eax
          mov eax, DWORD PTR _y$[ebp]
          cdq
          sub eax, edx
          sar eax, 1
          mov DWORD PTR _y$[ebp], eax
          jmp SHORT $LN2@wmain
      $LN1@wmain:
      
      ; 11   :     return x+y;
      
          mov eax, DWORD PTR _x$[ebp]
          add eax, DWORD PTR _y$[ebp]
      
      ; 12   : }
      
          pop edi
          pop esi
          pop ebx
          mov esp, ebp
          pop ebp
          ret 0
      _wmain  ENDP
      

      希望有帮助!

      【讨论】:

      • 谢谢,这很有用。我使用 ollydbg 是因为它是一个非常简洁的工具,它突出显示最后一条指令更改的寄存器值,绘制 JMP 指令的去向,对我来说,在 olly 中跟踪指令比在其他任何地方都方便得多。
      • 哦,是的,毫无疑问,像 OllyDbg 这样的完整程序集调试器是一个很棒的工具。我只是在为您的工具箱推荐另一个工具。
      猜你喜欢
      • 2021-09-22
      • 1970-01-01
      • 2011-01-14
      • 2011-04-13
      • 1970-01-01
      • 1970-01-01
      • 2023-03-26
      • 2016-12-10
      相关资源
      最近更新 更多