上一篇文章中主要介绍了ELF文件的基本定义和目标文件的分类,这篇文章中主要介绍下ELF文件格式
ELF文件主要提供了两个视图:链接视图及执行视图,分别针对程序运行过程的链接过程和执行过程。如下图所示:
单从相貌上来看,二者长得很像,除了肚子。可以看出链接视图采用的节(section)作为基本单位,而执行视图采用(segment)为基本单位。那么问题来了,这二者有什么区别?这是个设问句,区别如下:
Section与segment
在汇编语言中,代码段、数据段就是所谓的段(segment)。在可执行文件载入内存时,是以segment组织的,每个segment对应ELF文件中的program header中的一个条目,用来建立可执行文件映像。
在写汇编程序时,用.bss .data .txt或者data segment等这些段前指示,都是section,比如.text,告诉汇编器后面的代码放入.text section中。目标代码文件中的section和section header table中的条目是一一对应的。Section的信息用于链接器对代码的重定位。
可能还是不太懂,可以看下汇编程序的执行过程:
具体过程如下:
写一个汇编程序或通过编译器会产生文本文件hello.s。汇编器读取hello.s时会将其转换为目标文件hello.o,目标文件有若干个section组成,我们在汇编程序中声明的.section会成为目标文件中的section,此外汇编器还会自动添加一些section(比如符号表)。意思就是不同目标文件中相同的section会被合并成生成文件的一个section。
然后链接器把目标文件中的section合并成几个segment,生成可执行文件Hello.o。最后又加载器(loader)根据可执行文件中的segment信息加载运行这个程序。
大概是这样子的,吧?如果有不太对的地方欢迎指正。
然后针对两个视图中的每一部分进行详细解释,在此之前,我们熟悉一个命令readelf,其常用的命令格式如下
readelf -h [filename] //查看文件头
readelf -S [filename] //查看文件节头表
readelf -l [filename] //查看程序头表
readelf -s [filename] //查看符号表
readelf -a [filename] //查看重定位表、符号表等信息
readelf -r [filename] //只查看重定位表信息
/*注意这里的filename是elf文件的文件名,常见elf文件后缀有.o、.out等*/
1.ELF文件头解析
采用readelf -h xxx.o查看头部信息
我这里创建了一个简单的say hello的hello.o文件,头部信息如下
我们看一下文件头在elf.h文件中的结构体定义,会发现其有14个成员
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /*Magic number and other info */
Elf32_Half e_type; /*Object file type */
Elf32_Half e_machine; /*Architecture */
Elf32_Word e_version; /*Object file version */
Elf32_Addr e_entry; /*Entry point virtual address */
Elf32_Off e_phoff; /*Program header table file offset */
Elf32_Off e_shoff; /*Section header table file offset */
Elf32_Word e_flags; /*Processor-specific flags */
Elf32_Half e_ehsize; /*ELF header size in bytes */
Elf32_Half e_phentsize; /*Program header table entry size */
Elf32_Half e_phnum; /*Program header table entry count */
Elf32_Half e_shentsize; /*Section header table entry size */
Elf32_Half e_shnum; /*Section header table entry count */
Elf32_Half e_shstrndx; /*Section header string table index */
} Elf32_Ehdr;
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /*Magic number and other info */
Elf64_Half e_type; /*Object file type */
Elf64_Half e_machine; /*Architecture */
Elf64_Word e_version; /*Object file version */
Elf64_Addr e_entry; /*Entry point virtual address */
Elf64_Off e_phoff; /*Program header table file offset */
Elf64_Off e_shoff; /*Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /*ELF header size in bytes */
Elf64_Half e_phentsize; /*Program header table entry size */
Elf64_Half e_phnum; /*Program header table entry count */
Elf64_Half e_shentsize; /*Section header table entry size */
Elf64_Half e_shnum; /*Section header table entry count */
Elf64_Half e_shstrndx; /*Section header string table index */
} Elf64_Ehdr;
类似于程序头表、节头表,elf文件头同时定义了32位及64位结构体,二者的区别只有地址位数的不同,其他成员的数据长度及类型都是相同的:
如下所示:
/* Type of addresses. */
typedef uint32_t Elf32_Addr; /* 32位 */
typedef uint64_t Elf64_Addr; /* 64位 */
/* Type of file offsets. */
typedef uint32_t Elf32_Off; /* 32位 */
typedef uint64_t Elf64_Off; /* 64位 */
名称 | 大小 | 对齐 | 代表 |
---|---|---|---|
Elf32_Addr | 4 | 4 | uint32_t |
Elf64_Addr | 8 | 8 | uint64_t |
Elf32_Half/Elf64_Half | 2 | 2 | uint16_t |
Elf32_Off | 4 | 4 | uint32_t |
Elf64_Off | 8 | 8 | uint64_t |
Elf32_Sword/Elf64_Sword | 4 | 4 | int32_t |
Elf32_Word/Elf64_Word | 4 | 4 | uint32_t |
其14个成员如下图所示:
2.ELF节头表
采用readelf -S xxx.o查看节头表信息
查看源文件(以Elf32_Shdr为例)
/* Section header. */
typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr; /*Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Section size in bytes */
Elf32_Word sh_link; /* Link to another section */
Elf32_Word sh_info; /* Additional section information */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Entry size if section holds table */
} Elf32_Shdr;
在节头表中,包含10个成员,如图所示:
成员说明如下:
成员名称 | 说明 |
---|---|
sh_name | 给出节的名称,本身不是字符串,而是数值,代表目标文件字符串表中的一个索引值。 |
sh_type | 指出节类型 |
sh_flags | 标志位 |
sh_addr | 如果节将出现在进程的内存映像中,此成员指定该节数据在内存中的起始地址,否则,如该节数据不需要映射到内存中时,此字段为0 |
sh_offset | 该节数据在目标文件中的偏移量。需要注意的是:该节类型是SHT_NOBITS(如.bss节),说明该节在目标文件中并不占用空间大小,此时,该成员的数值只是概念上的偏移 |
sh_size | 节的大小(字节数)。当该节类型不是SHT_NOBITS时,成员数值就应该是该节在目标文件的实际大小。 |
sh_link | 节头部表的索引链接 |
sh_info | 附加信息,其会因为节类型的不同而不同。特别的,类型为SHT_REL或SHT_RELA的节,此时成员代表的是一个节头表索引值,指代需要做重定位操作的节 |
sh_addralign | 地址对齐的约束条件,数值0、1没有对其约束 |
sh_entsize | 如果节的内容是固定大小的表,例如符号表,此时成员sh_entsize给出的是每个表项的大小。如果该成员的数值为0,说明该节的数据内容并不是一张表 |
节类型(sh_type)说明:
类型 | 说明 |
---|---|
SHT_NULL | 说明节头表表项无效,目标中没有它对应的节 |
SHT_PROGBITS | 表明该节有程序定义的信息,这些信息的格式与意义完全由程序所定义 |
SHT_SYMTAB/SHT_DYNSYM | 说明节的数据内容为符号表,SHT_SYMTAB:为静态链接器提供符号信息,SHT_DYNSYM:为动态连接器提供符号信息 |
SHT_STRTAB | 说明该节是字符串表 |
SHT_RELA | 说明该节的内容是重定位表,表项带Addend |
SHT_HASH | 说明该节包含符号哈希表 |
SHT_DYNAMIC | 说明该节内容是用来提供给动态链接器使用的 |
SHT_NOTE | 包含系统构建人员需要使用的特殊信息 |
SHT_NOBITS | 在文件中不占空间,其他属性类似于SHT_PROGBITS |
SHT_REL | 说明该节内容是一个重定位表,表项不带Addend |
SHT_INIT_ARRAY | 说明该节存储的是一个函数指针数组 |
and | so on… |
节类型中的SHT_RELA和SHT_REL的区别其实不太大,知道他们都是重定位表,应该就可以了。
原文中是这么说的,英语好的同学可以琢磨一下>
SHT_RELA
The section holds relocation entries with explicit addends, such as type Elf32_Relafor the 32-bit class of object files. An object file may have multiple relocation sections.
SHT_REL
The section holds relocation entries without explicit addends, such as type Elf32_Rel for the 32-bit class of object files. An object file may have multiple relocation sections. See ‘‘Relocation’’ below for details.
标志位说明如下:
特殊节如下所示:
如果节sh_link的值为1,则其可链接到本程序的相关节
若sh_info的值为1,则其可链接到符号表内的索引+1
查看符号表:readelf -s hello.o
学习的时候要笑!接下来笑着>_<看程序头表
3.程序头表
采用readelf -l xxx.o查看文件的程序头表
查看定义
/* Program segment header. */
typedef struct
{
Elf32_Word p_type; /* Segment type */
Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;
有8个成员
P_type类型描述:
4.符号表
符号:对数据、函数或者代码(如全局变量或者函数)的引用
符号表:简单理解就是通过索引对数据、函数、变量的调用
例如:在使用printf()函数时,就会调用动态符号表.dynsym,因为在符号表.dynsym中存在一个指向该函数的符号条目。
在大多数共享库和可执行文件中,存在两个符号表:.symtab、.dynsym。
.dynsym保存了引用来自外部文件的全局符号,如printf这样的库函数。可以说.dynsym是.symtab的一个子集,因为.symtab中还存有可执行文件的本地符号,如全局变量或者代码定义的本地函数等,因此.symtab保存了所有的符号,而.dynsym只保存了动态/全局符号。
但存在一个问题,为什么同时定义包含相同部分的两个符号表呢?在采用readelf -S 查看可执行文件的输出时,我们发现:.dynsym被标记了ALLOC,即运行时会被内存调用,而.symtab并没有。可以看出,在程序运行的时候,.symtab并不是必需的。因此不会被装载到内存中。而.dynsym保存的符号只能在运行时被解析,因此是运行时动态链接器所需要的唯一符号。可以说:.dynsym符号表对于动态链接可执行文件的执行来说是必需的,而.symtab符号表只是用来调试和链接的,有时候为了节省空间,会将.symtab符号表删掉
符号表查看,readelf -s hello.o,如下图所示:
符号类型说明:
符号绑定说明:
5.重定位表
静态链接的过程分为两步:第一步是空间与地址的分配,第二步是符号解析与重定位;第二步是链接过程的核心。在重定位过程中,重定位表和符号表起着至关重要的作用,重定位表确定了需要被重定位的地址,符号表决定了其需要被替换成什么值。
可采用readelf -a test.o或readelf -r test.o查看文件重定位表,不同的是,后者只显示重定位表,如图所示:
静态链接中,目标文件中包含有用于重定位的表:代码段重定位表.rel.text;数据段重定位表.rel.data
动态链接中,目标文件的重定位表:.rel.dyn对数据引用的修正,修正的位置位于.got和数据段;.rel.plt对函数引用的修正,修正位置位于.got.plt。
如上图所示,我们可以观察到几种重定位入口类型
R_X86_64_RELATIVE, R_X86_64_GLOB_DAT,
R_X86_64_JUMP_SLO
R_X86_64_GLOB_DAT(.rel.dyn中针对.got)和R_X86_64_JUMP_SLO(.rel.plt中针对.got.plt)表示被修正的位置只需要直接将符号的地址填入。
而在重定位表中的Offset表明了当前符号在.got或.got.plt中的偏移,可以根据该值在GOT表中寻找对应的位置。