?? 漫談兼容內(nèi)核之八:elf映像的裝入(一).txt
字號:
漫談兼容內(nèi)核之八:ELF映像的裝入(一)
[align=center][b][size=4]漫談兼容內(nèi)核之八:ELF映像的裝入(一)[/size][/b][/align]
[align=center]毛德操[/align]
上一篇漫談中介紹了Wine的二進制映像裝入和啟動,現(xiàn)在我們來看看ELF映像的裝入和啟動。
一般而言,應(yīng)用軟件的編程不可能是“一竿子到底”、所有的代碼都自己寫的,程序員不可避免地、也許是不自覺地、都會使用一些現(xiàn)成的程序庫。對于C語言的編程,至少C程序庫是一定會用到的。從編譯/連接和運行的角度看,應(yīng)用程序和庫程序的連接有兩種方法。一種是固定的、靜態(tài)的連接,就是把需要用到的庫函數(shù)的目標(二進制)代碼從程序庫中抽取出來,連接進應(yīng)用軟件的目標映像中,或者甚至干脆把整個程序庫都連接進應(yīng)用軟件的映像中。這里所謂的連接包括兩方面的操作,一是把庫函數(shù)的目標代碼“定位”在應(yīng)用軟件目標映像中的某個位置上。由于不同應(yīng)用軟件本身的大小和結(jié)構(gòu)都可能不同,庫函數(shù)在目標映像中的位置是無法預(yù)先確定的。為此,程序庫中的代碼必須是可以浮動的,即“與位置無關(guān)”的,在編譯時必須加上-fPIC選項,這里PIC是“Position-Independent Code”的縮寫。一旦一個庫函數(shù)在映像中的位置確定以后,就要使應(yīng)用軟件中所有對此函數(shù)的調(diào)用都指向這個函數(shù)。早期的軟件都采用這種靜態(tài)的連接方法,好處是連接的過程只發(fā)生在編譯/連接階段,而且用到的技術(shù)也比較簡單。但是也有缺點,那就是具體庫函數(shù)的代碼往往重復(fù)出現(xiàn)在許多應(yīng)用軟件的目標映像中,從而造成運行時的資源浪費。另一方面,這也不利于軟件的發(fā)展,因為即使某個程序庫有了更新更好的版本,已經(jīng)與老版本靜態(tài)連接的應(yīng)用軟件也享受不到好處,而重新連接往往又不現(xiàn)實。再說,這也不利于將程序庫作為商品獨立發(fā)展的前景。于是就發(fā)展起了第二種連接方法,那就是動態(tài)連接。所謂動態(tài)連接,是指庫函數(shù)的代碼并不進入應(yīng)用軟件的目標映像,應(yīng)用軟件在編譯/連接階段并不完成跟庫函數(shù)的連接;而是把函數(shù)庫的映像也交給用戶,到啟動應(yīng)用軟件目標映像運行時才把程序庫的映像也裝入用戶空間(并加以定位)、再完成應(yīng)用軟件與庫函數(shù)的連接。說到程序庫,最基本、最重要的當(dāng)然是C語言庫、即libc或glibc。
這樣,就有了兩種不同的ELF格式映像。一種是靜態(tài)連接的,在裝入/啟動其運行時無需裝入函數(shù)庫映像、也無需進行動態(tài)連接。另一種是動態(tài)連接的,需要在裝入/啟動其運行時同時裝入函數(shù)庫映像并進行動態(tài)連接。顯然,Linux內(nèi)核應(yīng)該既支持靜態(tài)連接的ELF映像、也支持動態(tài)連接的ELF映像。進一步的分析表明:裝入/啟動ELF映像必需由內(nèi)核完成,而動態(tài)連接的實現(xiàn)則既可以在內(nèi)核中完成,也可在用戶空間完成。因此,GNU把對于動態(tài)連接ELF映像的支持作了分工:把ELF映像的裝入/啟動放在Linux內(nèi)核中;而把動態(tài)連接的實現(xiàn)放在用戶空間,并為此提供一個稱為“解釋器”的工具軟件,而解釋器的裝入/啟動也由內(nèi)核負責(zé)。
大家知道,在Linux系統(tǒng)中,目標映像的裝入/啟動是由系統(tǒng)調(diào)用execve()完成的,但是可以在Linux內(nèi)核上運行的二進制映像有a.out和ELF兩種。由于篇幅的關(guān)系,在“情景分析”一書中對于二進制映像只講了a.out格式映像的裝入/啟動,而沒有講ELF格式映像的裝入/啟動。這是因為如果講了ELF映像就不可避免地要講到動態(tài)連接、講到“解釋器”,那樣一來篇幅就大了。從對于裝入/啟動可執(zhí)行映像的過程的一般了解而言,光講a.out也許就夠了;可是考慮到ELF映像(以及Windows軟件的PE映像)對于兼容內(nèi)核開發(fā)的重要意義,還是有必要補上這一課。
本文先介紹裝入/啟動一個ELF映像時發(fā)生于Linux內(nèi)核中的操作,下一篇漫談則介紹發(fā)生于用戶空間的操作、即“解釋器”對于共享庫的操作。
1.系統(tǒng)空間的操作
內(nèi)核中實際執(zhí)行execv()或execve()系統(tǒng)調(diào)用的程序是do_execve(),這個函數(shù)先打開目標映像文件,并從目標文件的頭部(從第一個字節(jié)開始)讀入若干(128)字節(jié),然后調(diào)用另一個函數(shù)search_binary_handler(),在那里面讓各種可執(zhí)行程序的處理程序前來認領(lǐng)和處理。內(nèi)核所支持的每種可執(zhí)行程序都有個struct linux_binfmt數(shù)據(jù)結(jié)構(gòu),通過向內(nèi)核登記掛入一個隊列。而search_binary_handler(),則掃描這個隊列,讓各個數(shù)據(jù)結(jié)構(gòu)所提供的處理程序、即各種映像格式、逐一前來認領(lǐng)。如果某個格式的處理程序發(fā)現(xiàn)特征相符而,便執(zhí)行該格式映像的裝入和啟動。
我們從ELF格式映像的linux_binfmt數(shù)據(jù)結(jié)構(gòu)開始:
[code]#define load_elf_binary load_elf32_binary
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
};[/code]
這個數(shù)據(jù)結(jié)構(gòu)表明:ELF格式的二進制映像的認領(lǐng)、裝入和啟動是由load_elf_binary()完成的。而“共享庫”、即動態(tài)連接庫映像的裝入則由load_elf_library()完成。實際上共享庫的映像也是二進制的,但是一般說“二進制”映像是指帶有main()函數(shù)的、可以獨立運行并構(gòu)成一個進程主體的可執(zhí)行程序的二進制映像。另一方面,盡管裝入/啟動二進制映像的過程中蘊含了共享庫的裝入(否則無法運行),但是在此過程中卻并沒有調(diào)用load_elf_library(),而是通過別的函數(shù)進行,這個函數(shù)只是在sys_uselib()、即系統(tǒng)調(diào)用uselib()中通過函數(shù)指針load_shlib受到調(diào)用。所以,load_elf_library()所處理的是應(yīng)用軟件在運行時對于共享庫的動態(tài)裝入,而不是啟動進程時的靜態(tài)裝入。
下面我們就來看load_elf_binary()代碼,這個函數(shù)在fs/binfmt_elf.c中。由于篇幅的關(guān)系,本文只能以近似于偽代碼的形式列出經(jīng)過簡化整理的代碼(下同),有需要或興趣的讀者不妨結(jié)合源文件中的原始代碼閱讀。由于load_elf_binary()是個比較大的函數(shù),我們分段閱讀。
[code][sys_execve() > do_execve() > search_binary_handler() > load_elf_binary()]
static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{
. . . . . .
struct {
struct elfhdr elf_ex;
struct elfhdr interp_elf_ex;
struct exec interp_ex;
} *loc;
loc = kmalloc(sizeof(*loc), GFP_KERNEL);
. . . . . .
/* Get the exec-header */
loc->elf_ex = *((struct elfhdr *) bprm->buf);
. . . . . .
/* First of all, some simple consistency checks */
if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
goto out; //比對四個字符,必須是0x7f、‘E’、‘L’、和‘F’。
if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
goto out; //映像類型必須是ET_EXEC或ET_DYN。
if (!elf_check_arch(&loc->elf_ex))
goto out; //機器(CPU)類型必須相符。
. . . . . .[/code]
首先是認領(lǐng)。ELF映像文件的頭部應(yīng)該是個struct elfhdr數(shù)據(jù)結(jié)構(gòu),對于32位映像這實際上是struct elf32_hdr數(shù)據(jù)結(jié)構(gòu)、即Elf32_Ehdr,其定義如下所示:
[code]#define elfhdr elf32_hdr
typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT]; // EI_NIDENT = 16
Elf32_Half e_type; // 即unsigned shout
Elf32_Half e_machine; // 即 unsigned int
Elf32_Word e_version;
Elf32_Addr e_entry; /* Entry point */
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;[/code]
這個數(shù)據(jù)結(jié)構(gòu)的前16個字節(jié)是ELF映像的標志e_ident[ ],其中開頭的4個字節(jié)就是所謂“Magic Number”,應(yīng)該是“\177ELF”。除這4個字符比對相符以外,還要看映像的類型是否ET_EXEC和ET_DYN之一;前者表示可執(zhí)行映像,后者表示共享庫(此外還有ET_REL和ET_CORE,分別表示浮動地址模塊和dump映像)。同時,映像所適用的CPU類型(如x86或PPC)也須相符。如果這些條件都滿足,就算認領(lǐng)成功,下面就是進一步的處理了。進一步的處理當(dāng)然需要更多的信息,在Elf32_Ehdr中提供了兩個指針,或者說兩個(文件內(nèi)的)位移量,即e_phoff和e_shoff。如果非0的話,前者指向“程序頭(Program Header)”數(shù)組的起點;后者指向“區(qū)段頭(Section Header)”數(shù)組的起點。兩個數(shù)組的大小(元素的個數(shù))分別由e_phnum和e_shnum提供,而每個數(shù)組元素(表項)的大小由e_phentsize和e_shentsize提供。至于e_ehsize,則是映像頭部本身的大小。還有個值得特別說明的成分是e_entry,那就是該映像的程序入口,一般是_start()的起點。
人們常常提到二進制代碼映像中有所謂“程序段”“數(shù)據(jù)段”等等,那都屬于映像中的“區(qū)段”即“Section”。但是區(qū)段的種類遠遠不止這些而有很多,例如“符號表”就是一個區(qū)段,再如用于動態(tài)連接的信息、用于Debug的信息等等,都屬于不同的區(qū)段。而區(qū)段頭數(shù)組、或曰區(qū)段頭表,則為映像中的每一個區(qū)段都提供一個描述性的數(shù)據(jù)結(jié)構(gòu)。
而程序頭數(shù)組或曰程序頭表中的每一個表項,則是對一個“部(Segment)”的描述。一個部可以包含若干個區(qū)段,也可以只是一個簡單的數(shù)據(jù)結(jié)構(gòu)。整個ELF映像就是由文件頭、區(qū)段頭表、程序頭表、一定數(shù)量的區(qū)段、以及一定數(shù)量的部構(gòu)成。而ELF映像的裝入/啟動過程,則就是在各種頭部信息的指引下將某些部或區(qū)段裝入一個進程的用戶空間,并為其運行做好準備(例如裝入所需的共享庫),最后(在目標進程首次受調(diào)度運行時)讓CPU進入其程序入口的過程。讀者將會看到,這個過程很可能是嵌套的,因為在裝入一個映像的過程中很可能需要裝入另一個或另幾個別的映像。
我們繼續(xù)往下看:
[code][sys_execve() > do_execve() > search_binary_handler() > load_elf_binary()]
/* Now read in all of the header information */
. . . . . .
size = loc->elf_ex.e_phnum * sizeof(struct elf_phdr);
retval = -ENOMEM;
elf_phdata = (struct elf_phdr *) kmalloc(size, GFP_KERNEL);
if (!elf_phdata)
goto out;
retval = kernel_read(bprm->file, loc->elf_ex.e_phoff, (char *) elf_phdata, size);
. . . . . .
files = current->files; /* Refcounted so ok */
. . . . . .
retval = get_unused_fd();
. . . . . .
get_file(bprm->file);
fd_install(elf_exec_fileno = retval, bprm->file);
elf_ppnt = elf_phdata;
elf_bss = 0;
elf_brk = 0;
start_code = ~0UL;
end_code = 0;
start_data = 0;
end_data = 0;[/code]
這里通過kernel_read()讀入的是目標映像的整個程序頭表,這是一個struct elf_phdr、實際上是struct elf32_phdr結(jié)構(gòu)數(shù)組。這種數(shù)據(jù)結(jié)構(gòu)的定義為:
[code]typedef struct elf32_phdr{
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;[/code]
這里的p_type表示部的類型。
同時,這里還為已打開的目標映像文件在當(dāng)前進程的打開文件表中另外分配一個表項,類似于執(zhí)行了一次dup(),目的在于為目標文件維持兩個不同的上下文,以便從不同的位置上讀出。
接著是對elf_bss 、elf_brk、start_code、end_code等等變量的初始化。這些變量分別紀錄著當(dāng)前(到此刻為止)目標映像的bss段、代碼段、數(shù)據(jù)段、以及動態(tài)分配“堆” 在用戶空間的位置。除start_code的初始值為0xffffffff外,其余均為0。隨著映像內(nèi)容的裝入,這些變量也會逐步得到調(diào)整,讀者不妨自己留意這些變量在整個過程中的變化。
讀入了程序頭表,并對start_code等變量進行初始化以后,下面的第一步就是在程序頭表中尋找“解釋器”部、并加以處理的過程。
[code][sys_execve() > do_execve() > search_binary_handler() > load_elf_binary()]
for (i = 0; i < loc->elf_ex.e_phnum; i++) {
if (elf_ppnt->p_type == PT_INTERP) {
. . . . . .
retval = -ENOMEM;
elf_interpreter = (char *) kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
. . . . . .
retval = kernel_read(bprm->file, elf_ppnt->p_offset,
elf_interpreter, elf_ppnt->p_filesz);
. . . . . .
interpreter = open_exec(elf_interpreter);
retval = PTR_ERR(interpreter);
if (IS_ERR(interpreter))
goto out_free_interp;
retval = kernel_read(interpreter, 0, bprm->buf, BINPRM_BUF_SIZE);
. . . . . .
/* Get the exec headers */
loc->interp_ex = *((struct exec *) bprm->buf);
loc->interp_elf_ex = *((struct elfhdr *) bprm->buf);
break;
}
elf_ppnt++;
}[/code]
顯然,這個for循環(huán)的目的僅在于尋找和處理目標映像的“解釋器”部。ELF格式的二進制映像在裝入和啟動的過程中需要得到一個工具軟件的協(xié)助,其主要的目的在于為目標映像建立起跟共享庫的動態(tài)連接。這個工具稱為“解釋器”。一個ELF映像在裝入時需要用什么解釋器是在編譯/連接是就決定好了的,這信息就保存在映像的“解釋器”部中。“解釋器”部的類型為PT_INTERP,找到后就根據(jù)其位置p_offset和大小p_filesz把整個“解釋器”部讀入緩沖區(qū)。整個“解釋器”部實際上只是一個字符串,即解釋器的文件名,例如“/lib/ld-linux.so.2”。有了解釋器的文件名以后,就通過open_exec()打開這個文件,再通過kernel_read()讀入其開頭128個字節(jié),這就是映像的頭部。早期的解釋器映像是a.out格式的,現(xiàn)在已經(jīng)都是ELF格式的了,/lib/ld-linux.so.2就是個ELF映像。
下面是對解釋器映像頭部的處理,首先要確認其為ELF格式還是a.out格式。
[code][sys_execve() > do_execve() > search_binary_handler() > load_elf_binary()]
. . . . . .
/* Some simple consistency checks for the interpreter */
if (elf_interpreter) {
interpreter_type = INTERPRETER_ELF | INTERPRETER_AOUT;
/* Now figure out which format our binary is */
if ((N_MAGIC(loc->interp_ex) != OMAGIC) &&
(N_MAGIC(loc->interp_ex) != ZMAGIC) &&
(N_MAGIC(loc->interp_ex) != QMAGIC))
interpreter_type = INTERPRETER_ELF;
if (memcmp(loc->interp_elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
interpreter_type &= ~INTERPRETER_ELF;
. . . . . .
} else {
. . . . . .
}
/* OK, we are done with that, now set up the arg stuff,
and then start this sucker up */[/code]
至此,我們已為目標映像和解釋器映像的裝入作好了準備。可以讓當(dāng)前進程(線程)與其父進程分道揚鑣,轉(zhuǎn)化成真正意義上的進程,走自己的路了。
[code][sys_execve() > do_execve() > search_binary_handler() > load_elf_binary()]
/* Flush all traces of the currently running executable */
retval = flush_old_exec(bprm);
. . . . . .
/* OK, This is the point of no return */
current->mm->start_data = 0;
current->mm->end_data = 0;
current->mm->end_code = 0;
current->mm->mmap = NULL;
current->flags &= ~PF_FORKNOEXEC;
current->mm->def_flags = def_flags;
. . . . . .
/* Do this so that we can load the interpreter, if need be. We will
change some of these later */
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), executable_stack);
. . . . . .[/code]
可想而知,flush_old_exec()把當(dāng)前進程用戶空間的頁面都釋放了。這么一來,當(dāng)前進程的用戶空間是“一片白茫茫大地真干凈”,什么也沒有了,原有的物理頁面映射都已釋放。
現(xiàn)在要來重建用戶空間的映射了。一個新的映像要能運行,用戶空間堆棧是必須的,所以首先要把用戶空間的一個虛擬地址區(qū)間劃出來用于堆棧。進一步,當(dāng)CPU進入新映像的程序入口時,堆棧上應(yīng)該有argc、argv[]、envc、envp[]等參數(shù)。這些參數(shù)來自老的程序,需要通過堆棧把它們傳遞給新的映像。實際上,argv[]和envp[]中是一些字符串指針,光把指針傳給新映像,而不把相應(yīng)的字符串傳遞給新映像,那是毫無意義的。為此,在進入search_binary_handler()、從而進入load_elf_binary()之前,do_execve()已經(jīng)為這些字符串分配了若干頁面,并通過copy_strings()從用戶空間把這些字符串拷貝到了這些頁面中。現(xiàn)在則要把這些頁面再映射回用戶空間(當(dāng)然是在不同的地址上),這就是這里setup_arg_pages()要做的事。這些頁面映射的地址是在用戶空間堆棧的最頂部。對于x86處理器,用戶空間堆棧是從3GB邊界開始向下伸展的,首先就是存放著這些字符串的頁面,再往下才是真正意義上的用戶空間堆棧。而argc、argv[]這些參數(shù),則就在這真正意義上的用戶空間堆棧上。
下面就可以裝入新映像了。所謂“裝入”,實際上就是將映像的(部分)內(nèi)容映射到用戶(虛擬地址)空間的某些區(qū)間中去。在MMU的swap機制的作用下,這個過程甚至并不需要真的把映像的內(nèi)容讀入物理頁面,而把實際的讀入留待將來的缺頁中斷。
首先裝入的是目標映像本身。
[code][sys_execve() > do_execve() > search_binary_handler() > load_elf_binary()]
/* Now we do a little grungy work by mmaping the ELF image into
the correct location in memory. At this point, we assume that
?? 快捷鍵說明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -