1. 程式人生 > >Linux核心分析之七——Linux核心如何裝載和啟動一個可執行程式

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()

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}
如程式碼所示,第一個引數是檔名,即可執行檔名,二是argv引數,三是環境變數引數,在上述命令中,“ls”“ -l”被放入了argv這個引數中,接著函式呼叫do_execve_common():
/*
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類系統呼叫從未成功返回。新的程式開始了它的入口點處的執行。