优化你程序的大小 - 微观优化

3 篇文章 0 订阅
1 篇文章 0 订阅

原文:http://blogs.msdn.com/xiangfan/archive/2008/09/27/minimize-the-size-of-your-program-low-level.aspx

宏观优化: http://blog.csdn.net/nineforever/archive/2008/10/17/3092791.aspx

注:本文的主要目的在于最小化可执行文件的大小。里面提到的技巧并不适用于实际的应用。生成的PE文件是否有效依赖于特定的架构、操作系统和工具

你知道x86平台上最小的PE文件有多大吗?

答案在这里:http://www.phreedom.org/solar/code/tinype/

  • 最小的PE文件:97字节
  • Windows 2000上最小的PE文件:133字节
  • 能够从WebDAV下载文件并执行的最小的PE文件:133字节

PS:你可以使用IDA ProWinDBG或者OllyDbg来打开并调试这些TinyPE :P

编写TinyPE的技巧很简单:去除“冗余”的数据,并利用数据结构的相互重叠进行合并。这样PE文件格式本身占用的字节数就能被尽可能的减少(去除“冗余”的数据会导致无效的PE文件头,你的程序可能会无法正常运行)。这里,我们主要讨论如何优化实际代码的大小。

为了研究编译器生成的二进制代码,你可以使用编译器提供的/FAsc选项。

首先我们可以利用TinyPE中用到的技巧优化PE文件头。因为有很多OS的加载器没有使用的字段(注:对于实际的应用,不应当假设PE文件头中的字段没有被使用),为了优化程序的大小,我们可以将程序中用到的数据放到这些字段中。除了节省空间之外,这样做的另外一个好处是,这些数据的地址差别都在0x80以内,便于我们之后的优化。

好,让我们开始吧!

1、算法

a、局部变量初始化

通常,初始化需要很长的指令。

    unsigned long keyT[4]={0xCBDCEDFE,0x8798A9BA,0x43546576,0x00102132};

赋值的代码会被转换为类似的汇编:

    mov    DWORD PTR [ebp+offset8], imm32

这需要7个字节。那么初始化整个数组就需要7*4 = 28字节。

为此,我们可以将初始化改为循环。

    for (size_t i=0;i<16;++i) ((char *)keyT)[i]=0xFE-i*0x11;
    ((char *)keyT)[15]++;

上述代码可以这样实现:

    lea    edx, [ebp+keyT-1]
    xor    ecx, ecx
    mov    cl, 16
    mov    al, 0FEh
loc_4:
    inc    edx
    mov    [edx], al
    sub    al, 11h
    loop    loc_4
    inc    byte [edx]

28 -> 18字节!

b、memcpy

默认编译器会调用msvcrt.dll中的API。但是这样需要很多字节。另一种方法是直接使用汇编代码"rep movs"
如果我们仔细分析一下程序所使用的算法,我们可以发现它实际上将keyT数组向左循环移动了t个字节。

    memcpy(Buf1,(char *)keyT+t,0x10-t);
    memcpy((char *)Buf1+0x10-t,keyT,t);
    memcpy(keyT,Buf1,16);

这意味着大部分传给memcpy函数的参数都可以重用,这样就免去了许多存取的指令。

    ;ebx == t, edi == end of keyT
    xor    ecx, ecx
    mov    cl, 10h
    sub    cl, bl
    mov    esi, edi
    sub    esi, ecx
    lea    edi, [ebp+Buf1]
    push    edi
    rep    movsb

    mov    cl, bl
    lea    esi, [ebp+keyT]
    rep    movsb

    pop    esi
    mov    cl, 10h
    rep    movsb

多亏了参数的重用,后面两次memcpy只使用了12 个字节,而整段代码也只有35个字节!相比较而言,优化之前的代码需要54字节,这还不包括为了调用memcpy API需要在PE文件中添加的超过10个字节。

2、8-bit vs 32-bit

在32位环境下,数据和地址默认都是32位的。而32位的立即数和32位的长跳转占据了我们代码大小的很大一部分。
我们可以尽量将这些数据变小,从而使得它们能够使用8位进行表达。
例如,当我们调用下面的函数时:

    printf("%08X",keyT[k]);

编译器会生成如下的指令:

    push    hexstr+ImageBase    ; "%08X"
    call    [printf+ImageBase]

因为前面我们已经重新安排过数据,所以hexstr和printf的地址之差不超过80字节,这样我们可以改用:

    mov    eax, hexstr+ImageBase
    push    eax
    add    al, printf-hexstr
    call    [eax]

printf使用cdecl调用约定,由调用者负责压栈出栈。当我们将参数出栈的时候,我们可以将得到的"eax"重用给之后的"printf("/n");"。

    add    al, newline-hexstr
    push    eax
    add    al, printf-newline
    call    [eax]

22 -> 17字节!

另外,我们可以用两次短跳转代替长跳转(5 -> 4字节)

3、杂项

  • 避免使用16位的指令
  • 使用"loop"指令进行循环
  • 使用enter/leave指令初始化堆栈
  • 使用"lods/stos"进行内存读写
  • 使用"pusha/popa"替代多次push/pop
  • 有些指令用于"eax"寄存器时长度会略短一些
  • 有时8位的指令比32位的版本要短
  • 我们的程序并不需要返回特定的值,OS也不需要我们保存寄存器,所以最后的返回语句"xor eax, eax"和用于恢复寄存器的"push/pop"指令都可以省略(对于实际的应用,这可能会导致问题)
  • 寄存器调度非常重要。如果调度合理,可以避免大量的存取操作
  • 充分利用al,ah和类似的寄存器,因为x86平台上可用的寄存器实在太少

如果要获得更完整的优化代码大小的技巧,可以阅读Agner Fog撰写的手册

PS:不要忘记,PE文件头中,"Debug Directory"的size字段必须是0,否则系统载入你的PE文件时会crash。因为你的代码会和这个字段公用数据,所以你必须在你的代码中插入类似的指令:

    jmp    short skip_debug
    times 4 db 0 ;确保debug directory的size等于0
skip_debug:

这里是完整的汇编源代码:source (你可以用"nasmw -f bin"编译)和优化前后的结果比较:

 PE文件头代码数据总大小
优化前480272144896
优化后1361807323

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值