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

可能重复:
为什么发出这样复杂的代码来将有符号整数除以2的幂?

背景

我只是通过检查编译器生成的二进制代码来学习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的值最后将保持不变。 那么,这些指示的重点是什么?

整个东西

 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的模式,实际上在2的补码表示中为-1 。 如果原始y值为负,则以下SUB将有效地将1添加到EAX 。 如果原始y为正(或0),则符号扩展后EDX将保持为0 ,并且EAX将保持不变。

换句话说,当您使用带符号的y编写y /= 2时,编译器会生成执行更多类似操作的代码

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

或更好

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

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

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

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

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

编译器发出这个的原因:

 LEA EBX,DWORD PTR DS:[EBX] 

而不是语义上的等价物:

 NOP NOP NOP NOP NOP NOP 

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

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

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

希望有所帮助!