作者:姚开健
原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
1、ELF的文件格式。
通常我们将程序文件编译后得到的目标文件,在Linux上其格式就是ELF文件,就是 EXECUTABLE AND LINKABLE FORMAT,其格式如下所示:
我们从上可以知道,ELF文件最开始是一个ELF头,保存了路线图(road map),描述了该文件的组织情况。我们可以通过readelf -h命令来读取一个ELF文件的头部,其组成如下所示:
值得注意的是ELF32_addr e_entry,它保存的是文件开始执行的地址,通常是0x08048000。
除了ELF头部以外,还需要关注的是节,分别是.text,被编译程序的机器代码;.rodata,read only data,诸如printf语句中的形式串和switch语句的跳转表等只读数据;.data,已初始化的全局变量;.bss,未初始化的全局变量,在目标文件中不占实际的空间。可以通过一般程序来指明其分布:
如上所示,黑色字体的是程序的机器代码,保存在.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内核装载和运行一个可执行程序是一个很复杂的过程,设计到系统的许多方面,例如进程抽象,文件系统,内存管理,系统调用等。当exce类系统调用可执行程序完毕后回到原来的用户态时,其上下文已经被修改,exce调用代码已不在,可以说exce类系统调用从未成功返回。新的程序开始了它的入口点处的执行。