Linux内核r如何装载和启动一个可执行程序
原创作品转载请注明出处《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
1、基础
我们都知道,C语言的执行都必须经过预处理、编译、汇编、链接和执行等过程。这次实验我们就通过 GDB 来跟踪分析一个 execve 系统调用内核处理函数 sys_execve,深入理解 Linux 操作系统装载链接和运行可执行程序的过程。
还是以 hello_world.c 程序为例,搞清楚可执行程序是如何生成的:
#include <stdio.h>
int main()
{
printf("hello, world!\n");
return 0;
}
1.预处理,处理代码中的宏定义和 include
文件,并做语法检查
gcc -E hello_world.c -o hello_world.i
2.编译,生成汇编代码
gcc -S hello_world.i -o hello_world.s
3.汇编,生成 ELF
格式的目标代码
gcc -c hello_world.s -o hello_world.o
4.链接,生成可执行代码
gcc hello_world.o -o hello_world
5.执行程序
./hello_world hello, world!</span>
2、静态链接和动态链接
静态连接库就是把(lib)文件中用到的函数代码直接链接进目标程序,程序运行的时候不再需要其它的库文件;动态链接就是把调用的函数所在文件模块(DLL)和调用函数在文件中的位置等信息链接进目标程序,程序运行的时候再从DLL中寻找相应函数代码,因此需要相应DLL文件的支持。
静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib 中的指令都全部被直接包含在最终生成的 EXE 文件中了。但是若使用 DLL,该 DLL 不必被包含在最终 EXE 文件中,EXE 文件执行时可以“动态”地引用和卸载这个与 EXE 独立的 DLL 文件。静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。
3、装载程序
在sys_execve
处设置断点开始追踪分析:
我们再来看下面两个函数:
一个是search_binary_handler
int search_binary_handler(struct linux_binprm *bprm)
{
...
retry:
read_lock(&binfmt_lock);
//循环查找 linux_binfmt
list_for_each_entry(fmt, &formats, lh) {
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
bprm->recursion_depth++;
//对于 elf 文件,实际上执行的就是 load_elf_binary
retval = fmt->load_binary(bprm);
read_lock(&binfmt_lock);
...
}
另一个是load_elf_binary
static int load_elf_binary(struct linux_binprm *bprm)
{
...
//获取头
loc->elf_ex = *((struct elfhdr *)bprm->buf);
//读取头信息
if (loc->elf_ex.e_phentsize != sizeof(struct elf_phdr))
goto out;
if (loc->elf_ex.e_phnum < 1 ||
loc->elf_ex.e_phnum > 65536U / sizeof(struct elf_phdr))
goto out;
size = loc->elf_ex.e_phnum * sizeof(struct elf_phdr);
retval = -ENOMEM;
elf_phdata = kmalloc(size, GFP_KERNEL);
if (!elf_phdata)
goto out;
...
//读取可执行文件的解析器
for (i = 0; i < loc->elf_ex.e_phnum; i++) {
if (elf_ppnt->p_type == PT_INTERP) {
...
}
...
//如果需要装入解释器,并且解释器的映像是ELF格式的,就通过load_elf_interp()装入其映像,并把将来进入用户空间时的入口地址设置成load_elf_interp()的返回值,那显然是解释器的程序入口。而若不装入解释器,那么这个地址就是目标映像本身的程序入口。
if (elf_interpreter) {
unsigned long interp_map_addr = 0;
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_map_addr,
load_bias);
if (!IS_ERR((void *)elf_entry)) {
interp_load_addr = elf_entry;
elf_entry += loc->interp_elf_ex.e_entry;
}
if (BAD_ADDR(elf_entry)) {
retval = IS_ERR((void *)elf_entry) ?
(int)elf_entry : -EINVAL;
goto out_free_dentry;
}
reloc_func_desc = interp_load_addr;
allow_write_access(interpreter);
fput(interpreter);
kfree(elf_interpreter);
} else {
elf_entry = loc->elf_ex.e_entry;
if (BAD_ADDR(elf_entry)) {
retval = -EINVAL;
goto out_free_dentry;
}
}
}
我们会看到在load_elf_binary
中调用 start_thread
函数,它修改 int 0x80
压入内核堆栈的EIP
,当 load_elf_binary
执行完成,返回到 do_execve
然后再返回到sys_execve
时,系统调用的返回地址(EIP)已经被改写成了被装载的 ELF
程序的入口地址了。
4、总结
通过本次实验,我基本了解了程序装载和启动的流程,获益匪浅。