筆者之前參與過一個嵌入式智能手表項目,曾經碰到過這樣一個問題:手表的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
未完待續