笔者之前参与过一个嵌入式智能手表项目,曾经碰到过这样一个问题:手表的flash大小只有2M,这意味着只能在上面烧录2M大小的代码。随着开发不断进行,代码越写越多,编译出来的bin也越来越大。最后bin大小超过了2M, 就没法烧写了,很尴尬。最后只能想办法精简代码,当然这是在不影响功能的前提下精简代码。那如何精简代码呢?我们自然会想到先看看哪里的代码最多,比如使用的各个so的大小,so里边哪个源文件最大,源文件里边哪一个函数最耗空间等等,先做一个统计分析,然后再看一下怎么优化。那这个统计如何进行呢?这个就需要用到一些工具。
本文介绍的工具:bloaty就用来干这个活的,这是谷歌公司开源的一个项目,在GitHub上有源码,主要是用来查看可执行文件,链接库内存分布的。Bloaty对二进制文件进行深入分析,使用自定义的ELF、DWARF和Mach-O解析器,旨在将二进制文件的每个字节准确地定位到是属于哪个符号或编译单元。它甚至会反汇编二进制文件,寻找对匿名数据的引用。
下面是一个例子,用bloaty工具来分析bloaty二进制文件,看一下各个编译单元(源文件)所占的内存大小和占总大小的百分比:
./bloaty bloaty -d compileunitsFILE SIZE VM SIZE -------------- -------------- 34.8% 10.2Mi 43.4% 2.91Mi [163 Others]17.2% 5.08Mi 4.3% 295Ki third_party/protobuf/src/google/protobuf/descriptor.cc7.3% 2.14Mi 2.6% 179Ki third_party/protobuf/src/google/protobuf/descriptor.pb.cc4.6% 1.36Mi 1.1% 78.4Ki third_party/protobuf/src/google/protobuf/text_format.cc3.7% 1.10Mi 4.5% 311Ki third_party/capstone/arch/ARM/ARMDisassembler.c1.3% 399Ki 15.9% 1.07Mi third_party/capstone/arch/M68K/M68KDisassembler.c3.2% 980Ki 1.1% 75.3Ki third_party/protobuf/src/google/protobuf/generated_message_reflection.cc3.2% 965Ki 0.6% 40.7Ki third_party/protobuf/src/google/protobuf/descriptor_database.cc2.8% 854Ki 12.0% 819Ki third_party/capstone/arch/X86/X86Mapping.c2.8% 846Ki 1.0% 66.4Ki third_party/protobuf/src/google/protobuf/extension_set.cc2.7% 800Ki 0.6% 41.2Ki third_party/protobuf/src/google/protobuf/generated_message_util.cc2.3% 709Ki 0.7% 50.7Ki third_party/protobuf/src/google/protobuf/wire_format.cc2.1% 637Ki 1.7% 117Ki third_party/demumble/third_party/libcxxabi/cxa_demangle.cpp1.8% 549Ki 1.7% 114Ki src/bloaty.cc1.7% 503Ki 0.7% 48.1Ki third_party/protobuf/src/google/protobuf/repeated_field.cc1.6% 469Ki 6.2% 427Ki third_party/capstone/arch/X86/X86DisassemblerDecoder.c1.4% 434Ki 0.2% 15.9Ki third_party/protobuf/src/google/protobuf/message.cc1.4% 422Ki 0.3% 23.4Ki third_party/re2/re2/dfa.cc1.3% 407Ki 0.4% 24.9Ki third_party/re2/re2/regexp.cc1.3% 407Ki 0.4% 29.9Ki third_party/protobuf/src/google/protobuf/map_field.cc1.3% 397Ki 0.4% 24.8Ki third_party/re2/re2/re2.cc100.0% 29.5Mi 100.0% 6.69Mi TOTAL
Bloaty支持许多功能:
- 文件格式:ELF、Mach-O、PE/COFF(实验)、WebAssembly(实验)
- 数据来源:compilenit(如上所示)、符号、节、段等。
- 分层解析:将多个数据源合并为一个报告
- size diffs:查看二进制文件的增长位置,非常适合CI测试
- 单独的调试文件:剥离测试中的二进制文件,同时使调试数据可用于分析
- 灵活的解映射:解映射C++符号,可选择丢弃函数/模板参数
- 自定义数据源:regex重写内置数据源,用于自定义munging/bucketing
- 正则表达式过滤:过滤掉二进制文件中与给定正则表达式匹配或不匹配的部分
使用说明
$ ./bloaty bloatyFILE SIZE VM SIZE -------------- -------------- 30.0% 8.85Mi 0.0% 0 .debug_info24.7% 7.29Mi 0.0% 0 .debug_loc12.8% 3.79Mi 0.0% 0 .debug_str9.7% 2.86Mi 42.8% 2.86Mi .rodata6.9% 2.03Mi 30.3% 2.03Mi .text6.3% 1.85Mi 0.0% 0 .debug_line4.0% 1.19Mi 0.0% 0 .debug_ranges0.0% 0 15.0% 1.01Mi .bss1.6% 473Ki 0.0% 0 .strtab1.4% 435Ki 6.3% 435Ki .data0.8% 254Ki 3.7% 254Ki .eh_frame0.8% 231Ki 0.0% 0 .symtab0.5% 142Ki 0.0% 0 .debug_abbrev0.2% 56.8Ki 0.8% 56.8Ki .gcc_except_table0.1% 41.4Ki 0.6% 41.4Ki .eh_frame_hdr0.0% 11.4Ki 0.1% 9.45Ki [26 Others]0.0% 7.20Ki 0.1% 7.14Ki .dynstr0.0% 6.09Ki 0.1% 6.02Ki .dynsym0.0% 4.89Ki 0.1% 4.83Ki .rela.plt0.0% 4.59Ki 0.0% 0 [Unmapped]0.0% 3.30Ki 0.0% 3.23Ki .plt100.0% 29.5Mi 100.0% 6.69Mi TOTAL
“VM SIZE”列告诉二进制文件加载到内存时将占用多少空间。“文件大小”列告诉二进制文件在磁盘上占用的空间。这两者可能彼此非常不同:
- 有些数据存在于文件中,但没有加载到内存中,例如调试信息。
- 某些数据已映射到内存中,但文件中不存在。这主要适用于.bss部分(零初始化数据)。
Bloaty中的默认细分是分段的,但支持许多其他对二进制文件进行切片的方式,如符号和分段。如果使用调试信息进行编译,甚至可以按编译单元和内联进行分解!效果见第一个例子。
Size Diffs
可以使用Bloaty来查看二进制文件的大小是如何变化的。
例如,这里有几个不同版本的Bloaty之间的大小差异,显示了当我添加一些功能时它是如何增长的。
$ ./bloaty bloaty -- oldbloatyVM SIZE FILE SIZE-------------- --------------[ = ] 0 .debug_loc +688Ki +9.9%+19% +349Ki .text +349Ki +19%[ = ] 0 .debug_ranges +180Ki +11%[ = ] 0 .debug_info +120Ki +0.9%+23% +73.5Ki .rela.dyn +73.5Ki +23%+3.5% +57.1Ki .rodata +57.1Ki +3.5%+28e3% +53.9Ki .data +53.9Ki +28e3%[ = ] 0 .debug_line +40.2Ki +4.8%+2.3% +5.35Ki .eh_frame +5.35Ki +2.3%-6.0% -5 [Unmapped] +2.65Ki +215%+0.5% +1.70Ki .dynstr +1.70Ki +0.5%[ = ] 0 .symtab +1.59Ki +0.9%[ = ] 0 .debug_abbrev +1.29Ki +0.5%[ = ] 0 .strtab +1.26Ki +0.3%+16% +992 .bss 0 [ = ]+0.2% +642 [13 Others] +849 +0.2%+0.6% +792 .dynsym +792 +0.6%+16% +696 .rela.plt +696 +16%+16% +464 .plt +464 +16%+0.8% +312 .eh_frame_hdr +312 +0.8%[ = ] 0 .debug_str -19.6Ki -0.4%+11% +544Ki TOTAL +1.52Mi +4.6%
分层解析
Bloaty支持以多种不同的方式分解二进制文件。您可以将多个数据源组合到一个层次配置文件中。例如,我们可以在单个报告中使用分段和分段数据源:
$ ./bloaty -d segments,sections bloatyFILE SIZE VM SIZE -------------- -------------- 80.7% 23.8Mi 0.0% 0 [Unmapped]37.2% 8.85Mi NAN% 0 .debug_info30.6% 7.29Mi NAN% 0 .debug_loc15.9% 3.79Mi NAN% 0 .debug_str7.8% 1.85Mi NAN% 0 .debug_line5.0% 1.19Mi NAN% 0 .debug_ranges1.9% 473Ki NAN% 0 .strtab1.0% 231Ki NAN% 0 .symtab0.6% 142Ki NAN% 0 .debug_abbrev0.0% 4.59Ki NAN% 0 [Unmapped]0.0% 392 NAN% 0 .shstrtab0.0% 139 NAN% 0 .debug_macinfo0.0% 68 NAN% 0 .comment10.9% 3.21Mi 47.9% 3.21Mi LOAD #4 [R]89.3% 2.86Mi 89.3% 2.86Mi .rodata7.7% 254Ki 7.7% 254Ki .eh_frame1.7% 56.8Ki 1.7% 56.8Ki .gcc_except_table1.3% 41.4Ki 1.3% 41.4Ki .eh_frame_hdr0.0% 1 0.0% 1 [LOAD #4 [R]]6.9% 2.03Mi 30.3% 2.03Mi LOAD #3 [RX]99.8% 2.03Mi 99.8% 2.03Mi .text0.2% 3.23Ki 0.2% 3.23Ki .plt0.0% 28 0.0% 28 [LOAD #3 [RX]]0.0% 23 0.0% 23 .init0.0% 9 0.0% 9 .fini1.5% 439Ki 21.4% 1.44Mi LOAD #5 [RW]0.0% 0 70.1% 1.01Mi .bss99.1% 435Ki 29.6% 435Ki .data0.4% 1.63Ki 0.1% 1.63Ki .got.plt0.3% 1.46Ki 0.1% 1.46Ki .data.rel.ro0.1% 560 0.0% 560 .dynamic0.1% 384 0.0% 376 .init_array0.0% 32 0.0% 56 [LOAD #5 [RW]]0.0% 32 0.0% 32 .got0.0% 16 0.0% 16 .tdata0.0% 8 0.0% 8 .fini_array0.0% 0 0.0% 8 .tbss0.1% 23.3Ki 0.3% 23.3Ki LOAD #2 [R]30.7% 7.14Ki 30.7% 7.14Ki .dynstr25.9% 6.02Ki 25.9% 6.02Ki .dynsym20.8% 4.83Ki 20.8% 4.83Ki .rela.plt7.7% 1.78Ki 7.7% 1.78Ki .hash5.0% 1.17Ki 5.0% 1.17Ki .rela.dyn3.1% 741 3.1% 741 [LOAD #2 [R]]2.7% 632 2.7% 632 .gnu.hash2.2% 514 2.2% 514 .gnu.version1.6% 384 1.6% 384 .gnu.version_r0.2% 36 0.2% 36 .note.gnu.build-id0.1% 32 0.1% 32 .note.ABI-tag0.1% 28 0.1% 28 .interp0.0% 2.56Ki 0.0% 0 [ELF Headers]46.3% 1.19Ki NAN% 0 [19 Others]7.3% 192 NAN% 0 [ELF Headers]2.4% 64 NAN% 0 .comment2.4% 64 NAN% 0 .data2.4% 64 NAN% 0 .data.rel.ro2.4% 64 NAN% 0 .debug_abbrev2.4% 64 NAN% 0 .debug_info2.4% 64 NAN% 0 .debug_line2.4% 64 NAN% 0 .debug_loc2.4% 64 NAN% 0 .debug_macinfo2.4% 64 NAN% 0 .debug_ranges2.4% 64 NAN% 0 .debug_str2.4% 64 NAN% 0 .dynamic2.4% 64 NAN% 0 .dynstr2.4% 64 NAN% 0 .dynsym2.4% 64 NAN% 0 .eh_frame2.4% 64 NAN% 0 .eh_frame_hdr2.4% 64 NAN% 0 .fini2.4% 64 NAN% 0 .fini_array2.4% 64 NAN% 0 .gcc_except_table2.4% 64 NAN% 0 .gnu.hash100.0% 29.5Mi 100.0% 6.69Mi TOTAL
Bloaty为每个级别显示最多20行;其他值被分组到[other]bin中。使用-n<num>可覆盖此设置。如果传递-n 0,所有数据都将被输出,而不会将任何内容折叠到[Other]中
调试剥离的二进制文件
Bloaty支持从单独的二进制文件中读取调试信息/符号。这使您可以对剥离的二进制文件进行配置,即使是对于像“compilenits”或“symbol”这样需要这些额外信息的数据源也是如此。
Bloaty使用构建ID来验证二进制文件和调试文件是否匹配。否则,结果将是无稽之谈(这种不匹配听起来可能不太可能,但这是一个很容易犯的错误)。
如果您的二进制文件有一个生成ID,那么使用单独的调试文件非常简单,如下所示:
$ cp bloaty bloaty.stripped
$ strip bloaty.stripped
$ ./bloaty -d symbols --debug-file=bloaty bloaty.stripped
数据源
Bloaty有许多内置的数据源。这些都提供了不同的方法来查看二进制文件。您还可以通过将正则表达式应用于内置数据源来创建自己的数据源(请参阅下面的“自定义数据源”)。
虽然Bloaty处理二进制文件、共享对象、对象文件和静态库(.a文件),但有些数据源不处理对象文件。这尤其适用于读取调试信息的数据源。
Segments段
段是运行时加载程序用来确定二进制文件的哪些部分需要加载/映射到内存中的内容。通常只有几个部分:每组mmap()权限需要一个:
$ ./bloaty -d segments bloatyFILE SIZE VM SIZE -------------- -------------- 80.7% 23.8Mi 0.0% 0 [Unmapped]10.9% 3.21Mi 47.9% 3.21Mi LOAD #4 [R]6.9% 2.03Mi 30.3% 2.03Mi LOAD #3 [RX]1.5% 439Ki 21.4% 1.44Mi LOAD #5 [RW]0.1% 23.3Ki 0.3% 23.3Ki LOAD #2 [R]0.0% 2.56Ki 0.0% 0 [ELF Headers]100.0% 29.5Mi 100.0% 6.69Mi TOTAL
在这里,我们看到一个段被映射[RX](读/执行)和一个段映射[RW](读取/写入)。二进制文件的很大一部分没有加载到内存中,我们将其视为[未映射]。
对象文件和静态库没有段。然而,我们通过将部分按其标志分组来伪造它。这给了我们一个分解,有点像真实的片段。
$ ./bloaty -d segments CMakeFiles/libbloaty.dir/src/bloaty.cc.oFILE SIZE VM SIZE -------------- -------------- 87.5% 972Ki 0.0% 0 Section []8.2% 90.9Ki 78.3% 90.9Ki Section [AX]2.3% 25.2Ki 21.7% 25.2Ki Section [A]2.0% 22.6Ki 0.0% 0 [ELF Headers]0.1% 844 0.0% 0 [Unmapped]0.0% 24 0.1% 72 Section [AW]100.0% 1.09Mi 100.0% 116Ki TOTAL
未完待续