hkbyest 发表于 2007-9-20 17:17:49

32位代码优化常识

原作者: Benny/29A
    翻译改写:hume/冷雨飘心



[注意:这不是鹦鹉学舌的翻译,我尽量以我的理解传达原文的本意]

关于代码优化的文章实在太多了,遗憾的是大部分我都没有看,尽管他们就摆在我的床边(每当我要看的时候就忍不住打哈欠...嘿嘿).这篇文章较短所以翻了一下.

代码优化的含义:

代码优化的目标当然是体积小和速度快,但是在通常的情况下二者就象鱼和熊掌一样不能得兼,我们通常寻找的是这二者的折中,究竟应该偏向何方,那就得具体看我们的实际需要.

但有些常识是我们应该牢记的,下面就结合我们最常遇到的具体情况来漫谈一下:

1.寄存器清0
    我绝对不想再看到下面的写法:

Copy code
    1)   mov eax, 00000000h             5 bytes

    看起来上面的写法很符合逻辑,但你应当意识到还有更加优化的写法:

Copy code
    2)   sub eax, eax               2 bytes
    3)   xor eax, eax               2 bytes

    看看后面的字节数你就应该理解为什么要这么作了,除此之外,在速度上也没有损失,他们一样快,但你喜欢xor还是sub呢?我是比较喜欢xor,原因很简单,因为我数学不好....

    不过Microsoft比较喜欢sub....我们知道windows运行的慢....(呵呵,当然是玩笑这并不是真正原因X-D!)

2.测试寄存器是否为0
    我也不希望看到下面的代码:

Copy code
    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,试试下面的写法:

Copy code
    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寄存器,那么看看下面的,是不是更有启发?

Copy code
    4)   xchg eax, ecx               1 byte
          jecxz _label_               2 bytes
    在短跳转的情况下我们比2)和3)又节省了1字节.oh....___...

3.测试寄存器是否为0FFFFFFFFh
    一些API返回-1,因此如何测试这个值呢?看你可能又要这样:

Copy code
1)   cmp eax, 0ffffffffh             5 bytes
          je _label_                   2/6 bytes
    hey,不要这样,写代码的时候想一想,于是有了下面的写法:

Copy code
    2)   inc eax                     1 byte
          je _label_                   2/6 bytes
          dec eax                     1 byte

    可以节省3 bytes并且执行速度会更快.

4.置寄存器为0FFFFFFFFh
    看看假如你是Api的作者,如何返回-1?这样吗?

Copy code
    1)   mov eax, 0ffffffffh             5 bytes

    看了上面的不会再这么XXX了吧?看看下面的:

Copy code
    2)   xor eax, eax / sub eax, eax       2 bytes
          dec eax                     1 byte
    节省一个字!还有写法:

Copy code
    3)   stc                     1 byte
          sbb eax, eax               2 bytes
    这有时还可以优化掉1 byte:

Copy code
          jnc _label_
          sbb eax, eax               2 bytes only!
    _label_: ...

    我们为什么用asm呢?这就是原因.

5.寄存器清0并移入低字数值

Copy code
    1)   xor eax, eax               2 bytes
          mov ax, word ptr          4 bytes
    ????--->不会吧,这可能是最多初学者的写法了,我当然原来也是,看了benny的文章之后我决定改写

为:

Copy code
    2)   movzx eax, word ptr        4 bytes
    收获2 bytes!

    下面的

Copy code
    3)   xor eax, eax               2 bytes
          mov al, byte ptr          3 bytes

    就相应改为:

Copy code
    4)   movzx eax, byte ptr        4 bytes

    我们应当尽可能利用movzx

Copy code
    5)   xor eax, eax               2 bytes
          mov ax, bx                   3 bytes

    因为执行速度不慢并通常能节省字节...

Copy code
    6)   movzx eax, bx               3 bytes

6.关于push,下面是着重代码体积的优化,因为寄存器操作总要比内存操作要快.


Copy code
    1)   mov eax, 50h               5 bytes

    这样就小了1 word


Copy code
    2)   push 50h                     2 bytes
          pop eax                     1 byte
   
    当操作数只有1字节时候,push只有2 bytes,否则就是5 bytes,记住!
    下一个问题,向堆栈中压入7个0


Copy code
    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字节,显然不能满意,优化一下

Copy code
    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

    可以更紧凑,但会慢一点的形式如下:


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

    可以节省7字节....

    有时候你可能会从将一个值从一个内存地址转移到另外内存地址,并且要保存所有寄存器:


Copy code
    6)   push eax                     1 byte
          mov eax,              6 bytes
          mov , eax             6 bytes
          pop eax                     1 byte

    试试push,pop


Copy code
    7)   push dword ptr          6 bytes
          pop dword ptr          6 bytes
7.乘法

    当eax已经放入被乘数,要乘28h,如何来写?

Copy code
    1)   mov ecx, 28h               5 bytes
          mul ecx                     2 bytes

    好一点的写法如下:


Copy code
    2)   push 28h                     2 bytes
          pop ecx                     1 byte
          mul ecx                     2 bytes

    哇这个更好::


Copy code
3)   imul eax, eax, 28h             3 bytes

    intel在新CPU中提供新的指令并不是摆设,需要你的使用.

8.字符串操作


    你如何从内存取得一个字节呢?
    速度快的方案:

Copy code
    1)   mov al/ax/eax,              2/3/2 bytes
          inc esi                     1 byte

    代码小的方案:

