zoukankan      html  css  js  c++  java
  • 通过gdb跟踪Linux内核装载和启动可执行程序过程

    作者:吴乐 山东师范大学

    《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,
                &regs);
        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)=='	') || ((c)=='
    ') || (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可执行文件装载完成。

  • 相关阅读:
    如何做一个快乐的人
    嵌入式实时操作系统的可裁剪性及其实现
    Hello China操作系统运行截图(完整版)
    物联网操作系统的概念和特点
    Windows Phone 31日谈——第4日:设备方向
    Windows Phone 31日谈——第6日:工具栏
    Windows Phone 31 日谈——第8日:选择器
    Windows Phone 31日谈——第5日:系统主题
    Windows Phone 31 日谈——第10日:输入范围和文本框
    Windows Phone 31 日谈——第13日:位置服务
  • 原文地址:https://www.cnblogs.com/wule/p/4430398.html
Copyright © 2011-2022 走看看