標籤:
吳樂 山東師範大學
《Linux核心分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000
實驗目的:通過對一個簡單的可執行程式用gdb進行代碼的跟蹤,剖析linux核心是如何動態和靜態裝載和啟動程式的,進而總結linux核心可執行程式載入的過程。
一、實驗過程
1、編寫一個簡單的Exec的建立進程的函數
2、開啟gdb,並設定好如下斷點
3、開始跟蹤,找到第一個斷點。
(主程式還未建立子進程)
4、繼續在此斷點處逐步跟蹤
5、找到設定的第二個斷點,並列出
6、跟蹤到裝載new_ip處,查看其地址
7、明顯看到,此處載入的IP地址與程式入口地址相同
8、結束跟蹤,觀察其他斷點方法類似。
二、可執行檔的載入和運行
1、execve()系統調用的入口是sys_execve().代碼如下:
int sys_execve(struct pt_regs regs){ int error; char * filename; //將使用者空間的第一個參數(也就是可執行檔的路徑)複製到核心 filename = getname((char __user *) regs.ebx); error = PTR_ERR(filename); if (IS_ERR(filename)) goto out; error = do_execve(filename, (char __user * __user *) regs.ecx, (char __user * __user *) regs.edx, ®s); if (error == 0) { task_lock(current); current->ptrace &= ~PT_DTRACE; task_unlock(current); /* Make sure we don‘t return using sysenter.. */ set_thread_flag(TIF_IRET); } //釋放記憶體 putname(filename);out: return error;}
由此可見進行系統調用時,把參數依次放在ebx,ecx,edx,esi,edi,ebp寄存器.
注意其中第一個參數為可執行檔路徑,第二個參數為參數的個數,第三個參數為可執行檔對應的參數.
2、do_execve()是這個系統調用的主要部分,它的代碼如下:
int do_execve(char * filename, char __user *__user *argv, char __user *__user *envp, struct pt_regs * regs){ //linux_binprm:儲存可執行檔的一些參數 struct linux_binprm *bprm; struct file *file; unsigned long env_p; int retval; retval = -ENOMEM; bprm = kzalloc(sizeof(*bprm), GFP_KERNEL); if (!bprm) goto out_ret; //在核心中開啟這個可執行檔 file = open_exec(filename); retval = PTR_ERR(file); //如果開啟失敗 if (IS_ERR(file)) goto out_kfree; sched_exec(); bprm->file = file; bprm->filename = filename; bprm->interp = filename; //bprm初始化,主要是初始化bprm->mm retval = bprm_mm_init(bprm); if (retval) goto out_file; //計算參數個數 bprm->argc = count(argv, MAX_ARG_STRINGS); if ((retval = bprm->argc) goto out_mm; //環境變數個數 bprm->envc = count(envp, MAX_ARG_STRINGS); if ((retval = bprm->envc) goto out_mm; retval = security_bprm_alloc(bprm); if (retval) goto out; //把要負載檔案的前128 讀入bprm->buf retval = prepare_binprm(bprm); if (retval goto out; //copy第一個參數filename retval = copy_strings_kernel(1, &bprm->filename, bprm); if (retval goto out; //bprm->exec:參數的起始地址(從上往下方向) bprm->exec = bprm->p; //copy環境變數 retval = copy_strings(bprm->envc, envp, bprm); if (retval goto out; //環境變數存放的起始地址 env_p = bprm->p; //copy可執行檔所帶參數 retval = copy_strings(bprm->argc, argv, bprm); if (retval goto out; //環境變數的長度 bprm->argv_len = env_p - bprm->p; //到鏈表中尋找合適的載入模組 retval = search_binary_handler(bprm,regs); if (retval >= 0) { /* execve success */ free_arg_pages(bprm); security_bprm_free(bprm); acct_update_integrals(current); kfree(bprm); return retval; }out: free_arg_pages(bprm); if (bprm->security) security_bprm_free(bprm);out_mm: if (bprm->mm) mmput (bprm->mm);out_file: if (bprm->file) { allow_write_access(bprm->file); fput(bprm->file); }out_kfree: kfree(bprm);out_ret: return retval;}
3、在載入可執檔案的時候,需要遍曆formats這個鏈表,search_binary_handler()實現了這一功能。代碼如下:
int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs){ int try,retval; struct linux_binfmt *fmt;#ifdef __alpha__ /* handle /sbin/loader.. */ { struct exec * eh = (struct exec *) bprm->buf; if (!bprm->loader && eh->fh.f_magic == 0x183 && (eh->fh.f_flags & 0x3000) == 0x3000) { struct file * file; unsigned long loader; allow_write_access(bprm->file); fput(bprm->file); bprm->file = NULL; loader = bprm->vma->vm_end - sizeof(void *); file = open_exec("/sbin/loader"); retval = PTR_ERR(file); if (IS_ERR(file)) return retval; /* Remember if the application is TASO. */ bprm->sh_bang = eh->ah.entry bprm->file = file; bprm->loader = loader; retval = prepare_binprm(bprm); if (retval return retval; /* should call search_binary_handler recursively here, but it does not matter */ } }#endif retval = security_bprm_check(bprm); if (retval) return retval; /* kernel module loader fixup */ /* so we don‘t try to load run modprobe in kernel space. */ set_fs(USER_DS); retval = audit_bprm(bprm); if (retval) return retval; retval = -ENOENT; //這裡會迴圈兩次.待模組載入之後再遍曆一次 for (try=0; try read_lock(&binfmt_lock); list_for_each_entry(fmt, &formats, lh) { //載入函數 int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary; if (!fn) continue; if (!try_module_get(fmt->module)) continue; read_unlock(&binfmt_lock); //運行載入函數,如果載入末成功,則繼續遍曆 retval = fn(bprm, regs); //載入成功了 if (retval >= 0) { put_binfmt(fmt); allow_write_access(bprm->file); if (bprm->file) fput(bprm->file); bprm->file = NULL; current->did_exec = 1; proc_exec_connector(current); return retval; } read_lock(&binfmt_lock); put_binfmt(fmt); if (retval != -ENOEXEC || bprm->mm == NULL) break; if (!bprm->file) { read_unlock(&binfmt_lock); return retval; } } read_unlock(&binfmt_lock); //所有模組載入這個可執行檔失敗,則載入其它模組再試一次 if (retval != -ENOEXEC || bprm->mm == NULL) { break; //CONFIG_KMOD:動態載入模組標誌#ifdef CONFIG_KMOD }else{#define printable(c) (((c)==‘\t‘) || ((c)==‘\n‘) || (0x20 if (printable(bprm->buf[0]) && printable(bprm->buf[1]) && printable(bprm->buf[2]) && printable(bprm->buf[3])) break; /* -ENOEXEC */ request_module("binfmt-%04x", *(unsigned short *)(&bprm->buf[2]));#endif } } return retval;}
4、喚醒父進程的過程以及棧空間的布局代碼如下.
static int load_aout_binary(struct linux_binprm * bprm, struct pt_regs * regs){ …… …… current->mm->start_stack = (unsigned long) create_aout_tables((char __user *) bprm->p, bprm);#ifdef __alpha__ regs->gp = ex.a_gpvalue;#endif start_thread(regs, ex.a_entry, current->mm->start_stack); ……}Creat_aout_tables()代碼如下:static unsigned long __user *create_aout_tables(char __user *p, struct linux_binprm * bprm){ char __user * __user *argv; char __user * __user *envp; unsigned long __user *sp; //可執行檔的參數個數 int argc = bprm->argc; //環境變數的個數 int envc = bprm->envc; //sp初始化成p,也即bprm->p sp = (void __user *)((-(unsigned long)sizeof(char *)) & (unsigned long) p);#ifdef __sparc__ /* This imposes the proper stack alignment for a new process. */ sp = (void __user *) (((unsigned long) sp) & ~7); if ((envc+argc+3)&1) --sp;#endif#ifdef __alpha__/* whee.. test-programs are so much fun. */ put_user(0, --sp); put_user(0, --sp); if (bprm->loader) { put_user(0, --sp); put_user(0x3eb, --sp); put_user(bprm->loader, --sp); put_user(0x3ea, --sp); } put_user(bprm->exec, --sp); put_user(0x3e9, --sp);#endif sp -= envc+1; envp = (char __user * __user *) sp; sp -= argc+1; argv = (char __user * __user *) sp;#if defined(__i386__) || defined(__mc68000__) || defined(__arm__) || defined(__arch_um__) put_user((unsigned long) envp,--sp); put_user((unsigned long) argv,--sp);#endif put_user(argc,--sp); current->mm->arg_start = (unsigned long) p; while (argc-->0) { char c; put_user(p,argv++); do { get_user(c,p++); } while (c); } put_user(NULL,argv); current->mm->arg_end = current->mm->env_start = (unsigned long) p; while (envc-->0) { char c; put_user(p,envp++); do { get_user(c,p++); } while (c); } put_user(NULL,envp); current->mm->env_end = (unsigned long) p; return sp;}
ip這裡已經指向main函數入口地址了,此後的工作都由start_thread()函數完成。具體過程可參見我的另一片部落格:
http://www.cnblogs.com/wule/p/4404504.html
三、總結linux核心可執行程式載入的過程
首先建立父進程,然後通過調用fork()系統調用建立一個新的進程,然後新的進程調用execve()系統調用執行指定的ELF檔案。 主進程繼續返回等待新進程執行結束,然後重新等待使用者輸入命令。execve()系統調用被定義在unistd.h,它的原型如下:
int execve(const char *filenarne, char *const argv[], char *const envp[]);
它的三個參數分別是被執行的程式檔案名稱、執行參數和環境變最。Glibc對execvp()系統調用進行了封裝,提供了execl(), execlp(), execle(), execv()和execvp()等5個不同形式的exec系列API,它們只是在調用的參數形式上有所區別,但最終都會調用到execve()這個系統中。
調用execve()系統調用之後,再調用核心的入口sys_execve()。 sys_execve()進行一些參數的檢查複製之後,調用do_execve()。 因為可執行檔不止ELF一種,還有java程式和以“#!”開始的指令碼程式等, 所以do_execve()會首先檢查被執行檔案,讀取前128個位元組,特別是開頭4個位元組的魔數,用以判斷可執行檔的格式。 如果是解釋型語言的指令碼,前兩個位元組“#!"就構成了魔數,系統一旦判斷到這兩個位元組,就對後面的字串進行解析,以確定程式解譯器的路徑。
當do_execve()讀取了這128個位元組的檔案頭部之後,然後調用search_binary_handle()去搜尋和匹配合適的可執行檔裝載處理過程。Linux中所有被支援的可執行檔格式都有相應的裝載處理過程,search_binary_handle()會通過判斷檔案頭部的魔數確定檔案的格式,並且調用相應的裝載處理過程。如ELF用load_elf_binary(),a.out用load_aout_binary(),指令碼用load_script()。其中ELF裝載過程的主要步驟是:
①檢查ELF可執行檔格式的有效性,比如魔數、程式頭表中段(Segment)的數量。
②尋找動態連結的”.interp”段(該段儲存可執行檔所需要的動態連結器的路徑),設定動態連結器路徑。
③根據ELF可執行檔的程式頭表的描述,對ELF檔案進行映射,比如代碼、資料、唯讀資料。
④初始化ELF進程環境,比如進程啟動時EDX寄存器的地址應該是DT_FINI的地址(結束代碼地址)。
⑤將系統調用的返回地址修改成ELF可執行檔的進入點,這個進入點取決於程式的連結方式,對於靜態連結的ELF可執行檔,這個程式入口就是ELF檔案的檔案頭中e_enEry所指的地址;對於動態連結的ELF可執行檔,程式進入點是動態連結器。
當ELF被load_elf_binary()裝載完成後,函數返回至do_execve()在返回至sys_execve()。在load_elf_binary()中(第5步)系統調用的返回地址已經被改成ELF程式的入口地址了。 所以當sys_execve()系統調用從核心態返回到使用者態時,EIP寄存器直接跳轉到了ELF程式的入口地址,於是新的程式開始執行,ELF可執行檔裝載完成。
通過gdb跟蹤Linux核心裝載和啟動可執行程式過程