linux可執(zhí)行文件的加載和運(yùn)行之一 三:可執(zhí)行文件的加載和運(yùn)行 Execve系統(tǒng)調(diào)用可以調(diào)用一個可執(zhí)行文件完全代替當(dāng)前的進(jìn)程,它在libc中的封裝有幾個API: int execl(const charp a t* h n a m e, const char a* rg 0, ... /* (char *) 0 */); int execv(const charp a t* h n a m e, char *consta rgv [] ); int execle(const charp a t* h n a m e, const char a* rg 0, ... /* (char *)0, char *cones nt v p [] */); int execve(const charp a t* h n a m e, char *consta rgv [], char *consten vp [] ); int execlp(const charf i l e* n a m e, const char a* rg 0, ... /* (char *) 0 */); int execvp(const charf i l e* n a m e, char *consta rgv [] ); 我們深入內(nèi)核代碼來研究一下可執(zhí)行文件的加載過程.execve()系統(tǒng)調(diào)用的入口是sys_execve().代碼如下: asmlinkage int sys_execve(struct pt_regs regs) { int error; char * filename;
//將用戶空間的第一個參數(shù)(也就是可執(zhí)行文件的路徑)復(fù)制到內(nèi)核 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); } //釋放內(nèi)存 putname(filename); out: return error; } 系統(tǒng)調(diào)用的時候,把參數(shù)依次放在:ebx,ecx,edx,esi,edi,ebp寄存器.詳情請參閱本站 Linux中斷處理之系統(tǒng)調(diào)用>>.第一個參數(shù)為可執(zhí)行文件路徑,第二個參數(shù)為參數(shù)的個數(shù),第三個參數(shù)為可執(zhí)行文件對應(yīng)的參數(shù). do_execve()是這個系統(tǒng)調(diào)用的核心,它的代碼如下: int do_execve(char * filename, char __user *__user *argv, char __user *__user *envp, struct pt_regs * regs) { //linux_binprm:保存可執(zhí)行文件的一些參數(shù) 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;
//在內(nèi)核中打開這個可執(zhí)行文件 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;
//計算參數(shù)個數(shù) bprm->argc = count(argv, MAX_ARG_STRINGS); if ((retval = bprm->argc) goto out_mm;
//環(huán)境變量個數(shù) 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第一個參數(shù)filename retval = copy_strings_kernel(1, &bprm->filename, bprm); if (retval goto out; //bprm->exec:參數(shù)的起始地址(從上往下方向) bprm->exec = bprm->p; //copy環(huán)境變量 retval = copy_strings(bprm->envc, envp, bprm); if (retval goto out; //環(huán)境變量存放的起始地址 env_p = bprm->p; //copy可執(zhí)行文件所帶參數(shù) retval = copy_strings(bprm->argc, argv, bprm); if (retval goto out; //環(huán)境變量的長度 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; } 研究代碼之前,我們先考慮一下進(jìn)程的空間安排結(jié)構(gòu).在本站的中的malloc機(jī)制分析>>曾經(jīng)描述過.我們再次把進(jìn)程的空間結(jié)構(gòu)圖列出,如下如示:
用戶棧位于進(jìn)程空間的最高部份.那進(jìn)程初始化時,用戶棧存放的是什么呢?是參數(shù).進(jìn)程在執(zhí)行時會到棧中去取運(yùn)行時所需的參數(shù).這里所謂的參數(shù)包含了可執(zhí)行程序所帶的參數(shù)和環(huán)境變量.例如:在shell上執(zhí)行”echo hello,eric” .echo程序帶有二個參數(shù).argv[0] = “echo”,argv[1] = “hello,eric”即第一個參數(shù)為程序名稱.其后的參數(shù)分別是運(yùn)行進(jìn)程所帶的參數(shù).當(dāng)然,在上面這個例子中沒有列出環(huán)境變量.一般的.在參數(shù)后面都跟了一個NULL.表示參數(shù)已經(jīng)結(jié)束了,在上例中argv[1]后面的一個字節(jié)是NULL.如下圖所示:
這樣程序在運(yùn)行的時候就可以方便的確定參數(shù)及環(huán)境變量的個數(shù). 現(xiàn)在,我們可以分析代碼了. bprm_mm_init()是bprm的初始化函數(shù),我們跟蹤進(jìn)去看它是怎么樣初始化的. int bprm_mm_init(struct linux_binprm *bprm) { int err; struct mm_struct *mm = NULL;
//分配一個mm //mm_alloc我們在進(jìn)程創(chuàng)建的時候已經(jīng)分析過了,值得注意的是,它會調(diào)用mm_init()來為 //進(jìn)程的用戶空間建立PGD->PMD映射 bprm->mm = mm = mm_alloc(); err = -ENOMEM; if (!mm) goto err;
err = init_new_context(current, mm); if (err) goto err; //初始化bprm->mm err = __bprm_mm_init(bprm); if (err) goto err;
return 0;
err: if (mm) { bprm->mm = NULL; mmdrop(mm); }
return err; } 重點(diǎn)是在__bprm_mm_init(): static int __bprm_mm_init(struct linux_binprm *bprm) { int err = -ENOMEM; struct vm_area_struct *vma = NULL; struct mm_struct *mm = bprm->mm;
//分配一個VMA bprm->vma = vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL); if (!vma) goto err;
down_write(&mm->mmap_sem); vma->vm_mm = mm;
//STACK_TOP_MAX:進(jìn)程用戶空間的最高值 //對應(yīng)進(jìn)程的棧頂 vma->vm_end = STACK_TOP_MAX; vma->vm_start = vma->vm_end - PAGE_SIZE;
vma->vm_flags = VM_STACK_FLAGS; vma->vm_page_prot = vm_get_page_prot(vma->vm_flags); //將VM插入mm表示的進(jìn)程空間結(jié)構(gòu) err = insert_vm_struct(mm, vma); if (err) { up_write(&mm->mmap_sem); goto err; }
mm->stack_vm = mm->total_vm = 1; up_write(&mm->mmap_sem);
//bprm->p:用戶棧的棧指針 bprm->p = vma->vm_end - sizeof(void *);
return 0;
err: if (vma) { bprm->vma = NULL; kmem_cache_free(vm_area_cachep, vma); }
return err; } 上面的操作看起來比較隱晦,我們把它的操作用下面的圖表示:
在這里為bprm->mm的初始化下了這么多功夫是為什么呢?它跟進(jìn)程的mm有什么關(guān)系?不急,繼續(xù)耐著性子看代碼,我們會看到它的用途的. 繼續(xù)分析do_execve()中所調(diào)用的子函數(shù). Count()來用計算可執(zhí)行文件的參數(shù)或者環(huán)境變量的個數(shù).它的代碼如下: static int count(char __user * __user * argv, int max) { int i = 0;
if (argv != NULL) { for (;;) { char __user * p; //在內(nèi)核空間中取argv的值
//取值失敗 if (get_user(p, argv)) return -EFAULT; //如果為空。說明已經(jīng)取到了NULL。結(jié)束了 if (!p) break; argv++; //參數(shù)個數(shù)超過了允許的最大值 if(++i > max) return -E2BIG; cond_resched(); } } return i; } 這個函數(shù)的原理是利用參數(shù)后面是以NULL結(jié)尾的,不懂的請回個頭去看下上面的分析. 疑問:在取參數(shù)個數(shù)的時候,會進(jìn)行用戶空間到內(nèi)核空間的copy.但是這里僅僅是得知它的個數(shù),在后面的操作中,還會繼續(xù)去取參數(shù)值放到bprm->mm表示的空間中.這里有兩次拷copy.可不可把這兩個過程放在一起.省掉一次從用戶空間到內(nèi)核空間的COPY呢? prepare_binprm()會將文件的前128字節(jié)copy到bprm->buf.代碼片段如下所示: int prepare_binprm(struct linux_binprm *bprm) { …… …… memset(bprm->buf,0,BINPRM_BUF_SIZE); //#define BINPRM_BUF_SIZE 128 return kernel_read(bprm->file,0,bprm->buf,BINPRM_BUF_SIZE); } 將具體的參數(shù)COPY到bprm->mm所表示的存儲空間中是由copy_strings()完成的.它的代碼有一點(diǎn)繁鎖.如下示: /* 參數(shù)含義: argc:參數(shù)個數(shù) argv:參數(shù)數(shù)組 */ static int copy_strings(int argc, char __user * __user * argv, struct linux_binprm *bprm) { struct page *kmapped_page = NULL; char *kaddr = NULL; unsigned long kpos = 0; int ret;
while (argc-- > 0) { char __user *str; int len; unsigned long pos;
//取數(shù)組相應(yīng)項,將其放至str中
//COPY失敗,或者參數(shù)長度非法 if (get_user(str, argv+argc) || !(len = strnlen_user(str, MAX_ARG_STRLEN))) { ret = -EFAULT; goto out; }
//判斷參數(shù)長度是否超過允許的最大值 if (!valid_arg_len(bprm, len)) { ret = -E2BIG; goto out; }
/* We're going to work our way backwords. */ //當(dāng)前的位置 pos = bprm->p; str += len; bprm->p -= len;
while (len > 0) { int offset, bytes_to_copy;
offset = pos % PAGE_SIZE; if (offset == 0) offset = PAGE_SIZE;
bytes_to_copy = offset; if (bytes_to_copy > len) bytes_to_copy = len;
offset -= bytes_to_copy; pos -= bytes_to_copy; str -= bytes_to_copy; len -= bytes_to_copy;
if (!kmapped_page || kpos != (pos & PAGE_MASK)) { struct page *page;
//根據(jù)映射關(guān)系得到pos地址在bprm->mm中所映射的頁面 page = get_arg_page(bprm, pos, 1); if (!page) { ret = -E2BIG; goto out; }
if (kmapped_page) { flush_kernel_dcache_page(kmapped_page); //斷開臨時映射 kunmap(kmapped_page); //減少引用計數(shù) put_arg_page(kmapped_page); } kmapped_page = page; //將臨時映射到內(nèi)核 kaddr = kmap(kmapped_page); kpos = pos & PAGE_MASK; flush_arg_page(bprm, kpos, kmapped_page); } //copy參數(shù)至剛才映射的頁面 if (copy_from_user(kaddr+offset, str, bytes_to_copy)) { ret = -EFAULT; goto out; } } } ret = 0; out: if (kmapped_page) { flush_kernel_dcache_page(kmapped_page); kunmap(kmapped_page); put_arg_page(kmapped_page); } return ret; } 我們在前面看到,并沒有給VM映射實(shí)際的內(nèi)存,在這里COPY參數(shù)的時候,必然會引起缺頁異常,再由缺頁異常程序處理缺頁的情況. 經(jīng)過上面的過程之后,bprm->mm表示的存儲空間如下所示:
經(jīng)過一系統(tǒng)的初始化之后,可以尋找該文件的加載module了.這是由search_binary_handler()完成的.在深入到這段代碼之前.我們有必要討論一下linux可執(zhí)文件模塊的組織.
在linux內(nèi)核,用linux_binfmt結(jié)構(gòu)來表示每一個加載模塊.它的定義如下: struct linux_binfmt { //用來構(gòu)成鏈表 struct list_head lh; //所屬的module struct module *module; //加載可執(zhí)行文件 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 limit); unsigned long min_coredump; /* minimal dump size */ int hasvdso; } 結(jié)構(gòu)中的lh將之組成一個鏈表,這個鏈表的表頭是formats. 為了說明,我們來看一下如何注冊一個可執(zhí)行文件的加載模塊. int register_binfmt(struct linux_binfmt * fmt) { if (!fmt) return -EINVAL; write_lock(&binfmt_lock); //將其添加之鏈表 list_add(&fmt->lh, &formats); write_unlock(&binfmt_lock); return 0; } 所以,在加載可執(zhí)文件的時候,只要遍歷formats這個鏈表,然后依次按module加載這個可執(zhí)行文件.這正是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; //這里會循環(huán)兩次.待模塊加載之后再遍歷一次 for (try=0; try read_lock(&binfmt_lock); list_for_each_entry(fmt, &formats, lh) { //加載函數(shù) & |
|