Linux内核分析之七——Linux内核如何装载和启动一个可执行程序

作者:姚开健

原创作品转载请注明出处

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

1、ELF的文件格式。

通常我们将程序文件编译后得到的目标文件,在Linux上其格式就是ELF文件,就是 EXECUTABLE AND LINKABLE FORMAT,其格式如下所示:

Linux内核分析之七——Linux内核如何装载和启动一个可执行程序_第1张图片

我们从上可以知道,ELF文件最开始是一个ELF头,保存了路线图(road map),描述了该文件的组织情况。我们可以通过readelf -h命令来读取一个ELF文件的头部,其组成如下所示:

Linux内核分析之七——Linux内核如何装载和启动一个可执行程序_第2张图片

值得注意的是ELF32_addr e_entry,它保存的是文件开始执行的地址,通常是0x08048000。

除了ELF头部以外,还需要关注的是节,分别是.text,被编译程序的机器代码;.rodata,read only data,诸如printf语句中的形式串和switch语句的跳转表等只读数据;.data,已初始化的全局变量;.bss,未初始化的全局变量,在目标文件中不占实际的空间。可以通过一般程序来指明其分布:

Linux内核分析之七——Linux内核如何装载和启动一个可执行程序_第3张图片

如上所示,黑色字体的是程序的机器代码,保存在.text节,红色字体是未初始化的全局变量保存在.bss节,蓝色字体是已初始化的全局变量,保存在.data节。

除了以上比较重要的节以外,其他节的信息可以在网上一些ELF文件格式分析文章(http://www.xfocus.net/articles/200105/174.html)找到说明,在此仅简略地说明比较重要的,常见的节。

2、可执行文件的装载

当系统要开始执行一个新程序时,通常会有exec类系统调用来执行装载可执行文件到内存中。其一般步骤包括为新执行的程序分配页框,将函数调用的参数int argc, char* argv[](即我们所说的main函数参数)传入到可执行文件中,有时候还会有char* const envp[]这个环境变量参数如在shell中输入命令ls -l,那么这个shell进程就把“ls”,当前目录,“-l”这三个字符串放入参数中,接着调用do_execve()

9int do_execve(struct filename *filename,
1550	const char __user *const __user *__argv,
1551	const char __user *const __user *__envp)
1552{
1553	struct user_arg_ptr argv = { .ptr.native = __argv };
1554	struct user_arg_ptr envp = { .ptr.native = __envp };
1555	return do_execve_common(filename, argv, envp);
1556}
如代码所示,第一个参数是文件名,即可执行文件名,二是argv参数,三是环境变量参数,在上述命令中,“ls”“ -l”被放入了argv这个参数中,接着函数调用do_execve_common():

/*
1428 * sys_execve() executes a new program.
1429 */
1430static int do_execve_common(struct filename *filename,
1431				struct user_arg_ptr argv,
1432				struct user_arg_ptr envp)
1433{
1434	struct linux_binprm *bprm;
1435	struct file *file;
1436	struct files_struct *displaced;
1437	int retval;
1438
1439	if (IS_ERR(filename))
1440		return PTR_ERR(filename);
接着再调用exce_binprm()。在这些函数调用中都是为了找到要执行的可执行文件,如“ls”程序的可执行文件,然后需要找到当前可执行文件的对应格式的解析模块,search_binary_handler,如下:

1369    list_for_each_entry(fmt, &formats, lh) {
1370        if (!try_module_get(fmt->module))
1371            continue;
1372        read_unlock(&binfmt_lock);
1373        bprm->recursion_depth++;
1374        retval = fmt->load_binary(bprm);
1375        read_lock(&binfmt_lock);

其中format是一个链表,函数会遍历这个链表,并调用每个节点的load_binary,并把bprm这个结构体传过去,如果load_binary成功应答了结构体中的文件格式,则说明找到了对应可执行文件格式的装载程序,遍历结束。对于ELF格式的可执行文件fmt->load_binary(bprm);执行的应该是load_elf_binary,其内部是和ELF文件格式解析,节选部分代码所示:

static int load_elf_binary(struct linux_binprm *bprm)
572{
573	struct file *interpreter = NULL; /* to shut gcc up */
574 	unsigned long load_addr = 0, load_bias = 0;
575	int load_addr_set = 0;
576	char * elf_interpreter = NULL;
577	unsigned long error;
578	struct elf_phdr *elf_ppnt, *elf_phdata;
579	unsigned long elf_bss, elf_brk;
580	int retval, i;
581	unsigned int size;
582	unsigned long elf_entry;
583	unsigned long interp_load_addr = 0;
584	unsigned long start_code, end_code, start_data, end_data;
585	unsigned long reloc_func_desc __maybe_unused = 0;
586	int executable_stack = EXSTACK_DEFAULT;
587	struct pt_regs *regs = current_pt_regs();
588	struct {
589		struct elfhdr elf_ex;
590		struct elfhdr interp_elf_ex;
591	} *loc;
592
593	loc = kmalloc(sizeof(*loc), GFP_KERNEL);
594	if (!loc) {
595		retval = -ENOMEM;
596		goto out_ret;
597	}
598
599	/* Get the exec-header */
600	loc->elf_ex = *((struct elfhdr *)bprm->buf);
601
602	retval = -ENOEXEC;
603	/* First of all, some simple consistency checks */
604	if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
605		goto out;

接着函数会调用start_thread()函数:

start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
199{
200	set_user_gs(regs, 0);
201	regs->fs		= 0;
202	regs->ds		= __USER_DS;
203	regs->es		= __USER_DS;
204	regs->ss		= __USER_DS;
205	regs->cs		= __USER_CS;
206	regs->ip		= new_ip;
207	regs->sp		= new_sp;
208	regs->flags		= X86_EFLAGS_IF;
209	/*
210	 * force it to the iret return path by making it look as if there was
211	 * some work pending.
212	 */
213	set_thread_flag(TIF_NOTIFY_RESUME);
214}
215EXPORT_SYMBOL_GPL(start_thread);
216

注意这个函数调用的参数二,new_ip,这是可执行文件的入口执行的地址,也就是在我们上面所说的文件头的地址0x080480000的旁边0x08048094(.text代码节的开始地址),这是函数start_thread会修改保存在内核态堆栈但是属于用户态寄存器的的eip和esp,使它们分别指向程序解释器的入口点(开始地址)和新的用户态堆栈的栈底,接着从内核保存在用户态堆栈的信息(如环境变量参数指针数组等),为自己创建一个基本的执行上下文,接着还有为新的执行程序的共享库做一些初始化工作,此时新的执行程序装载完毕, 开始跳转到入口点(开始地址)地址执行。

此时程序的内存映像是:

Linux内核分析之七——Linux内核如何装载和启动一个可执行程序_第4张图片


总结

Linux内核装载和运行一个可执行程序是一个很复杂的过程,设计到系统的许多方面,例如进程抽象,文件系统,内存管理,系统调用等。当exce类系统调用可执行程序完毕后回到原来的用户态时,其上下文已经被修改,exce调用代码已不在,可以说exce类系统调用从未成功返回。新的程序开始了它的入口点处的执行。



你可能感兴趣的:(Linux内核分析之七——Linux内核如何装载和启动一个可执行程序)