生活的天平本不平衡,只有通过努力改变其偏向。

Win32汇编代码优化

2008-04-23

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

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

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

代码优化的含义:

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

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

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

ASM代码
  1. 1)      mov eax, 00000000h                    5 bytes

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

ASM代码
  1. 2)      sub eaxeax 2 bytes
    3)      xor eaxeax 2 bytes

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

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

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

ASM代码
  1. 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,试试下面的写法:

ASM代码
  1. 2)      or eaxeax 2 bytes
    je _label_                            2/6 (short/near)3)      test eaxeax 2 bytes
    je _label_                            2/6 (short/near)

呵呵,只有4/8 bytes,看看我们可节省多少字节啊3/4字节…那么接下来的问题是你喜欢OR还是TEST呢,就我个人 而言,比较喜欢TEST,因为test不改变任何寄存器,并不向任何寄存器写入内容,这通常能在pentium机上取得更快的执行速度.

别高兴的太早,因为还有更值得我们高兴的事情,假如你要判断的的是eax寄存器,那么看看下面的,是不是更有启发?

ASM代码
  1. 4)      xchg eaxecxbyte
    jecxz _label_                        2 bytes

在短跳转的情况下我们比2)和3)又节省了1字节.oh….___…

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

ASM代码
  1. 1)      cmp eax, 0ffffffffh                  5 bytes
    je _label_                            2/6 bytes

hey,不要这样,写代码的时候想一想,于是有了下面的写法:

ASM代码
  1. 2)      inc eaxbyte
    je _label_                            2/6 bytes
    dec eaxbyte

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

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

ASM代码
  1. 1)      mov eax, 0ffffffffh                  5 bytes

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

ASM代码
  1. 2)      xor eaxeaxsub eaxeax 2 bytes
    dec eaxbyte

节省一个字!还有写法:

ASM代码
  1. 3)      stcbyte
    sbb eaxeax 2 bytes

这有时还可以优化掉1 byte:

ASM代码
  1. jnc _label_
    sbb eaxeax 2 bytes only!
    _label_: …

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

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

ASM代码:
  1. 1)      xor eaxeax 2 bytes
    mov axword ptr [esi+xx]            4 bytes

????—>不会吧,这可能是最多初学者的写法了,我当然原来也是,看了benny的文章之后我决定改写

为:

ASM代码:
  1. 2)      movzx eaxword ptr [esi+xx]          4 bytes

收获2 bytes!

下面的

ASM代码:
  1. 3)      xor eaxeax 2 bytes
    mov albyte ptr [esi+xx]            3 bytes

就相应改为:

ASM代码:
  1. 4)      movzx eaxbyte ptr [esi+xx]          4 bytes

我们应当尽可能利用movzx

ASM代码:
  1. 5)      xor eaxeax 2 bytes
    mov axbx 3 bytes

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

ASM代码:
  1. 6)      movzx eaxbx 3 bytes

6.关于push,

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

ASM代码:
  1. 1)      mov eax, 50h                          5 bytes

这样就小了1 word

ASM代码:
  1. 2)      push 50h                              2 bytes
    pop eaxbyte

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

ASM代码:
  1. 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字节,显然不能满意,优化一下

ASM代码:
  1. 4)      xor eaxeax 2 bytes
    push eaxbyte
    push eaxbyte
    push eaxbyte
    push eaxbyte
    push eaxbyte
    push eaxbyte
    push eaxbyte

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

ASM代码:
  1. 5)      push 7                                2 bytes
    pop ecxbyte
    _label_:  push 0                                2 bytes
    loop _label_                          2 bytes

可以节省7字节….

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

ASM代码:
  1. 6)      push eaxbyte
    mov eax, [ebp + xxxx]                  6 bytes
    mov [ebp + xxxx], eax 6 bytes
    pop eaxbyte

试试push,pop

ASM代码:
  1. 7)      push dword ptr [ebp + xxxx]            6 bytes
    pop dword ptr [ebp + xxxx]            6 bytes

7.乘法

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

ASM代码:
  1. 1)      mov ecx, 28h                          5 bytes
    mul ecx 2 bytes

好一点的写法如下:

ASM代码:
  1. 2)      push 28h                              2 bytes
    pop ecxbyte
    mul ecx 2 bytes

哇这个更好::

ASM代码:
  1. 3)      imul eaxeax, 28h                    3 bytes

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

8.字符串操作
你如何从内存取得一个字节呢?
速度快的方案:

ASM代码:
  1. 1)      mov al/ax/eax, [esi]                  2/3/2 bytes
    inc esibyte

代码小的方案:

ASM代码:
  1. 2)      lodsb/w/d                            1 byte

我比较喜欢lod因为他小,虽然速度慢了点.

如何到达字符串尾呢?

ASM代码:
  1. JQwerty’s method: 
  2. 9)      lea esi, [ebp + asciiz]              6 bytes
    s_check: lodsbbyte
    test alal 2 bytes
    jne s_check                          2 bytesSuper’s method:

    10)    lea edi, [ebp + asciiz]              6 bytes
    xor alal 2 bytes
    s_check: scasbbyte
    jne s_check                          2 byte

选择哪一个?Super的在386以下的更快,JQwerty的在486以及pentium上更快,体积一样,选择由你.

9.复杂一点的…

假设你有一个DWORD表,ebx指向表的开始,ecx是指针,你想给每个doword加1,看看如何作:

ASM代码:
  1. 1)      pushadbyte
    imul ecxecx, 4                      3 bytes
    add ebxecx 2 bytes
    inc dword ptr [ebx]                  2 bytes
    popadbyte

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

