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_binarystatic 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);elseelf_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 指向解释器的入口)