Linux核心分析之七——Linux核心如何裝載和啟動一個可執行程式
作者:姚開健
原創作品轉載請註明出處
《Linux核心分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000
1、ELF的檔案格式。
通常我們將程式檔案編譯後得到的目標檔案,在Linux上其格式就是ELF檔案,就是 EXECUTABLE AND LINKABLE FORMAT,其格式如下所示:
我們從上可以知道,ELF檔案最開始是一個ELF頭,儲存了路線圖(road map),描述了該檔案的組織情況。我們可以通過readelf -h命令來讀取一個ELF檔案的頭部,其組成如下所示:
值得注意的是ELF32_addr e_entry,它儲存的是檔案開始執行的地址,通常是0x08048000。
除了ELF頭部以外,還需要關注的是節,分別是.text,被編譯程式的機器程式碼;.rodata,read only data,諸如printf語句中的形式串和switch語句的跳轉表等只讀資料;.data,已初始化的全域性變數;.bss,未初始化的全域性變數,在目標檔案中不佔實際的空間。可以通過一般程式來指明其分佈:
如上所示,黑色字型的是程式的機器程式碼,儲存在.text節,紅色字型是未初始化的全域性變數儲存在.bss節,藍色字型是已初始化的全域性變數,儲存在.data節。
除了以上比較重要的節以外,其他節的資訊可以在網上一些ELF檔案格式分析文章(http://www.xfocus.net/articles/200105/174.html)找到說明,在此僅簡略地說明比較重要的,常見的節。
2、可執行檔案的裝載
當系統要開始執行一個新程式時,通常會有exec類系統呼叫來執行裝載可執行檔案到記憶體中。其一般步驟包括為新執行的程式分配頁框,將函式呼叫的引數int argc, char* argv[](即我們所說的main函式引數)傳入到可執行檔案中,有時候還會有char* const envp[]這個環境變數引數,如在shell中輸入命令ls -l,那麼這個shell程序就把“ls”,當前目錄,“-l”這三個字串放入引數中,接著呼叫do_execve()
如程式碼所示,第一個引數是檔名,即可執行檔名,二是argv引數,三是環境變數引數,在上述命令中,“ls”“ -l”被放入了argv這個引數中,接著函式呼叫do_execve_common():9int do_execve(struct filename *filename, 1550 const char __user *const __user *__argv, 1551 const char __user *const __user *__envp) 1552{ 1553 struct user_arg_ptr argv = { .ptr.native = __argv }; 1554 struct user_arg_ptr envp = { .ptr.native = __envp }; 1555 return do_execve_common(filename, argv, envp); 1556}
/*
1428 * sys_execve() executes a new program.
1429 */
1430static int do_execve_common(struct filename *filename,
1431 struct user_arg_ptr argv,
1432 struct user_arg_ptr envp)
1433{
1434 struct linux_binprm *bprm;
1435 struct file *file;
1436 struct files_struct *displaced;
1437 int retval;
1438
1439 if (IS_ERR(filename))
1440 return PTR_ERR(filename);
接著再呼叫exce_binprm()。在這些函式呼叫中都是為了找到要執行的可執行檔案,如“ls”程式的可執行檔案,然後需要找到當前可執行檔案的對應格式的解析模組,search_binary_handler,如下:
1369 list_for_each_entry(fmt, &formats, lh) {
1370 if (!try_module_get(fmt->module))
1371 continue;
1372 read_unlock(&binfmt_lock);
1373 bprm->recursion_depth++;
1374 retval = fmt->load_binary(bprm);
1375 read_lock(&binfmt_lock);
其中format是一個連結串列,函式會遍歷這個連結串列,並呼叫每個節點的load_binary,並把bprm這個結構體傳過去,如果load_binary成功應答了結構體中的檔案格式,則說明找到了對應可執行檔案格式的裝載程式,遍歷結束。對於ELF格式的可執行檔案fmt->load_binary(bprm);執行的應該是load_elf_binary,其內部是和ELF檔案格式解析,節選部分程式碼所示:
static int load_elf_binary(struct linux_binprm *bprm)
572{
573 struct file *interpreter = NULL; /* to shut gcc up */
574 unsigned long load_addr = 0, load_bias = 0;
575 int load_addr_set = 0;
576 char * elf_interpreter = NULL;
577 unsigned long error;
578 struct elf_phdr *elf_ppnt, *elf_phdata;
579 unsigned long elf_bss, elf_brk;
580 int retval, i;
581 unsigned int size;
582 unsigned long elf_entry;
583 unsigned long interp_load_addr = 0;
584 unsigned long start_code, end_code, start_data, end_data;
585 unsigned long reloc_func_desc __maybe_unused = 0;
586 int executable_stack = EXSTACK_DEFAULT;
587 struct pt_regs *regs = current_pt_regs();
588 struct {
589 struct elfhdr elf_ex;
590 struct elfhdr interp_elf_ex;
591 } *loc;
592
593 loc = kmalloc(sizeof(*loc), GFP_KERNEL);
594 if (!loc) {
595 retval = -ENOMEM;
596 goto out_ret;
597 }
598
599 /* Get the exec-header */
600 loc->elf_ex = *((struct elfhdr *)bprm->buf);
601
602 retval = -ENOEXEC;
603 /* First of all, some simple consistency checks */
604 if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
605 goto out;
接著函式會呼叫start_thread()函式:
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
199{
200 set_user_gs(regs, 0);
201 regs->fs = 0;
202 regs->ds = __USER_DS;
203 regs->es = __USER_DS;
204 regs->ss = __USER_DS;
205 regs->cs = __USER_CS;
206 regs->ip = new_ip;
207 regs->sp = new_sp;
208 regs->flags = X86_EFLAGS_IF;
209 /*
210 * force it to the iret return path by making it look as if there was
211 * some work pending.
212 */
213 set_thread_flag(TIF_NOTIFY_RESUME);
214}
215EXPORT_SYMBOL_GPL(start_thread);
216
注意這個函式呼叫的引數二,new_ip,這是可執行檔案的入口執行的地址,也就是在我們上面所說的檔案頭的地址0x080480000的旁邊0x08048094(.text程式碼節的開始地址),這是函式start_thread會修改儲存在核心態堆疊但是屬於使用者態暫存器的的eip和esp,使它們分別指向程式直譯器的入口點(開始地址)和新的使用者態堆疊的棧底,接著從核心儲存在使用者態堆疊的資訊(如環境變數引數指標陣列等),為自己建立一個基本的執行上下文,接著還有為新的執行程式的共享庫做一些初始化工作,此時新的執行程式裝載完畢,開始跳轉到入口點(開始地址)地址執行。
此時程式的記憶體映像是:
總結
Linux核心裝載和執行一個可執行程式是一個很複雜的過程,設計到系統的許多方面,例如程序抽象,檔案系統,記憶體管理,系統呼叫等。當exce類系統呼叫可執行程式完畢後回到原來的使用者態時,其上下文已經被修改,exce呼叫程式碼已不在,可以說exce類系統呼叫從未成功返回。新的程式開始了它的入口點處的執行。