關於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 <puts@plt> 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函數,它使用了puts@plt標號。要分析這段匯編代碼,需要先了解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
加到上面的匯編代碼,我們看一下puts@plt是什么內容:
ezreal@ez:~/workdir$ objdump -d hello ... Disassembly of section .plt: 0000000000400400 <puts@plt-0x10>: 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 <puts@plt>: 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 <__libc_start_main@plt>: 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 <__gmon_start__@plt>: 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>
我們看到puts@plt包含3條指令,程序中所有對puts的調用都會先來到這里。還可以看出除了PLT0(puts@plt-0x10標號)外,其余PLT項形式都是一樣的,最后的jmpq指令都是跳轉到400400即PLT0處。整個PLT表就像一個數組,除PLT0外所有指令第一條都是一個間接尋址。以puts@plt為例,從0x200c02(%rip)處的注釋可以看到,這條指令跳轉到了GOT中的一項,其內容為0x601018即地址0x400406處(0x601018-0x200c02),也即puts@plt的第二條指令。(RIP相對尋址模式)
