• linux装载可执行程序简析


    朱宇轲 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

    linux中主要的可执行文件为ELF文件,我们可以将它装载到自己的程序中,这次我们就将分析linux装载可执行程序的过程。

    首先明确一点,装载可执行程序有两种方式:静态链接与动态链接。所谓静态链接,就是在程序执行之前完成所有链接工作,组成一个可执行文件,放到内存执行。这样做的缺点是,当有多个文件要链接同一份可执行文件时,内存中会有多份这个可执行文件的拷贝,这在一定程度上就是一种对内存的浪费。因此,人们又发明了动态链接的概念,它指的是程序执行前并不将所有的模块组装在一起,而是在需要用到这个模块的时候再完成链接工作,这样相比静态链接就更加灵活,也节省了内存。

    动态链接分为装载时动态链接和运行时动态链接,大家可有兴趣可以进一步了解一下。

     基础知识普及完毕,接着我们来分析linux具体是如何装载可执行程序的。

    linux装载可执行程序的系统调用是execve,它和fork函数一样,在执行的过程中会更改执行完毕后返回的代码段。

    它的工作是首先读入传入的文件名、参数和环境变量,然后调用解析链表寻找解析该可执行文件的结构:

    list_for_each_entry(fmt, &formats, lh) {
            if (!try_module_get(fmt->module))
                continue;
            read_unlock(&binfmt_lock);
            bprm->recursion_depth++;
            retval = fmt->load_binary(bprm);
            read_lock(&binfmt_lock);
    

    比如我们读入了ELF文件,那它就要在链表中寻找ELF文件的解析器。这里注意运用了观察者模式:linux会将各种解析器预先注册,当添加了新的解析器后,就会更改解析的链表。比如ELF文件中是这样的:

    static struct linux_binfmt elf_format = {
      .module     = THIS_MODULE,
      .load_binary    = load_elf_binary,
      .load_shlib = load_elf_library,
      .core_dump  = elf_core_dump,
      .min_coredump   = ELF_EXEC_PAGESIZE,
    };
    

     在这里,它定义load_lef_binary为解析器load_binary的具体实现(其实就是一种多态),之后将该结构体注册到解析器的链表中,从此再遇到ELF文件,搜索解析器链表,就可以找到专门解析这种文件的解析器了。

    static int __init init_elf_binfmt(void)
    {
        register_binfmt(&elf_format);
        return 0;
    }
    

    在ELF自己的解析函数load_elf_binary中,对于静态链接和动态链接,处理过程是不一样的。

    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)) {
    			/*
    			 * load_elf_interp() returns relocation
    			 * adjustment
    			 */
    			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;
    		}
    	}
    ...
    start_thread(regs,elf_entry,bprm->p);

    对于动态链接,这段代码直接执行elf_interpreter的部分,此时会装载一个动态链接器,由它再进行具体的内存管理,这里暂且不讨论。

    对于静态链接,则直接执行else的部分,此时会将ELF代码段的入口地址付给elf_entry变量。

    之后会执行start_thread函数,该函数将进程上下文压栈,同时将elf_entry赋给ip,对于静态链接来说,也就是使代码跳出内核态后执行的第一条代码就是ELF的入口处代码。

    这样一来,就可以实现装载可执行程序的功能了。

    总结:

      本博客讨论了linux装载可执行程序的过程,装载的可执行程序分为静态和动态链接两种方式。在解析可执行文件时,linux利用了多态机制和观察者模式,并在解析过程中改变内核堆栈的EIP地址,从而实现将执行的下一条代码更改到可执行程序的作用。

    实验过程是基于实验楼的系统,这里显示几张截图:

     

    设置sys_execve、load_elf_bianry、start_thread等系统调用作为断点进行调试,从第三张图可以发现,传入start_thread的elf_entry地址和我们链接的可执行文件hello的地址是一样的,这就说明,通过start_thread,系统将hello的程序入口地址设置为系统堆栈EIP的指向,从而使得重新回到用户态之后首先执行hello。

    今天的分析就到这里,谢谢大家。

  • 相关阅读:
    Ubuntu kylin 14.04 安装问题未解决
    C 语言实例
    C 语言实例
    C 语言实例
    C 语言实例
    C 语言实例
    C 语言实例
    C 语言实例
    C 语言实例
    C 语言实例
  • 原文地址:https://www.cnblogs.com/wickedpriest/p/4438514.html
Copyright © 2020-2023  润新知