Copy code
    2)   lodsb/w/d                   1 byte

    我比较喜欢lod因为他小,虽然速度慢了点.
   
    如何到达字符串尾呢?
    JQwerty's method:


Copy code
    9)   lea esi,          6 bytes
    s_check: lodsb                     1 byte
          test al, al               2 bytes
          jne s_check               2 bytes

    Super's method:

    10)   lea edi,          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,看看如何作:

Copy code
    1)   pushad                     1 byte
          imul ecx, ecx, 4               3 bytes
          add ebx, ecx               2 bytes
          inc dword ptr              2 bytes
          popad                     1 byte

    可以优化一点,但是好像没人用:


Copy code
      2)   inc dword ptr          3 bytes

    一条指令就节省6字节,而且速度更快,更易读,但好像没有什么人用?...why?
    还可以有立即数:

Copy code
      3)   pushad                     1 byte
          imul ecx, ecx, 4               3 bytes
          add ebx, ecx               2 bytes
          add ebx, 1000h               6 bytes
          inc dwor ptr              2 bytes
          popad                     1 byte

    优化为:

Copy code
      4)   inc dword ptr    7 bytes

    节省了8字节!
   

    看一下lea指令能为我们干点什么呢?
          lea eax,

    eax的最后结果是什么呢?正确答案是12345678h.

    假设 EBP = 1
          lea eax,
    结果是123456789h....呵呵比较一下:

Copy code
          lea eax,          6 bytes
          ==========================
          mov eax, 12345678h             5 bytes
          add eax, ebp               2 bytes

    5) 看看:

Copy code
          mov eax, 12345678h             5 bytes
          add eax, ebp               2 bytes
          imul ecx, 4               3 bytes
          add eax, ecx               2 bytes

    6) 用lea来进行一些计算我门将从体积上得到好处:


Copy code
      lea eax,    7 bytes

    速度上一条lea指令更快!不影响标志位...记住下面的格式,在许多地方善用他们你可以节省时间和空间.

OPCODE


10.下面是关于病毒重定位优化的,惧毒人士请绕行...
   
    下面的代码你不应该陌生

Copy code
    1)   call gdelta
    gdelta: pop ebp
          sub ebp, offset gdelta

    在以后的代码中我们这样使用delta来避免重定位问题
      
Copy code
lea eax,

    这样的指令在应用内存数据的时候是不可避免的,如果能优化一下,我门将会得到数倍收益,打开你的sice或者trw或者ollydbg等调试器,看看:


Copy code
    3)   lea eax,          6 bytes
   
    假如是下面这样   

Copy code
    4)   lea eax,              3 bytes

    也就是说如果ebp后面变量是1字节的话,总的指令就只有3字节   
    修改一下最初的格式变为:


Copy code
    5)   call gdelta
    gdelta: pop ebp

    在某些情况下我们的指令就只有3字节了,可以节省3字节,嘿嘿,让我们看看:

Copy code
    6)   lea eax,    3 bytes

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

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

Copy code

    1)   xor edx, edx               2 bytes, but faster

    2)   cdq                     1 byte, but slower

    我一直使用cdq,为什么不呢?体积更小...


    下面这种情况一般不要使用esp和ebp,使用其他寄存器.
    -----------------------------------------------------------


Copy code
    1)   mov eax,                3 bytes
    2)   mov eax,                3 bytes
    3)   mov eax,                2 bytes


    交换寄存器中4个字节的顺序?用bswap
    ---------------------------------------------------------

Copy code
          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 时如何节省时间:
    ------------------------------------------


Copy code
    1)   cmp reg,                slower

    2)   cmp , reg               1 cycle faster


    乘2除2如何节省时间和空间?
    ------------------------------------------------------------

Copy code
    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指令
    ------------------------


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

    2)   loop _label_               2 bytes

    再看:

Copy code
    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上执行速度会慢一点...


    比较:
    ---------------------------------------------------------

Copy code
    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上会有较好的执行速度:

Copy code
    4)   mov ecx, edx               2 bytes


    比较:
    --------------------------------------------

    1) 未优化:

Copy code
    lbl1: mov al, 5                   2 bytes
          stosb                     1 byte
          mov eax,                2 bytes
          stosb                     1 byte
          ret                     1 byte
    lbl2: mov al, 6                   2 bytes
          stosb                     1 byte
          mov eax,                2 bytes
          stosb                     1 byte
          ret                     1 byte
                                    ---------
                                    14 bytes
    2) 优化了:

Copy code
    lbl1: mov al, 5                   2 bytes
    lbl:   stosb                     1 byte
          mov eax,                2 bytes
          stosb                     1 byte
          ret                     1 byte
    lbl2: mov al, 6                   2 bytes
          jmp lbl                     2 bytes
                                    ---------
                                    11 bytes

    读取常数变量,试试在指令中直接定义:
    -----------------------------   

Copy code
          mov eax,          6 bytes
          ...

    mov , eax         6 bytes
          ...
          ...
    variable dd   12345678h             4 bytes

    2) 优化为:


Copy code
      mov eax, 12345678h             5 bytes
    variable = dword ptr $ - 4
          ...
          ...
          mov , eax         6 bytes

    呵呵,好久没看到这么有趣的代码了,前提是编译的时候支持代码段的写入属性要被设置.
   
    最后介绍未公开指令SALC,现在的调试器都支持...什么含义呢:就是CF位置1的话就将al置为0xff

Copy code
    ------------------------------------------------------------------

    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...
页: [1]
查看完整版本: 32位代码优化常识