簡介 前面目的檔章節介紹 ELF 檔案格式
此章節說明如果有兩個目的檔要如何連結
這裡使用 a.c 和 b.c 兩個檔案當範例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 # a.c extern int shared; int main() { int a = 100; swap(&a, &shared); return 0; } # b.c int shared = 1; void swap(int *a, int *b) { *a ^= *b ^= *a ^= *b; } readelf -s a.o Symbol table '.symtab' contains 11 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.c 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 5: 0000000000000000 0 SECTION LOCAL DEFAULT 6 6: 0000000000000000 0 SECTION LOCAL DEFAULT 7 7: 0000000000000000 0 SECTION LOCAL DEFAULT 5 8: 0000000000000000 44 FUNC GLOBAL DEFAULT 1 main 9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND shared 10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap $ readelf -s b.o Symbol table '.symtab' contains 10 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS b.c 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 2 4: 0000000000000000 0 SECTION LOCAL DEFAULT 3 5: 0000000000000000 0 SECTION LOCAL DEFAULT 5 6: 0000000000000000 0 SECTION LOCAL DEFAULT 6 7: 0000000000000000 0 SECTION LOCAL DEFAULT 4 8: 0000000000000000 4 OBJECT GLOBAL DEFAULT 2 shared(全域符號) 9: 0000000000000000 74 FUNC GLOBAL DEFAULT 1 swap (全域符號)
空間和位址分配 連結器就是將不同目的檔合成一個
其中有兩種方法,一種將不同的目的檔直接疊加在後面
另一種方法是類似區段合併(.text 合併、.data 合併)
之後所講的空間分配為虛擬位址空間
的分配
目前連結器採用兩步連結
方式
第一步空間與位址分配,取得目的檔各區間的長度、屬性、位置
並將符號表中所有符號定義和引用收集起來統一放到全域符號表
第二步符號解析和重定,符號解析和重定,調整程式碼中位址。
連結器可用ld
來連結
1 2 3 4 5 $ld a.o b.o -e main -o ab # -e main 表示進入點是 main # 如果不加預設由 _start 進入,警告如下 # ld: warning: cannot find entry symbol _start; defaulting to 00000000004000e8
下面看目的檔的分配
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 $ objdump -h a.o Sections: Idx Name Size VMA LMA File off Algn 0 .text 0000002c 0000000000000000 0000000000000000 00000040 2**0 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE 1 .data 00000000 0000000000000000 0000000000000000 0000006c 2**0 CONTENTS, ALLOC, LOAD, DATA 2 .bss 00000000 0000000000000000 0000000000000000 0000006c 2**0 ALLOC ...... $ objdump -h b.o Sections: Idx Name Size VMA LMA File off Algn 0 .text 0000004a 0000000000000000 0000000000000000 00000040 2**0 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .data 00000004 0000000000000000 0000000000000000 0000008c 2**2 CONTENTS, ALLOC, LOAD, DATA 2 .bss 00000000 0000000000000000 0000000000000000 00000090 2**0 ALLOC $ objdump -h ab Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000076 00000000004000e8 00000000004000e8 000000e8 2**0 CONTENTS, ALLOC, LOAD, READONLY, CODE 2 .data 00000004 00000000006001b8 00000000006001b8 000001b8 2**2 CONTENTS, ALLOC, LOAD, DATA
VMA = 虛擬位址,LMA = 載入位址,一般來說會一樣
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 a.o +---------------+ | File Header | 0x40 +---------------+ | .text | 0x2C +---------------+ b.o +---------------+ | File Header | 0x40 +---------------+ | .text | 0x4a +---------------+ | .data | 0x04 +---------------+ ab.o +---------------+ | File Header | 0xe8 +---------------+ | .text | 0x76 (a.o + b.o) +---------------+ | .data | 0x04 (b.o) +---------------+ VM +---------------+ 0x6001BC | .data | Size = 0x4 +---------------+ 0x6001b8 +---------------+ 0x40015E | .text | Size = 0x76 +---------------+ 0x4000e8
這裡書上有提到 ELF 可執行檔預設由 0x08048000 開始,不過實際上卻沒有?
符號位址確定 上面第一步已完成,主要就是分配 ab 區段的 VMA 起始位址
符號解析與重定 空間位址分配好之後,就進入符號解析與重定步驟
先使用 objdump 對 a.o 進行反組譯
程式裡面使用的都是 VMA,例如下面的 main 起始位址就是 0x0
未進行空見分配前目的檔都是以 0x0 開始,空間分配結束才會知道 VMA
此函式的 main 佔用 0x2b byte,共 12 行指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $ objdump -d a.o a.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <main>: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 48 83 ec 10 sub $0x10,%rsp 8: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp) f: 48 8d 45 fc lea -0x4(%rbp),%rax 13: be 00 00 00 00 mov $0x0,%esi 18: 48 89 c7 mov %rax,%rdi 1b: b8 00 00 00 00 mov $0x0,%eax 20: e8 00 00 00 00 callq 25 <main+0x25> 25: b8 00 00 00 00 mov $0x0,%eax 2a: c9 leaveq 2b: c3 retq
下面指令是將shared
的引用指定到esi
暫存器
1 13: be 00 00 00 00 mov $0x0,%esi
全域建構與解構 實際上程式並不是由 main 開始執行
main 之前會做一些事情,例如堆積分配初始化 malloc、free
建構式也是在 main 之前執行,一般入口為 _start (Glibs)
因此在 ELF 中還定義兩種區段,.init 和 .fini
ABI 不同編譯器編出的目的檔不一定可以連結
如果要能連結需要滿足一些條件
1 2 3 4 同樣的目的檔格式 同樣的名稱修飾標準 同樣的記憶體配置 ......
這些和可執行檔相容性相關的內容稱為 ABI
ABI 是二進位層面的介面,例如 C++ 物件記憶體配置是 ABI 的一部分
有一個包好的函示庫為 libc.a 位於/usr/lib/x86_64-linux-gnu$
使用ar
來看此函示庫包了什麼目的檔
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 $ar -t libc.a 1 init-first.o 2 libc-start.o 3 sysdep.o 4 version.o 5 check_fds.o 6 libc-tls.o 7 elf-init.o 8 dso_handle.o 9 errno.o 10 init-arch.o 11 errno-loc.o 12 hp-timing.o 13 iconv_open.o 14 iconv.o 15 iconv_close.o 16 gconv_open.o 17 gconv.o 18 gconv_close.o 19 gconv_db.o 20 gconv_conf.o 21 gconv_builtin.o 22 gconv_simple.o 23 gconv_trans.o 24 gconv_cache.o 25 gconv_dl.o 26 catnames.o 27 mb_cur_max.o 28 setlocale.o 29 findlocale.o 30 loadlocale.o 31 loadarchive.o 32 localeconv.o 33 nl_langinfo.o ...... # 查看符號表 $objdump -t libc.a 5977 printf.o: file format elf64-x86-64 5978 5979 SYMBOL TABLE: 5980 0000000000000000 l d .text 0000000000000000 .text 5981 0000000000000000 l d .data 0000000000000000 .data 5982 0000000000000000 l d .bss 0000000000000000 .bss 5983 0000000000000000 l d .comment 0000000000000000 .comment 5984 0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack 5985 0000000000000000 l d .eh_frame 0000000000000000 .eh_frame 5986 0000000000000000 g F .text 000000000000009e __printf 5987 0000000000000000 *UND* 0000000000000000 stdout 5988 0000000000000000 *UND* 0000000000000000 vfprintf 5989 0000000000000000 g F .text 000000000000009e printf 5990 0000000000000000 g F .text 000000000000009e _IO_printf
最小的程式 參考
一般的 Helloworld 使用 main 加 printf
不過這邊實際上會用到 C 的函示庫,這裡希望脫離函示庫的概念
但實際上測試後並沒有出現字串,不知是否是平台問題
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 char * str = "Hello world!\n"; void print() { asm( "movl $13,%%edx \n\t" "movl $0,%%ecx \n\t" "movl $0,%%ebx \n\t" "movl $4,%%eax \n\t" "int $0x80 \n\t" ::"r"(str):"edx","ecx","ebx"); } void exit() { asm( "movl $42,%ebx \n\t" "movl $1,%eax \n\t" "int $0x80 \n\t"); } void nomain() { print(); exit(); }