目录
ELF(Executable and Linkable Format)。
.plt .got.plt Procedure Linkage Table
ELF(Executable and Linkable Format)。
·Executable:可执行。ELF文件将参与程序的执行(Execution)工作。包括二进制程序的运行以及动态库.so文件的加载。
·Linkable:可链接。ELF文件是编译链接工作的重要参与者。
ELF文件格式
ELF header
64位和32位ELF文件头结构包含了同名的成员域,只是某些成员域的长度不同
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
ElfN_Addr e_entry;
ElfN_Off e_phoff;
ElfN_Off e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
} ElfN_Ehdr;
基本类型
以下类型用于N位体系结构(N = 32,64,ElfN代表Elf32或Elf64,uintN_t代表uint32_t或uint64_t)
Elf64_Addr(作用为Unsigned program address,表示程序内的地址,无符号)为8字节长,ELF32_Addr为4字节,等同于64或32位平台的指针类型。
Elf64_Off(作用为Unsigned file offset,表示文件偏移量,无符号)为8字节长(等同于64位平台的long),Elf32_Off为4字节(等同于32位平台的int)。
Elf64_Half(作用为Unsigned medium integer,表示中等大小的整数,无符号)和Elf32_Half都是2字节, uint16_t。
Elf64_Word(作用为Unsigned integer,无符号整型)和Elf32_Word都是4字节,等同于int32_t。
e_ident
e_ident[0-3]:前4个元素构成魔幻数(Magic Number),取值分别为'0x7f'、'E'、'L'、'F'。
e_ident[EL_CLASS=4]:ELF文件是32位的(取值为1)还是64位的(取值为2)。
e_ident[EL_DATA=5]:数据的字节序是小端(Little Endian,取值为1)还是大端(Big Endian,取值为2)。
e_ident[EL_VERSION=6]:ELF文件版本,正常情况下该元素取值为1。
e_ident其余元素为字节对齐用。
e_type
该成员域的长度为2个字节(类型为Elf64_Half),指明ELF文件的类型。
e_machine
该成员域长度也为2个字节,指明该ELF文件对应哪种CPU架构。
e_flags
和处理器相关的标识。其取值和解释依赖e_machine
以ARM平台为例,介绍它的取值情况。·在ARM32位平台(e_machine被定义为标记符EM_ARM,值为40)上,e_flags取值为0x02(标记符为EF_ARM_HASENTY),表示该ELF文件包含有效e_entry值。为什么头结构中已经定义了e_entry,而ARM平台上还需要这个参数呢?原来,在ARM平台上,e_entry取值可以为0。而这和ELF规范中ELF文件头结构的e_entry为0表示没有e_entry的含义相冲突。所以在ARM平台上,e_entry为0的真正含义就由e_flags来决定。·在ARM64位平台(e_machine取值为183,标记符为EM_AARCH64)上,e_flags就没有特殊的取值
e_version
该成员取值同e_ident[EL_VERSION]。
e_entry
如果ELF文件是一个可执行程序的话,操作系统加载它后将跳转到e_entry的位置去执行该程序的代码。e_entry是虚拟内存地址,不是实际内存地址
e_phoff
ph是program header的缩写。由图4-1可知,program header table是执行视图中必须要包含的信息。e_phoff指明ph table在该ELF文件的起始位置(从文件头开始算起的偏移量)。
e_shoff
sh是section header的缩写。同e_phoff类似,如果该ELF文件包含sh table的话,该成员域指明sh table在文件的起始位置。
e_ehsize
eh是elf header的缩写。该成员域表示ELF文件头结构的长度,64位ELF文件头结构长度为64。
e_phentsize和e_phnum
这两个成员域指明ph table中每个元素的长度和该table中包含多少个元素。注意,ph表元素的长度是固定的,由此可计算ph table的大小是e_phentsize(ph entry size,每个元素的长度)×e_phnum(entry number,元素个数)。
e_shentsize和e_shum
说明sh table中每个元素的长度以及sh table中包含多少个元素
e_shstrndx
根据ELF规范,每个section都会有一个名字(用字符串表示)。这些字符串存储在一个类型为String的section里。这个section在sh table中的索引号就是e_shstrndx。
Program Header Table
Execution View中ELF必须包含Program Header Table,PH Table描述的是segment的信息
typedef struct {
uint32_t p_type; #segmentt标记符
uint32_t p_flags; #segment标记符
Elf64_Off p_offset; #该segment位于文件的起始位置
Elf64_Addr p_vaddr; #该segment加载到进程虚拟内存空间时指定的内存地址。
Elf64_Addr p_paddr; #该segment对应的物理地址。
uint64_t p_filesz; #该segment在文件中占据的大小,其值可以为0
uint64_t p_memsz; #segment在内存中占据的空间,其值可以为0
uint64_t p_align; #segment加载到内存后其首地址需要按p_align的要求进行对齐
} Elf64_Phdr;
查看PH Table
readelf -l main.out
显Section到Segment的映射关系:ELF文件物理上并不包含所谓的Segment,Segment其实是一个或多个Section按一定映射关系组合而来的。区间落在[p_offset,p_offset+p_filesz]范围内的Section属于同一个Segment。
p_type
pt_flags
Section Header Table
Section Header Table描述的是Section的Header信息,并不是Section本身的内容。
sh_name:每个section都有一个名字。ELF有一个专门存储Section名字的Section(Section Header String Table Section,简写为shstrtab)。这里的sh_name指向shstrtab的某个位置,该位置存储了本Section名字的字符串。
sh_type:section的类型,不同类型的Section存储不同的内容。比如.shstrtab的类型就是SHT_STRTAB,它存储字符串。
sh_flags:Section的属性。下文将详细介绍sh_type和sh_flags。
sh_addr:如果该Section被加载到内存的话(可执行程序或动态库),sh_addr指明应该加载到内存什么位置(进程的虚拟地址空间)。
sh_offset:表明该Section真正的内容在文件什么位置。
sh_size:section本身的大小。不同类型的Section分别对应不同的数据结构。
查看 Section Header Table
readelf --sections main.o
或
readelf -S main.o
根据ELF规范,sh table表第0项是占位用的,所以其值全为0
.shstrtab section
Section Header String Table的简写,Section的名字是字符串,这些字符串信息存储在Section Header String Table中
将指定名字或索引的section的内容转换成字符信息打印出来
readelf -p [section名|section索引] main.o
.text section
用于存储程序的指令
.text section的sh_type为SHT_PROGBITS(取值为1),意为Program Bits,即完全由应用程序自己决定(程序的机器指令当然是由程序自己决定的),sh_flags为SHF_ALLOC(当ELF文件加载到内存时,表示该Section会分配内存)和SHF_EXECINSTR(表示该Section包含可执行的机器指令)
用 "objdump-S-d main.o"可反编译.text的内容。"-S"参数表示结合源码进行反汇编。这要求编译main.o的时候使用gcc-g参数。
.bss section
block storage segment的缩写
.bss section包含了一块内存区域,这块区域在ELF文件被加载到进程空间时会由系统创建并设置这块内存的内容为0。注意,.bss section在ELF文件里不占据任何文件的空间,所以其sh_type为SHF_NOBITS(取值为8)
.bss的sh_flags取值必须为SHF_ALLOC和SHF_WRITE(表示该区域的内存是可写的。同时,因为该区域要初始化为0,所以要求该区域内存可写)。
打印指定section的内容
readelf -x section名 main.o
.data section
.data和.bss类似,但是它包含的数据不会初始化为0。这种情况下就需要在文件中包含对应的信息了。所以.data的sh_type为SHF_PROGBITS,但sh_flags和.bss一样。读者可以尝试在main.c中定义一个比如"char c='f'"这样的变量就能看到.data section的变化了。
.rodata section
包含只读数据的信息,比如main.c中printf里的字符串就属于这一类。它的sh_flags只能为SHF_ALLOC。
.symtab section
里边存储的是符号表(Symbol Table)。.symtab section的类型为SHT_SYMTAB。一般而言,符号表主要用于编译链接,也可以参与动态库的加载。
st_name:该符号的名称,指向.strtab section某个索引位置。
st_info:说明该符号的类型和绑定属性(binding attributes)。
st_other:说明该符号的可见性(Visibility)。它往往和st_info配合使用,用法见上图所示的三个宏。
st_shndx:symbol table中每一项元素都和其他section有关系。st_shndx就是这个相关section的索引号
st_value:符号的值,不同类型的ELF文件该变量的含义不同。比如:对于relocatable类型,st_value:表示该符号位于相关section(索引号为st_shndx)的具体位置。而对于shared和executable类型,st_value为该符号的虚拟内存地址。
st_size:和这个符号关联的数据的长度
查看符号表
readelf -s main.o
type
Bind
·SHN_ABS:取值为0xFFF1,ABS是Absolute之意,表示这个符号的值是固定不变的。·SHN_UNDEF:取值为0。UNDEF是Undefine的意思,表示该符号的定义在别的ELF文件中,此处只是引用它,程序在链接时会处理UNDEF符号项。
.dynsymtab section
.symtab section往往包含了全部的符号表信息,但不是其中所有符号信息都会参与动态链接,所以ELF还专门定义一个.dynsym section(类型为SHT_DYNSYM),这个section存储的仅是动态链接需要的符号信息。
.rel和.rela section
.rel和.rela section都与重定位有关,重定位最主要的作用就是将符号的使用之处和符号的定义之处关联起来。比如printf,这个函数符号是在别的ELF文件中定义的。将符号的使用之处和它的定义之处关联起来的方法有两种:1.编译链接过程中,最终生成可执行文件或动态库文件时,编译链接器将根据ELF文件中的重定位表计算最终的符号的位置。2.加载动态库时,加载器也会根据重定位信息修改对应的符号使用之处,使得动态库能正常工作。
根据重定位表项所用数据结构的不同,重定位表Section的命名略有不同。
“.relname”:以“.rel”开头,后跟其他常见的section名,比如.rel.text、.rel.data等。
·“.relaname”:以“.rela”开始,后面跟其他常见的section名,比如.rela.text、.rela.data等。
·r_offset:是一个偏移量,具体用法和ELF类型有关。
·r_info:r_info由两个信息组成(组成方式参考图4-13中的宏),分别是,该重定位项针对符号表哪一项(即目标项的索引号,sym)和重定位的类型(type,不同处理器有不同的类型)。
·Elf64_Rela还多了一个成员域r_addend,它代表一个常量值,用于计算最终的重定位信息的位置。
.got Global Offset Table
GOT是一个表,存储的是符号的绝对地址。但绝对地址不可能由编译器决定,由runtime linker设置
首先,GOT[1]和GOT[2]这两项内容存储的是解释器的信息和符号解析处理函数的入口地址。解释器为Program Header中索引号为1的元素,其类型为PT_INTERP,值为/linux64/ld-linux-x86-64.so.2。这个东西其实就是链接器ld。当可执行程序使用动态库的时候,编译器会将ld的信息放到ELF对应位置上。ld加载那些执行时需要的动态库(根据可执行程序ELF文件里的.dynsym表等信息),设置好GOT等对应项。最后,系统的控制权将交还给可执行程序。这时,我们的程序才真正运行起来。
那么符号的地址是在什么时候计算出来的呢?如果设置环境变量LD_BIND_NOW(export LD_BIND_NOW=1),对于运行中调用dlopen来加载so文件的程序而言,就需设置dlopen的flag参数为RTLOAD_NOW。则启动时计算,这种做法的一个主要缺点在于它使得那些大量依赖动态库的程序的加载时间变长。如果设置环境变量LD_BIND_NOT(ld默认采用这种策略),对于dlopen来说,设置flag为RTLOAD_LAZY,则用的时候再计算,这种方式可以让ld尽快把控制权交给可执行程序本身,从而提升程序启动速度。
程序本身是不知道ld到底会使用哪种方法的,所以编译器生成的二进制文件必须同时支持这两种方法。这就需要第二个辅助手段,PLT。
.plt .got.plt Procedure Linkage Table
ELF将GOT表分为.got和.got.plt两个表,其内容没什么区别,存储的都是符号的地址,只不过是.got.plt专门存储函数符号的地址
其表项存储的是一段小小的代码,这段代码能帮助我们触发符号地址的计算以及跳转到正确的符号地址上。
类似这种具有辅助功能的代码,在软件界有一个很形象的比喻,叫Trampoline(蹦床) Code。
PLT[0]存储的是跳转到GOT表Resolver的指令。"pushl got_plus_4"是GOT[1]的元素压栈,"jmp*got_plus_8"则是跳转到GOT[2]所存储的地址上去执行,也就是前面说的Resolver。
PLT[1],*name1_in_GOT表示符号name1在GOT对应项的内容,我们以GOT[name1]表示。如果该符号的地址还没有计算的话,GOT[name1]存储的是其后一条指令(此处是"pushl$offset")的地址。"pushl$offset"的目的是将计算这个符号地址所需的参数压栈(offset的含义我们后面再介绍)。然后,Trampoline Code接着执行到"jmp.PLT0@PC",即跳转到PLT[0]的代码处执行(其结果就是跳到Resolver里了)。
如果这个符号地址已经计算过,则GOT[name1]存储的就是正确的地址。"jmp*name1_in_GOT"也就直接跳转到目标地址上了。
查看.got.plt的值
readelf -x .got.plt main