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

来源:互联网 发布:linux下软件安装 编辑:程序博客网 时间:2024/05/19 03:23

作者:姚开健

原创作品转载请注明出处

《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,1550const char __user *const __user *__argv,1551const char __user *const __user *__envp)1552{1553struct user_arg_ptr argv = { .ptr.native = __argv };1554struct user_arg_ptr envp = { .ptr.native = __envp };1555return 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,1431struct user_arg_ptr argv,1432struct user_arg_ptr envp)1433{1434struct linux_binprm *bprm;1435struct file *file;1436struct files_struct *displaced;1437int retval;14381439if (IS_ERR(filename))1440return 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{573struct file *interpreter = NULL; /* to shut gcc up */574 unsigned long load_addr = 0, load_bias = 0;575int load_addr_set = 0;576char * elf_interpreter = NULL;577unsigned long error;578struct elf_phdr *elf_ppnt, *elf_phdata;579unsigned long elf_bss, elf_brk;580int retval, i;581unsigned int size;582unsigned long elf_entry;583unsigned long interp_load_addr = 0;584unsigned long start_code, end_code, start_data, end_data;585unsigned long reloc_func_desc __maybe_unused = 0;586int executable_stack = EXSTACK_DEFAULT;587struct pt_regs *regs = current_pt_regs();588struct {589struct elfhdr elf_ex;590struct elfhdr interp_elf_ex;591} *loc;592593loc = kmalloc(sizeof(*loc), GFP_KERNEL);594if (!loc) {595retval = -ENOMEM;596goto out_ret;597}598599/* Get the exec-header */600loc->elf_ex = *((struct elfhdr *)bprm->buf);601602retval = -ENOEXEC;603/* First of all, some simple consistency checks */604if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)605goto out;

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

start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)199{200set_user_gs(regs, 0);201regs->fs= 0;202regs->ds= __USER_DS;203regs->es= __USER_DS;204regs->ss= __USER_DS;205regs->cs= __USER_CS;206regs->ip= new_ip;207regs->sp= new_sp;208regs->flags= X86_EFLAGS_IF;209/*210 * force it to the iret return path by making it look as if there was211 * some work pending.212 */213set_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类系统调用从未成功返回。新的程序开始了它的入口点处的执行。



1 0
原创粉丝点击