date: 2014-10-24 12:09java
這部分詳情請參考APUE(第2版)第8章。linux
有6種不一樣的exec函數可供使用,這些函數最終都是經過系統調用execve來實現的:編程
<unistd.h> int execl(const char *pathname, const char *arg1, ... /* (char*)0 */ ); int execlp(const char *filename, const char *arg1, ... /* (char*)0 */ ); int execle(const char *pathname, const char *arg1, ... /* (char*)0, char * const *envp */); int execv(const char *pathname, char * const argv[]); int execvp(const char *filename, char * const argv[]); int execve(const char *pathname, char * const argv[], char * const envp[]);
它們間的關係以下圖:數組
在《進程四要素》中咱們簡單看了下task_struct結構,其中有6個與進程相關的ID:xss
ID | 意義 | 備註 |
---|---|---|
uid/gid | 實際用戶ID/實際組ID | 我其實是誰 |
euid/egid | 有效用戶ID/有效組ID | 我還具備哪些額外的「特權」 |
suid/sgid | 保存的設置用戶ID/保存的設置組ID | 由exec函數保存 |
一般進程的有效ID就是用戶的實際ID,但當進程執行一個程序文件時(經過execve系統調用),若是可執行文件設置了set-user-ID(設置用戶ID)位或set-group-ID(設置組ID)位,那麼執行該程序文件的進程,其有效用戶ID將被設置爲程序文件的全部者ID,其有效組ID將被被設置爲程序文件所在組的ID,這樣,進程就具備一些額外的「特權「了。同時execve系統調用還會將設置後的有效用戶ID保存到「保存的設置用戶ID」中(對「保存的設置組ID」也是一樣的處理),以方便其餘函數使用,好比setuid函數需會根據「保存的設置用戶ID」來判斷是否能夠將進程的有效ID設置爲某個指定的用戶ID。函數
這部分咱們重點關注下以下問題:佈局
這裏假定execve執行的程序文件爲aout格式的,具體來講是aout格式中的「非可重入代碼」,便可執行程序包含正文段(text)、數據段(data)和未初始化數據段(bss)。雖然aout格式已非主流,elf纔是當前流行的可執行程序文件的格式,但elf格式比較複雜,涉及到動態加載(loader)與動態連接(linker),而aout格式相對簡單,用來了解上述問題是比較合適的。這些問題的答案一樣適用於elf格式(或其餘格式)的可執行文件。ui
系統調用execve的內核入口爲sys_execve,定義在<arch/kernel/process.c>中:代理
asmlinkage int sys_execve(struct pt_regs regs) { int error; char * filename; filename = getname((char *) regs.ebx); error = PTR_ERR(filename); if (IS_ERR(filename)) goto out; error = do_execve(filename, (char **) regs.ecx, (char **) regs.edx, ®s); if (error == 0) current->ptrace &= ~PT_DTRACE; putname(filename); out: return error; }
regs.ebx保存着系統調用execve的第一個參數,便可執行文件的路徑名。由於路徑名存儲在用戶空間中,這裏要經過getname拷貝到內核空間中。getname在拷貝文件名時,先申請了一個page做爲緩衝,而後再從用戶空間拷貝字符串。爲何要申請一個頁面而不使用進程的系統空間堆棧?首先這是一個絕對路徑名,可能比較長,其次進程的系統空間堆棧大約爲7K,比較緊缺,不宜濫用。用完文件名後,在函數的末尾調用putname釋放掉申請的那個頁面。指針
sys_execve的核心是調用do_execve函數,傳給do_execve的第一個參數是已經拷貝到內核空間的路徑名filename,第二個和第三個參數仍然是系統調用execve的第二個參數argv和第三個參數envp,它們表明的傳給可執行文件的參數和環境變量仍然保留在用戶空間中。
do_execve定義在<fs/exec.c>中。它的主要流程(忽略掉異常狀況的處理)以下:
可執行文件(目標文件)做爲一個文件以外,還有一些其餘的專屬信息,爲了將運行一個可執行文件時所需的信息組織在一塊兒,內核定義了linux_binprm結構,其定義以下:
<include/linux/binfmts.h> struct linux_binprm{ char buf[BINPRM_BUF_SIZE]; struct page *page[MAX_ARG_PAGES]; unsigned long p; /* current top of mem */ int sh_bang; struct file * file; int e_uid, e_gid; kernel_cap_t cap_inheritable, cap_permitted, cap_effective; int argc, envc; char * filename; /* Name of binary */ unsigned long loader, exec; };
buf用來從可執行文件中讀入前128個字節,據此能夠判斷處可執行文件的類型(好比aout、elf、java、或者腳本等)。
page是一個物理頁面指針數組,這些物理頁面用來存儲execve系統調用中參數argv以及envp所指向的字符串表。數組的size爲MAX_ARG_PAGES(32),但具體會分配多少個物理頁面,取決於argv已經envp所指向的字符串表的大小。
p用來指向page數組所表明的存儲空間的「遊標」。
file便可執行文件對應的文件表項。
當可執行文件設置了set-user-ID或者set-group-ID,e_uid和e_gid分別用來存儲可執行文件的全部者ID和所在組ID.
filename指向可執行文件的路徑(該路徑字符串已經拷貝到內核空間)。
每一種可執行文件都有對應的「裝載器」,用來處理可執行文件的加載甚至是連接,此即linux_binfmt結構。其定義以下:
<include/linux/binfmts.h> struct linux_binfmt { struct linux_binfmt * next; struct module *module; int (*load_binary)(struct linux_binprm *, struct pt_regs * regs); int (*load_shlib)(struct file *); int (*core_dump)(long signr, struct pt_regs * regs, struct file * file); unsigned long min_coredump; /* minimal dump size */ };
其中關鍵的是幾個函數指針,顧名思義,load_binary用來加載可執行文件;load_shlib用來加載共享庫;而core_dump用來生成轉儲文件。
不一樣的「加載器」經過next指針構成一個鏈表,鏈表頭即爲formats。
每一個加載器就像是內核爲每種格式的可執行文件設置的代理人,每當執行一個可執行文件時,內核遍歷formats中的每一個代理人,查看該可執行文件是否歸某個代理人處理,若是對上了號,代理人則「認領」該可執行文件,負責後續的加載、執行等事務。這就是search_binary_handler函數的主要工做工程。但具體狀況比這複雜,須要考慮內核還沒有爲某種格式的可執行文件設置代理人的情形。
aout格式對應的inux_binfmt結構爲aout_format,其定義以下:
<fs/binfmt_aout.c> static struct linux_binfmt aout_format = { NULL, THIS_MODULE, load_aout_binary, load_aout_library, aout_core_dump, PAGE_SIZE };
可見aout類可執行文件的加載函數爲load_aout_binary,這是流程圖中的重點。
在可執行文件加載完成,而且傳遞給main函數的argc和argv參數處理完畢後,load_aout_binary調用start_thread來設置子進程返回用戶空間後的入口(即main函數)以及用戶空間堆棧的棧頂指針。
start_thread(regs, ex.a_entry, current->mm->start_stack);
start_thread的實現以下:
<include/asm/processor.h> #define start_thread(regs, new_eip, new_esp) do { \ __asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0)); \ set_fs(USER_DS); \ regs->xds = __USER_DS; \ regs->xes = __USER_DS; \ regs->xss = __USER_DS; \ regs->xcs = __USER_CS; \ regs->eip = new_eip; \ regs->esp = new_esp; \ } while (0)
可見,這裏將aout文件的入口ex. a_entry寫進eip,而將準備好argc以及argv以後用戶空間堆棧的棧頂current->mm->start_stack寫進esp,這樣當從系統調用返回到子進程的用戶空間中時,將從aout文件的入口main函數開始執行,而且經過esp能夠獲取傳遞給main函數的argc和argv參數。