通过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)==‘\t‘) || ((c)==‘\n‘) || (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可执行文件装载完成。

 

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。