ASM代码:
  1. 2)      inc dword ptr [ebx+4*ecx]            3 bytes

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

ASM代码:
  1. 3)      pushadbyte
    imul ecxecx, 4                      3 bytes
    add ebxecx 2 bytes
    add ebx, 1000h                        6 bytes
    inc dwor ptr [ebx]                    2 bytes
    popadbyte

优化为:

ASM代码:
  1. 4)      inc dword ptr [ebx+4*ecx+1000h]      7 bytes

节省了8字节!

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

ASM代码:
  1. lea eax, [12345678h]

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

假设 EBP = 1

ASM代码:
  1. lea eax, [ebp + 12345678h]

结果是123456789h….呵呵比较一下:

ASM代码:
  1. lea eax, [ebp + 12345678h]            6 bytes
    ==========================
    mov eax, 12345678h                    5 bytes
    add eaxebp 2 bytes

5) 看看:

ASM代码:
  1. mov eax, 12345678h                    5 bytes
    add eaxebp 2 bytes
    imul ecx, 4                          3 bytes
    add eaxecx 2 bytes

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

ASM代码:
  1. lea eax, [ebp+ecx*4+12345678h]        7 bytes

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

OPCODE [BASE + INDEX*SCALE + DISPLACEMENT]
10.下面是关于病毒重定位优化的,惧毒人士请绕行…

下面的代码你不应该陌生

ASM代码:
  1. 1)      call gdelta
    gdelta: pop ebp
    sub ebpoffset gdelta

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

ASM代码:
  1. lea eax, [ebp + variable]

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

ASM代码:
  1. 3)      lea eax, [ebp + 401000h]              6 bytes

假如是下面这样

ASM代码:
  1. 4)      lea eax, [ebp + 10h]                  3 bytes

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

ASM代码:
  1. 5)      call gdelta
    gdelta: pop ebp

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

ASM代码:
  1. 6)      lea eax, [ebp + variable - gdelta]    3 bytes

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

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

ASM代码:
  1. 1)      xor edxedx 2 bytes, but faster2)      cdqbyte, but slower

我一直使用cdq,为什么不呢?体积更小…
下面这种情况一般不要使用espebp,使用其他寄存器.
———————————————————–

ASM代码:
  1. 1)      mov eax, [ebp]                        3 bytes
    2)      mov eax, [esp]                        3 bytes
    3)      mov eax, [ebx]                        2 bytes

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

ASM代码:
  1. mov eax, 12345678h                    5 bytesbswap eax 2 bytes
  2. eax = 78563412h now

Wanna save some bytes replacin’ CALL ?
—————————————

ASM代码:
  1. 1)      call _label_                          5 bytes
    retbyte2)      jmp _label_                          2/5 (SHORT/NEAR)

如果仅仅是优化,并且不需要传递参数,请尽量用jmp代替call

比较 reg/mem 时如何节省时间:
——————————————

ASM代码:
  1. 1)      cmp reg, [mem]                        slower2)      cmp [mem], reg                        1 cycle faster

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

ASM代码:
  1. 1)      mov eax, 1000h
    mov ecx, 4                            5 bytes
    xor edxedx 2 bytes
    div ecx 2 bytes2)      shr eax, 4                            3 bytes

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

    4)      shl eax, 4                            3 bytes

loop指令
————————

ASM代码:
  1. 1)      dec ecxbyte
    jne _label_                          2/6 bytes (SHORT/NEAR)2)      loop _label_                          2 bytes

再看:

ASM代码:
  1. 3)      je $+5                                2 bytes
    dec ecxbyte
    jne _label_                          2 bytes4)      loopXX _label_ (XX = E, NE, Z or NZ)  2 bytes

loop体积小,但486以上的cpu上执行速度会慢一点…
比较:
———————————————————

ASM代码:
  1. 1)      push eaxbyte
    push ebxbyte
    pop eaxbyte
    pop ebxbyte
    2)      xchg eaxebxbyte3)      xchg ecxedx 2 bytes

如果仅仅是想移动数值,用mov,在pentium上会有较好的执行速度:

ASM代码:
  1. 4)      mov ecxedx 2 bytes

比较:
——————————————–

1) 未优化:

ASM代码:
  1. lbl1:  mov al, 5                            2 bytes
    stosbbyte
    mov eax, [ebx]                        2 bytes
    stosbbyte
    retbyte
    lbl2:  mov al, 6                            2 bytes
    stosbbyte
    mov eax, [ebx]                        2 bytes
    stosbbyte
    retbyte
    ———
    14 bytes

2) 优化了:

ASM代码:
  1. lbl1:  mov al, 5                            2 bytes
    lbl:   stosbbyte
    mov eax, [ebx]                        2 bytes
    stosbbyte
    retbyte
    lbl2:  mov al, 6                            2 bytes
    jmp lbl                              2 bytes
    ———
    11 bytes

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

ASM代码:

  1. mov eax, [ebp + variable]            6 bytes
    mov [ebp + variable], eax 6 bytes


    variable dd 12345678h                    4 bytes

2) 优化为:

ASM代码:
  1. mov eax, 12345678h                    5 bytes
    variable = dword ptr $ - 4


    mov [ebp + variable], eax 6 bytes

呵呵,好久没看到这么有趣的代码了,前提是编译的时候支持代码段的写入属性要被设置.

最后介绍未公开指令SALC,现在的调试器都支持…什么含义呢:就是CF位置1的话就将al置为0xff
——————————————————————

ASM代码:

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 ;)

作者:lonkil | 分类目录:编程开发 | 标签:

发表评论

电子邮件地址不会被公开。 必填项已用 * 标注

*

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>