Linux内核如何装载和启动一个可执行程序
一、程序编译运行过程
- 预处理
- 编译
- .asm汇编
- 链接
- .o目标文件
- 装载
- .out可执行文件
- 进入内存和执行
二、链接的两种方式
静态链接
静态链接是在链接时将库的内容加入到可执行程序中的做法。因为要将所有需要的库文件放到同一个文件中,所以占用空间会比较大,但是执行效率非常高。
动态链接
动态链接是当需要某个头文件时动态的去库中去找,并不用像静态链接那样去提前全部加载进去。这样链接出来的文件相对来说空间较小,但是效率略逊于静态链接。
动态链接分装载时动态链接和运行时动态链接。两者在gcc下指令相同,但是使用方式略有不同。
三、Linux下的三种目标文件格式
- 可重定位文件( .o ):二进制代码和数据,由各个数据节(section)构成,从地址0开始。
- 可执行文件:可运行的二进制代码和数据。
- 共享目标文件( .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文件的格式,这里就不再赘述。
由此我们可以大致分析出其执行流程:
- 检查以及分析头部。
- 检查是静态链接还是动态链接,如果为静态链接直接加载文件,如果是动态链接则加载动态链接器。
- 初始化ELF文件执行环境( 如修改入口点,加载文件内容等 )。
- 执行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格式的解析,和新的代码的堆栈信息、数据信息以及寄存器上下文的设定。替换完成后根据链接的不同方式设置相应的启示位置,最后执行程序。
参考文献
- http://m.blog.csdn.net/blog/jy02326166/37593735
- http://blog.163.com/sxs_solo/blog/static/263333820085272152395/
李若森
原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。