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

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