那麼如果讓我們來做乘法的優化,我們會怎麼做呢?很顯然位移是必須要被利用的,但是除此之外微軟的編譯器還利用了lea指令,但是乘法的優化是非常多變的,本小節的目的是讓各位讀者再看見某一塊指令時知道“哦!這是乘法...”就可以了。
我們先看看簡單的位移優化,經過筆者的總結,當乘數爲2的次方,且大於8時編譯器纔會使用此優化,讓我們看看優化前與優化後的效果:
源碼:
int nNum = 16;
printf("%p",nNum*argc);
Debug:
mov [ebp+nNum], 10h
mov eax, [ebp+nNum]
imul eax, [ebp+argc] ; 乘法運算
mov esi, esp
push eax
push offset Format ; "%p"
call ds:__imp__printf
add esp, 8
Release:
mov eax, [esp+argc]
shl eax, 4 ; 左移4位,變形的乘法運算(2^4=16)
push eax
push offset Format ; "%p"
call ds:__imp__printf
add esp, 8
按照我們平時對編譯器的理解,如果我我們乘的是17的話,那麼編譯器肯定會將其分解爲一個左位移再加一個加法,事實真的是如此嗎?我們直接看看乘以17後的Release版的彙編代碼:
mov eax, [esp+argc]
mov ecx, eax
shl ecx, 4 ; 變形乘法
add ecx, eax ; 果然如此!
push ecx
push offset Format ; "%p"
call ds:__imp__printf
add esp, 8
嗯,如果比2的次方多1會採用加法,那麼比2的次方少1是否會左位移後在加一個減法操作呢?我們共同看看這個Release版彙編代碼:
shl ecx, 4
sub ecx, eax
看來我們想的沒錯,除此之外,我們就要思考一下lea指令在乘法中的優化及應用了。
1.8.2、乘法優化之lea
衆所周知,lea是Intel工程師比較得意的一條指令,它的作用是傳遞操作數地址,並具有指令週期短、與邏輯指令流水線無關等特點,下面我們就一起欣賞一下微軟的編譯器是怎樣使用它來優化乘法的。
爲了節約版面,這裏我直接給出一個較全面的例子,可以幫助各位讀者快速瞭解乘法lea的優化方案,先看源代碼:
int _tmain(int argc, _TCHAR* argv[])
{
int a = 1, b, c, d, e, f, g;
b = argc+a*4+6;
c = argc+a*3+6;
d = argc*2;
e = argc*3;
f = argc*4;
g = argc*11;
printf("%d %d %d %d %d %d",b,c,d,e,f,g);
return 0;
}
我們先看DeBug版:
.text:00412FF0 push ebp
.text:00412FF1 mov ebp, esp
.text:00412FF3 sub esp, 114h
......
.text:0041300E mov [ebp+a], 1 ; 給局部變量a賦值
.text:00413015 mov eax, [ebp+a] ;
.text:00413018 mov ecx, [ebp+argc] ;
.text:0041301B lea edx, [ecx+eax*4+6] ; edx = argc+a*4+6
.text:0041301F mov [ebp+b], edx
.text:00413022 mov eax, [ebp+a]
.text:00413025 imul eax, 3 ; 先做了a*3
.text:00413028 mov ecx, [ebp+argc] ;
.text:0041302B lea edx, [ecx+eax+6] ; edx = argc+eax+6 (eax=a*3)
.text:0041302F mov [ebp+c], edx
.text:00413032 mov eax, [ebp+argc] ;
.text:00413035 shl eax, 1 ; eax = eax*2 (用到了位移優化)
.text:00413037 mov [ebp+d], eax
.text:0041303A mov eax, [ebp+argc] ;
.text:0041303D imul eax, 3 ; eax = eax*3 (直接使用了乘法指令)
.text:00413040 mov [ebp+e], eax
.text:00413043 mov eax, [ebp+argc] ;
.text:00413046 shl eax, 2 ; eax = eax*4 (用到了位移優化)
.text:00413049 mov [ebp+f], eax
.text:0041304C mov eax, [ebp+argc] ;
.text:0041304F imul eax, 0Bh ; eax = eax*11 (直接使用了乘法指令)
.text:00413052 mov [ebp+g], eax
.text:00413055 mov esi, esp
.text:00413057 mov eax, [ebp+g]
.text:0041305A push eax
.text:0041305B mov ecx, [ebp+f]
.text:0041305E push ecx
.text:0041305F mov edx, [ebp+e]
.text:00413062 push edx
.text:00413063 mov eax, [ebp+d]
.text:00413066 push eax
.text:00413067 mov ecx, [ebp+c]
.text:0041306A push ecx
.text:0041306B mov edx, [ebp+b]
.text:0041306E push edx
.text:0041306F push offset Format ; "%d %"
.text:00413074 call ds:__imp__printf
.text:0041307A add esp, 1Ch
......
.text:00413084 xor eax, eax
.text:00413086 pop edi
.text:00413087 pop esi
.text:00413088 pop ebx
.text:00413089 add esp, 114h
......
.text:00413096 mov esp, ebp
.text:00413098 pop ebp
.text:00413099 retn
通過上面的例子我們可以看到即便是DeBug版,編譯器仍然應用了一些優化方案,那麼Release版編譯器究竟會將上面的代碼變成什麼樣子的呢,請過目:
.text:00401000 mov eax, [esp+argc]
.text:00401004 mov ecx, eax
.text:00401006 imul ecx, 0Bh ; ecx = ecx*11 (直接使用了乘法指令)【標註1】
.text:00401009 push ecx
.text:0040100A lea edx, ds:0[eax*4] ; edx = argc*4
.text:00401011 push edx
.text:00401012 lea ecx, [eax+eax*2] ; ecx = argc+argc*2 (這是一個lea優化,原代碼爲e=argc*3)
.text:00401015 push ecx
.text:00401016 lea edx, [eax+eax] ; edx = argc+argc (這是一個lea優化,原代碼爲d=argc*2)
.text:00401019 push edx
.text:0040101A lea ecx, [eax+9] ; ecx = argc+9 (這是一個lea優化,原代碼爲c=argc+a*3+6,且a*3+6被直接預先計算成了9)
.text:0040101D push ecx
.text:0040101E add eax, 0Ah ; argc = argc + 10 (這是一個很特別的優化,源代碼爲b=argc+a*4+6)【標註2】
.text:00401021 push eax
.text:00401022 push offset Format ; "%d %d %d %d %d %d"
.text:00401027 call ds:__imp__printf
.text:0040102D add esp, 1Ch
.text:00401030 xor eax, eax
.text:00401032 retn
我想看到這裏之後部分讀者肯定已經被編譯器的強大所折服了吧?
我們在本小節前面提到過“lea具有指令週期短、與邏輯指令流水線無關等特點”,但是爲什麼編譯器在優化時會交替使用ecx與edx兩個寄存器呢?難道一個寄存器不能解決問題嗎?答案是肯定的,原理很簡單,我們的push指令可是與邏輯指令流水線有關的。
另外在“標註1”的地方我們發現編譯器在這裏直接使用了乘法指令而並沒有將其優化成例如“lea ecx, [eax+eax*5]”,這是因爲lea後面的比例因子必須爲2的倍數,因此我們想象的這條指令是無法被執行的。但是也有個別版本的編譯器會將其拆分爲兩條lea指令,但這種情況並不常見,因此無須深究。
而“標註2”所示的這個優化恐怕會難倒一大批人,爲什麼同樣的乘法帶加法運算,一個用的是lea,而另一個卻是用的add呢?其實這個問題可簡可繁,往簡單了說,上面那條指令之所以沒有直接用add是因爲會改變寄存器eax的值,對後面的計算造成不便。而往難了說,那我們就要討論爲什麼add要比lea的優先級高,我個人認爲這還是出於指令週期上的考慮,雖然lea在邏輯處理器中只佔用1個指令週期,並且可以再mmx協處理器中成對執行,但是其相對負荷產生還是要遠高於像add這種“原生態”指令的。
代碼逆向(七)——乘法的識別與優化原理
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.