天天看點

ELF檔案加載與動态連結(一)ELF檔案加載過程分析ELF檔案符号的動态解析

關于ELF檔案的詳細介紹,推薦閱讀: ELF檔案格式分析 —— 滕啟明。

ELF檔案由ELF頭部、程式頭部表、節區頭部表以及節區4部分組成。

ELF檔案加載與動态連結(一)ELF檔案加載過程分析ELF檔案符号的動态解析

通過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(p[email protected]标号)外,其餘PLT項形式都是一樣的,最後的jmpq指令都是跳轉到400400即PLT0處。整個PLT表就像一個數組,除PLT0外所有指令第一條都是一個間接尋址。以[email protected]為例,從0x200c02(%rip)處的注釋可以看到,這條指令跳轉到了GOT中的一項,其内容為0x601018即位址0x400406處(0x601018-0x200c02),也即[email protected]的第二條指令。(RIP相對尋址模式)

轉載于:https://www.cnblogs.com/gm-201705/p/9901555.html