背景:
最近一直在思考,嵌入式产品的代码中存在着很多的无用的函数和全局变量,这些函数和全局量占用着版本的大小,进而对Flash的要求也更多了。但这些通用人工去排查毕竟不现实,那有什么办法可以做到自动的检测出这些没有用的函数和全局变量?
这次的总结有一点感受很深刻,那就是“世上并不缺少美,缺少的是发现美的眼睛”。
思考:
针对引言中的思考,我主要从以下两个方向去思考:
1. 是否可以通过符号进行入手,通过查找符号不存在被调用的来判断函数或全局变量未被使用;
2. 编译过程中,无用的函数或全局变量是否可以不链接进版本中;
方向1——检查没有使用到的符号
很多人也许会说,在内部使用的函数都加上static不就好了吗?这也是一样办法,但排除不了团队编程过程中,有些人不加static,甚至在头文件中添加声明。
那么,从符号调用角度我主要想到了以前出现段错误经常会用到使用objdump命令进行反汇编来看程序的汇编语言,其中函数的调用关系可以看得出来,那么从汇编语言中是不是可以检查到哪些函数都没有被调用过?下面做一个简单的测试。
- #include <stdio.h>
- int lUnusedGrobleValue = 2;
- int lUsedGrobleValue = 1;
- int unUsedFun()
- {
- printf("%d", lUnusedGrobleValue);
- return 0;
- }
- int usedFun()
- {
- printf("It's a usedFun, usedGrobleValue=%d\r\n", lUsedGrobleValue);
- }
- int main()
- {
- printf("hello world\n");
- usedFun();
- return 0;
- }
从上面可以看出,存在函数unUsedFun和lUnusedGrobleValue是我们希望找出来的。
1. 下面使用nm命令来看一下正常情况下编译出来的进程带的符号信息:
- sammor@ubuntu:~/test$ gcc test.c -o test
- sammor@ubuntu:~/test$ ls -l test
- -rwxr-xr-x 1 sammor sammor 7362 2014-10-22 22:00 test
- sammor@ubuntu:~/test$ nm --defined-only test
- 08049f28 d _DYNAMIC
- 08049ff4 d _GLOBAL_OFFSET_TABLE_
- 0804854c R _IO_stdin_used
- 08049f18 d __CTOR_END__
- 08049f14 d __CTOR_LIST__
- 08049f20 D __DTOR_END__
- 08049f1c d __DTOR_LIST__
- 08048654 r __FRAME_END__
- 08049f24 d __JCR_END__
- 08049f24 d __JCR_LIST__
- 0804a020 A __bss_start
- 0804a010 D __data_start
- 08048500 t __do_global_ctors_aux
- 08048390 t __do_global_dtors_aux
- 0804a014 D __dso_handle
- 080484f2 T __i686.get_pc_thunk.bx
- 08049f14 d __init_array_end
- 08049f14 d __init_array_start
- 080484f0 T __libc_csu_fini
- 08048480 T __libc_csu_init
- 0804a020 A _edata
- 0804a028 A _end
- 0804852c T _fini
- 08048548 R _fp_hw
- 080482d4 T _init
- 08048360 T _start
- 0804a020 b completed.7108
- 0804a010 W data_start
- 0804a024 b dtor_idx.7110
- 080483f0 t frame_dummy
- 0804a018 D lUnusedGrobleValue
- 0804a01c D lUsedGrobleValue
- 08048457 T main
- 08048414 T unUsedFun
- 08048438 T usedFun
分析1: 从以上的验证看出正常编译情况下,没有用到的函数和全局变量也都被链接进来了,这说明这些无用的函数和全局变量确实在影响着我们最终输出的进程文件大小
那如何找出这些无用的函数和全局变量,继续进行反汇编来看下结果。
- sammor@ubuntu:~/test$ vim test.dump
- test: file format elf32-i386
- Disassembly of section .init:
- 080482d4 <_init>:
- ...
- 08048310 <printf@plt-0x10>:
- ...
- 08048320 <printf@plt>:
- ...
- 08048330 <puts@plt>:
- ...
- 08048340 <__gmon_start__@plt>:
- 08048350 <__libc_start_main@plt>:
- 080483f0 <frame_dummy>:
- #以上省略很不相关的汇编信息..
- 08048414 <unUsedFun>: // unUsedFun函数的反汇编
- 8048414: 55 push %ebp
- 8048415: 89 e5 mov %esp,%ebp
- 8048417: 83 ec 18 sub $0x18,%esp
- 804841a: 8b 15 18 a0 04 08 mov 0x804a018,%edx —— (0804a018 D lUnusedGrobleValue)
- 8048420: b8 50 85 04 08 mov $0x8048550,%eax
- 8048425: 89 54 24 04 mov %edx,0x4(%esp)
- 8048429: 89 04 24 mov %eax,(%esp)
- 804842c: e8 ef fe ff ff call 8048320 <printf@plt>
- 8048431: b8 00 00 00 00 mov $0x0,%eax
- 8048436: c9 leave
- 8048437: c3 ret
- 08048438 <usedFun>:// usedFun函数的反汇编
- 8048438: 55 push %ebp
- 8048439: 89 e5 mov %esp,%ebp
- 804843b: 83 ec 18 sub $0x18,%esp
- 804843e: 8b 15 1c a0 04 08 mov 0x804a01c,%edx —— (0804a01c D lUsedGrobleValue)
- 8048444: b8 54 85 04 08 mov $0x8048554,%eax
- 8048449: 89 54 24 04 mov %edx,0x4(%esp)
- 804844d: 89 04 24 mov %eax,(%esp)
- 8048450: e8 cb fe ff ff call 8048320 <printf@plt>
- 804844d: 89 04 24 mov %eax,(%esp)
- 8048450: e8 cb fe ff ff call 8048320 <printf@plt>
- 8048455: c9 leave
- 8048456: c3 ret
- 08048457 <main>: // main函数的反汇编
- 8048457: 55 push %ebp
- 8048458: 89 e5 mov %esp,%ebp
- 804845a: 83 e4 f0 and $0xfffffff0,%esp
- 804845d: 83 ec 10 sub $0x10,%esp
- 8048460: c7 04 24 79 85 04 08 movl $0x8048579,(%esp)
- 8048467: e8 c4 fe ff ff call 8048330 <puts@plt>
- 804846c: e8 c7 ff ff ff call 8048438 <usedFun>
- 8048471: b8 00 00 00 00 mov $0x0,%eax
- 8048476: c9 leave
- 8048477: c3 ret
- 8048478: 90 nop
- 8048479: 90 nop
- 804847a: 90 nop
- 804847b: 90 nop
- 804847c: 90 nop
- 804847d: 90 nop
- 804847e: 90 nop
- 804847f: 90 nop
- 0804852c <_fini>:
- ......
分析2:从这个反汇编文件中,我们可以看出如下:
1. 函数被调用时,在汇编语言中是以call命令进行用(在我验证的arm板上是bl命令,说明在不同的架构上还是有差异的)。
2. 全局变量函数中被使用时,是以mov命令进行。
从以上两点分析来看,如果要做到从dump文件中检查出无用的函数和全局变量,则需要从符号表中找到所有定义的函数和全局变量,在dump文件中分析。
解析原则:
待思考,当前这也是只思路,也许有更好的办法可以解决这个问题。
方向2——编译过程中,无用的函数或全局变量是否可以不链接进版本中
这个方向一直在思想困惑着,也没有去具体去找资源,突然今天在和一朋友聊到这个想法的时候,这位朋友和我说了他们那边通过编译选项来做到的,毕竟他们也是嵌入式设备,flash的空间非常小。他告诉我使用的了编译选项是编译过程添加-ffunction-sections和-fdata-sections, 链接过程添加选项-Wl,--gc-sections。
使用以上两个编译选项做一下试验:
- sammor@ubuntu:~/test$ gcc -c -ffunction-sections -fdata-sections test.c
- sammor@ubuntu:~/test$ gcc -Wl,--gc-sections test.o -o test
- sammor@ubuntu:~/test$ nm --defined-only test
- 08049f28 d _DYNAMIC
- 08049ff4 d _GLOBAL_OFFSET_TABLE_
- 08048528 R _IO_stdin_used
- 08049f18 d __CTOR_END__
- 08049f14 d __CTOR_LIST__
- 08049f20 D __DTOR_END__
- 08049f1c d __DTOR_LIST__
- 08048630 r __FRAME_END__
- 08049f24 d __JCR_END__
- 08049f24 d __JCR_LIST__
- 0804a014 A __bss_start
- 080484e0 t __do_global_ctors_aux
- 08048390 t __do_global_dtors_aux
- 080484d2 T __i686.get_pc_thunk.bx
- 08049f14 d __init_array_end
- 08049f14 d __init_array_start
- 080484d0 T __libc_csu_fini
- 08048460 T __libc_csu_init
- 0804a014 A _edata
- 0804a01c A _end
- 0804850c T _fini
- 080482d4 T _init
- 08048360 T _start
- 0804a014 b completed.7108
- 0804a018 b dtor_idx.7110
- 080483f0 t frame_dummy
- 0804a010 D lUsedGrobleValue
- 08048432 T main
- 08048413 T usedFun
分析:从符号表来看,没有用到的函数unUsedFun和全局变量lUnusedGrobleValue已经不在符号表里面了。而且和不加编译选项编译出来的进程相比减小了181字节。
- -rwxr-xr-x 1 sammor sammor 7181 2014-10-22 23:54 test
- -rwxr-xr-x 1 sammor sammor 7362 2014-10-22 22:00 test1
结论:编译过程添加-ffunction-sections和-fdata-sections, 链接过程添加选项-Wl,--gc-sections,可以使得编译出来的进程去除无用函数和全局变量符号,减少进程大小。
具体原理:
先看看gcc官方文档对这几个选项的说明。
从说明可以大概看说意思是:
1. 编译过程中添加-ffunction-sections和-fdata-sections会在输出文件object中给每个函数和全局变量控制在一个section中并以对应的函数名或全局变量名命名,
2. 链接过程中-Wl,--gc-sections,因为链接时查找符号是以section为单元进行引用的,对于没有引用到的符号,对应的section也不会引进来,故排除掉了无用的函数和全局变更,从而减少可执行文件的大小。
注意:从文档来看,这些选项对-g和gprof会有些影响,需要关注下,具体影响我还没去深挖。
这里用readelf命令查看section来比较下使用与未使用编译选项的结果做一下检查,结果如下:
a.未添加编译选项查看:
- sammor@ubuntu:~/test$ gcc -c test.c -o test.nooption
- sammor@ubuntu:~/test$ readelf -t test.nooption
- There are 11 section headers, starting at offset 0x154:
- Section Headers:
- [Nr] Name
- Type Addr Off Size ES Lk Inf Al
- Flags
- [ 0]
- NULL 00000000 000000 000000 00 0 0 0
- [00000000]:
- [ 1] .text
- PROGBITS 00000000 000034 000064 00 0 0 4
- [00000006]: ALLOC, EXEC
- [ 2] .rel.text
- REL 00000000 00044c 000048 08 9 1 4
- [00000000]:
- [ 3] .data
- PROGBITS 00000000 000098 000008 00 0 0 4
- [00000003]: WRITE, ALLOC
- [ 4] .bss
- NOBITS 00000000 0000a0 000000 00 0 0 4
- [00000003]: WRITE, ALLOC
- [ 5] .rodata
- PROGBITS 00000000 0000a0 000035 00 0 0 4
- [00000002]: ALLOC
- [ 6] .comment
- PROGBITS 00000000 0000d5 00002b 01 0 0 1
- [00000030]: MERGE, STRINGS
- [ 7] .note.GNU-stack
- PROGBITS 00000000 000100 000000 00 0 0 1
- [00000000]:
- [ 8] .shstrtab
- STRTAB 00000000 000100 000051 00 0 0 1
- [00000000]:
- [ 9] .symtab
- SYMTAB 00000000 00030c 0000f0 10 10 8 4
- [00000000]:
- [10] .strtab
- STRTAB 00000000 0003fc 00004f 00 0 0 1
- [00000000]:
b.添加编译选项查看。
- sammor@ubuntu:~/test$ readelf -t test.o
- There are 18 section headers, starting at offset 0x1b4:
- Section Headers:
- [Nr] Name
- Type Addr Off Size ES Lk Inf Al
- Flags
- [ 0]
- NULL 00000000 000000 000000 00 0 0 0
- [00000000]:
- [ 1] .text
- PROGBITS 00000000 000034 000000 00 0 0 4
- [00000006]: ALLOC, EXEC
- [ 2] .data
- PROGBITS 00000000 000034 000000 00 0 0 4
- [00000003]: WRITE, ALLOC
- [ 3] .bss
- NOBITS 00000000 000034 000000 00 0 0 4
- [00000003]: WRITE, ALLOC
- [ 4] .data.lUnusedGrobleValue
- PROGBITS 00000000 000034 000004 00 0 0 4
- [00000003]: WRITE, ALLOC
- [ 5] .data.lUsedGrobleValue
- PROGBITS 00000000 000038 000004 00 0 0 4
- [00000003]: WRITE, ALLOC
- [ 6] .rodata
- PROGBITS 00000000 00003c 000035 00 0 0 4
- [00000002]: ALLOC
- [ 7] .text.unUsedFun
- PROGBITS 00000000 000071 000024 00 0 0 1
- [00000006]: ALLOC, EXEC
- [ 8] .rel.text.unUsedFun
- REL 00000000 000614 000018 08 16 7 4
- [00000000]:
- [ 9] .text.usedFun
- PROGBITS 00000000 000095 00001f 00 0 0 1
- [00000006]: ALLOC, EXEC
- [10] .rel.text.usedFun
- REL 00000000 00062c 000018 08 16 9 4
- [00000000]:
- [11] .text.main
- PROGBITS 00000000 0000b4 000021 00 0 0 1
- [00000006]: ALLOC, EXEC
- [12] .rel.text.main
- REL 00000000 000644 000018 08 16 11 4
- [00000000]:
- [13] .comment
- PROGBITS 00000000 0000d5 00002b 01 0 0 1
- [00000030]: MERGE, STRINGS
- [14] .note.GNU-stack
- PROGBITS 00000000 000100 000000 00 0 0 1
- [00000000]:
- [15] .shstrtab
- STRTAB 00000000 000100 0000b2 00 0 0 1
- [00000000]:
- [16] .symtab
- SYMTAB 00000000 000484 000140 10 17 13 4
- [00000000]:
- [17] .strtab
- STRTAB 00000000 0005c4 00004f 00 0 0 1
- [00000000]:
通过比较看出,添加编译选项的输出文件中,确实以每个符号进行取名作为一个section,最终链接出来的可执行文件也不包含无用的函数和全局变量了。
相关文章:
http://elinux.org/Function_sections
http://blog.sina.com.cn/s/blog_602f87700100t0t5.html