代碼逆向(七)——乘法的識別與優化原理

 那麼如果讓我們來做乘法的優化,我們會怎麼做呢?很顯然位移是必須要被利用的,但是除此之外微軟的編譯器還利用了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這種“原生態”指令的。

發佈了41 篇原創文章 · 獲贊 4 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章