zoukankan      html  css  js  c++  java
  • Linux内核如何装载和启动一个可执行程序

    一、程序编译运行过程

    1. 预处理
    2. 编译
    3. .asm汇编
    4. 链接
    5. .o目标文件
    6. 装载
    7. .out可执行文件
    8. 进入内存和执行

    二、链接的两种方式

    静态链接

      静态链接是在链接时将库的内容加入到可执行程序中的做法。因为要将所有需要的库文件放到同一个文件中,所以占用空间会比较大,但是执行效率非常高。

    动态链接

      动态链接是当需要某个头文件时动态的去库中去找,并不用像静态链接那样去提前全部加载进去。这样链接出来的文件相对来说空间较小,但是效率略逊于静态链接。

      动态链接分装载时动态链接和运行时动态链接。两者在gcc下指令相同,但是使用方式略有不同。

    三、Linux下的三种目标文件格式

    1. 可重定位文件( .o ):二进制代码和数据,由各个数据节(section)构成,从地址0开始。
    2. 可执行文件:可运行的二进制代码和数据。
    3. 共享目标文件( .so ):一种特殊类型的可重定位目标文件,动态加载链接。

    Linux上,目标文件的格式称为可执行和可链接格式(ELF)。

    ELF格式

    具体ELF可重定位目标文件文件格式详见:http://blog.csdn.net/skywalker_leo/article/details/8564840

    四、execve系统调用的执行过程分析

    do_execve

    首先是do_execve以及其调用的关键函数的代码:

     1 int do_execve(struct filename *filename,
     2     const char __user *const __user *__argv,
     3     const char __user *const __user *__envp)
     4 {
     5     // ...
     6     return do_execve_common(filename, argv, envp);
     7 }
     8 
     9 static int do_execve_common(struct filename *filename,
    10                 struct user_arg_ptr argv,
    11                 struct user_arg_ptr envp)
    12 {
    13     sched_exec();
    14     // ...
    15     retval = bprm_mm_init(bprm);
    16     retval = prepare_binprm(bprm);
    17     // ...
    18     retval = copy_strings_kernel(1, &bprm->filename, bprm);
    19     retval = copy_strings(bprm->envc, envp, bprm);
    20     retval = copy_strings(bprm->argc, argv, bprm);
    21     // ...
    22     retval = exec_binprm(bprm);
    23     // ...
    24 }
    25 
    26 static int exec_binprm(struct linux_binprm *bprm)
    27 {
    28     // ...
    29     ret = search_binary_handler(bprm);
    30     // ...
    31     return ret;
    32 }

    上述代码中do_execve函数调用了do_execve_common函数,do_execve_common又调用了exec_binprm函数,在exec_binprm中又调用了search_binary_handler函数。至此我们可以总结出一个调用关系:

    do_execve() -> do_execve_common() -> exec_binprm() -> search_binary_handler()

    search_binary_handler

    首先是代码部分:

    int search_binary_handler(struct linux_binprm *bprm)
    {
        struct linux_binfmt *fmt;
        // ...
        list_for_each_entry(fmt, &formats, lh) {
            // ...
            retval = fmt->load_binary(bprm);
            // ...
        }
        // ...
    }

    我们可以发现这个函数是依次遍历所有格式,依据不同格式相应不同的load_binary函数。而linux_binfmt的结构体格式如下:

    1 struct linux_binfmt {
    2     struct list_head lh;
    3     struct module *module;
    4     int (*load_binary)(struct linux_binprm *);
    5     int (*load_shlib)(struct file *);
    6     int (*core_dump)(struct coredump_params *cprm);
    7     unsigned long min_coredump;    /* minimal dump size */
    8 };

    这里我们发现load_binary本身是个函数指针,所以在search_binary_handler中的

    retval = fmt->load_binary(bprm);

     这条语句其实是对应着不同的函数调用。

    load_elf_binary

    首先是代码部分:

      1 static int load_elf_binary(struct linux_binprm *bprm)
      2 {
      3     // ....
      4     struct pt_regs *regs = current_pt_regs();  // 获取当前进程的寄存器存储位置
      5 
      6     // 获取elf前128个字节,作为魔数
      7     loc->elf_ex = *((struct elfhdr *)bprm->buf);
      8 
      9     // 检查魔数是否匹配
     10     if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
     11         goto out;
     12 
     13     // 如果既不是可执行文件也不是动态链接程序,就错误退出
     14     if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
     15         // ...
     16     // 读取所有的头部信息
     17     // 读入程序的头部分
     18     retval = kernel_read(bprm->file, loc->elf_ex.e_phoff,
     19                  (char *)elf_phdata, size);
     20 
     21     // 遍历elf的程序头
     22     for (i = 0; i < loc->elf_ex.e_phnum; i++) {
     23         // 如果存在解释器头部
     24         if (elf_ppnt->p_type == PT_INTERP) {
     25             // ...
     26             // 读入解释器名
     27             retval = kernel_read(bprm->file, elf_ppnt->p_offset,
     28                          elf_interpreter,
     29                          elf_ppnt->p_filesz);
     30     
     31             // 打开解释器文件
     32             interpreter = open_exec(elf_interpreter);
     33 
     34             // 读入解释器文件的头部
     35             retval = kernel_read(interpreter, 0, bprm->buf,
     36                          BINPRM_BUF_SIZE);
     37 
     38             // 获取解释器的头部
     39             loc->interp_elf_ex = *((struct elfhdr *)bprm->buf);
     40             break;
     41         }
     42         elf_ppnt++;
     43     }
     44 
     45     // 释放空间、删除信号、关闭带有CLOSE_ON_EXEC标志的文件
     46     retval = flush_old_exec(bprm);
     47 
     48     setup_new_exec(bprm);
     49 
     50     // 为进程分配用户态堆栈,并塞入参数和环境变量
     51     retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
     52                  executable_stack);
     53     current->mm->start_stack = bprm->p;
     54 
     55     // 将elf文件映射进内存
     56     for(i = 0, elf_ppnt = elf_phdata;
     57         i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
     58 
     59         if (unlikely (elf_brk > elf_bss)) {
     60             unsigned long nbyte;
     61                 
     62             // 生成BSS
     63             retval = set_brk(elf_bss + load_bias,
     64                      elf_brk + load_bias);
     65             // ...
     66         }
     67 
     68         // 可执行程序
     69         if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {
     70             elf_flags |= MAP_FIXED;
     71         } else if (loc->elf_ex.e_type == ET_DYN) { // 动态链接库
     72             // ...
     73         }
     74 
     75         // 创建一个新线性区对可执行文件的数据段进行映射
     76         error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
     77                 elf_prot, elf_flags, 0);
     78 
     79         }
     80     }
     81 
     82     // 加上偏移量
     83     loc->elf_ex.e_entry += load_bias;
     84 
     85     // ....
     86 
     87 
     88     // 创建一个新的匿名线性区,来映射程序的bss段
     89     retval = set_brk(elf_bss, elf_brk);
     90 
     91     // 如果是动态链接
     92     if (elf_interpreter) {
     93         unsigned long interp_map_addr = 0;
     94 
     95         // 调用一个装入动态链接程序的函数 此时elf_entry指向一个动态链接程序的入口
     96         elf_entry = load_elf_interp(&loc->interp_elf_ex,
     97                         interpreter,
     98                         &interp_map_addr,
     99                         load_bias);
    100         // ...
    101     } else {
    102         // elf_entry是可执行程序的入口
    103         elf_entry = loc->elf_ex.e_entry;
    104         // ....
    105     }
    106 
    107     // 修改保存在内核堆栈,但属于用户态的eip和esp
    108     start_thread(regs, elf_entry, bprm->p);
    109     retval = 0;
    110     // ...
    111 }

    由于前面已经介绍了ELF文件的格式,这里就不再赘述。

    由此我们可以大致分析出其执行流程:

    1. 检查以及分析头部。
    2. 检查是静态链接还是动态链接,如果为静态链接直接加载文件,如果是动态链接则加载动态链接器。
    3. 初始化ELF文件执行环境( 如修改入口点,加载文件内容等 )。
    4. 执行start_thread函数。

    start_thread

    首先是代码部分:

     1 void
     2 start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
     3 {
     4     set_user_gs(regs, 0); // 将用户态的寄存器清空
     5     regs->fs        = 0;
     6     regs->ds        = __USER_DS;
     7     regs->es        = __USER_DS;
     8     regs->ss        = __USER_DS;
     9     regs->cs        = __USER_CS;
    10     regs->ip        = new_ip; // 新进程的运行位置- 动态链接程序的入口处
    11     regs->sp        = new_sp; // 用户态的栈顶
    12     regs->flags        = X86_EFLAGS_IF;
    13     
    14     set_thread_flag(TIF_NOTIFY_RESUME);
    15 }

    这里将寄存器清空,然后开辟一个新的栈空间,赋予新的寄存器值。

    五、exec*和fork的区别:

    fork是linux的系统调用,用来创建子进程。子进程和父进程唯一不同的在于pid的不同。

    当系统调用exec时,旧的进程中的程序会完全被新的程序替代,其他部分也会被新的程序完全替换掉(如正文、数据、栈等)。这时旧的程序会死掉,而pid并没有发生任何变化。

    一般在执行完fork后,其子进程会执行exec调用,所以vfork产生了,具体有兴趣可以自己去查下vfork。

     

    六、本次实验的操作过程及实验截图

    首先在终端中更新最新版本的menu文件夹并编译执行,生成新的系统文件。

    然后用gdb进行进一步的调试,并在以下地方设置断点并且跟踪运行。

    gdb调试命令:

    1 qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S

    中断位置:

    1 b sys_execve
    2 b do_execve
    3 b do_execve_common
    4 b exec_binprm
    5 b search_binary_handler
    6 b load_elf_binary
    7 b start_thread

    七、总结

    系统装载和启动一个新的程序依次调用一下函数:

    sys_execve() -> do_execve() -> do_execve_common() -> exec_binprm() -> search_binary_handler() -> load_elf_binary() -> start_thread()

    exec的本质是进程程序的替换过程。过程的重点在于ELF格式的解析,和新的代码的堆栈信息、数据信息以及寄存器上下文的设定。替换完成后根据链接的不同方式设置相应的启示位置,最后执行程序。

     

    参考文献

    1. http://m.blog.csdn.net/blog/jy02326166/37593735
    2. http://blog.163.com/sxs_solo/blog/static/263333820085272152395/

     

    李若森

    原创作品转载请注明出处

    《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

  • 相关阅读:
    Neo4j使用
    Neo4j安装
    textCNN原理
    一小时彻底搞懂RabbitMQ
    微服务配置中心 Apollo 源码解析——Admin 发送发布消息
    new jup在新一代中存在
    elasticsearch 之 深入探秘type底层数据结构
    Elasticsearch修改分词器以及自定义分词器
    ElasticSearch解决深度分页性能存在的问题使用scoll来解决
    elasticsearchBouncing Results问题
  • 原文地址:https://www.cnblogs.com/Hitman_47/p/4442367.html
Copyright © 2011-2022 走看看