linux中ELF加载过程分析


sys_execve

   | - do_execve

|

| - search_binary_handler        

           |- linux_binfmt= elf_format

       |- elf_format-> load_elf_binary

| - elf_entry = load_elf_interp()

          |-  

| if  (BAD_ADDR(elf_entry))

                |     force_sig(SIGSEGV, current);

|     retval =-EINVAL;


binfmt_elf.c: line 1024

              elf_entry = loc->elf_ex.e_entry;

              if (BAD_ADDR(elf_entry)) {

                     force_sig(SIGSEGV, current);

                     retval = -EINVAL;

                     goto out_free_dentry;

              }



ELF可行档的载入:



内核中实际执行execv()或execve()系统调用的程序是do_execve(),这个函数先打开目标映像文件,并从目标文件的头部(从 第一个字节开始)读入若干(128)字节,然后调用另一个函数search_binary_handler(),在那里面让各种可执行程序的处理程序前来 认领和处理。内核所支持的每种可执行程序都有个struct linux_binfmt数据结构,通过向内核登记挂入一个队列。而search_binary_handler(),则扫描这个队列,让各个数据结构所 提供的处理程序、即各种映像格式、逐一前来认领。如果某个格式的处理程序发现特征相符而,便执行该格式映像的装入和启动。
我们从ELF格式映像的linux_binfmt数据结构开始:

[Copy to clipboard]

CODE:

#define load_elf_binary load_elf32_binary

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
};



ELF格式的二进制映像的认领、装入和启动是由load_elf_binary()完成的。而“共享库”、即动态连接库映像的装入则由 load_elf_library()完成。实际上共享库的映像也是二进制的,但是一般说“二进制”映像是指带有main()函数的、可以独立运行并构成 一个进程主体的可执行程序的二进制映像。



CODE:

[sys_execve() > do_execve() > search_binary_handler() > load_elf_binary()]

整个ELF映像就是由文件头、区段头表、程序头表、一定数量的区段、以及一定数量的部构成

而ELF映像的装入/启动过程,则就是在各种头部信息的指引下将某些部或区段装入一个进程的用户空间,并为其运行做好准备(例如装入所需的共享库),最后(在目标进程首次受调度运行时)让CPU进入其程序入口的过程。



接着是对elf_bss 、elf_brk、start_code、end_code等等变量的初始化。这些变量分别纪录着当前(到此刻为止)目标映像的bss段、代码段、数据 段、以及动态分配“堆” 在用户空间的位置。除start_code的初始值为0xffffffff外,其余均为0。随着映像内容的装入,这些变量也会逐步得到调整,读者不妨自己 留意这些变量在整个过程中的变化。
读入了程序头表,并对start_code等变量进行初始化以后,下面的第一步就是在程序头表中寻找“解释器”部、并加以处理的过程。



ELF格式的二进制映像在装入和启动的过程中需要得到一个工具软件的协助,其主要的目的在于为目标映像建立起跟共享库的动态连接。这个工具称为 “解释器”。一个ELF映像在装入时需要用什么解释器是在编译/连接是就决定好了的,这信息就保存在映像的“解释器”部中。“解释器”部的类型为 PT_INTERP,找到后就根据其位置p_offset和大小p_filesz把整个“解释器”部读入缓冲区。整个“解释器”部实际上只是一个字符串, 即解释器的文件名,例如“/lib/ld-linux.so.2”。有了解释器的文件名以后,就通过open_exec()打开这个文件,再通过 kernel_read()读入其开头128个字节,这就是映像的头部。早期的解释器映像是a.out格式的,现在已经都是ELF格式的了,/lib /ld-linux.so.2就是个ELF映像。


程序段的载入:

还是从目标映像的程序头表中搜索,这一次是寻找类型为PT_LOAD的部(Segment)。在二进制映像中,只有类型为PT_LOAD的部才是需要装入的。
找到一个PT_LOAD片以后,先要确定其装入地址。正如代码前面的注释所述,这里先假定装入地址是固定的,然后再根据映像是否允许浮动而作出调 整。具体片头数据结构中的p_vaddr提供了映像在连接时确定的装入地址vaddr。如果映像的类型为ET_EXEC,(或者 load_addr_set已经被设置成1,见下)那么装入地址就是固定的。而若类型为ET_DYN、即共享库,那么即使装入地址固定也要加上一个偏移 量,代码中给出了计算方法,其中ELF_ET_DYN_BASE对于x86定义为(TASK_SIZE / 3 * 2),所以这是2GB边界,而ELF_PAGESTART表示按页面边界对齐。
确定了装入地址以后,就通过elf_map()、实际上是elf32_map()、建立用户空间虚存区间与目标映像文件中某个连续区间之间的映 射。这个函数基本上就是do_mmap(),其返回值就是实际映射的(起始)地址。对于类型为ET_EXEC的可执行程序映像而言,代码中的 load_bias是0,所以装入的起点就是映像自己提供的地址vaddr。另一方面,对于ET_EXEC,由于参数中的elf_flags中的 MAP_FIXED标志位为1,所以给定的映射地址是刚性的而不容许变通,如果与已经映射的区间有冲突就以失败告终。不过,目标映像的映射是从一片空白开 始的,所以实际上不可能失败。顺便提一下,现在又多了一种ELF格式的目标映像,称为FDPIC,其装入地址就是可浮动的。
即使总的装入地址是浮动的,一旦装入了第一个Segment以后,下一个Segment的装入地址就应该是固定的了,所以这里一方面把load_addr_set设置成1,


[sys_execve() > do_execve() > search_binary_handler() > load_elf_binary()]

if (elf_interpreter) {
if (interpreter_type == INTERPRETER_AOUT)
elf_entry = load_aout_interp(&loc->interp_ex, interpreter);
else
elf_entry = load_elf_interp(&loc->interp_elf_ex, interpreter, &interp_load_addr);
. . . . . .
reloc_func_desc = interp_load_addr;

allow_write_access(interpreter);
fput(interpreter);
kfree(elf_interpreter);
} else {
elf_entry = loc->elf_ex.e_entry;
}


这段程序的逻辑很简单:如果需要装入解释器,并且解释器的映像是ELF格式的,就通过load_elf_interp()装入其映像,并把将来进 入用户空间时的入口地址设置成load_elf_interp()的返回值,那显然是解释器的程序入口。而若不装入解释器,那么这个地址就是目标映像本身 的程序入口。
显然,关键的操作是由load_elf_interp()完成的,所以我们追下去看load_elf_interp()的代码。



do_brk()从用户空间分配一段空间。这段代码总体上与前面映射目标映像的那一段相似。注意解释器映像的类型一般都是ET_DYN,所以load_addr可能不等于0。





进程可使用exec系统调用执行新的命令。exec系统调用将一次性释放全部虚拟内存空间,之后生成新的空间并将新的命令影射入内。

    do_execve(文件路径,参数。环境)
        打开文件(open_namei函数)
        计算exec后的UID/GID,读入文件头(prepare_binprm函数)
        读入命令名,环境变量,起动参数(copy_strings函数)
        呼叫各种不同二进制文件的操作函数(search_binary_handler函数)

    ELF格式的话,经由search_binary_handler函数呼叫load_elf_binary函数。如果是动态联结,同时影射动态联结器(ld*.so)

    load_elf_binary(linux_binprm* bprm,pt_regs* regs)
        分析ELF文件头
        读入程序的头部分(kernel_read函数)
        if(存在解释器头部){
                读入解释器名(ld*.so)(kernel_read函数)  |(zalem note:可用
                打开解释器文件(open_exec函数)                | objdump -s -j .interp xxx
            读入解释器文件的头部(kernel_read函数)   |命令查看,
        )                                                          |linux下是/lib/ld-linux.so.x)
        释放空间,清楚信号,关闭指定了close-on-exec标识的文件(flush_old_exec函数)
        生成堆栈空间,塞入环境变量/参数部分(setup_arg_pages函数)
        for(可引导的所有的程序头){
                将文件影射入内存空间(elf_map,do_mmap 函数)
        }
        if(为动态联结){
                影射动态联结器(load_elf_interp函数)
        }
        释放文件(sys_close函数)
        确定执行中的UID,GID(compute_creds函数)
        生成bss领域(set_brk函数)
        bss领域清零(padzero函数)
        设定从exec返回时的IP,SP(start_thread函数)(动态联结时的IP指向解释器的入口)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值