背景前言
最近遇到一个棘手的问题,编译生成的可执行文件过大而无法上电。这就好像一个人过于肥胖致其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 文件表头
ELF文件展示文件的基本属性信息,例如文件类型,大小端,ABI版本,32位(64位)等基本属性信息。同时包含程序头表信息(用于程序加载时创建映像,在此我们不讨论),节字头表信息等其它信息。在链接库文件时可能会报”不合适的库文件“这时注意下ELF文件头表信息一般就可能知道原因了。
2,节头表
包含符号表,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这个符号我们并没有使用,但是其仍然存在于我们最终的可执行文件中,反汇编结果如下。这个也就是我们所要讨论的优化点。
二、优化方式
(1) 编译
上面看到某些无用符号被链接进入了最终产品,那么是否存在方法将其剔出呢?链接器的默认链接最小单元仍然是文件(同编译)。也就是说如果某个文件中有一个符号被链接,该文件的所有符号都将被一同链接。
GCC编译器提供了编译优化选项,告诉编译器将单个符号(函数和数据)作为最小链接单元。下面我们重新将上面hello.cpp重新编译。
`-fdata-sections -ffunction-sections`
加选项后
加选项前
对比两个节头,表明显看到加入选项后每个符号自成一个节字。
(2) 链接
获得可连接文件后,我们还得告诉连接器,剔除无用符号。
--gc-sections 删除未使用的节(在某些目标上)
--no-gc-sections 不删除未使用的节(默认)
最后我们重新编译再看看最后的结果。
elf.png
从上图明显看出无用符号被剔除了。
(3) 问题
上述方式可以完美剔除无用符号,但是“杀敌一千自损八百”这个道理一直都是成立的。产品中可能会剔除某些调试符号例如桩函数。当然解决方式也很简单。我们可以针对不同功能模块单独编译优化。如果你发现某些倒霉蛋被优化掉了,那么你可以将其CPP include 进未被优化的模块就OK了。前面说过了默认链接是以文件为最小单位的。
结束
一切看起总是那么完美。为了减小影响面,我们最终只优化了一个我们认为无用符号较多的库。实际的结果也十分可喜,最后产品尺寸减少了近九百分点(10M左右),我们又可以愉快的玩耍好长时间了~~。
网友评论