作者:姚开健
原创作品转载请注明出处
《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(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
此时程序的内存映像是:
总结
Linux内核装载和运行一个可执行程序是一个很复杂的过程,设计到系统的许多方面,例如进程抽象,文件系统,内存管理,系统调用等。当exce类系统调用可执行程序完毕后回到原来的用户态时,其上下文已经被修改,exce调用代码已不在,可以说exce类系统调用从未成功返回。新的程序开始了它的入口点处的执行。