检查Visual Studio C ++编译器生成的代码,第1部分

时间:2009-11-01 20:06:56

标签: c assembly x86

  

可能重复:
  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代码(sandbox.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

您可以将其全部删除,EAX和ECX的值最后将保持不变。那么,这些指示的重点是什么?

4 个答案:

答案 0 :(得分:11)

整件事

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

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

诀窍很简单:对于否定的y符号扩展,11111...1格式为EDX,实际上{2}补码表示为-1。如果原始SUB值为负,则以下EAX会有效地将y添加1。如果原始y为正(或0),则EDX将在符号扩展后保留0EAX将保持不变。

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

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

或更好

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

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

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

答案 1 :(得分:5)

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

CDQSUB EAX,EDX操作可确保除法将负数舍入为零 - 否则SAR将向下舍入,从而给出负数的错误结果。

答案 2 :(得分:2)

编译器发出此错误的原因:

LEA EBX,DWORD PTR DS:[EBX]

而不是语义上的等价物:

NOP
NOP
NOP
NOP
NOP
NOP

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

答案 3 :(得分:1)

这并没有真正回答这个问题,但这是一个有用的暗示。您可以让Visual Studio为您生成asm文件,而不是乱丢OllyDbg.exe,它有额外的好处,它可以作为注释放在原始源代码中。对于您当前的小项目来说,这不是什么大问题,但随着项目的增长,您可能会花费相当多的时间来确定哪些汇编代码与哪些源代码匹配。

从命令行,您需要/ 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

希望有所帮助!