《Linux内核分析》 week8作业-Linux加载和启动一个可执行程序
一.ELF文件格式
ELF(Executable and Linking Format)是x86 Linux系统下常用的目标文件格式,有三种主要类型:
- 适于连接的可重定位文件,可与其他目标文件一起创建可执行文件和共享目标文件。
- 适于执行的可执行文件,用于提供程序的进程映像,加载的内存执行。
- 共享目标文件,连接器可将它与其他可重定位文件和共享目标文件连接成其他目标文件。
文件格式
ELF header在文件开始处描述了整个文件的组织,Section提供了目标文件的各项信息,Program header table指出怎样创建进程映像,含有每个program header的入口,section header table包含每个Section的入口,给出名字、大小等信息。
二.ELF文件的加载过程
从编译/链接和运行的角度看,应用程序和库程序的连接有两种方式。一种是固定的、静态的链接,将所需要的库函数的目标代码从程序库中抽取出来,链接进应用软件的目标映像中;另一种是动态链接,库函数的代码不进入应用软件的目标映像,而是将函数库的映像也交给用户,到启动应用软件时才把程序库的映像装入用户空间。
Linux内核既支持静态链接的ELF映像,也支持动态链接的ELF映像,而且装入/启动映像必须由内核完成,而动态链接的实现既可以在内核中完成,也可以在用户空间完成。
内核空间的加载过程
内核中实际执行execve()系统调用的程序do_execve(),这个函数先打开目标文件映像,并从读入目标文件的头部(即ELF头部字段),然后调用另一个函数seach_binary_handler(),在此函数里,它会搜索Linux可支持的可执行文件类型队列,寻找与之匹配的可执行程序的处理程序。如果类型匹配,则调用load_binary函数指针所指向的处理函数来处理目标映像文件。对于ELF文件格式中,处理函数是load_elf_binary函数。
内核对所支持的每种可执行的程序类型都有个struct linux_binfmt的数据结构。定义如下:
struct linux_binfmt{ struct linux_binfmt* next; struct module* module; int (*load_binary)(struct linux_binprm*,struct pt_regs* regs); int (*load_shlib)(struct file*); int (*core_dump)(long signr,struct pt_regs* regs,struct file* file); unsigned long min_coredump; int hasvdso; }
其中load_binary函数指针指向的就是一个可执行程序的处理函数。
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, .hasvdso = 1 };
search_binary_handler寻找文件格式对应的解析模块,如下:
..... 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); 1376 put_binfmt(fmt); 1377 bprm->recursion_depth--; 1378 if (retval < 0 && !bprm->mm) { 1379 /* we got to flush_old_exec() and failed after it */ 1380 read_unlock(&binfmt_lock); 1381 force_sigsegv(SIGSEGV, current); 1382 return retval; 1383 } 1384 if (retval != -ENOEXEC || !bprm->file) { 1385 read_unlock(&binfmt_lock); 1386 return retval; 1387 } 1388 } .....
load_elf_binary函数主要就是对ELF文件的解析过程了.
614 elf_ppnt = elf_phdata; …… 623 for (i = 0; i < loc->elf_ex.e_phnum; i++) { 624 if (elf_ppnt->p_type == PT_INTERP) { …… 635 elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL); …… 640 retval = kernel_read(bprm->file, elf_ppnt->p_offset, 641 elf_interpreter, 642 elf_ppnt->p_filesz); …… 682 interpreter = open_exec(elf_interpreter); …… 695 retval = kernel_read(interpreter, 0, bprm->buf, 696 BINPRM_BUF_SIZE); …… 703 /* Get the exec headers */ …… 705 loc->interp_elf_ex = *((struct elfhdr *)bprm->buf); 706 break; 707 } 708 elf_ppnt++; 709 }
其中的for循环的目的在于寻找和处理目标映像的"解释器"段。“解释器"段的类型为PT_INTERP,读到后就根据其位置的p_offset和大小p_offsize把整个"解释器"的内容读入缓冲区,解释器的内容只是一个字符串,例如"/lib/ld-linux.so.2",然后就通过open_exec函数打开这个解释器文件。
814 for(i = 0, elf_ppnt = elf_phdata; 815 i < loc->elf_ex.e_phnum; i++, elf_ppnt++) { …… 819 if (elf_ppnt->p_type != PT_LOAD) 820 continue; …… 870 error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, 871 elf_prot, elf_flags); …… 920 }
这里确定装入地址,然后通过elf_map()建立用户空间虚拟地址空间与目标映像文件中某个连续区间的映射,其返回值就是实际映射的起始地址。
946 if (elf_interpreter) { …… 951 elf_entry = load_elf_interp(&loc->interp_elf_ex, 952 interpreter, 953 &interp_load_addr); …… 965 } else { 966 elf_entry = loc->elf_ex.e_entry; …… 972 }
当是动态链接时,需要装入解释器,就通过load_elf_interp装入映像,返回解释器映像的入口地址。而对于静态链接时,则不需要装入解释器,那么这个入口地址就是目标映像本身的入口地址。
991 create_elf_tables(bprm, &loc->elf_ex, 992 (interpreter_type == INTERPRETER_AOUT), 993 load_addr, interp_load_addr); …… 1028 start_thread(regs, elf_entry, bprm->p);
在完成装入,启动用户空间的映像运行之前,还需要为目标映像和解释器准备好一些有关的信息,这些信息例如常规的argc、envc等,需要复制到用户空间,使它们进入解释器或目标映像的程序入口时出现在用户空间堆栈上。这就是create_elf_tables的作用。
最后,start_thread()这个宏操作会将eip和esp改成新的地址,就使CPU在返回用户空间时进入新的入口地址。
三.ELF文件加载和链接的实验总结
用户通过shell执行程序,shell通过execve进入系统调用.sys_execve经过一系列过程,并最终通过ELF文件的处理函数load_elf_binary将用户程序和ELF解释器加载进内存,并将控制权交给解释器。ELF解释器进行相关库的加载,并最终把控制权交给用户程序。