分享

linux可执行文件的加载和运行

 看风景D人 2014-09-16

    linux可执行文件的加载和运行之一

    

    三:可执行文件的加载和运行

    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机制分析>>曾经描述过.我们再次把进程的空间结构图列出,如下如示:

    

    

    用户栈位于进程空间的最高部份.那进程初始化时,用户栈存放的是什么呢?是参数.进程在执行时会到栈中去取运行时所需的参数.这里所谓的参数包含了可执行程序所带的参数和环境变量.例如:在shell上执行”echo hello,eric” .echo程序带有二个参数.argv[0] = “echo”,argv[1] = “hello,eric”即第一个参数为程序名称.其后的参数分别是运行进程所带的参数.当然,在上面这个例子中没有列出环境变量.一般的.在参数后面都跟了一个NULL.表示参数已经结束了,在上例中argv[1]后面的一个字节是NULL.如下图所示:

    

    

    这样程序在运行的时候就可以方便的确定参数及环境变量的个数.

    现在,我们可以分析代码了.

    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;

    }

    上面的操作看起来比较隐晦,我们把它的操作用下面的图表示:

    

    

    在这里为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表示的存储空间如下所示:

    

    

    经过一系统的初始化之后,可以寻找该文件的加载module了.这是由search_binary_handler()完成的.在深入到这段代码之前.我们有必要讨论一下linux可执文件模块的组织.

    

    在linux内核,用linux_binfmt结构来表示每一个加载模块.它的定义如下:

    struct linux_binfmt {

     //用来构成链表

     struct list_head lh;

     //所属的module

     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 limit);

     unsigned long min_coredump; /* minimal dump size */

     int hasvdso;

    }

    结构中的lh将之组成一个链表,这个链表的表头是formats.

    为了说明,我们来看一下如何注册一个可执行文件的加载模块.

    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;

    }

    所以,在加载可执文件的时候,只要遍历formats这个链表,然后依次按module加载这个可执行文件.这正是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) {

     //加载函数

&       

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多