64位乘法分析:
VC6 Console Code:
int main(int argc, char* argv[]) { ULONGLONG a = 0x87654321; ULONGLONG b = 0x100000001; a*=b; //::MessageBox(NULL,"Content","Title",MB_OK); return 0; } |
Debug模式下汇编Code:
9: ULONGLONG a = 0x87654321; 00401028 mov dword ptr [ebp-8],87654321h 0040102F mov dword ptr [ebp-4],0 10: ULONGLONG b = 0x100000001; 00401036 mov dword ptr [ebp-10h],1 0040103D mov dword ptr [ebp-0Ch],1 11: a*=b; 00401044 mov eax,dword ptr [ebp-0Ch] 00401047 push eax ;压入b高4 bytes 00401048 mov ecx,dword ptr [ebp-10h] 0040104B push ecx ;压入b低4 bytes 0040104C mov edx,dword ptr [ebp-4] 0040104F push edx ;压入a高4 bytes 00401050 mov eax,dword ptr [ebp-8] 00401053 push eax ;压入a低4 bytes 00401054 call _allmul (004010d0) ; _allmul计算64位乘法 00401059 mov dword ptr [ebp-8],eax 0040105C mov dword ptr [ebp-4],edx 12: 13: // ::MessageBox(NULL,"Content","Title",MB_OK); 14: return 0; 0040105F xor eax,eax |
_allmul:
--- intel/llmul.asm ------------------------------------------------- _allmul: 004010D0 mov eax,dword ptr [esp+8] ;取a高4字节 004010D4 mov ecx,dword ptr [esp+10h] ;取b高4字节 004010D8 or ecx,eax ; 当ecx==0 && eax == 0时, go on 004010DA mov ecx,dword ptr [esp+0Ch] ;取 b低4字节 004010DE jne hard (004010e9) 004010E0 mov eax,dword ptr [esp+4] 004010E4 mul eax,ecx 004010E6 ret 10h hard: 004010E9 push ebx ;保存ebx,使原栈内多偏移了4个字节 004010EA mul eax,ecx ; a高4字节*b低4字节 004010EC mov ebx,eax ;只保存乘法结果的低4字节到ebx 004010EE mov eax,dword ptr [esp+8] ;取a低4字节,因为push ebx,所以a在栈中多偏移4字节。 004010F2 mul eax,dword ptr [esp+14h] ;a低4字节*b高4字节 004010F6 add ebx,eax ;将两次乘法低4位结果保存到ebx 004010F8 mov eax,dword ptr [esp+8] ;a低4字节 004010FC mul eax,ecx ;a低4字节*b低4字节 004010FE add edx,ebx ;加上之前的运算结果低4字节ebx。 00401100 pop ebx 00401101 ret 10h |
代码详解:
004010D8 or ecx,eax ; 当ecx==0 && eax == 0时, go on 004010DA mov ecx,dword ptr [esp+0Ch] ;取 b低4字节 004010DE jne hard (004010e9) |
● or ecx,eax 该条指令判断了ecx与eax是否同时为0。即仅当
eax==0 && ecx==0时,or ecx,eax的运算结果ecx才==0,此时才会置标志位ZF=1。否则ZF=0。所以,与后面的jne实现逻辑:
if(!(eax==0&&ecx==0))
jmp hard (004010e9);
所以当 ecx==0 && eax==0,即a和b的高4字节都为0 时,此时a*b即为进行普通的32位寄存器运算,运算结果高4字节放在edx,低4字节放在eax。
or后紧跟mov 是为了保存 b低4字节到ecx,且因为mov等传送指令不会影响标志寄存器。
跳到hard标号处则是真正的“64位”乘法运算了:
算法很简单,即是小学乘法展开公式:(a+b)*(c+d) = a*c+a*d+b*c+b*d。
我们令aHigh4B为a高4字节,aLow4B为a低4字节,则得:
a*b = (aHigh4B<<32+aLow4B)*( bHigh4B<<32+bLow4B)
左移32位,即左移4字节。由于(aHigh4B<<32)* (bHigh4B<<32)即该式结果等于(aHigh4B* bHigh4B)<<64,所以肯定会溢出,所以该结果忽略。为什么忽略?因为a*b运算的最终结果要保存为64位,所以忽略超出64位的值。于是该式拆分得:
(aHigh4B<<32 )* bHigh4B + aLow4B*( bHigh4B<<32) + aLow4B* bLow4
(aHigh4B<<32 )* bHigh4B对应指令为:
004010EA mul eax,ecx ; a高4字节*b低4字节 004010EC mov ebx,eax ;只保存乘法结果的低4字节到ebx |
mul运算后,edx为结果高4字节,eax为结果低4字节。但为什么只保存eax低4字节的结果?而不保存edx高4字节的结果?
因为(eax*ecx)<<32 = (edx:eax)<<32。所以真实结果的高4字节edx左移32位再次溢出了64位,所以只需保存eax即可。之后将两次左移32位的乘法操作结果低4字节用ebx累积下来,再加上两个低4字节的乘法高位结果,即为最终运算结果的高4字节edx。
---------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------