郑重声明: 该文章转载于以下博客园–博主yooooooo
ELF文件结构描述
ELF目标文件格式最前部ELF文件头(ELF Header),它包含了描述了整个文件的基本属性,比如ELF文件版本、目标机器型号、程序入口地址等。其中ELF文件与段有关的重要结构就是段表(Section Header Table)
ELF文件格式
- 可重定向文件:文件保存着代码和适当的数据,用来和其他的目标文件一起来创建一个可执行文件或者是一个共享目标文件。(目标文件或者静态库文件,即linux通常后缀为.a和.o的文件)
- 可执行文件:文件保存着一个用来执行的程序。(例如bash,gcc等)
- 共享目标文件:共享库。文件保存着代码和合适的数据,用来被下连接编辑器和动态链接器链接。(linux下后缀为.so的文件。)
另外的windows下为pe格式的文件;
ELF视图

当ELF文件被加载到内存中后,系统会将多个具有相同权限(flg值)section合并一个segment。操作系统往往以页为基本单位来管理内存分配,一般页的大小为4096B,即4KB的大小。同时,内存的权限管理的粒度也是以页为单位,页内的内存是具有同样的权限等属性,并且操作系统对内存的管理往往追求高效和高利用率这样的目标。ELF文件在被映射时,是以系统的页长度为单位的,那么每个section在映射时的长度都是系统页长度的整数倍,如果section的长度不是其整数倍,则导致多余部分也将占用一个页。而我们从上面的例子中知道,一个ELF文件具有很多的section,那么会导致内存浪费严重。这样可以减少页面内部的碎片,节省了空间,显著提高内存利用率。
文件头(ELF header)
ELF文件头结构及相关常数被定义在“/usr/include/elf.h”,因为ELF文件在各种平台下都通用,ELF文件有32位版本和64位版本的ELF文件的文件头内容是一样的,只不过有些成员的大小不一样。它的文件图也有两种版本:分别叫“Elf32_Ehdr”和“Elf64_Ehdr”。
typedef struct {
unsigned char e_ident[16]; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; Elf32_Off e_phoff; Elf32_Off e_shoff; Elf32_Word e_flags; Elf32_Half e_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; Elf32_Half e_shnum; Elf32_Half e_shstrndx; }Elf32_Ehdr;

