出处:http://ericxiao.cublog.cn/ 可执行文件的加载和运行 Execve系统调用可以调用一个可执行文件完全代替当前的进程,它在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 [] ); 我们深入内核代码来研究一下可执行文件的加载过程.execve()系统调用的入口是sys_execve().代码如下: asmlinkage 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寄存器.详情请参阅本站 Linux中断处理之系统调用>>.第一个参数为可执行文件路径,第二个参数为参数的个数,第三个参数为可执行文件对应的参数. 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; } 研究代码之前,我们先考虑一下进程的空间安排结构.在本站的中的malloc机制分析>>曾经描述过.我们再次把进程的空间结构图列出,如下如示: screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL Mouse wheel to zoom in/out';}" onmouseover="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open('http://blogimg.chinaunix.net/blog/upfile2/080411170145.jpg');}" onmousewheel="return imgzoom(this);" alt="" /> 用户栈位于进程空间的最高部份.那进程初始化时,用户栈存放的是什么呢?是参数.进程在执行时会到栈中去取运行时所需的参数.这里所谓的参数包含了可执行程序所带的参数和环境变量.例如:在shell上执行”echo hello,eric” .echo程序带有二个参数.argv[0] = “echo”,argv[1] = “hello,eric”即第一个参数为程序名称.其后的参数分别是运行进程所带的参数.当然,在上面这个例子中没有列出环境变量.一般的.在参数后面都跟了一个NULL.表示参数已经结束了,在上例中argv[1]后面的一个字节是NULL.如下图所示: screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL Mouse wheel to zoom in/out';}" onmouseover="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open('http://blogimg.chinaunix.net/blog/upfile2/080411170239.jpg');}" onmousewheel="return imgzoom(this);" alt="" /> 这样程序在运行的时候就可以方便的确定参数及环境变量的个数. 现在,我们可以分析代码了. bprm_mm_init()是bprm的初始化函数,我们跟踪进去看它是怎么样初始化的. int bprm_mm_init(struct linux_binprm *bprm) { int err; struct mm_struct *mm = NULL; //分配一个mm //mm_alloc我们在进程创建的时候已经分析过了,值得注意的是,它会调用mm_init()来为 //进程的用户空间建立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; } 重点是在__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:进程用户空间的最高值 //对应进程的栈顶 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表示的进程空间结构 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; } 上面的操作看起来比较隐晦,我们把它的操作用下面的图表示: screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL Mouse wheel to zoom in/out';}" onmouseover="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open('http://blogimg.chinaunix.net/blog/upfile2/080411170401.jpg');}" onmousewheel="return imgzoom(this);" alt="" /> 在这里为bprm->mm的初始化下了这么多功夫是为什么呢?它跟进程的mm有什么关系?不急,继续耐着性子看代码,我们会看到它的用途的. 继续分析do_execve()中所调用的子函数. Count()来用计算可执行文件的参数或者环境变量的个数.它的代码如下: static int count(char __user * __user * argv, int max) { int i = 0; if (argv != NULL) { for (;;) { char __user * p; //在内核空间中取argv的值 //取值失败 if (get_user(p, argv)) return -EFAULT; //如果为空。说明已经取到了NULL。结束了 if (!p) break; argv ; //参数个数超过了允许的最大值 if( i > max) return -E2BIG; cond_resched(); } } return i; } 这个函数的原理是利用参数后面是以NULL结尾的,不懂的请回个头去看下上面的分析. 疑问:在取参数个数的时候,会进行用户空间到内核空间的copy.但是这里仅仅是得知它的个数,在后面的操作中,还会继续去取参数值放到bprm->mm表示的空间中.这里有两次拷copy.可不可把这两个过程放在一起.省掉一次从用户空间到内核空间的COPY呢? prepare_binprm()会将文件的前128字节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); } 将具体的参数COPY到bprm->mm所表示的存储空间中是由copy_strings()完成的.它的代码有一点繁锁.如下示: /* 参数含义: argc:参数个数 argv:参数数组 */ 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; //取数组相应项,将其放至str中 //COPY失败,或者参数长度非法 if (get_user(str, argv argc) || !(len = strnlen_user(str, MAX_ARG_STRLEN))) { ret = -EFAULT; goto out; } //判断参数长度是否超过允许的最大值 if (!valid_arg_len(bprm, len)) { ret = -E2BIG; goto out; } /* We're going to work our way backwords. */ //当前的位置 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; //根据映射关系得到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); //减少引用计数 put_arg_page(kmapped_page); } kmapped_page = page; //将临时映射到内核 kaddr = kmap(kmapped_page); kpos = pos & PAGE_MASK; flush_arg_page(bprm, kpos, kmapped_page); } //copy参数至刚才映射的页面 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映射实际的内存,在这里COPY参数的时候,必然会引起缺页异常,再由缺页异常程序处理缺页的情况. 经过上面的过程之后,bprm->mm表示的存储空间如下所示: screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.alt='Click here to open new window\nCTRL Mouse wheel to zoom in/out';}" onmouseover="if(this.width>screen.width*0.7) {this.resized=true; this.width=screen.width*0.7; this.style.cursor='hand'; this.alt='Click here to open new window\nCTRL Mouse wheel to zoom in/out';}" onclick="if(!this.resized) {return true;} else {window.open('http://blogimg.chinaunix.net/blog/upfile2/080411170452.jpg');}" onmousewheel="return imgzoom(this);" alt="" /> 经过一系统的初始化之后,可以寻找该文件的加载module了.这是由search_binary_handler()完成的.在深入到这段代码之前.我们有必要讨论一下linux可执文件模块的组织. |
|小黑屋|最新主题|手机版|微赢网络技术论坛 ( 苏ICP备08020429号 )
GMT+8, 2024-9-30 09:34 , Processed in 0.209303 second(s), 12 queries , Gzip On, MemCache On.
Powered by Discuz! X3.5
© 2001-2023 Discuz! Team.