发新话题
打印

[转载]32位代码优化常识

[转载]32位代码优化常识

本文出在 Hume的ASM专页「 http://humeasm.yeah.net/


     原作者:  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 [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 eax, [ebp + variable]        6 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...

TOP

发新话题