--- GA: G-B8G2L9X80S --- # Ch4 靜態連結 [toc] ## 4.1 空間與地址分配 有兩個 Object File,如何將他們連結成一個可執行檔 ```c= /* a.c */ extern int shared; int main() { int a = 100; swap(&a, &shared); } /* b.c */ int shared = 1; void swap(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; } ``` ### 4.1.1 按續疊加 - 直接串接所有的檔案,沒用。 > 會造成記憶體碎片,不是一個很好的方案 ### 4.1.2 相似段合併 - 多個檔案的 `.text` 合併成一個、`.data`合併成一個、`.bss` 合併成一個,依此類推。 - Linker 為 Object file 分配地址與空間包含的意義: - 輸出可執行檔的檔案本身 - 可執行檔在跑起來後的虛擬位址的空間 - 兩步連接(Two-Pass Linking) 將連接過程分兩步: - 1. 空間與地址分配 - 掃過所有 Object file,獲取 Section size, attribute, offset 資訊 - 合併各個 Section 並且計算出合併後的 Section 的長度和位置 - 並收集 symbol 統一放到一個 global symbol table - 2. 符號解析與重定位 - 讀取 Section, Relocation info - 並進行符號解析、重定位、調整程式中的地址 - 使用 `ld` 將 `a.o` 和 `b.o` Link 起來: - 64bit ```bash= gcc -c a.c -fno-stack-protector -o a.o gcc -c b.c -fno-stack-protector -o b.o ld a.o b.o -e main -o ab ``` - 32bit ```bash= gcc -m32 -c a.c -fno-stack-protector -o a.o gcc -m32 -c b.c -fno-stack-protector -o b.o ld -m elf_i386 a.o b.o -e main -o ab ``` > https://stackoverflow.com/questions/4492799/undefined-reference-to-stack-chk-fail > https://blog.csdn.net/wuqindeyunque/article/details/131593357 > https://www.zhihu.com/question/304260625 > https://unix.stackexchange.com/questions/472653/running-a-custom-compiled-executable-returns-no-such-file-or-directory > https://unix.stackexchange.com/questions/13391/getting-not-found-message-when-running-a-32-bit-binary-on-a-64-bit-system ```= $ objdump -w -h a.o a.o: file format elf64-x86-64 Sections: Idx Name Size VMA LMA File off Algn Flags 0 .text 00000032 0000000000000000 0000000000000000 00000040 2**0 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 1 .data 00000000 0000000000000000 0000000000000000 00000072 2**0 CONTENTS, ALLOC, LOAD, DATA 2 .bss 00000000 0000000000000000 0000000000000000 00000072 2**0 ALLOC 3 .comment 0000002c 0000000000000000 0000000000000000 00000072 2**0 CONTENTS, READONLY 4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 0000009e 2**0 CONTENTS, READONLY 5 .note.gnu.property 00000020 0000000000000000 0000000000000000 000000a0 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 6 .eh_frame 00000038 0000000000000000 0000000000000000 000000c0 2**3 CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA $ objdump -w -h b.o b.o: file format elf64-x86-64 Sections: Idx Name Size VMA LMA File off Algn Flags 0 .text 00000031 0000000000000000 0000000000000000 00000040 2**0 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .data 00000004 0000000000000000 0000000000000000 00000074 2**2 CONTENTS, ALLOC, LOAD, DATA 2 .bss 00000000 0000000000000000 0000000000000000 00000078 2**0 ALLOC 3 .comment 0000002c 0000000000000000 0000000000000000 00000078 2**0 CONTENTS, READONLY 4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000a4 2**0 CONTENTS, READONLY 5 .note.gnu.property 00000020 0000000000000000 0000000000000000 000000a8 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 6 .eh_frame 00000038 0000000000000000 0000000000000000 000000c8 2**3 CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA $ objdump -w -h ab ab: file format elf64-x86-64 Sections: Idx Name Size VMA LMA File off Algn Flags 0 .note.gnu.property 00000020 00000000004001c8 00000000004001c8 000001c8 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 1 .text 00000063 0000000000401000 0000000000401000 00001000 2**0 CONTENTS, ALLOC, LOAD, READONLY, CODE 2 .eh_frame 00000058 0000000000402000 0000000000402000 00002000 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 3 .data 00000004 0000000000404000 0000000000404000 00003000 2**2 CONTENTS, ALLOC, LOAD, DATA 4 .comment 0000002b 0000000000000000 0000000000000000 00003004 2**0 CONTENTS, READONLY ``` :::info 2024/01/08: 實驗了蠻多次都沒辦法產生和書上一樣位址,VMA 都有點怪怪的 ::: ### 4.1.3 符號地址的確定 - 在書中的例子: - `.text` 的起始地址為 `0x08048094` - `.data` 的起始地址為 `0x08049180` - `.text` 中的 `main()`, `shared()`, `swap()` 的地址可以經由計算得出。 - 計算方法就是有一個起始值再加上偏移量,例如: `main()` 就是 `0x08048094 + X`。 但是因為 `main()` 在 `.text` 段中是第一個,所以 X = 0。故 `main()` 的起始地址為 `0x08048094`。 - 偏移量實際為多少在這個章節沒有提到。 ## 4.2 符號解析與重定位 ### 4.2.1 Relocation 在 Linker 完成空間和地址的分配後,就進入了符號解析和重定位的步驟 - 使用 `objdump -d a.o` 看看反組譯的結果: ```= $ objdump -d -M intel ./a.o ./a.o: file format elf32-i386 Disassembly of section .text: 00000000 <main>: 0: f3 0f 1e fb endbr32 4: 8d 4c 24 04 lea ecx,[esp+0x4] 8: 83 e4 f0 and esp,0xfffffff0 b: ff 71 fc push DWORD PTR [ecx-0x4] e: 55 push ebp f: 89 e5 mov ebp,esp 11: 53 push ebx 12: 51 push ecx 13: 83 ec 10 sub esp,0x10 16: e8 fc ff ff ff call 17 <main+0x17> 1b: 05 01 00 00 00 add eax,0x1 20: c7 45 f4 64 00 00 00 mov DWORD PTR [ebp-0xc],0x64 ; &a = ebp-0xc 27: 83 ec 08 sub esp,0x8 2a: 8b 90 00 00 00 00 mov edx,DWORD PTR [eax+0x0] ; shared 30: 52 push edx 31: 8d 55 f4 lea edx,[ebp-0xc] 34: 52 push edx 35: 89 c3 mov ebx,eax 37: e8 fc ff ff ff call 38 <main+0x38> ; swap() 3c: 83 c4 10 add esp,0x10 3f: b8 00 00 00 00 mov eax,0x0 44: 8d 65 f8 lea esp,[ebp-0x8] 47: 59 pop ecx 48: 5b pop ebx 49: 5d pop ebp 4a: 8d 61 fc lea esp,[ecx-0x4] 4d: c3 ret ``` - 幾個觀察: - a.o 只有一個函式,就是 `main()` - 總共的大小為 0x4d bytes,指令有 27 條 - 在編譯 a.c 的時候: - 編譯器不知道 `shared` ,所以他會用一個 `0x0000 0000` 代替那個記憶體位置。 - 我們可以看到 mov 指令中有關 `shared` 的地址為 `0x0000 0000` ``` Mov v [8b 90] [00 00 00 00] ==> mov edx,DWORD PTR [eax+0x0] ^ &shared ``` - 在 0x37 的地方呼叫了 `swap()`,`0xe8` near call 使用相對位置,相對下一行指令位置的偏移量,也就是說在呼叫 `call` 之前 CPU 會先去計算 `swap()` 的記憶體位置: `0x3c + 0xffff fffc = 0x38`。但是可以發現 `0x38` 沒有 `swap()`。 - 編譯器不知道 `swap()` 的位置,所以在相對位置的填上了一個假的值(`0xffff fffc`)。 ``` 0x37: [e8] [fc ff ff ff] 0x3c: 83 c4 10 add esp,0x10 ``` - 編譯器在這兩個地方用虛假的值來代替實際的值,真正的工作留給了 linker。 - 從 4.1.3 可以知道 linker 在工作時就有能力將真的地址計算出來。所以 linker 只要幾將算好的位置填入就可以得到正確的程式碼。 - 可以用 `objdump -d ab` 看到 `mov` 和 `call` 使用正確的位置。 ```= $ objdump -d -M intel ./ab ./ab: file format elf32-i386 Disassembly of section .text: 08049000 <main>: 8049000: f3 0f 1e fb endbr32 8049004: 8d 4c 24 04 lea ecx,[esp+0x4] 8049008: 83 e4 f0 and esp,0xfffffff0 804900b: ff 71 fc push DWORD PTR [ecx-0x4] 804900e: 55 push ebp 804900f: 89 e5 mov ebp,esp 8049011: 53 push ebx 8049012: 51 push ecx 8049013: 83 ec 10 sub esp,0x10 8049016: e8 33 00 00 00 call 804904e <__x86.get_pc_thunk.ax> 804901b: 05 e5 2f 00 00 add eax,0x2fe5 8049020: c7 45 f4 64 00 00 00 mov DWORD PTR [ebp-0xc],0x64 8049027: 83 ec 08 sub esp,0x8 804902a: c7 c2 0c c0 04 08 mov edx,0x804c00c 8049030: 52 push edx 8049031: 8d 55 f4 lea edx,[ebp-0xc] 8049034: 52 push edx 8049035: 89 c3 mov ebx,eax 8049037: e8 16 00 00 00 call 8049052 <swap> ; 0x804903c + 0x16 = 0x08049052 804903c: 83 c4 10 add esp,0x10 804903f: b8 00 00 00 00 mov eax,0x0 8049044: 8d 65 f8 lea esp,[ebp-0x8] 8049047: 59 pop ecx 8049048: 5b pop ebx 8049049: 5d pop ebp 804904a: 8d 61 fc lea esp,[ecx-0x4] 804904d: c3 ret$ ... 08049052 <swap>: 8049052: f3 0f 1e fb endbr32 8049056: 55 push ebp 8049057: 89 e5 mov ebp,esp 8049059: 83 ec 10 sub esp,0x10 804905c: e8 ed ff ff ff call 804904e <__x86.get_pc_thunk.ax> 8049061: 05 9f 2f 00 00 add eax,0x2f9f 8049066: 8b 45 08 mov eax,DWORD PTR [ebp+0x8] 8049069: 8b 00 mov eax,DWORD PTR [eax] 804906b: 89 45 fc mov DWORD PTR [ebp-0x4],eax 804906e: 8b 45 0c mov eax,DWORD PTR [ebp+0xc] 8049071: 8b 10 mov edx,DWORD PTR [eax] 8049073: 8b 45 08 mov eax,DWORD PTR [ebp+0x8] 8049076: 89 10 mov DWORD PTR [eax],edx 8049078: 8b 45 0c mov eax,DWORD PTR [ebp+0xc] 804907b: 8b 55 fc mov edx,DWORD PTR [ebp-0x4] 804907e: 89 10 mov DWORD PTR [eax],edx 8049080: 90 nop 8049081: c9 leave 8049082: c3 ret ``` > 為啥 link 前與與 link 後 mov 的指令變了?? > `8b 90` -> `c7 c2` > 兩個差別 > ![image](https://hackmd.io/_uploads/rJ7UjQ2d6.png) > ![image](https://hackmd.io/_uploads/S1EriX3_a.png) ### 4.2.2 Relocation Table - 在 ELF 中,Relocation Table 用來保存重定位(Relocate)相關的資訊 - 對於每個需要 relocate 的 section 都有相對應的 relocation table - 名稱 = 需要重定位的段 + 前綴 `.rel` - e.g. `.text` -> `.rel.text` - 可以使用 `objdump -r` 觀察 relocation table ``` $ objdump -r a.o a.o: file format elf32-i386 RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 00000017 R_386_PC32 __x86.get_pc_thunk.ax 0000001c R_386_GOTPC _GLOBAL_OFFSET_TABLE_ 0000002c R_386_GOT32X shared 00000038 R_386_PLT32 swap ``` 每個要被重定位的地方叫做**重定位入口(Relocation Entry)** 對應到上面 ``` 2a: 8b 90 00 00 00 00 mov edx,DWORD PTR [eax+0x0] ; shared ``` 和 ``` 37: e8 fc ff ff ff call 38 <main+0x38> ; swap() ``` 對於 32-bit 的 intel x86 來說,重定位表的是 `Elf32_Rel` 的陣列: - `r_offset` - 對於 Object File - 要修正的位置的 address offset(相對於該 section 開始位址) - 對於 Executable, shared object - 要修正的位置的 virtual address (後面會講) - `r_info` - 重定位入口的類型 - 該 32-bit word 的低 8 位代表類型,高 24 位代表該重定位入口的符號在符號表的 index - 每種不同的 CPU 有自己的定位方法 - 對於 executable 和 shared object 來說,這些 entry 是 dynamic link <iframe frameborder="0" scrolling="no" style="width:100%; height:184px;" allow="clipboard-write" src="https://emgithub.com/iframe.html?target=https%3A%2F%2Fgithub.com%2Fbminor%2Fglibc%2Fblob%2Fmaster%2Felf%2Felf.h%23L673-L677&style=github&type=code&showBorder=on&showLineNumbers=on&showFileMeta=on&showFullPath=on&showCopy=on"></iframe> ```c= typedef struct { Elf32_Addr r_offset; /* Address */ Elf32_Word r_info; /* Relocation type and symbol index */ } Elf32_Rel; ``` ### 4.2.3 符號解析 之所以要 Linking 是因為我們的 object file 中用到的符號被定義在其他 object file 中,所以要 Linking 如果我們手賤只 link `a.o` 的話,會噴錯: ``` $ ld a.o ld: a.o: in function `main': a.c:(.text+0x2b): undefined reference to `shared' ld: a.c:(.text+0x35): undefined reference to `swap' ``` - `shared` 和 `swap()` 在 b.c 裡面,故 a.o 的沒有辦法解析這兩個符號的確切位置。下表為 a.c 的 symbol table,可以看到 `shared` 和 `swap()` 的 index 為 `UND`,即為 undefined。 - 在掃瞄完所有 object file 後,linker 看到這裡有 `UND` 預期都可以在 global symbol table 中找到,如果沒找到會回報符號未定義錯誤。 ```= $ readelf -s a.o Symbol table '.symtab' contains 14 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FILE LOCAL DEFAULT ABS a.c 2: 00000000 0 SECTION LOCAL DEFAULT 1 3: 00000000 0 SECTION LOCAL DEFAULT 3 4: 00000000 0 SECTION LOCAL DEFAULT 4 5: 00000000 0 SECTION LOCAL DEFAULT 6 6: 00000000 0 SECTION LOCAL DEFAULT 7 7: 00000000 0 SECTION LOCAL DEFAULT 8 8: 00000000 0 SECTION LOCAL DEFAULT 5 9: 00000000 91 FUNC GLOBAL DEFAULT 1 main 10: 00000000 0 NOTYPE GLOBAL DEFAULT UND shared 11: 00000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_ 12: 00000000 0 NOTYPE GLOBAL DEFAULT UND swap 13: 00000000 0 NOTYPE GLOBAL DEFAULT UND __stack_chk_fail ``` ### 4.2.4 指令修正方式 對於 32 bit x86 的 ELF 來說,Relocation Entry 的修正指令巡址方式只有: - 絕對位址 near call 32位 - 相對位址 near call 32位 這兩種指令修正方式的長度都為 32bit(4byte),且都是 near call ![image](https://hackmd.io/_uploads/SyDz_hMYT.png) ![圖片](https://hackmd.io/_uploads/B1k2IaGt6.png) ![image](https://hackmd.io/_uploads/r16NIpGFT.png) ## 4.3 COMMON block Weak symbol 的機制允許同一個符號在多個不同的文件中出現,重新複習一下之前遇到的幾種情況: 1. 兩個以上的 strong symbol - 第一種情況很好處理:因為不合法。 2. 一個 strong symbol 加上 一個以上的 weak symbol - 第二種找出 strong symbol 即可。 3. 兩個以上的 weak symbol - 第三種 linker 會找出佔用記憶體最大的那個定義作為依據。這是怎麼辦到的?需要 linker 的一種機制:COMMON block。 ### COMMON block > COMMON block 這是源自於 Fortran 的技術。Fortran 的每一個變數都需要透程式設計師在事前就先宣告好每一個大小,而在 Fortran 這個叫做 COMMON block,當出現了大小不一樣的 COMMOM block,Fortran 會以最大的那個為準。 - 現代的 linker 在處理 weak symbol 時每個 symbol 都會需要一個 COMMON block。 ![image](https://hackmd.io/_uploads/H1mH5Irta.png) - 一個小例子,`SimpleSection.c` 的 `global_uninit_var` 的資訊是這樣的: ``` st_name = 'global_uninit_var' st_value = 4 st_size = 4 // COMMON block size st_info = 0x11 STB_GLOBAL STT_OBJECT st_other = 0 st_shndx = 0xfff2 SHN_COMMON ``` 如果 `global_uninit_var` 的型態不一樣,那麼 `st_size` 就有可能會改變。 - 如果 weak symbol 的大小大於 strong symbol,linker 會報錯。 ### 多個 weak symbol 的情況下 linker 會如何處理? - linker 在處理要選擇誰為最大的時候,linker 需要先拜訪過所有的輸入,找出最大的 COMMON block。 - linker 拜訪完之後可以知道真正的大小是多少,這樣才有辦法在 `.bss` 中分配空間。但是沒有宣告初始值的 global variable 還是會放在 `.bss`。 - GCC 提供的一個 attribute 讓一個 global variable 不會出現在 COMMON block 裡面: ```cpp= int global __attribute__((nocommon)); ``` 但是這將來會被 linker 視為 strong symbol,如果符號重複定義,那麼會出現 symbol redefined 錯誤。 ``` $ cat a.c int global_a; int main() { } $ cat b.c double global_a; $ gcc -c -fno-stack-protector a.c $ gcc -c -fno-stack-protector b.c $ ld a.o b.o -e main -o ab $ readelf -s ./a.o Symbol table '.symtab' contains 14 entries: Num: Value Size Type Bind Vis Ndx Name ... 9: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_a ... $ readelf -s ./b.o Symbol table '.symtab' contains 12 entries: Num: Value Size Type Bind Vis Ndx Name . .. 10: 0000000000000008 8 OBJECT GLOBAL DEFAULT COM global_a ... $ readelf -s ./ab Symbol table '.symtab' contains 16 entries: Num: Value Size Type Bind Vis Ndx Name ... 11: 0000000000404008 8 OBJECT GLOBAL DEFAULT 5 global_a ... ``` ## 4.4 C++ 相關問題 C++ 的語言特性的複雜使得其比 C 會遇到的問題更多,最主要有以下: - 重複程式碼消除 - 全域 ctor, dtor - 二進位(ABI)兼容問題 - 不同編譯器與連接器之間不通用 ### 4.4.1 重複程式碼消除 C++ 很多時候會產生重複的程式碼: template, extern inline function, virtual function table,在不同 object file 產生同樣的程式碼,造成空間浪費。 #### Template 的重複程式碼消除 - 生成 target object 時, template 的 instance 會分別自成 section,如下面 section table 的 `.text._Z3fooIiET_S0_` 和 `.text_Z3fooIdET_S0_` ``` $ cat main.cpp #include <iostream> template<typename T> T foo(T t) { return t * (T)10; } int main() { foo(1); foo(2.0); } $ g++ -c main.cpp -o main.o $ readelf -W -S main.o There are 21 section headers, starting at offset 0x810: Section Headers: [Nr] Name Type Address Off Size ES Flg Lk Inf Al [ 0] NULL 0000000000000000 000000 000000 00 0 0 0 [ 1] .group GROUP 0000000000000000 000040 000008 04 18 20 4 [ 2] .group GROUP 0000000000000000 000048 00000c 04 18 21 4 [ 3] .text PROGBITS 0000000000000000 000054 000090 00 AX 0 0 1 [ 4] .rela.text RELA 0000000000000000 0005d0 0000d8 18 I 18 3 8 [ 5] .data PROGBITS 0000000000000000 0000e4 000000 00 WA 0 0 1 [ 6] .bss NOBITS 0000000000000000 0000e4 000001 00 WA 0 0 1 [ 7] .rodata PROGBITS 0000000000000000 0000e8 000018 00 A 0 0 8 [ 8] .text._Z3fooIiET_S0_ PROGBITS 0000000000000000 000100 000019 00 AXG 0 0 1 [ 9] .text._Z3fooIdET_S0_ PROGBITS 0000000000000000 000119 000020 00 AXG 0 0 1 [10] .rela.text._Z3fooIdET_S0_ RELA 0000000000000000 0006a8 000018 18 IG 18 9 8 [11] .init_array INIT_ARRAY 0000000000000000 000140 000008 08 WA 0 0 8 [12] .rela.init_array RELA 0000000000000000 0006c0 000018 18 I 18 11 8 [13] .comment PROGBITS 0000000000000000 000148 00002c 01 MS 0 0 1 [14] .note.GNU-stack PROGBITS 0000000000000000 000174 000000 00 0 0 1 [15] .note.gnu.property NOTE 0000000000000000 000178 000020 00 A 0 0 8 [16] .eh_frame PROGBITS 0000000000000000 000198 0000b8 00 A 0 0 8 [17] .rela.eh_frame RELA 0000000000000000 0006d8 000078 18 I 18 16 8 [18] .symtab SYMTAB 0000000000000000 000250 000288 18 19 19 8 [19] .strtab STRTAB 0000000000000000 0004d8 0000f6 00 0 0 1 [20] .shstrtab STRTAB 0000000000000000 000750 0000bb 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), l (large), p (processor specific) $ echo _Z3fooIiET_S0_ | c++filt int foo<int>(int) $ echo _Z3fooIdET_S0_ | c++filt double foo<double>(double) ``` - 如果這些重複的 instance 出現在各個不同的 target object,那麼同名的 section(`PROGBITS`) 就會有很多個,經 linker 連結後這些重複的 `PROGBITS` 會合併為一個,最後在合併進入 `.text` 中。 - 有用到繼承和 virtual function 的 class 也會遇到相同問題,在多個 object file 產生一樣的 virtual function table (`vtbl`);default ctor, default copy ctor, assignment operator 也有類似問題 - 函數級別連接 - 現代編譯器有提供的功能,對於任意一個 function 或 variable 時,就把他單獨放到 section 中,去做重複程式碼消除,最後去看有用到的才輸出到執行檔中,可以很大程度減少執行檔的大小 - 缺點: compile, link 時間變長,因為要去算各個 function 的依賴關系,然後存到 section 中,隨著數量變多,relocation 的時間也變長 - msvc: `/Gy`, gcc: `-ffunction-sections`, `-fdata-sections` ### 4.4.2 全域 ctor, dtor - 某些事情必須在 `main()` 的之前或是之後執行,像是 C++ global object 的 ctor 或 dtor - 介紹兩個 section: - `.init` 在 `main()` 被呼叫前,glibc 會先執行這裡的程式 - `.fini` 當 `main()` 正常退出時 glibc 會執行這裡的程式 ### 4.4.3 C++ ABI > 不同編譯器編譯出來的 object file 往往不行相互 link,這和符號修飾標準、變數的記憶體分布方式、function 的調用方式等有關,而這類就稱作 ABI(Application Binary Interface) 影響 ABI 的因素:硬體架構、程式語言、編譯器、連接器、作業系統 - C 語言的 ABI - premitive type 的 memory order, alignment - struct, union 的儲存與分布 - 外部符號(external linkage)與自訂義的符號的命名方式與解析 - e.g. C語言 function 符號 `_func` - function 調用方式: 參數 push 順序、return value 要放哪 - stack 的分布: 參數和 local variable 在 stack 的位置,參數傳遞方式 - register 使用約定,在 function call 那些 register 可以改,那些要保存 - C++ 的 ABI - 繼承 class 的記憶體分布: base class, virtual base class 在 inherited class 中的位置 - member function 的調用方式 - vtable 的內容排布,vtable ptr 在 object 中的位置 - template 如何實例化 - name mangling - global variable 的 ctor 與 dtor - Exception 機制 - 標準庫實作,RTTI 實現方式 - inline function - 這些 ABI 問題很多人想要統一,但是到目前為止沒有機會統一 > 32bit: System V i386 ABI > https://stackoverflow.com/questions/18133812/where-is-the-x86-64-system-v-abi-documented > https://itanium-cxx-abi.github.io/cxx-abi/ ## 4.5 Static Linking - 從一個簡單的 Hello World 開始都會使用到很多的外部函式 - 作業系統提供 API - 程式語言提供 library - 一個靜態庫就是一坨目標文件的集合,將多個 object file 打包形成的文件 - linux: `xxx.a` - windows: `xxx.lib` ### 觀察兩個系統下的 target object file - Linux 的 `ar` 工具可以看到一個 target object 使用了哪些 target object ``` $ ar -t /lib/x86_64-linux-gnu/libc.a init-first.o libc-start.o sysdep.o version.o check_fds.o libc-tls.o elf-init.o dso_handle.o ... 1742 個 ``` - Windows 可以用 `lib /list` 看使用了哪些 object file ### 觀察 printf 在哪裡 - `objdump -t lib.a | less` ``` printf.o: file format elf64-x86-64 SYMBOL TABLE: 0000000000000000 l d .text 0000000000000000 .text 0000000000000000 l d .data 0000000000000000 .data 0000000000000000 l d .bss 0000000000000000 .bss 0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack 0000000000000000 l d .note.gnu.property 0000000000000000 .note.gnu.property 0000000000000000 l d .eh_frame 0000000000000000 .eh_frame 0000000000000000 g F .text 00000000000000c9 __printf 0000000000000000 *UND* 0000000000000000 stdout 0000000000000000 *UND* 0000000000000000 .hidden __vfprintf_internal 0000000000000000 *UND* 0000000000000000 _GLOBAL_OFFSET_TABLE_ 0000000000000000 *UND* 0000000000000000 __stack_chk_fail 0000000000000000 g F .text 00000000000000c9 _IO_printf 0000000000000000 g F .text 00000000000000c9 printf ``` - 範例: 嘗試手動編譯看看 `hello.c` - 底下的範例最後沒編譯出來: 因為實際 printf 還依賴了其他東西 - 而這就 linker 在做的事情 ``` $ gcc -c -fno-builtin hello.c $ mkdir libc && cd libc && ar -x /lib/x86_64-linux-gnu/libc.a $ ld hello.o printf.o ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000 ld: libc/printf.o: in function `__printf': (.text+0x75): undefined reference to `stdout' ld: (.text+0xa8): undefined reference to `__vfprintf_internal' ld: (.text+0xc5): undefined reference to `__stack_chk_fail' ld: a.out: hidden symbol `__vfprintf_internal' isn't defined ld: final link failed: bad value ``` - 但即使直接去 link `libc.a` 還是不行得到完整的程式,因為 linux 上的 lib 很多 - 可以用 `gcc -static -v -fno-builtin hello.c` 去觀察 :::spoiler `gcc -static -v -fno-builtin hello.c` ``` $ gcc -static -v -fno-builtin hello.c Using built-in specs. COLLECT_GCC=gcc COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper OFFLOAD_TARGET_NAMES=nvptx-none:hsa OFFLOAD_TARGET_DEFAULT=1 Target: x86_64-linux-gnu Configured with: ../src/configure -v --with-pkgversion='Ubuntu 9.4.0-1ubuntu1~20.04.2' --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-9-9QDOt0/gcc-9-9.4.0/debian/tmp-nvptx/usr,hsa --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu Thread model: posix gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.2) COLLECT_GCC_OPTIONS='-static' '-v' '-fno-builtin' '-mtune=generic' '-march=x86-64' /usr/lib/gcc/x86_64-linux-gnu/9/cc1 -quiet -v -imultiarch x86_64-linux-gnu hello.c -quiet -dumpbase hello.c -mtune=generic -march=x86-64 -auxbase hello -version -fno-builtin -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o /tmp/ccE75qZ4.s GNU C17 (Ubuntu 9.4.0-1ubuntu1~20.04.2) version 9.4.0 (x86_64-linux-gnu) compiled by GNU C version 9.4.0, GMP version 6.2.0, MPFR version 4.0.2, MPC version 1.1.0, isl version isl-0.22.1-GMP GGC heuristics: --param ggc-min-expand=97 --param ggc-min-heapsize=125590 ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu" ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/9/include-fixed" ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/9/../../../../x86_64-linux-gnu/include" #include "..." search starts here: #include <...> search starts here: /usr/lib/gcc/x86_64-linux-gnu/9/include /usr/local/include /usr/include/x86_64-linux-gnu /usr/include End of search list. GNU C17 (Ubuntu 9.4.0-1ubuntu1~20.04.2) version 9.4.0 (x86_64-linux-gnu) compiled by GNU C version 9.4.0, GMP version 6.2.0, MPFR version 4.0.2, MPC version 1.1.0, isl version isl-0.22.1-GMP GGC heuristics: --param ggc-min-expand=97 --param ggc-min-heapsize=125590 Compiler executable checksum: 01da938ff5dc2163489aa33cb3b747a7 COLLECT_GCC_OPTIONS='-static' '-v' '-fno-builtin' '-mtune=generic' '-march=x86-64' as -v --64 -o /tmp/ccIUP9w6.o /tmp/ccE75qZ4.s GNU assembler version 2.34 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.34 COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/9/:/usr/lib/gcc/x86_64-linux-gnu/9/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/9/:/usr/lib/gcc/x86_64-linux-gnu/ LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/9/:/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/9/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/9/../../../:/lib/:/usr/lib/ COLLECT_GCC_OPTIONS='-static' '-v' '-fno-builtin' '-mtune=generic' '-march=x86-64' /usr/lib/gcc/x86_64-linux-gnu/9/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/9/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper -plugin-opt=-fresolution=/tmp/cc24MkA7.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_eh -plugin-opt=-pass-through=-lc --build-id -m elf_x86_64 --hash-style=gnu --as-needed -static -z relro /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/9/crtbeginT.o -L/usr/lib/gcc/x86_64-linux-gnu/9 -L/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/9/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/9/../../.. /tmp/ccIUP9w6.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crtn.o COLLECT_GCC_OPTIONS='-static' '-v' '-fno-builtin' '-mtune=generic' '-march=x86-64' ``` ::: - 這裡是先用編譯器前端 `cc1` 把 `hello.c` 編譯成 `/tmp/ccE75qZ4.s` ``` /usr/lib/gcc/x86_64-linux-gnu/9/cc1 -quiet -v -imultiarch x86_64-linux-gnu hello.c -quiet -dumpbase hello.c -mtune=generic -march=x86-64 -auxbase hello -version -fno-builtin -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o /tmp/ccE75qZ4.s ``` - 使用 `as` 把前面產生組語組譯 ``` as -v --64 -o /tmp/ccIUP9w6.o /tmp/ccE75qZ4.s ``` - 再用 `collect2` 去 link - `collect2` 是 `ld` 的包裝,底下會呼叫 `ld` 去做事 - 可以看到實際 link 了很多 library ``` /usr/lib/gcc/x86_64-linux-gnu/9/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/9/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper -plugin-opt=-fresolution=/tmp/cc24MkA7.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_eh -plugin-opt=-pass-through=-lc --build-id -m elf_x86_64 --hash-style=gnu --as-needed -static -z relro /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/9/crtbeginT.o -L/usr/lib/gcc/x86_64-linux-gnu/9 -L/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/9/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/9/../../.. /tmp/ccIUP9w6.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crtn.o ``` > Q: 為什麼 static 的 libc 裡面很多 object file 都只包含一個 function e.g. `printf.o` 裏頭只有 `printf()` > A: 節省空間,避免 link 到沒用的東西 ## 4.6 連接過程控制 絕大部分情況我們用 Linker 提供的默認連接規則就好,但在寫 OS Kernel、BIOS、Bootloader、嵌入式系統、driver等程式時,通常都會對 section 的位址有限制。 ### 4.6.1 Linker Script 想要控制 Linker 可以有三種方法: - 用命令列指定參數 - 將指令直接存在 object file 裏頭,通常 compiler 就是通過這種方法傳遞指令給 linker - e.g. msvc 會把指令放在 `.drectve` 中,在原始碼可以用 `#pragma comment(linker, "/INCREMENTAL:NO")` 去指定 linker 指令 - 使用 linker script - 本章主要 for `ld` - msvc 也有,叫做 Module-Definition File `.def` 可以使用 `ld -verbose` 來觀察 ld 預設使用的 linker script 或是在 `/usr/lib/x86_64-linux-gnu/ldscripts` 裏頭可以找到,副檔名代表 for 不同輸出格式: `.x` 可執行檔、`.xs` Shared Library ```console= $ ls -al /usr/lib/x86_64-linux-gnu/ldscripts ... -rw-r--r-- 1 root root 9450 Nov 30 10:16 elf_x86_64.x -rw-r--r-- 1 root root 9253 Nov 30 10:16 elf_x86_64.xbn -rw-r--r-- 1 root root 9096 Nov 30 10:16 elf_x86_64.xc -rw-r--r-- 1 root root 9376 Nov 30 10:16 elf_x86_64.xce -rw-r--r-- 1 root root 9316 Nov 30 10:16 elf_x86_64.xd -rw-r--r-- 1 root root 8997 Nov 30 10:16 elf_x86_64.xdc -rw-r--r-- 1 root root 9277 Nov 30 10:16 elf_x86_64.xdce -rw-r--r-- 1 root root 9596 Nov 30 10:16 elf_x86_64.xde -rw-r--r-- 1 root root 8958 Nov 30 10:16 elf_x86_64.xdw -rw-r--r-- 1 root root 9238 Nov 30 10:16 elf_x86_64.xdwe -rw-r--r-- 1 root root 9695 Nov 30 10:16 elf_x86_64.xe -rw-r--r-- 1 root root 9418 Nov 30 10:16 elf_x86_64.xn -rw-r--r-- 1 root root 4759 Nov 30 10:16 elf_x86_64.xr -rw-r--r-- 1 root root 8938 Nov 30 10:16 elf_x86_64.xs -rw-r--r-- 1 root root 8619 Nov 30 10:16 elf_x86_64.xsc -rw-r--r-- 1 root root 8899 Nov 30 10:16 elf_x86_64.xsce -rw-r--r-- 1 root root 9218 Nov 30 10:16 elf_x86_64.xse -rw-r--r-- 1 root root 8580 Nov 30 10:16 elf_x86_64.xsw -rw-r--r-- 1 root root 8860 Nov 30 10:16 elf_x86_64.xswe -rw-r--r-- 1 root root 4783 Nov 30 10:16 elf_x86_64.xu -rw-r--r-- 1 root root 9057 Nov 30 10:16 elf_x86_64.xw -rw-r--r-- 1 root root 9337 Nov 30 10:16 elf_x86_64.xwe ... ``` 可以 `ld -T my-own-script` 來指定自己寫的腳本 ### 4.6.2 最小的 Hello World > linux x86 syscall table: https://faculty.nps.edu/cseagle/assembly/sys_call.html > linux x86_64 syscall table: https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/ > https://syscall.sh/ 以下程式示範了如何不用 libc 甚至是 `main()` 來實作一個簡單的 Hello World 程式,主要用到兩個 syscall: `write` 和 `exit` ![image](https://hackmd.io/_uploads/rJYacJ3K6.png) ![image](https://hackmd.io/_uploads/rJBoqk3Fa.png) ![image](https://hackmd.io/_uploads/BJ6351hKa.png) ```c= #include <stdio.h> #include <string.h> char hello[] = "Hello, World!\n"; const size_t hello_size = sizeof(hello)/sizeof(char); void print() { asm ( "mov rax, 1;" "mov rdi, 0;" "mov rsi, %0;" "mov rdx, %1;" "syscall;" : : "r"(hello), "r"(hello_size) : "rdi", "rsi", "rdx", "rax" ); } void exit() { asm( "mov rax, 60;" "mov rdi, 0;" "syscall;" : : : "rax", "rdi" ); } void nomain() { print(); exit(); } ``` ```console= $ gcc -masm=intel -c -fno-builtin TinyHelloWorld.c $ ld -static -e nomain -o TinyHelloWorld TinyHelloWorld.o $ ./TinyHelloWorld Hello, World! ``` > https://stackoverflow.com/questions/5397677/how-to-set-a-variable-in-gcc-with-intel-syntax-inline-assembly > https://stackoverflow.com/questions/51905409/referencing-memory-operands-in-intel-syntax-gnu-c-inline-assembly > https://stackoverflow.com/questions/3323445/what-is-the-difference-between-asm-asm-and-asm > https://gcc.gnu.org/onlinedocs/gcc/extensions-to-the-c-language-family/how-to-use-inline-assembly-language-in-c-code.html#how-to-use-inline-assembly-language-in-c-code > https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html > https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_chapter/ld_3.html ### 4.6.3 使用 `ld` linking script - Linker script - 32 bit ```ldscript= ENTRY(nomain) SECTIONS { . = 0x08048000 + SIZEOF_HEADERS; tinytext : { *(.text) *(.data) *(.rodata) } /DISCARD/ : { *(.comment) } } ``` - 64 bit ```ldscript= ENTRY(nomain) SECTIONS { . = 0x400000 + SIZEOF_HEADERS; tinytext : { *(.text) *(.data) *(.rodata) } /DISCARD/ : { *(.comment) } } ``` :::info > 為何 ELF 裝載位置根據 CPU 固定位置是 i386: `0x08048000`, x86_64: `0x400000`? > https://stackoverflow.com/questions/18296276/base-address-of-elf > https://stackoverflow.com/questions/2966426/why-do-virtual-memory-addresses-for-linux-binaries-start-at-0x8048000 > https://www.quora.com/On-Linux-why-does-the-text-segment-start-at-0x08048000-What-is-stored-below-that-address ::: 使用上面的 linker script 去連結 TinyHelloWorld 看看,用 `readelf` 和 `objdump` 觀察結果,可以看到產生了一個 `.tinytext` section,且所有的 symbol 都在該 section,與我們指定的 link script 預期一樣。 ```console= $ readelf -W -S ./TinyHelloWorld There are 8 section headers, starting at offset 0x3e8: Section Headers: [Nr] Name Type Address Off Size ES Flg Lk Inf Al [ 0] NULL 0000000000000000 000000 000000 00 0 0 0 [ 1] .note.gnu.property NOTE 0000000000400120 000120 000020 00 A 0 0 8 [ 2] .eh_frame PROGBITS 0000000000400140 000140 000078 00 A 0 0 8 [ 3] tinytext PROGBITS 00000000004001b8 0001b8 000080 00 WAX 0 0 8 [ 4] .comment PROGBITS 0000000000000000 000238 00002b 01 MS 0 0 1 [ 5] .symtab SYMTAB 0000000000000000 000268 000108 18 6 6 8 [ 6] .strtab STRTAB 0000000000000000 000370 00002e 00 0 0 1 [ 7] .shstrtab STRTAB 0000000000000000 00039e 00004a 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), l (large), p (processor specific) $ objdump -t ./TinyHelloWorld ./TinyHelloWorld: file format elf64-x86-64 SYMBOL TABLE: 0000000000400120 l d .note.gnu.property 0000000000000000 .note.gnu.property 0000000000400140 l d .eh_frame 0000000000000000 .eh_frame 00000000004001b8 l d tinytext 0000000000000000 tinytext 0000000000000000 l d .comment 0000000000000000 .comment 0000000000000000 l df *ABS* 0000000000000000 TinyHelloWorld.c 00000000004001b8 g F tinytext 000000000000002e print 0000000000400220 g O tinytext 000000000000000f hello 0000000000400201 g F tinytext 000000000000001f nomain 00000000004001e6 g F tinytext 000000000000001b exit 0000000000400230 g O tinytext 0000000000000008 siz ``` 還可以用 `strip` 指令去把 symbol table 拔掉: ```= $ strip ./TinyHelloWorld -o TinyHelloWorld_strip $ readelf -W -S ./TinyHelloWorld_strip There are 5 section headers, starting at offset 0x270: Section Headers: [Nr] Name Type Address Off Size ES Flg Lk Inf Al [ 0] NULL 0000000000000000 000000 000000 00 0 0 0 [ 1] .note.gnu.property NOTE 0000000000400120 000120 000020 00 A 0 0 8 [ 2] .eh_frame PROGBITS 0000000000400140 000140 000078 00 A 0 0 8 [ 3] tinytext PROGBITS 00000000004001b8 0001b8 000080 00 WAX 0 0 8 [ 4] .shstrtab STRTAB 0000000000000000 000238 000031 00 0 0 1 ``` > 世界上最小的 ELF: https://github.com/arjun024/tiny-elf > https://www.muppetlabs.com/~breadbox/software/tiny/teensy.html ### 4.6.4 ld script 語法 ![image](https://hackmd.io/_uploads/rkpagehYa.png) ![image](https://hackmd.io/_uploads/rkVCge2Fp.png) 主要語法分成兩種: - 命令語句: `ENTRY(nomain)` - 賦值語句: `. = 0x400000 + SIZEOF_HEADERS;` 輸出的 Section 名稱會被輸出格式限制: e.g. 輸出 a.out 格式就只能是 `.text`, `.data`, `.bss` 指定 `/DISCARD/` 作為 section 名稱,代表裡面的段都要被丟棄 section 名稱還可以是正則表達式 可以指定檔案(object file)的seciton: `file1.o(.data)` `*(.data)` 代表 match 所有輸入 object file 的 `.data` section > https://blog.louie.lu/2016/11/06/10%E5%88%86%E9%90%98%E8%AE%80%E6%87%82-linker-scripts/ > https://evshary.com/2018/06/02/linker-script-%E7%B0%A1%E5%96%AE%E6%95%99%E5%AD%B8/ > https://wen00072.github.io/blog/2014/12/14/study-on-the-linker-script-2-setcion-command/ ### BFD (Binary File Descriptor library) 處理不同的 object file 格式的一個 library,讓 gcc 系列的工具都會使用這個 library,想要支援一種新的 object file 格式就讓 bfd 支援就好了 ```bash= $ sudo apt install binutils-dev ``` 範例: 列出 bfd 支援的格式 ```c= #include <stdio.h> #include "bfd.h" int main() { const char** t = bfd_target_list(); while(*t) { printf("%s\n", *t); t++; } } ``` ``` $ gcc -o bfd_example bfd_example.c -lbfd $ ./bfd_example elf64-x86-64 elf32-i386 elf32-iamcu elf32-x86-64 pei-i386 pei-x86-64 ... ``` ``` Me: came up with small project idea about using GNU bfd library GPT4: It sounds like you're interested in creating a small project using the GNU Binary File Descriptor (BFD) library. Here are a few project ideas: 1. **Object File Analyzer:** You can create a program that analyzes object files. This could include extracting and displaying information such as the symbol table, section headers, and relocation entries. 2. **Disassembler:** Another project could be a disassembler that uses the BFD library to read binary files and convert them into assembly language code. 3. **Linker:** You could also try creating your own linker. This would be a complex project, but it would certainly give you a deep understanding of how linking works. 4. **Binary File Editor:** Create a tool that allows for the manipulation of binary files. This could include operations like adding, deleting, or modifying sections or symbols. 5. **Cross-Platform Binary Converter:** Use the BFD library to create a tool that can convert binary files from one format to another, allowing them to be used on different platforms. Remember, working with the BFD library can be complex, as it provides a lot of low-level functionality. Be prepared to spend some time understanding how it works. Good luck with your project! ```