ELF檔案載入與動態連結(一)
關於ELF檔案的詳細介紹,推薦閱讀: ELF檔案格式分析 —— 滕啟明。
ELF檔案由ELF頭部、程式頭部表、節區頭部表以及節區4部分組成。
通過objdump工具和readelf工具,可以觀察ELF檔案詳細資訊。
ELF檔案載入過程分析
從編譯、連結和執行的角度,應用程式和庫程式的連結有兩種方式。一種是靜態連結,庫程式的二進位制程式碼連結進應用程式的映像中;一種是動態連結,庫函式的程式碼不放入應用程式映像,而是在啟動時,將庫程式的映像載入到應用程式程序空間。
在動態連結中,GNU將動態連結ELF檔案的工作做了分工:ELF映像的載入與啟動由Linux核心完成,而動態連結過程由使用者空間glibc實現。並提供了一個“直譯器”工具ld-linux.so.2。
Linux核心中,使用struct linux_binfmt結構定義一個ELF檔案載入
/* binfmts.h */ struct linux_binfmt { struct list_head lh; struct module *module; int (*load_binary)(struct linux_binprm *, struct pt_regs * regs); int (*load_shlib)(struct file *); int (*core_dump)(struct coredump_params *cprm); unsigned long min_coredump; /* minimal dump size */ };
load_binary函式指標指向的是一個可執行程式的處理函式。我們研究的ELF檔案格式的定義如下:
/* binfmt_elf.c */ static struct linux_binfmt elf_format = { .module = THIS_MODULE, .load_binary = load_elf_binary, .load_shlib = load_elf_library, .core_dump = elf_core_dump, .min_coredump = ELF_EXEC_PAGESIZE, };
Linux核心將這個資料結構註冊到可執行程式佇列,當執行一個可執行程式時,所有註冊的處理程式(這裡的load_elf_binary)逐一前來認領,若發現格式相符,則載入並啟動該程式。
static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs) { struct file *interpreter = NULL; /* to shut gcc up */ unsigned long load_addr = 0, load_bias = 0; int load_addr_set = 0; char * elf_interpreter = NULL; //"直譯器" /*......*/ struct { struct elfhdr elf_ex; struct elfhdr interp_elf_ex; } *loc; //elf頭結構 loc = kmalloc(sizeof(*loc), GFP_KERNEL); /*......*/ /* Get the exec-header */ loc->elf_ex = *((struct elfhdr *)bprm->buf); //bprm->buf是核心讀的的128位元組映像頭 retval = -ENOEXEC; /* First of all, some simple consistency checks */ if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0) //檢視檔案頭4個位元組,判斷是否為"\177ELF" goto out; if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN) //是否為可執行檔案或共享庫? goto out; /*......*/ /* Now read in all of the header information */ /*......*/ retval = kernel_read(bprm->file, loc->elf_ex.e_phoff, // kernel_read讀取整個程式頭表 (char *)elf_phdata, size); /*......*/ for (i = 0; i < loc->elf_ex.e_phnum; i++) { //這個大for迴圈功能是載入"直譯器" if (elf_ppnt->p_type == PT_INTERP) { //PT_INTERP指"直譯器"段 /* This is the program interpreter used for * shared libraries - for now assume that this * is an a.out format binary */ /*......*/ retval = kernel_read(bprm->file, elf_ppnt->p_offset, //根據位置p_offset和大小p_filesz將"直譯器"讀入 elf_interpreter, //這裡讀入的其實是"直譯器"名字"/lib/ld-linux.so.2" elf_ppnt->p_filesz); /*......*/ /* make sure path is NULL terminated */ retval = -ENOEXEC; if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0') goto out_free_interp; interpreter = open_exec(elf_interpreter); //開啟"直譯器" retval = PTR_ERR(interpreter); if (IS_ERR(interpreter)) goto out_free_interp; /* * If the binary is not readable then enforce * mm->dumpable = 0 regardless of the interpreter's * permissions. */ would_dump(bprm, interpreter); retval = kernel_read(interpreter, 0, bprm->buf, //讀入128位元組的"直譯器"頭部 BINPRM_BUF_SIZE); /*......*/ /* Get the exec headers */ loc->interp_elf_ex = *((struct elfhdr *)bprm->buf); break; } elf_ppnt++; } /*......*/ /* Some simple consistency checks for the interpreter */ if (elf_interpreter) { //對"直譯器"段的校驗 /*......*/ } /*......*/ for(i = 0, elf_ppnt = elf_phdata; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) { int elf_prot = 0, elf_flags; unsigned long k, vaddr; if (elf_ppnt->p_type != PT_LOAD) //搜尋型別為"PT_LOAD"的段(需載入的段) continue; if (unlikely (elf_brk > elf_bss)) { /*......*/ } /*......*/ } error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, 0); //建立使用者虛擬地址空間與對映檔案某連續區間的對映 /*......*/ } /*......*/ if (elf_interpreter) { //如果要載入"直譯器"(都是靜態連結的情況) unsigned long uninitialized_var(interp_map_addr); elf_entry = load_elf_interp(&loc->interp_elf_ex, interpreter, &interp_map_addr, load_bias); //載入"直譯器"映像 if (!IS_ERR((void *)elf_entry)) { /* * load_elf_interp() returns relocation * adjustment */ interp_load_addr = elf_entry; elf_entry += loc->interp_elf_ex.e_entry; //使用者空間入口地址設定為elf_entry } if (BAD_ADDR(elf_entry)) { force_sig(SIGSEGV, current); retval = IS_ERR((void *)elf_entry) ? (int)elf_entry : -EINVAL; goto out_free_dentry; } reloc_func_desc = interp_load_addr; allow_write_access(interpreter); fput(interpreter); kfree(elf_interpreter); } else { //有動態連結存在 elf_entry = loc->elf_ex.e_entry; //使用者空間入口地址設定為映像本身地址 if (BAD_ADDR(elf_entry)) { force_sig(SIGSEGV, current); retval = -EINVAL; goto out_free_dentry; } } kfree(elf_phdata); /*......*/ start_thread(regs, elf_entry, bprm->p); //修改eip與esp為新的地址,程式從核心返回應用態時的入口 /*......*/ /* error cleanup */ /*......*/ }
我們這樣一個Hello world程式,除非在編譯時指定-static選項,否則都是動態連結的:
#include <stdio.h> int main() { printf("Hello world.\n"); return 0; }
Hello world程式被記憶體載入記憶體後,控制權先交給“直譯器”,“直譯器”完成動態庫的裝載後,再將控制權交給使用者程式。
ELF檔案符號的動態解析
“直譯器”將所有動態庫檔案載入到記憶體後,形成一個連結串列,後面的符號解析過程主要是在這個連結串列中搜索符號的定義。
我們以上面Hello world程式為例,分析程式如何呼叫動態庫中的printf函式:
000000000040052d <main>: 40052d: 55 push %rbp 40052e: 48 89 e5 mov %rsp,%rbp 400531: bf d4 05 40 00 mov $0x4005d4,%edi 400536: e8 d5 fe ff ff callq 400410 <[email protected]> 40053b: b8 00 00 00 00 mov $0x0,%eax 400540: 5d pop %rbp 400541: c3 retq 400542: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 400549: 00 00 00 40054c: 0f 1f 40 00 nopl 0x0(%rax)
從彙編程式碼看到,printf呼叫被換成了puts,其中callq指令就是呼叫的puts函式,它使用了[email protected]標號。要分析這段彙編程式碼,需要先了解2個基本概念:GOT(global offset table)和PLT(procedure linkage table)
GOT
當程式引用某個動態庫中的符號時(如puts()函式),編譯連結階段並不知道這個符號在記憶體中的具體位置,只有在動態連結器將共享庫載入到記憶體後,即在執行階段,符號地址才會最終確定。因此要有一個結構來儲存符號的絕對地址,這就是GOT。這樣通過表中的某一項,就可以引用某符號的地址。
GOT表前3項是保留項,用於儲存特殊的資料結構地址,其中GOT[1]儲存共享庫列表地址,上文提到“直譯器”載入的所有共享庫以列表形式組織。GOT[2]儲存函式_dl_runtime_resolve的地址,這個函式的主要作用是找到某個符號的地址,並把它寫到相應GOT項中,然後將控制轉移到目標函式。
PLT
在編譯連結時,連結器不能將控制從一個可執行檔案或共享庫檔案轉到另外一個,因為如前面所說的,這時函式地址還未確定。因此連結器將控制轉移到PLT中的一項,PLT通過引用GOT的絕對地址,實現控制轉移。
實際在通過objdump檢視ELF檔案,GOT表在名稱為.got.plt的section中,PLT表在名稱為.plt的section中。
21 .got 00000008 0000000000600ff8 0000000000600ff8 00000ff8 2**3 CONTENTS, ALLOC, LOAD, DATA 22 .got.plt 00000030 0000000000601000 0000000000601000 00001000 2**3 CONTENTS, ALLOC, LOAD, DATA
加到上面的彙編程式碼,我們看一下[email protected]是什麼內容:
[email protected]:~/workdir$ objdump -d hello ... Disassembly of section .plt: 0000000000400400 <[email protected]>: 400400: ff 35 02 0c 20 00 pushq 0x200c02(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8> 400406: ff 25 04 0c 20 00 jmpq *0x200c04(%rip) # 601010 <_GLOBAL_OFFSET_TABLE_+0x10> 40040c: 0f 1f 40 00 nopl 0x0(%rax) 0000000000400410 <[email protected]>: 400410: ff 25 02 0c 20 00 jmpq *0x200c02(%rip) # 601018 <_GLOBAL_OFFSET_TABLE_+0x18> 400416: 68 00 00 00 00 pushq $0x0 40041b: e9 e0 ff ff ff jmpq 400400 <_init+0x20> 0000000000400420 <[email protected]>: 400420: ff 25 fa 0b 20 00 jmpq *0x200bfa(%rip) # 601020 <_GLOBAL_OFFSET_TABLE_+0x20> 400426: 68 01 00 00 00 pushq $0x1 40042b: e9 d0 ff ff ff jmpq 400400 <_init+0x20> 0000000000400430 <[email protected]>: 400430: ff 25 f2 0b 20 00 jmpq *0x200bf2(%rip) # 601028 <_GLOBAL_OFFSET_TABLE_+0x28> 400436: 68 02 00 00 00 pushq $0x2 40043b: e9 c0 ff ff ff jmpq 400400 <_init+0x20>
我們看到[email protected]包含3條指令,程式中所有對puts的呼叫都會先來到這裡。還可以看出除了PLT0([email protected]標號)外,其餘PLT項形式都是一樣的,最後的jmpq指令都是跳轉到400400即PLT0處。整個PLT表就像一個數組,除PLT0外所有指令第一條都是一個間接定址。以[email protected]為例,從0x200c02(%rip)處的註釋可以看到,這條指令跳轉到了GOT中的一項,其內容為0x601018即地址0x400406處(0x601018-0x200c02),也即[email protected]的第二條指令。(RIP相對定址模式)