在ELF文件头中,我们需要重点关注以下几个字段:
- e_entry:程序入口地址 e_ehsize:ELF Header结构大小
- e_phoff、e_phentsize、e_phnum:描述Program Header Table的偏移、大小、结构。
- e_shoff、e_shentsize、e_shnum:描述Section Header Table的偏移、大小、结构。
- e_shstrndx:这一项描述的是字符串表在Section Header Table中的索引,值25表示的是Section
- Header Table中第25项是字符串表(String Table)。
段表(Section Header Table)
typedef struct {
Elf32_Word sh_name; //section的名字 Elf32_Word sh_type; //section类别 Elf32_Word sh_flags; //section在进程中执行的特性(读、写) Elf32_Addr sh_addr; //在内存中开始的虚地址 Elf32_Off sh_offset; //此section在文件中的偏移 Elf32_Word sh_size; Elf32_Word sh_link; Elf32_Word sh_info; Elf32_Word sh_addralign; Elf32_Word sh_entsize; }
表(Section)
符号表(.dynsym)
符号表包含用来定位、重定位程序中符号定义和引用的信息,简单的理解就是符号表记录了该文件中的所有符号,所谓的符号就是经过修饰了的函数名或者变量名,不同的编译器有不同的修饰规则。例如符号_ZL15global_static_a,就是由global_static_a变量名经过修饰而来。
符号表项的格式如下:
typedef struct {
Elf32_Word st_name; //符号表项名称。如果该值非0,则表示符号名的字 //符串表索引(offset),否则符号表项没有名称。 Elf32_Addr st_value; //符号的取值。依赖于具体的上下文,可能是一个绝对值、一个地址等等。 Elf32_Word st_size; //符号的尺寸大小。例如一个数据对象的大小是对象中包含的字节数。 unsigned char st_info; //符号的类型和绑定属性。 unsigned char st_other; //未定义。 Elf32_Half st_shndx; //每个符号表项都以和其他节区的关系的方式给出定义。 //此成员给出相关的节区头部表索引。 } Elf32_sym;
重定位表
字符串表(.dynstr)
装载elf文件步骤
首先在用户层面,shell进行会调用fork()系统调用创建一个新进程 – 新进程调用execve()系统调用执行制定的ELF文件 – 原来的shell进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入。
execve()系统调用的原型如下:
int execve(const char *filename, char *const argv[], char *const envp[]);
它所对应的三个参数分别是程序文件名, 执行参数, 环境变量,通过对内核代码的分析,我们知道execve()系统调用的相应入口是sys_execve(),在sys_execve之后,内核会分别调用do_execve(), search_binary_handle(), load_elf_binary 等等,其中do_execve()是最主要的函数,所以接下来我们主要对它来进行具体分析。
do_execve
具体看一下下面链接:Linux进程启动过程分析do_execve(可执行程序的加载和运行)—Linux进程的管理与调度(十一)
int do_execve(struct filename *filename, const char __user *const __user *__argv, const char __user *const __user *__envp) {
return do_execve_common(filename, argv, envp); } //do_execve_common static int do_execve_common(struct filename *filename, struct user_arg_ptr argv, struct user_arg_ptr envp) {
// 检查进程的数量限制 // 选择最小负载的CPU,以执行新程序 sched_exec(); // 填充 linux_binprm结构体 retval = prepare_binprm(bprm); // 拷贝文件名、命令行参数、环境变量 retval = copy_strings_kernel(1, &bprm->filename, bprm); retval = copy_strings(bprm->envc, envp, bprm); retval = copy_strings(bprm->argc, argv, bprm); // 调用里面的 search_binary_handler retval = exec_binprm(bprm); // exec执行成功 } // exec_binprm static int exec_binprm(struct linux_binprm *bprm) {
// 扫描formats链表,根据不同的文本格式,选择不同的load函数 ret = search_binary_handler(bprm); // ... return ret; }
- 如果想要了解elf文件格式,可以在命令行下面man elf,Linux手册中有参考.
- 在do_exec()中会调用do_execve_common(),这个函数的参数与do_exec()一模一样
- 在do_execve_common()中的sched_exec(),会选择一个负载最小的CPU来执行新进程,这里我们可以得知Linux内核中是做了负载均衡的.
- 在这段代码中间出现了变量bprm,这个是一个重要的结构体struct linux_binfmt,下面我贴出此结构体的具体定义:
/* * This structure is used to hold the arguments that are used when loading binaries. */ // 内核中注释表明了这个结构体是用于保存载入二进制文件的参数. struct linux_binprm {
char buf[BINPRM_BUF_SIZE]; #ifdef CONFIG_MMU struct vm_area_struct *vma; unsigned long vma_pages; #else //... unsigned interp_flags; unsigned interp_data; unsigned long loader, exec; };
- 在do_execve_common()中的searchbinaryhandler(),这个函数回去搜索和匹配合适的可执行文件装载处理过程,下面这个函数的精简代码:
int search_binary_handler(struct linux_binprm *bprm) {
// 遍历formats链表 list_for_each_entry(fmt, &formats, lh) {
if (!try_module_get(fmt->module)) continue; read_unlock(&binfmt_lock); bprm->recursion_depth++; // 应用每种格式的load_binary方法 retval = fmt->load_binary(bprm); read_lock(&binfmt_lock); put_binfmt(fmt); bprm->recursion_depth--; // ... } return retval; }
这里需要说明的是,这里的fmt变量的类型是struct linux_binfmt *, 但是这一个类型与之前在do_execve_common()中的bprm是不一样的,具体定义如下:
这里的linux_binfmt对象包含了一个单链表,这个单链表中的第一个元素的地址存储在formats这个变量中list_for_each_entry依次应用load_binary的方法,同时我们可以看到这里会有递归调用,bprm会记录递归调用的深度装载ELF可执行程序的load_binary的方法叫做load_elf_binary方法.
下面会进行具体分析:
/* * This structure defines the functions that are used to load the binary formats that * linux accepts. */ struct linux_binfmt {
struct list_head lh; //单链表表头 struct module *module; int (*load_binary)(struct linux_binprm *); int (*load_shlib)(struct file *); int (*core_dump)(struct coredump_params *cprm); unsigned long min_coredump; /* minimal dump size */ };
== load_elf_binary()==
static int load_elf_binary(struct linux_binprm *bprm) {
// .... struct pt_regs *regs = current_pt_regs(); // 获取当前进程的寄存器存储位置 // 获取elf前128个字节,作为魔数 loc->elf_ex = *((struct elfhdr *)bprm->buf); // 检查魔数是否匹配 if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0) goto out; // 如果既不是可执行文件也不是动态链接程序,就错误退出 if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN) // // 读取所有的头部信息 // 读入程序的头部分 retval = kernel_read(bprm->file, loc->elf_ex.e_phoff, (char *)elf_phdata, size); // 遍历elf的程序头 for (i = 0; i < loc->elf_ex.e_phnum; i++) {
// 如果存在解释器头部 if (elf_ppnt->p_type == PT_INTERP) {
// // 读入解释器名 retval = kernel_read(bprm->file, elf_ppnt->p_offset, elf_interpreter, elf_ppnt->p_filesz); // 打开解释器文件 interpreter = open_exec(elf_interpreter); // 读入解释器文件的头部 retval = kernel_read(interpreter, 0, bprm->buf, BINPRM_BUF_SIZE); // 获取解释器的头部 loc->interp_elf_ex = *((struct elfhdr *)bprm->buf); break; } elf_ppnt++; } // 释放空间、删除信号、关闭带有CLOSE_ON_EXEC标志的文件 retval = flush_old_exec(bprm); setup_new_exec(bprm); // 为进程分配用户态堆栈,并塞入参数和环境变量 retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), executable_stack); current->mm->start_stack = bprm->p; // 将elf文件映射进内存 for(i = 0, elf_ppnt = elf_phdata; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
if (unlikely (elf_brk > elf_bss)) {
unsigned long nbyte; // 生成BSS retval = set_brk(elf_bss + load_bias, elf_brk + load_bias); // ... } // 可执行程序 if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {
elf_flags |= MAP_FIXED; } else if (loc->elf_ex.e_type == ET_DYN) {
// 动态链接库 // ... } // 创建一个新线性区对可执行文件的数据段进行映射 error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, 0); } } // 加上偏移量 loc->elf_ex.e_entry += load_bias; // .... // 创建一个新的匿名线性区,来映射程序的bss段 retval = set_brk(elf_bss, elf_brk); // 如果是动态链接 if (elf_interpreter) {
unsigned long interp_map_addr = 0; // 调用一个装入动态链接程序的函数 此时elf_entry指向一个动态链接程序的入口 elf_entry = load_elf_interp(&loc->interp_elf_ex, interpreter, &interp_map_addr, load_bias); // ... } else {
// elf_entry是可执行程序的入口 elf_entry = loc->elf_ex.e_entry; // .... } // 修改保存在内核堆栈,但属于用户态的eip和esp start_thread(regs, elf_entry, bprm->p); retval = 0; // }
这段代码相当之长,我们做了相当大的精简,虽然对主要部分做了注释,但是为了方便我还是把主要过程阐述一遍:
- 检查ELF的可执行文件的有效性,比如魔数,程序头表中段(segment)的数量
- 寻找动态链接的.interp段,设置动态链接路径
- 根据ELF可执行文件的程序头表的描述,对ELF文件进行映射,比如代码,数据,只读数据
- 初始化ELF进程环境
- 将系统调用的返回地址修改为ELF可执行程序的入口点,这个入口点取决于程序的连接方式,对于静态链接的程序其入口就是e_entry,而动态链接的程序其入口是动态链接器
- 最后调用start_thread,修改保存在内核堆栈,但属于用户态的eip和esp,该函数代码如下:
start_thread
void start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp) {
set_user_gs(regs, 0); // 将用户态的寄存器清空 regs->fs = 0; regs->ds = __USER_DS; regs->es = __USER_DS; regs->ss = __USER_DS; regs->cs = __USER_CS; regs->ip = new_ip; // 新进程的运行位置- 动态链接程序的入口处 regs->sp = new_sp; // 用户态的栈顶 regs->flags = X86_EFLAGS_IF; set_thread_flag(TIF_NOTIFY_RESUME); }
总结
如你所见,执行程序的过程是一个十分复杂的过程,exec本质在于替换fork()后,根据制定的可执行文件对进程中的相应部分进行替换,最后根据连接方式的不同来设置好执行起始位置,然后开始执行进程。
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/227509.html原文链接:https://javaforall.net
