彙編代碼優化

 32位代碼優化常識

            原作者:  Benny/29A
            翻譯改寫:hume/冷雨飄心

    [注意:這不是鸚鵡學舌的翻譯,我儘量以我的理解傳達原文的本意]

    關於代碼優化的文章實在太多了,遺憾的是大部分我都沒有看,儘管他們就擺在我的牀邊(每當我要看的時候就忍不住打哈欠...嘿嘿).這篇文章較短所以翻了一下.

    代碼優化的含義:

    代碼優化的目標當然是體積小和速度快,但是在通常的情況下二者就象魚和熊掌一樣不能得兼,我們通常尋找的是這二者的折中,究竟應該偏向何方,那就得具體看我們的實際需要.

    但有些常識是我們應該牢記的,下面就結合我們最常遇到的具體情況來漫談一下:

    1.寄存器清0
            我絕對不想再看到下面的寫法:
            1)      mov eax, 00000000h                    ;5 bytes
          
            看起來上面的寫法很符合邏輯,但你應當意識到還有更加優化的寫法:
            2)      sub eax, eax                          ;2 bytes

            3)      xor eax, eax                          ;2 bytes
            看看後面的字節數你就應該理解爲什麼要這麼作了,除此之外,在速度上也沒有損失,他們一樣快,但你喜歡xor還是sub呢?我是比較喜歡xor,原因很簡單,因爲我數學不好....
            不過Microsoft比較喜歡sub....我們知道windows運行的慢....(呵呵,當然是玩笑這並不是真正原因X-D!)

    2.測試寄存器是否爲0
            我也不希望看到下面的代碼:
            1)      cmp eax, 00000000h                    ;5 bytes
                    je _label_                            ;2/6 bytes (short/near)

            [* 注意很多指令針對eax作了優化,你要儘可能多地實用eax,比如CMP EAX, 12345678h (5 bytes)
            如果你使用其他寄存器,就是6bytes *]
          
            讓我們看看,簡單的比較指令居然要用7/11 bytes,No No No,試試下面的寫法:
            2)      or eax, eax                          ;2 bytes
                    je _label_                            ;2/6 (short/near)

            3)      test eax, eax                        ;2 bytes
                    je _label_                            ;2/6 (short/near)

            呵呵,只有4/8 bytes,看看我們可節省多少字節啊3/4字節...那麼接下來的問題是你喜歡OR還是TEST呢,就我個人而言,比較喜歡TEST,因爲test不改變任何寄存器,並不向任何寄存器寫入內容,這通常能在pentium機上取得更快的執行速度.
          
            別高興的太早,因爲還有更值得我們高興的事情,假如你要判斷的的是eax寄存器,那麼看看下面的,是不是更有啓發?
            4)      xchg eax, ecx                        ;1 byte
                    jecxz _label_                        ;2 bytes
            在短跳轉的情況下我們比2)和3)又節省了1字節.oh....___...

    3.測試寄存器是否爲0FFFFFFFFh
            一些API返回-1,因此如何測試這個值呢?看你可能又要這樣:
            1)      cmp eax, 0ffffffffh                  ;5 bytes
                    je _label_                            ;2/6 bytes
            hey,不要這樣,寫代碼的時候想一想,於是有了下面的寫法:
            2)      inc eax                              ;1 byte
                    je _label_                            ;2/6 bytes
                    dec eax                              ;1 byte

            可以節省3 bytes並且執行速度會更快.

    4.置寄存器爲0FFFFFFFFh
            看看假如你是Api的作者,如何返回-1?這樣嗎?
            1)      mov eax, 0ffffffffh                  ;5 bytes

            看了上面的不會再這麼XXX了吧?看看下面的:
            2)      xor eax, eax / sub eax, eax          ;2 bytes
                    dec eax                              ;1 byte
            節省一個字!還有寫法:
            3)      stc                                  ;1 byte
                    sbb eax, eax                          ;2 bytes
            這有時還可以優化掉1 byte:
                    jnc _label_
                    sbb eax, eax                          ;2 bytes only!
          _label_: ...

          我們爲什麼用asm呢?這就是原因.

    5.寄存器清0並移入低字數值
            1)      xor eax, eax                          ;2 bytes
                    mov ax, word ptr [esi+xx]            ;4 bytes
            ????--->不會吧,這可能是最多初學者的寫法了,我當然原來也是,看了benny的文章之後我決定改寫爲:
            2)      movzx eax, word ptr [esi+xx]          ;4 bytes
            收穫2 bytes!

            下面的
            3)      xor eax, eax                          ;2 bytes
                    mov al, byte ptr [esi+xx]            ;3 bytes

            就相應改爲:
            4)      movzx eax, byte ptr [esi+xx]          ;4 bytes

            我們應當儘可能利用movzx
            5)      xor eax, eax                          ;2 bytes
                    mov ax, bx                            ;3 bytes

            因爲執行速度不慢並通常能節省字節...
            6)      movzx eax, bx                        ;3 bytes

    6.關於push,下面是着重代碼體積的優化,因爲寄存器操作總要比內存操作要快.

            1)      mov eax, 50h                          ;5 bytes

            這樣就小了1 word

            2)      push 50h                              ;2 bytes
                    pop eax                              ;1 byte
          
            當操作數只有1字節時候,push只有2 bytes,否則就是5 bytes,記住!
            下一個問題,向堆棧中壓入7個0

            3)      push 0                                ;2 bytes
                    push 0                                ;2 bytes
                    push 0                                ;2 bytes
                    push 0                                ;2 bytes
                    push 0                                ;2 bytes
                    push 0                                ;2 bytes
                    push 0                                ;2 bytes

          佔用14字節,顯然不能滿意,優化一下
            4)      xor eax, eax                          ;2 bytes
                    push eax                              ;1 byte
                    push eax                              ;1 byte
                    push eax                              ;1 byte
                    push eax                              ;1 byte
                    push eax                              ;1 byte
                    push eax                              ;1 byte
                    push eax                              ;1 byte

            可以更緊湊,但會慢一點的形式如下:

            5)      push 7                                ;2 bytes
                    pop ecx                              ;1 byte
          _label_:  push 0                                ;2 bytes
                    loop _label_                          ;2 bytes

            可以節省7字節....

            有時候你可能會從將一個值從一個內存地址轉移到另外內存地址,並且要保存所有寄存器:

            6)      push eax                              ;1 byte
                    mov eax, [ebp + xxxx]                  ;6 bytes
                    mov [ebp + xxxx], eax                  ;6 bytes
                    pop eax                                ;1 byte

            試試push,pop

            7)      push dword ptr [ebp + xxxx]            ;6 bytes
                    pop dword ptr [ebp + xxxx]            ;6 bytes
    7.乘法
      
            當eax已經放入被乘數,要乘28h,如何來寫?
            1)      mov ecx, 28h                          ;5 bytes
                    mul ecx                              ;2 bytes

          好一點的寫法如下:

            2)      push 28h                              ;2 bytes
                    pop ecx                              ;1 byte
                    mul ecx                              ;2 bytes

            哇這個更好::

            3)      imul eax, eax, 28h                    ;3 bytes

            intel在新CPU中提供新的指令並不是擺設,需要你的使用.

    8.字符串操作


            你如何從內存取得一個字節呢?
            速度快的方案:
            1)      mov al/ax/eax, [esi]                  ;2/3/2 bytes
                    inc esi                              ;1 byte

            代碼小的方案:
            2)      lodsb/w/d                            ;1 byte

            我比較喜歡lod因爲他小,雖然速度慢了點.
          
            如何到達字符串尾呢?
          JQwerty's method:

            9)      lea esi, [ebp + asciiz]              ;6 bytes
          s_check: lodsb                                ;1 byte
                    test al, al                          ;2 bytes
                    jne s_check                          ;2 bytes

            Super's method:

            10)    lea edi, [ebp + asciiz]              ;6 bytes
                    xor al, al                            ;2 bytes
          s_check: scasb                                ;1 byte
                    jne s_check                          ;2 byte

          選擇哪一個?Super的在386以下的更快,JQwerty的在486以及pentium上更快,體積一樣,選擇由你.

    9.複雜一點的...

            假設你有一個DWORD表,ebx指向表的開始,ecx是指針,你想給每個doword加1,看看如何作:
            1)      pushad                                ;1 byte
                    imul ecx, ecx, 4                      ;3 bytes
                    add ebx, ecx                          ;2 bytes
                    inc dword ptr [ebx]                  ;2 bytes
                    popad                                ;1 byte

            可以優化一點,但是好像沒人用:

            2)      inc dword ptr [ebx+4*ecx]            ;3 bytes

            一條指令就節省6字節,而且速度更快,更易讀,但好像沒有什麼人用?...why?
            還可以有立即數:
            3)      pushad                                ;1 byte
                    imul ecx, ecx, 4                      ;3 bytes
                    add ebx, ecx                          ;2 bytes
                    add ebx, 1000h                        ;6 bytes
                    inc dwor ptr [ebx]                    ;2 bytes
                    popad                                ;1 byte

            優化爲:
            4)      inc dword ptr [ebx+4*ecx+1000h]      ;7 bytes

            節省了8字節!
          

            看一下lea指令能爲我們乾點什麼呢?
                    lea eax, [12345678h]

            eax的最後結果是什麼呢?正確答案是12345678h.

            假設 EBP = 1
                    lea eax, [ebp + 12345678h]
            結果是123456789h....呵呵比較一下:
                    lea eax, [ebp + 12345678h]            ;6 bytes
                    ==========================
                    mov eax, 12345678h                    ;5 bytes
                    add eax, ebp                          ;2 bytes

            5) 看看:
                    mov eax, 12345678h                    ;5 bytes
                    add eax, ebp                          ;2 bytes
                    imul ecx, 4                          ;3 bytes
                    add eax, ecx                          ;2 bytes

            6) 用lea來進行一些計算我門將從體積上得到好處:

                    lea eax, [ebp+ecx*4+12345678h]        ;7 bytes

            速度上一條lea指令更快!不影響標誌位...記住下面的格式,在許多地方善用他們你可以節省時間和空間.
                    OPCODE <SIZE PTR> [BASE + INDEX*SCALE + DISPLACEMENT]

    10.下面是關於病毒重定位優化的,懼毒人士請繞行...
          
            下面的代碼你不應該陌生
            1)      call gdelta
            gdelta: pop ebp
                    sub ebp, offset gdelta

            在以後的代碼中我們這樣使用delta來避免重定位問題
                    lea eax, [ebp + variable]

            這樣的指令在應用內存數據的時候是不可避免的,如果能優化一下,我門將會得到數倍收益,打開你的sice或者trw或者ollydbg等調試器,看看:
            3)      lea eax, [ebp + 401000h]              ;6 bytes
          
            假如是下面這樣    
            4)      lea eax, [ebp + 10h]                  ;3 bytes

            也就是說如果ebp後面變量是1字節的話,總的指令就只有3字節      
            修改一下最初的格式變爲:

            5)      call gdelta
            gdelta: pop ebp

            在某些情況下我們的指令就只有3字節了,可以節省3字節,嘿嘿,讓我們看看:
            6)      lea eax, [ebp + variable - gdelta]    ;3 bytes

            和上面的是等效的,但是我們可以節省3字節,看看CIH...

    11.其他技巧:
          如果EAX小於80000000h,edx清0:
            --------------------------------------------------

            1)      xor edx, edx                          ;2 bytes, but faster

            2)      cdq                                  ;1 byte, but slower

            我一直使用cdq,爲什麼不呢?體積更小...


            下面這種情況一般不要使用esp和ebp,使用其他寄存器.
            -----------------------------------------------------------

            1)      mov eax, [ebp]                        ;3 bytes
            2)      mov eax, [esp]                        ;3 bytes

            3)      mov eax, [ebx]                        ;2 bytes


            交換寄存器中4個字節的順序?用bswap
            ---------------------------------------------------------
                    mov eax, 12345678h                    ;5 bytes

                    bswap eax                            ;2 bytes

                    ;eax = 78563412h now    

            Wanna save some bytes replacin' CALL ?
            ---------------------------------------

            1)      call _label_                          ;5 bytes
                    ret                                  ;1 byte

            2)      jmp _label_                          ;2/5 (SHORT/NEAR)

            如果僅僅是優化,並且不需要傳遞參數,請儘量用jmp代替call
          

            比較 reg/mem 時如何節省時間:
            ------------------------------------------

            1)      cmp reg, [mem]                        ;slower

            2)      cmp [mem], reg                        ;1 cycle faster


            乘2除2如何節省時間和空間?
            ------------------------------------------------------------
            1)      mov eax, 1000h
                    mov ecx, 4                            ;5 bytes
                    xor edx, edx                          ;2 bytes
                    div ecx                              ;2 bytes

            2)      shr eax, 4                            ;3 bytes

            3)      mov ecx, 4                            ;5 bytes
                    mul ecx                              ;2 bytes

            4)      shl eax, 4                            ;3 bytes
          

            loop指令
            ------------------------

            1)      dec ecx                              ;1 byte
                    jne _label_                          ;2/6 bytes (SHORT/NEAR)

            2)      loop _label_                          ;2 bytes

            再看:
            3)      je $+5                                ;2 bytes
                    dec ecx                              ;1 byte
                    jne _label_                          ;2 bytes

            4)      loopXX _label_ (XX = E, NE, Z or NZ)  ;2 bytes
            loop體積小,但486以上的cpu上執行速度會慢一點...


          比較:
            ---------------------------------------------------------
            1)      push eax                              ;1 byte
                    push ebx                              ;1 byte
                    pop eax                              ;1 byte
                    pop ebx                              ;1 byte
        
        
            2)      xchg eax, ebx                        ;1 byte

            3)      xchg ecx, edx                        ;2 bytes
            如果僅僅是想移動數值,用mov,在pentium上會有較好的執行速度:
            4)      mov ecx, edx                          ;2 bytes


            比較:
            --------------------------------------------

            1) 未優化:
            lbl1:  mov al, 5                            ;2 bytes
                    stosb                                ;1 byte
                    mov eax, [ebx]                        ;2 bytes
                    stosb                                ;1 byte
                    ret                                  ;1 byte
            lbl2:  mov al, 6                            ;2 bytes
                    stosb                                ;1 byte
                    mov eax, [ebx]                        ;2 bytes
                    stosb                                ;1 byte
                    ret                                  ;1 byte
                                                          ---------
                                                          ;14 bytes
            2) 優化了:
            lbl1:  mov al, 5                            ;2 bytes
            lbl:    stosb                                ;1 byte
                    mov eax, [ebx]                        ;2 bytes
                    stosb                                ;1 byte
                    ret                                  ;1 byte
            lbl2:  mov al, 6                            ;2 bytes
                    jmp lbl                              ;2 bytes
                                                          ---------
                                                          ;11 bytes

          讀取常數變量,試試在指令中直接定義:
          -----------------------------                  

    ...
                    mov [ebp + variable], eax            ;6 bytes
                    ...
                    ...
          variable dd      12345678h                    ;4 bytes

            2) 優化爲:

                    mov eax, 12345678h                    ;5 bytes
          variable = dword ptr $ - 4
                    ...
                    ...
                    mov [ebp + variable], eax            ;6 bytes

            呵呵,好久沒看到這麼有趣的代碼了,前提是編譯的時候支持代碼段的寫入屬性要被設置.
          
            最後介紹未公開指令SALC,現在的調試器都支持...什麼含義呢:就是CF位置1的話就將al置爲0xff
            ------------------------------------------------------------------

            1)      jc _lbl1                              ;2 bytes
                    mov al, 0                            ;2 bytes
                    jmp _end                              ;2 bytes
              _lbl: mov al, 0ffh                          ;2 bytes
              _end: ...

            2)      SALC  db    0d6h                    ;1 byte ;)
    ------------------------------------------------------------------>over...

                                                      2002---hume
    轉自:http:// www.PEdiy.com
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章