載入和動態鏈接 從編譯/鏈接和運行的角度看,應用程式和庫程式的連接有兩種方式。 一種是固定的、靜態的連接,就是把需要用到的庫函數的目標代碼(二進位)代碼從程式庫中抽取出來,鏈接進應用軟體的目標映像中; 另一種是動態鏈接,是指庫函數的代碼並不進入應用軟體的目標映像,應用軟體在編譯/鏈接階段並不完成跟庫 ...
載入和動態鏈接
從編譯/鏈接和運行的角度看,應用程式和庫程式的連接有兩種方式。
一種是固定的、靜態的連接,就是把需要用到的庫函數的目標代碼(二進位)代碼從程式庫中抽取出來,鏈接進應用軟體的目標映像中;
另一種是動態鏈接,是指庫函數的代碼並不進入應用軟體的目標映像,應用軟體在編譯/鏈接階段並不完成跟庫函數的鏈接,而是把函數庫的映像也交給用戶,到啟動應用軟體目標映像運行時才把程式庫的映像也裝入用戶空間(並加以定位),再完成應用軟體與庫函數的連接。
這樣,就有了兩種不同的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;
}