加載和動態鏈接
從編譯/鏈接和運行的角度看,應用程序和庫程序的連接有兩種方式。
一種是固定的、靜態的連接,就是把需要用到的庫函數的目標代碼(二進制)代碼從程序庫中抽取出來,鏈接進應用軟件的目標映像中;
另一種是動態鏈接,是指庫函數的代碼並不進入應用軟件的目標映像,應用軟件在編譯/鏈接階段並不完成跟庫函數的鏈接,而是把函數庫的映像也交給用戶,到啓動應用軟件目標映像運行時才把程序庫的映像也裝入用戶空間(並加以定位),再完成應用軟件與庫函數的連接。
這樣,就有了兩種不同的ELF格式映像。
- 一種是靜態鏈接的,在裝入/啓動其運行時無需裝入函數庫映像、也無需進行動態連接。
- 另一種是動態連接,需要在裝入/啓動其運行時同時裝入函數庫映像並進行動態鏈接。
Linux內核既支持靜態鏈接的ELF映像,也支持動態鏈接的ELF映像,而且裝入/啓動ELF映像必需由內核完成,而動態連接的實現則既可以在內核中完成,也可在用戶空間完成。
因此,GNU把對於動態鏈接ELF映像的支持作了分工:
把ELF映像的裝入/啓動入在Linux內核中;而把動態鏈接的實現放在用戶空間(glibc),併爲此提供一個稱爲”解釋器”(ld-linux.so.2)的工具軟件,而解釋器的裝入/啓動也由內核負責,這在後面我們分析ELF文件的加載時就可以看到
這部分主要說明ELF文件在內核空間的加載過程,下一部分對用戶空間符號的動態解析過程進行說明。
Linux可執行文件類型的註冊機制
在說明ELF文件的加載過程以前,我們先回答一個問題,就是:
爲什麼Linux可以運行ELF文件? 內核對所支持的每種可執行的程序類型都有個struct linux_binfmt的數據結構,這個結構我們在前面的博文中我們已經提到, 但是沒有詳細講. 其定義如下
/* * This structure defines the functions that are used to load the binary formats that * linux accepts. */ struct linux_binfmt { struct list_head lh; struct module *module; int (*load_binary)(struct linux_binprm *); int (*load_shlib)(struct file *); int (*core_dump)(struct coredump_params *cprm); unsigned long min_coredump; /* minimal dump size */ };
linux_binfmt定義在include/linux/binfmts.h中
linux支持其他不同格式的可執行程序, 在這種方式下, linux能運行其他操作系統所編譯的程序, 如MS-DOS程序, 活BSD Unix的COFF可執行格式, 因此linux內核用struct linux_binfmt來描述各種可執行程序。
linux內核對所支持的每種可執行的程序類型都有個struct linux_binfmt的數據結構,
其提供了3種方法來加載和執行可執行程序
函數 | 描述 |
---|---|
load_binary | 通過讀存放在可執行文件中的信息爲當前進程建立一個新的執行環境 |
load_shlib | 用於動態的把一個共享庫捆綁到一個已經在運行的進程, 這是由uselib()系統調用激活的 |
core_dump | 在名爲core的文件中, 存放當前進程的執行上下文. 這個文件通常是在進程接收到一個缺省操作爲”dump”的信號時被創建的, 其格式取決於被執行程序的可執行類型 |
所有的linux_binfmt對象都處於一個鏈表中, 第一個元素的地址存放在formats變量中, 可以通過調用register_binfmt()和unregister_binfmt()函數在鏈表中插入和刪除元素, 在系統啓動期間, 爲每個編譯進內核的可執行格式都執行registre_fmt()函數. 當實現了一個新的可執行格式的模塊正被裝載時, 也執行這個函數, 當模塊被卸載時, 執行unregister_binfmt()函數.
當我們執行一個可執行程序的時候, 內核會list_for_each_entry遍歷所有註冊的linux_binfmt對象, 對其調用load_binrary方法來嘗試加載, 直到加載成功爲止.
其中的load_binary函數指針指向的就是一個可執行程序的處理函數。而我們研究的ELF文件格式的linux_binfmt結構對象elf_format, 定義如下, 在/fs/binfmt.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, .hasvdso = 1 };
要支持ELF文件的運行,則必須向內核登記註冊elf_format這個linux_binfmt類型的數據結構,加入到內核支持的可執行程序的隊列中。內核提供兩個函數來完成這個功能,一個註冊,一個註銷,即:
int register_binfmt(struct linux_binfmt * fmt) int unregister_binfmt(struct linux_binfmt * fmt)
當需要運行一個程序時,則掃描這個隊列,依次調用各個數據結構所提供的load處理程序來進行加載工作,ELF中加載程序即爲load_elf_binary
,內核中已經註冊的可運行文件結構linux_binfmt會讓其所屬的加載程序load_binary逐一前來認領需要運行的程序binary,如果某個格式的處理程序發現相符後,便執行該格式映像的裝入和啓動
內核空間的加載過程load_elf_binary
內核中實際執行execv()或execve()系統調用的程序是do_execve(),這個函數先打開目標映像文件,並從目標文件的頭部(第一個字節開始)讀入若干(當前Linux內核中是128)字節(實際上就是填充ELF文件頭,下面的分析可以看到),然後調用另一個函數search_binary_handler(),在此函數裏面,它會搜索我們上面提到的Linux支持的可執行文件類型隊列,讓各種可執行程序的處理程序前來認領和處理。如果類型匹配,則調用load_binary函數指針所指向的處理函數來處理目標映像文件。
在ELF文件格式中,處理函數是load_elf_binary函數,下面主要就是分析load_elf_binary函數的執行過程(說明:因爲內核中實際的加載需要涉及到很多東西,這裏只關注跟ELF文件的處理相關的代碼)
其流程如下:
- 填充並且檢查目標程序ELF頭部
- load_elf_phdrs加載目標程序的程序頭表
- 如果需要動態鏈接, 則尋找和處理解釋器段
- 檢查並讀取解釋器的程序表頭
- 裝入目標程序的段segment
- create_elf_tables填寫目標文件的參數環境變量等必要信息
- start_kernel宏準備進入新的程序入口
填充並且檢查目標程序ELF頭部
struct pt_regs *regs = current_pt_regs(); struct { struct elfhdr elf_ex; struct elfhdr interp_elf_ex; } *loc; struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE; loc = kmalloc(sizeof(*loc), GFP_KERNEL); if (!loc) { retval = -ENOMEM; goto out_ret; } /* Get the exec-header 使用映像文件的前128個字節對bprm->buf進行了填充 */ loc->elf_ex = *((struct elfhdr *)bprm->buf); retval = -ENOEXEC; /* First of all, some simple consistency checks 比較文件頭的前四個字節 。*/ if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0) goto out; /* 還要看映像的類型是否ET_EXEC和ET_DYN之一;前者表示可執行映像,後者表示共享庫 */ if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN) goto out;
在load_elf_binary之前,內核已經使用映像文件的前128個字節對bprm->buf進行了填充,563行就是使用這此信息填充映像的文件頭(具體數據結構定義見第一部分,ELF文件頭節),然後567行就是比較文件頭的前四個字節,查看是否是ELF文件類型定義的“\177ELF”。除這4個字符以外,還要看映像的類型是否ET_EXEC和ET_DYN之一;前者表示可執行映像,後者表示共享庫。
load_elf_phdrs加載目標程序的程序頭表
elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file); if (!elf_phdata) goto out;
而這個load_elf_phdrs函數就是通過kernel_read讀入整個program header table。從函數代碼中可以看到,一個可執行程序必須至少有一個段(segment),而所有段的大小之和不能超過64K(65536u)
/** * load_elf_phdrs() - load ELF program headers * @elf_ex: ELF header of the binary whose program headers should be loaded * @elf_file: the opened ELF binary file * * Loads ELF program headers from the binary file elf_file, which has the ELF * header pointed to by elf_ex, into a newly allocated array. The caller is * responsible for freeing the allocated data. Returns an ERR_PTR upon failure. */ static struct elf_phdr *load_elf_phdrs(struct elfhdr *elf_ex, struct file *elf_file) { struct elf_phdr *elf_phdata = NULL; int retval, size, err = -1; /* * If the size of this structure has changed, then punt, since * we will be doing the wrong thing. */ if (elf_ex->e_phentsize != sizeof(struct elf_phdr)) goto out; /* Sanity check the number of program headers... */ if (elf_ex->e_phnum < 1 || elf_ex->e_phnum > 65536U / sizeof(struct elf_phdr)) goto out; /* ...and their total size. */ size = sizeof(struct elf_phdr) * elf_ex->e_phnum; if (size > ELF_MIN_ALIGN) goto out; elf_phdata = kmalloc(size, GFP_KERNEL); if (!elf_phdata) goto out; /* Read in the program headers */ retval = kernel_read(elf_file, elf_ex->e_phoff, (char *)elf_phdata, size); if (retval != size) { err = (retval < 0) ? retval : -EIO; goto out; } /* Success! */ err = 0; out: if (err) { kfree(elf_phdata); elf_phdata = NULL; } return elf_phdata; }
如果需要動態鏈接, 則尋找和處理解釋器段
這個for循環的目的在於尋找和處理目標映像的”解釋器”段。
“解釋器”段的類型爲PT_INTERP,
找到後就根據其位置的p_offset和大小p_filesz把整個”解釋器”段的內容讀入緩衝區。
“解釋器”段實際上只是一個字符串,
即解釋器的文件名,如”/lib/ld-linux.so.2”, 或者64位機器上對應的叫做”/lib64/ld-linux-x86-64.so.2”
有了解釋器的文件名以後,就通過open_exec()打開這個文件,再通過kernel_read()讀入其開關128個字節,即解釋器映像的頭部。*
for (i = 0; i < loc->elf_ex.e_phnum; i++) { /* 3.1 檢查是否有需要加載的解釋器 */ if (elf_ppnt->p_type == PT_INTERP) { /* This is the program interpreter used for * shared libraries - for now assume that this * is an a.out format binary */ /* 3.2 根據其位置的p_offset和大小p_filesz把整個"解釋器"段的內容讀入緩衝區 */ retval = kernel_read(bprm->file, elf_ppnt->p_offset, elf_interpreter, elf_ppnt->p_filesz); if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0') goto out_free_interp; /* 3.3 通過open_exec()打開解釋器文件 */ interpreter = open_exec(elf_interpreter); /* Get the exec headers 3.4 通過kernel_read()讀入解釋器的前128個字節,即解釋器映像的頭部。*/ retval = kernel_read(interpreter, 0, (void *)&loc->interp_elf_ex, sizeof(loc->interp_elf_ex)); break; } elf_ppnt++; }
可以使用readelf -l查看program headers, 其中的INTERP段標識了我們程序所需要的解釋器
readelf -l testelf_normal
readelf -l testelf_dynamic
readelf -l test_static
我們可以看到testelf_normal和testelf_dynamic都是動態鏈接的需要解釋器
而testelf_static則是靜態鏈接的不需要解釋器
檢查並讀取解釋器的程序表頭
如果需要加載解釋器, 前面經過一趟for循環已經找到了需要的解釋器信息elf_interpreter, 他也是當作一個ELF文件, 因此跟目標可執行程序一樣, 我們需要load_elf_phdrs加載解釋器的程序頭表program header table
/* 4. 檢查並讀取解釋器的程序表頭 */ /* Some simple consistency checks for the interpreter 4.1 檢查解釋器頭的信息 */ if (elf_interpreter) { retval = -ELIBBAD; /* Not an ELF interpreter */ /* Load the interpreter program headers 4.2 讀入解釋器的程序頭 */ interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex, interpreter); if (!interp_elf_phdata) goto out_free_dentry;
至此我們已經把目標執行程序和其所需要的解釋器都加載初始化, 並且完成檢查工作, 也加載了程序頭表program header table, 下面開始加載程序的段信息
裝入目標程序的段segment
這段代碼從目標映像的程序頭中搜索類型爲PT_LOAD的段(Segment)。在二進制映像中,只有類型爲PT_LOAD的段纔是需要裝入的。當然在裝入之前,需要確定裝入的地址,只要考慮的就是頁面對齊,還有該段的p_vaddr域的值(上面省略這部分內容)。確定了裝入地址後,就通過elf_map()建立用戶空間虛擬地址空間與目標映像文件中某個連續區間之間的映射,其返回值就是實際映射的起始地址。
*/ for(i = 0, elf_ppnt = elf_phdata; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) { /* 5.1 搜索PT_LOAD的段, 這個是需要裝入的 */ if (elf_ppnt->p_type != PT_LOAD) continue; /* 5.2 檢查地址和頁面的信息 */ //////////// // ...... /////////// /* 5.3 虛擬地址空間與目標映像文件的映射 確定了裝入地址後, 就通過elf_map()建立用戶空間虛擬地址空間 與目標映像文件中某個連續區間之間的映射, 其返回值就是實際映射的起始地址 */ error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size); }
填寫程序的入口地址
完成了目標程序和解釋器的加載, 同時目標程序的各個段也已經加載到內存了, 我們的目標程序已經準備好了要執行了, 但是還缺少一樣東西, 就是我們程序的入口地址, 沒有入口地址, 操作系統就不知道從哪裏開始執行內存中加載好的可執行映像
這段程序的邏輯非常簡單: 如果需要裝入解釋器,就通過load_elf_interp裝入其映像, 並把將來進入用戶空間的入口地址設置成load_elf_interp()的返回值,即解釋器映像的入口地址。
而若不裝入解釋器,那麼這個入口地址就是目標映像本身的入口地址。
if (elf_interpreter) { unsigned long interp_map_addr = 0; elf_entry = load_elf_interp(&loc->interp_elf_ex, interpreter, &interp_map_addr, load_bias, interp_elf_phdata); /* 入口地址是解釋器映像的入口地址 */ } else { /* 入口地址是目標程序的入口地址 */ elf_entry = loc->elf_ex.e_entry; } }
create_elf_tables填寫目標文件的參數環境變量等必要信息
在完成裝入,啓動用戶空間的映像運行之前,還需要爲目標映像和解釋器準備好一些有關的信息,這些信息包括常規的argc、envc等等,還有一些“輔助向量(Auxiliary Vector)”。這些信息需要複製到用戶空間,使它們在CPU進入解釋器或目標映像的程序入口時出現在用戶空間堆棧上。這裏的create_elf_tables()就起着這個作用。
install_exec_creds(bprm); retval = create_elf_tables(bprm, &loc->elf_ex, load_addr, interp_load_addr); if (retval < 0) goto out; /* N.B. passed_fileno might not be initialized? */ current->mm->end_code = end_code; current->mm->start_code = start_code; current->mm->start_data = start_data; current->mm->end_data = end_data; current->mm->start_stack = bprm->p;
start_thread宏準備進入新的程序入口
最後,start_thread()這個宏操作會將eip和esp改成新的地址,就使得CPU在返回用戶空間時就進入新的程序入口。如果存在解釋器映像,那麼這就是解釋器映像的程序入口,否則就是目標映像的程序入口。那麼什麼情況下有解釋器映像存在,什麼情況下沒有呢?如果目標映像與各種庫的鏈接是靜態鏈接,因而無需依靠共享庫、即動態鏈接庫,那就不需要解釋器映像;否則就一定要有解釋器映像存在。
start_thread宏是一個體繫結構相關的函數,請定義可以參照http://lxr.free-electrons.com/ident?v=4.6;i=start_thread
總結
簡單來說可以分成這幾步
- 讀取並檢查目標可執行程序的頭信息, 檢查完成後加載目標程序的程序頭表
- 如果需要解釋器則讀取並檢查解釋器的頭信息, 檢查完成後加載解釋器的程序頭表
- 裝入目標程序的段segment, 這些纔是目標程序二進制代碼中的真正可執行映像
- 填寫程序的入口地址(如果有解釋器則填入解釋器的入口地址, 否則直接填入可執行程序的入口地址)
- create_elf_tables填寫目標文件的參數環境變量等必要信息
- start_kernel宏準備進入新的程序入口
ELF文件中符號的動態解析過程
前面我們提到了內核空間中ELF文件的加載工作
內核的工作
- 內核首先讀取ELF文件頭部,再讀如各種數據結構,從這些數據結構中可知各段或節的地址及標識,然後調用mmap()把找到的可加載段的內容加載到內存中。同時讀取段標記,以標識該段在內存中是否可讀、可寫、可執行。其中,文本段是程序代碼,只讀且可執行,而數據段是可讀且可寫。
- 從PT_INTERP的段中找到所對應的動態鏈接器名稱,並加載動態鏈接器。通常是/lib/ld-linux.so.2.
- 內核把新進程的堆棧中設置一些標記對,以指示動態鏈接器的相關操作。
- 內核把控制權傳遞給動態鏈接器。
動態鏈接器的工作並不是在內核空間完成的, 而是在用戶空間完成的, 比如C語言程序則交給C運行時庫來完成, 這個並不是我們今天內核學習的重點, 而是由glic完成的,但是其一般過程如下
動態鏈接器的工作
- 動態鏈接器檢查程序對共享庫的依賴性,並在需要時對其進行加載。
- 動態鏈接器對程序的外部引用進行重定位,並告訴程序其引用的外部變量/函數的地址,此地址位於共享庫被加載在內存的區間內。動態鏈接還有一個延遲定位的特性,即只有在“真正”需要引用符號時才重定位,這對提高程序運行效率有極大幫助。
- 動態鏈接器執行在ELF文件中標記爲.init的節的代碼,進行程序運行的初始化。 動態鏈接器把控制傳遞給程序,從ELF文件頭部中定義的程序進入點(main)開始執行。在a.out格式和ELF格式中,程序進入點的值是顯式存在的,而在COFF格式中則是由規範隱含定義。
- 程序開始執行
具體的信息可以參照 Intel平臺下Linux中ELF文件動態鏈接的加載、解析及實例分析(一): 加載 Intel平臺下linux中ELF文件動態鏈接的加載、解析及實例分析(二): 函數解析與卸載
附錄(load_elf_binary函數註釋)
static int load_elf_binary(struct linux_binprm *bprm) { 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; unsigned long error; struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL; unsigned long elf_bss, elf_brk; int retval, i; unsigned long elf_entry; unsigned long interp_load_addr = 0; unsigned long start_code, end_code, start_data, end_data; unsigned long reloc_func_desc __maybe_unused = 0; int executable_stack = EXSTACK_DEFAULT; /* 從寄存器重獲取參數信息 */ struct pt_regs *regs = current_pt_regs(); struct { struct elfhdr elf_ex; struct elfhdr interp_elf_ex; } *loc; struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE; loc = kmalloc(sizeof(*loc), GFP_KERNEL); if (!loc) { retval = -ENOMEM; goto out_ret; } /* 1 填充並且檢查ELF頭部 */ /* Get the exec-header 1.1 填充ELF頭信息 在load_elf_binary之前 內核已經使用映像文件的前128個字節對bprm->buf進行了填充, 這裏使用這此信息填充映像的文件頭 */ loc->elf_ex = *((struct elfhdr *)bprm->buf); retval = -ENOEXEC; /* 1.2 First of all, some simple consistency checks 比較文件頭的前四個字節,查看是否是ELF文件類型定義的"\177ELF"*/ if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0) goto out; /* 1.3 除前4個字符以外,還要看映像的類型是否ET_EXEC和ET_DYN之一;前者表示可執行映像,後者表示共享庫 */ if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN) goto out; /* 1.4 檢查特定的目標機器標識 */ if (!elf_check_arch(&loc->elf_ex)) goto out; if (!bprm->file->f_op->mmap) goto out; /* 2. load_elf_phdrs 加載程序頭表 load_elf_phdrs函數就是通過kernel_read讀入整個program header table 從函數代碼中可以看到,一個可執行程序必須至少有一個段(segment), 而所有段的大小之和不能超過64K。 */ elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file); if (!elf_phdata) goto out; /* bss段,brk段先初始化爲0 */ elf_ppnt = elf_phdata; elf_bss = 0; elf_brk = 0; /* code代碼段 */ start_code = ~0UL; end_code = 0; /* data數據段 */ start_data = 0; end_data = 0; /* 3. 尋找和處理解釋器段 這個for循環的目的在於尋找和處理目標映像的"解釋器"段。 "解釋器"段的類型爲PT_INTERP, 找到後就根據其位置的p_offset和大小p_filesz把整個"解釋器"段的內容讀入緩衝區。 "解釋器"段實際上只是一個字符串, 即解釋器的文件名,如"/lib/ld-linux.so.2"。 有了解釋器的文件名以後,就通過open_exec()打開這個文件, 再通過kernel_read()讀入其開關128個字節,即解釋器映像的頭部。*/ for (i = 0; i < loc->elf_ex.e_phnum;/* e_phnumc存儲了程序頭表的數目*/ i++) { /* 3.1 解釋器"段的類型爲PT_INTERP */ if (elf_ppnt->p_type == PT_INTERP) { /* This is the program interpreter used for * shared libraries - for now assume that this * is an a.out format binary */ retval = -ENOEXEC; if (elf_ppnt->p_filesz > PATH_MAX || elf_ppnt->p_filesz < 2) goto out_free_ph; retval = -ENOMEM; /* 爲動態連接器分配空間並讀取加載 */ elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL); if (!elf_interpreter) goto out_free_ph; /* 3.2 根據其位置的p_offset和大小p_filesz把整個"解釋器"段的內容讀入緩衝區 */ retval = kernel_read(bprm->file, elf_ppnt->p_offset, elf_interpreter, elf_ppnt->p_filesz); if (retval != elf_ppnt->p_filesz) { if (retval >= 0) retval = -EIO; goto out_free_interp; } /* make sure path is NULL terminated */ retval = -ENOEXEC; if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0') goto out_free_interp; /* 3.3 通過open_exec()打開解釋器文件 內核把新進程的堆棧中設置一些標記對, 以指示動態鏈接器的相關操作,詳見open_exec實現 */ 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); /* Get the exec headers 3.4 通過kernel_read()讀入解釋器的前128個字節,即解釋器映像的頭部。*/ retval = kernel_read(interpreter, 0, (void *)&loc->interp_elf_ex, sizeof(loc->interp_elf_ex)); if (retval != sizeof(loc->interp_elf_ex)) { if (retval >= 0) retval = -EIO; goto out_free_dentry; } break; } /* 循環檢查所有的程序頭看是否有動態連接器 */ elf_ppnt++; } elf_ppnt = elf_phdata; for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) switch (elf_ppnt->p_type) { case PT_GNU_STACK: if (elf_ppnt->p_flags & PF_X) executable_stack = EXSTACK_ENABLE_X; else executable_stack = EXSTACK_DISABLE_X; break; case PT_LOPROC ... PT_HIPROC: retval = arch_elf_pt_proc(&loc->elf_ex, elf_ppnt, bprm->file, false, &arch_state); if (retval) goto out_free_dentry; break; } /* 4. 檢查並讀取解釋器的程序表頭 */ /* Some simple consistency checks for the interpreter 4.1 檢查解釋器頭的信息 */ /* 檢查是否由動態連接器,無論是否有動態連接器都會執行elf文件 */ if (elf_interpreter) { retval = -ELIBBAD; /* Not an ELF interpreter */ if (memcmp(loc->interp_elf_ex.e_ident, ELFMAG, SELFMAG) != 0) goto out_free_dentry; /* Verify the interpreter has a valid arch */ if (!elf_check_arch(&loc->interp_elf_ex)) goto out_free_dentry; /* Load the interpreter program headers 4.2 讀入解釋器的程序頭 */ interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex, interpreter); if (!interp_elf_phdata) goto out_free_dentry; /* Pass PT_LOPROC..PT_HIPROC headers to arch code */ elf_ppnt = interp_elf_phdata; for (i = 0; i < loc->interp_elf_ex.e_phnum; i++, elf_ppnt++) switch (elf_ppnt->p_type) { case PT_LOPROC ... PT_HIPROC: retval = arch_elf_pt_proc(&loc->interp_elf_ex, elf_ppnt, interpreter, true, &arch_state); if (retval) goto out_free_dentry; break; } } /* * Allow arch code to reject the ELF at this point, whilst it's * still possible to return an error to the code that invoked * the exec syscall. */ retval = arch_check_elf(&loc->elf_ex, !!interpreter, &loc->interp_elf_ex, &arch_state); if (retval) goto out_free_dentry; /* Flush all traces of the currently running executable 在此清除掉了父進程的所有相關代碼 */ retval = flush_old_exec(bprm); if (retval) goto out_free_dentry; /* Do this immediately, since STACK_TOP as used in setup_arg_pages may depend on the personality. */ /* 設置elf可執行文件的特性 */ SET_PERSONALITY2(loc->elf_ex, &arch_state); if (elf_read_implies_exec(loc->elf_ex, executable_stack)) current->personality |= READ_IMPLIES_EXEC; if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space) current->flags |= PF_RANDOMIZE; setup_new_exec(bprm); /* Do this so that we can load the interpreter, if need be. We will change some of these later 爲下面的動態連接器執行獲取內核空間page */ retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), executable_stack); if (retval < 0) goto out_free_dentry; current->mm->start_stack = bprm->p; /* Now we do a little grungy work by mmapping the ELF image into the correct location in memory. 5 裝入目標程序的段segment 這段代碼從目標映像的程序頭中搜索類型爲PT_LOAD的段(Segment)。在二進制映像中,只有類型爲PT_LOAD的段纔是需要裝入的。 當然在裝入之前,需要確定裝入的地址,只要考慮的就是頁面對齊,還有該段的p_vaddr域的值(上面省略這部分內容)。 確定了裝入地址後,就通過elf_map()建立用戶空間虛擬地址空間與目標映像文件中某個連續區間之間的映射,其返回值就是實際映射的起始地址。 */ /* 按照先前獲取的程序頭表,循環將所有的可執行文件加載到內存中 */ 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; unsigned long total_size = 0; /* 5.1 搜索PT_LOAD的段, 這個是需要裝入的 */ if (elf_ppnt->p_type != PT_LOAD) continue; if (unlikely (elf_brk > elf_bss)) { unsigned long nbyte; /* 5.2 檢查地址和頁面的信息 */ /* There was a PT_LOAD segment with p_memsz > p_filesz before this one. Map anonymous pages, if needed, and clear the area. */ retval = set_brk(elf_bss + load_bias, elf_brk + load_bias); if (retval) goto out_free_dentry; nbyte = ELF_PAGEOFFSET(elf_bss); if (nbyte) { nbyte = ELF_MIN_ALIGN - nbyte; if (nbyte > elf_brk - elf_bss) nbyte = elf_brk - elf_bss; if (clear_user((void __user *)elf_bss + load_bias, nbyte)) { /* * This bss-zeroing can fail if the ELF * file specifies odd protections. So * we don't check the return value */ } } } if (elf_ppnt->p_flags & PF_R) elf_prot |= PROT_READ; if (elf_ppnt->p_flags & PF_W) elf_prot |= PROT_WRITE; if (elf_ppnt->p_flags & PF_X) elf_prot |= PROT_EXEC; elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE; vaddr = elf_ppnt->p_vaddr; if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) { elf_flags |= MAP_FIXED; } else if (loc->elf_ex.e_type == ET_DYN) { /* Try and get dynamic programs out of the way of the * default mmap base, as well as whatever program they * might try to exec. This is because the brk will * follow the loader, and is not movable. */ load_bias = ELF_ET_DYN_BASE - vaddr; if (current->flags & PF_RANDOMIZE) load_bias += arch_mmap_rnd(); load_bias = ELF_PAGESTART(load_bias); total_size = total_mapping_size(elf_phdata, loc->elf_ex.e_phnum); if (!total_size) { retval = -EINVAL; goto out_free_dentry; } } /* 5.3 虛擬地址空間與目標映像文件的映射 確定了裝入地址後, 就通過elf_map()建立用戶空間虛擬地址空間 與目標映像文件中某個連續區間之間的映射, 其返回值就是實際映射的起始地址 */ error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size); if (BAD_ADDR(error)) { retval = IS_ERR((void *)error) ? PTR_ERR((void*)error) : -EINVAL; goto out_free_dentry; } if (!load_addr_set) { load_addr_set = 1; load_addr = (elf_ppnt->p_vaddr - elf_ppnt->p_offset); if (loc->elf_ex.e_type == ET_DYN) { load_bias += error - ELF_PAGESTART(load_bias + vaddr); load_addr += load_bias; reloc_func_desc = load_bias; } } k = elf_ppnt->p_vaddr; if (k < start_code) start_code = k; if (start_data < k) start_data = k; /* * Check to see if the section's size will overflow the * allowed task size. Note that p_filesz must always be * <= p_memsz so it is only necessary to check p_memsz. */ if (BAD_ADDR(k) || elf_ppnt->p_filesz > elf_ppnt->p_memsz || elf_ppnt->p_memsz > TASK_SIZE || TASK_SIZE - elf_ppnt->p_memsz < k) { /* set_brk can never work. Avoid overflows. */ retval = -EINVAL; goto out_free_dentry; } k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz; if (k > elf_bss) elf_bss = k; if ((elf_ppnt->p_flags & PF_X) && end_code < k) end_code = k; if (end_data < k) end_data = k; k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz; if (k > elf_brk) elf_brk = k; } /* 更新讀入內存中相關信息的記錄 */ loc->elf_ex.e_entry += load_bias; elf_bss += load_bias; elf_brk += load_bias; start_code += load_bias; end_code += load_bias; start_data += load_bias; end_data += load_bias; /* Calling set_brk effectively mmaps the pages that we need * for the bss and break sections. We must do this before * mapping in the interpreter, to make sure it doesn't wind * up getting placed where the bss needs to go. */ /* 使用set_brk調整bss段的大小 */ retval = set_brk(elf_bss, elf_brk); if (retval) goto out_free_dentry; if (likely(elf_bss != elf_brk) && unlikely(padzero(elf_bss))) { retval = -EFAULT; /* Nobody gets to see this, but.. */ goto out_free_dentry; } /* 6 填寫程序的入口地址 這段程序的邏輯非常簡單: 如果需要裝入解釋器,就通過load_elf_interp裝入其映像, 並把將來進入用戶空間的入口地址設置成load_elf_interp()的返回值, 即解釋器映像的入口地址。 而若不裝入解釋器,那麼這個入口地址就是目標映像本身的入口地址。 */ if (elf_interpreter) { /* 存在動態鏈接器 內核把控制權傳遞給動態鏈接器。 動態鏈接器檢查程序對共享庫的依賴性, 並在需要時對其進行加載,由load_elf_interp完成 unsigned long interp_map_addr = 0; elf_entry = load_elf_interp(&loc->interp_elf_ex, interpreter, &interp_map_addr, load_bias, interp_elf_phdata); 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; } if (BAD_ADDR(elf_entry)) { 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)) { retval = -EINVAL; goto out_free_dentry; } } kfree(interp_elf_phdata); kfree(elf_phdata); set_binfmt(&elf_format); #ifdef ARCH_HAS_SETUP_ADDITIONAL_PAGES retval = arch_setup_additional_pages(bprm, !!elf_interpreter); if (retval < 0) goto out; #endif /* ARCH_HAS_SETUP_ADDITIONAL_PAGES */ /* 7 create_elf_tables填寫目標文件的參數環境變量等必要信息 在完成裝入,啓動用戶空間的映像運行之前,還需要爲目標映像和解釋器準備好一些有關的信息,這些信息包括常規的argc、envc等等,還有一些"輔助向量(Auxiliary Vector)"。 這些信息需要複製到用戶空間,使它們在CPU進入解釋器或目標映像的程序入口時出現在用戶空間堆棧上。這裏的create_elf_tables()就起着這個作用。 */ install_exec_creds(bprm); /* 在內存中生成elf映射表 */ retval = create_elf_tables(bprm, &loc->elf_ex, load_addr, interp_load_addr); if (retval < 0) goto out; /* N.B. passed_fileno might not be initialized? 調整內存映射內容 */ current->mm->end_code = end_code; current->mm->start_code = start_code; current->mm->start_data = start_data; current->mm->end_data = end_data; current->mm->start_stack = bprm->p; if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) { current->mm->brk = current->mm->start_brk = arch_randomize_brk(current->mm); #ifdef compat_brk_randomized current->brk_randomized = 1; #endif } if (current->personality & MMAP_PAGE_ZERO) { /* Why this, you ask??? Well SVr4 maps page 0 as read-only, and some applications "depend" upon this behavior. Since we do not have the power to recompile these, we emulate the SVr4 behavior. Sigh. */ error = vm_mmap(NULL, 0, PAGE_SIZE, PROT_READ | PROT_EXEC, MAP_FIXED | MAP_PRIVATE, 0); } #ifdef ELF_PLAT_INIT /* * The ABI may specify that certain registers be set up in special * ways (on i386 %edx is the address of a DT_FINI function, for * example. In addition, it may also specify (eg, PowerPC64 ELF) * that the e_entry field is the address of the function descriptor * for the startup routine, rather than the address of the startup * routine itself. This macro performs whatever initialization to * the regs structure is required as well as any relocations to the * function descriptor entries when executing dynamically links apps. */ ELF_PLAT_INIT(regs, reloc_func_desc); #endif /* 8 最後,start_thread()這個宏操作會將eip和esp改成新的地址,就使得CPU在返回用戶空間時就進入新的程序入口。如果存在解釋器映像,那麼這就是解釋器映像的程序入口,否則就是目標映像的程序入口。那麼什麼情況下有解釋器映像存在,什麼情況下沒有呢?如果目標映像與各種庫的鏈接是靜態鏈接,因而無需依靠共享庫、即動態鏈接庫,那就不需要解釋器映像;否則就一定要有解釋器映像存在。 對於一個目標程序, gcc在編譯時,除非顯示的使用static標籤,否則所有程序的鏈接都是動態鏈接的,也就是說需要解釋器。由此可見,我們的程序在被內核加載到內存,內核跳到用戶空間後並不是執行我們程序的,而是先把控制權交到用戶空間的解釋器,由解釋器加載運行用戶程序所需要的動態庫(比如libc等等),然後控制權纔會轉移到用戶程序。 */ /* 開始執行程序,這時已經是子進程了 */ start_thread(regs, elf_entry, bprm->p); retval = 0; out: kfree(loc); out_ret: return retval; /* error cleanup */ out_free_dentry: kfree(interp_elf_phdata); allow_write_access(interpreter); if (interpreter) fput(interpreter); out_free_interp: kfree(elf_interpreter); out_free_ph: kfree(elf_phdata); goto out; }