ELF文件
1 ELF文件结构
图中显示了ELF可重定位文件的构成,ELF文件头的开始16个字节描述了文件中字的大小和字节序(大端模式还是小端模式)。文件头还包含了ELF头的大小,文件类型(可重定位,可执行和共享),机器类型,节头表的位置和大小。节头表中的每项对应于文件中的一个节,用于描述节的位置和大小。
ELF文件头:
ELF头对应的代码定义为:
#define EI_NIDENT 16 typedef struct elf32_hdr{ unsigned char e_ident[EI_NIDENT]; //开始的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; //elf文件头大小 Elf32_Half e_phentsize; //程序头表项的大小 Elf32_Half e_phnum; //程序头表中表项项的个数 Elf32_Half e_shentsize; //节头表项大小 Elf32_Half e_shnum; //节头表中表项的个数 Elf32_Half e_shstrndx; //节头表的字符串节所在节头表中下标 } Elf32_Ehdr;
节头表项对应的代码定义为:
typedef struct elf32_shdr { Elf32_Word sh_name; //节的名字,在符号表中的下标 Elf32_Word sh_type; //节的类型,描述符号,代码,数据,重定位等 Elf32_Word sh_flags; //读写执行标记 Elf32_Addr sh_addr; //节在执行时的虚拟地址 Elf32_Off sh_offset; //节在文件中的偏移量 Elf32_Word sh_size; //节的大小 Elf32_Word sh_link; //其它节的索引 Elf32_Word sh_info; //节的其它信息 Elf32_Word sh_addralign; //节对齐 Elf32_Word sh_entsize; //节拥有固定大小项的大小 } Elf32_Shdr;
在ELF文件的ELF头和节头表直接是节头表描述的节本身。一个典型的重定位目标文件包含了下面的节:
.text:机器代码
.rodata:只读数据,例如printf的字符串常量参数,jump跳转表。
.data:已初始化的全局C变量。本地C变量在运行程序的栈中。
.bss:未初始化的全局变量。这个节在文件中并不占用实际的空间。
.symtab:符号表,包含了本模块定义和引用的全局符号信息,不包含局部变量的信息。
.rel.text:.text节中需要重定位的信息。一般,任何调用外部函数或者引用全局变量的指令都需要被修改,但是,如果指令调用本地函数,则不需要被修改。重定位信息在可执行文件中可以不需要。
.rel.data:重定位任何被这个模块定义和引用的全局变量信息。通常在一个全局变量的值是另一个全局变量地址或外部函数地址的情况下需要重新修改这个值。
.debug:包含调试信息
.line:在.text节中的机器指令与原始C代码所在的行之间的映射。
.strtab:在.symtab和.debug节中所用的字符串表。
程序头表项对应定义:
typedef struct elf32_phdr{ Elf32_Word p_type; //段的类型,LOAD,DYNAMIC等 Elf32_Off p_offset; //段在文件中的偏移量 Elf32_Addr p_vaddr; //段的物理地址 Elf32_Addr p_paddr; //段的虚拟地址 Elf32_Word p_filesz; //段在文件中的大小 Elf32_Word p_memsz; //段在内存中的大小 Elf32_Word p_flags; //读写执行标记 Elf32_Word p_align; //段的对齐 } Elf32_Phdr;
只有可执行文件和共享对象文件有程序头表,程序头表描述了程序中的段,以及程序文件中的节是映射到哪个段中的信息。当程序被装载进内存中时,是以段为单位进行虚拟内存地址映射的。
2 符号和符号表
每个重定位目标模块m都包含一个符号表来描述被m定义和引用的符号。在链接器上下文中,有以下三种符号:
- 全局链接符号。模块m定义的,可以被其它模块引用。全局链接符号包括非静态的函数和非静态的全局变量。
- 外部链接符号。被模块m引用但定义在其它模块的符号,可以是其它函数定义的全局链接符号。
- 本地链接符号。只能被模块m内部使用,本地链接符号包括静态函数和静态变量(包括静态全局变量和静态局部变量)。
本地链接符号和本地程序符号是不同的,本地链接符号在.symtab表中出现,当本地程序符号不会出现在.symtab表中,而是出现在程序运行时栈中。
需要注意的是静态成员变量和全局变量并不会出现在程序运行时栈中,编译器会在.data或则.bss段分配空间给它们。
符号表是汇编器根据编译器编译到汇编语言中的符号建立的。符号表包含在节.symtab中,它是一个数组,数组的元素定义如下:
typedef struct elf32_sym{ Elf32_Word st_name; //符号名字,在字符串表中的下标 Elf32_Addr st_value; //符号在节的偏移量,或者虚拟地址 Elf32_Word st_size; //对象的字节大小 unsigned char st_info; //类型和绑定属性,变量,函数,节,源文件名等类型,全局或则局部属性,低四位表示类型,高四位表示属性 unsigned char st_other; //没有使用 Elf32_Half st_shndx; //符号所在的节在节头表中的下标 } Elf32_Sym;
st_shndx表示符号所在节在节头表中的下标,但存在三个特殊的虚拟节,这些节在节头表中并没有表项,如COMMON是表示未初始化的数据对象,UNDEF表示符号是未定义的,ABS表示符号不应该被重定位,就是用绝对地址表示。
3 解决全局符号多重定义
在编译的时候,编译器会告诉汇编器每个全局符号是强符号还是弱符号,汇编器则将这些信息放入重定位文件对象的符号表中。函数和初始化的全局变量是强符号,未初始化的全局变量是弱符号。
在给出了符号的强弱表示后,链接器就根据下面的规则来处理符号的多重定义:
- 多个强符号是不允许的
- 有一个强符号和多个弱符号,选择强符号
- 有多个弱符号,选择其中的任意一个