美文网首页
产品瘦身

产品瘦身

作者: 池塘游泳的蜗牛 | 来源:发表于2018-11-21 20:59 被阅读0次

    背景前言

      最近遇到一个棘手的问题,编译生成的可执行文件过大而无法上电。这就好像一个人过于肥胖致其11路公共汽车罢工一个道理。导致该结果的原因也很明确,一个开源组件(对于该组件的使用个人持保留意见,因为我们引入了一个不太适合自己并且无法驾驭的洪水猛兽。暂且认为选择的那个人当时大脑短路了,谁又没有短路时候呢)。说到这不得不赞赏下老外。那些脾气差的鬼魅一怒之下往往会给你搞个新玩意出来,而我们只会百度与GOOGLE。久而久之的结果就是人家有GOOGLE、MS、INTEL,我们最后就只剩下了”厉害了我的国“,有时甚至到了让人感觉吹牛逼都可以改变世界的地步。
      言归正传,后面的内容我会尽量使用通俗的语言MAKE MY MIND CLEAR。如果没有那么请原谅,因为我语文确实不怎么好

    一、可执行文件的生成过程

    这个过程可能大家都知道,不过为了文章连贯性在此还得赘述下。
         [预处理] -> [编译] -> [汇编] -> [链接]
    可执行文件的生成大致经历了上述四个过程。不过对于预处理和汇编两个过程,我们真的无能为力。这样一来我们的突破口就只能在编译、链接两个过程。

    (1)编译

    一般的工程是由大量的.h与.cpp文件构成。头文件的作用在于前向声明,cpp为最基本的编译单元。为了方便物理隔离,我们将基础的逻辑处理分布在了瀚海的CPP中。人都是视觉动物,就好像大家都喜欢美的事物一样,不过太过投入一不小心就撞墙上了。太过零碎的CPP导致编译器需要重复的进行文件IO操作进而影响编译效率,故此我们又发明了"BIGCPP"。将众多的CPP打包成一个CPP文件。整个过程看起来有点“脱裤子放屁,多此一举”的感觉,不过好处确是显而易见。众多CPP最终会在编译器废寝忘食的工作下生成相应的可重定向文件。

    (2)链接

    链接器将生成的可重定向文件,链接成可执行文件。至此可执行文件生成了。一切看看起来那么简单,不过事实真的是那样么? 就好像一个人还不知道人家长啥样你就说人家美或丑一样。

    (3) ELF 文件格式

    Linux 环境下的可执行文件都遵从ELF文件格式。ELF文件一般包含下面三个表头:
    1,ELF 文件表头

    x86.png

    ELF文件展示文件的基本属性信息,例如文件类型,大小端,ABI版本,32位(64位)等基本属性信息。同时包含程序头表信息(用于程序加载时创建映像,在此我们不讨论),节字头表信息等其它信息。在链接库文件时可能会报”不合适的库文件“这时注意下ELF文件头表信息一般就可能知道原因了。
    2,节头表

    结头.png
    包含符号表,bss,data, got表地址等相关信息。从上图节头表可以看出符号表偏移地址为0x00001918,size为0x660占用空间最大,所以我们的突破口落在了符号表上。
    3,符号表 fuhao.png

    符号表展示了程序运行过程中,所使用的符号信息。下面我们具体分析下符号表相关信息。

    ├── build
    ├── build.sh
    ├── CMakeLists.txt
    ├── inc
    │   └── hello.h
    └── src
        ├── hello.cpp
        └── main.cpp
    
    
    /***hello.h***/
    #ifndef _TEST_HELLO_WORLD_PRINT_   
    #define _TEST_HELLO_WORLD_PRINT_
    
    extern "C" void print_hello();
    
    int rubbish_function();
    
    #endif
    
    /***hello.cpp***/
    #include<stdio.h>
    #include"hello.h"
    
    void  print_hello()
    {
            printf("hello world \n ");
    }
    
    int rubbish_function()
    {
            int  a = 1;
            int b = 2;
            int c = a + b;
            return c;
    }
    
    
    
    #include "hello.h"
    int main()
    {
          print_hello();
    };
    
    

    如上图显示
    49: 0000000000400542 34 FUNC GLOBAL DEFAULT 13 _Z16rubbish_functionv
    rubbish_function这个符号我们并没有使用,但是其仍然存在于我们最终的可执行文件中,反汇编结果如下。这个也就是我们所要讨论的优化点。

    huibian.png
    二、优化方式
    (1) 编译

    上面看到某些无用符号被链接进入了最终产品,那么是否存在方法将其剔出呢?链接器的默认链接最小单元仍然是文件(同编译)。也就是说如果某个文件中有一个符号被链接,该文件的所有符号都将被一同链接。
     GCC编译器提供了编译优化选项,告诉编译器将单个符号(函数和数据)作为最小链接单元。下面我们重新将上面hello.cpp重新编译。

                                     `-fdata-sections  -ffunction-sections`
    

    加选项后

    hou.png

    加选项前

    qian.png

    对比两个节头,表明显看到加入选项后每个符号自成一个节字。

    (2) 链接

    获得可连接文件后,我们还得告诉连接器,剔除无用符号。

     --gc-sections               删除未使用的节(在某些目标上)
     --no-gc-sections            不删除未使用的节(默认)
    
    

    最后我们重新编译再看看最后的结果。


    elf.png

    从上图明显看出无用符号被剔除了。

    (3) 问题

    上述方式可以完美剔除无用符号,但是“杀敌一千自损八百”这个道理一直都是成立的。产品中可能会剔除某些调试符号例如桩函数。当然解决方式也很简单。我们可以针对不同功能模块单独编译优化。如果你发现某些倒霉蛋被优化掉了,那么你可以将其CPP include 进未被优化的模块就OK了。前面说过了默认链接是以文件为最小单位的。

    结束

    一切看起总是那么完美。为了减小影响面,我们最终只优化了一个我们认为无用符号较多的库。实际的结果也十分可喜,最后产品尺寸减少了近九百分点(10M左右),我们又可以愉快的玩耍好长时间了~~。

    相关文章

      网友评论

          本文标题:产品瘦身

          本文链接:https://www.haomeiwen.com/subject/lalqqqtx.html