可能重复:
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
int mainCRTStartup()
{
int x=5;int y=1024;
while(x) { x--; y/=2; }
return x+y;
}
cl /c /O2 /Oy- /MD sandbox.c
link /NODEFAULTLIB /MANIFEST:NO /SUBSYSTEM:CONSOLE sandbox.obj
以下从入口点开始。
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的值最后将保持不变。那么,这些指示的重点是什么?
答案 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
将在符号扩展后保留0
,EAX
将保持不变。
换句话说,当您使用签名y /= 2
编写y
时,编译器会生成执行更多类似以下内容的代码
y = (y < 0 ? y + 1 : y) >> 1;
或更好
y = (y + (y < 0)) >> 1;
注意,C ++标准不要求将除法结果舍入为零,因此即使对于有符号类型,编译器也有权进行单次移位。但是,通常编译器会按照建议向零舍入(或提供控制行为的选项)。
P.S。我不确定LEA
指令的目的是什么。这确实是一个无操作。但是,我怀疑这可能只是插入代码中的占位符指令以进行进一步修补。如果我没记错的话,MS编译器有一个选项,强制在每个函数的开头和结尾插入占位符指令。将来,修补程序可以使用CALL
或JMP
指令覆盖来执行修补程序代码。选择此特定LEA
只是因为它产生了一个正确长度的无操作占位符指令。当然,它可能是完全不同的东西。
答案 1 :(得分:5)
lea ebx,[ebx]
只是一个NOP操作。它的目的是在内存中对齐循环的开头,这将使它更快。正如你在这里看到的那样,循环的开始从地址0x00401010开始,由于这条指令,它可以被16整除。
CDQ
和SUB 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
希望有所帮助!