转:https://www.cnblogs.com/ilocker/p/4541936.html
ELF Format 笔记(一)—— 概述
ELF Object files 参与程序的链接和执行,从这两个角度分别有两种视图:
ELF header 位于文件的最开始处,描述整个文件的组织结构。
Program Header Table 告诉系统如何创建进程镜像,在执行程序时必须存在,在 relocatable files 中则不需要。每个 program header 分别描述一个 segment,包括 segment 在文件和内存中的大小及地址等等。
执行视图中的 segment 其实是由很多个 section 组成。在一个进程镜像中通常具有 text segment 和 data segment 等等。
section 中可以包含数据或代码,典型的 section 包括.data(存放初始化过的全局变量)、.text(包含程序指令代码)、.rodata(包含程序中的只读数据)、.symtab(存放符号表)等等。
每个 section 都需在 section header table 中有一个对应的项,描述 section 的名字、大小、类型和属性等等。
在 ELF Object files 中,只有 ELF header 的位置固定,sections 和 segments 的位置都不固定。在解析时,通过 ELF header 的相应成员可以定位到他们的位置。
下面是在描述 ELF Object files 文件格式时经常使用的数据类型:
ELF Format 笔记(二)—— ELF Header
以 32 位的 ELF header 数据结构为例:
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT];
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;
1、e_ident
ELF 标识。是一个 16 字节大小的数组,其各个索引位置的字节数据有固定的含义。
/*
* e_ident[] identification indexes
* See http://www.caldera.com/developers/gabi/2000-07-17/ch4.eheader.html
*/
#define EI_MAG0 0 /* file ID */
#define EI_MAG1 1 /* file ID */
#define EI_MAG2 2 /* file ID */
#define EI_MAG3 3 /* file ID */
#define EI_CLASS 4 /* file class */
#define EI_DATA 5 /* data encoding */
#define EI_VERSION 6 /* ELF header version */
#define EI_OSABI 7 /* OS/ABI ID */
#define EI_ABIVERSION 8 /* ABI version */
#define EI_PAD 9 /* start of pad bytes */
#define EI_NIDENT 16 /* Size of e_ident[] */
/* e_ident[] magic number */
#define ELFMAG0 0x7f /* e_ident[EI_MAG0] */
#define ELFMAG1 'E' /* e_ident[EI_MAG1] */
#define ELFMAG2 'L' /* e_ident[EI_MAG2] */
#define ELFMAG3 'F' /* e_ident[EI_MAG3] */
#define ELFMAG "\177ELF" /* magic */
#define SELFMAG 4 /* size of magic */
/* e_ident[] file class */
#define ELFCLASSNONE 0 /* invalid */
#define ELFCLASS32 1 /* 32-bit objs */
#define ELFCLASS64 2 /* 64-bit objs */
#define ELFCLASSNUM 3 /* number of classes */
/* e_ident[] data encoding */
#define ELFDATANONE 0 /* invalid */
#define ELFDATA2LSB 1 /* Little-Endian */
#define ELFDATA2MSB 2 /* Big-Endian */
#define ELFDATANUM 3 /* number of data encode defines */
/* e_ident[] Operating System/ABI */
#define ELFOSABI_SYSV 0 /* UNIX System V ABI */
#define ELFOSABI_HPUX 1 /* HP-UX operating system */
#define ELFOSABI_NETBSD 2 /* NetBSD */
#define ELFOSABI_LINUX 3 /* GNU/Linux */
#define ELFOSABI_HURD 4 /* GNU/Hurd */
#define ELFOSABI_86OPEN 5 /* 86Open common IA32 ABI */
#define ELFOSABI_SOLARIS 6 /* Solaris */
#define ELFOSABI_MONTEREY 7 /* Monterey */
#define ELFOSABI_IRIX 8 /* IRIX */
#define ELFOSABI_FREEBSD 9 /* FreeBSD */
#define ELFOSABI_TRU64 10 /* TRU64 UNIX */
#define ELFOSABI_MODESTO 11 /* Novell Modesto */
#define ELFOSABI_OPENBSD 12 /* OpenBSD */
#define ELFOSABI_ARM 97 /* ARM */
#define ELFOSABI_STANDALONE 255 /* Standalone (embedded) application */
/* e_ident */
#define IS_ELF(ehdr) ((ehdr).e_ident[EI_MAG0] == ELFMAG0 && \
(ehdr).e_ident[EI_MAG1] == ELFMAG1 && \
(ehdr).e_ident[EI_MAG2] == ELFMAG2 && \
(ehdr).e_ident[EI_MAG3] == ELFMAG3)
前 4 个字节 e_ident[EI_MAG0] ~ e_ident[EI_MAG3] 的内容固定为 0x7f、’E’、’L’、’F’,标识这是一个 ELF文件。
e_ident[EI_CLASS] 指明文件类别:0(无效目标文件);1(32 位目标文件);2(64 位目标文件)。
e_ident[EI_DATA] 指明字节序,规定该文件是大端还是小端:0(无效编码格式);1(小端);2(大端)。
e_ident[EI_VERSION] 指明 ELF 文件头的版本。
从 e_ident[EI_PAD] 到 e_ident[EI_NIDENT-2] 之间的 6 个字节保留。
2、e_type
文件类型。常见的:1(可重定位文件:“.o 文件”);2(可执行文件);3(共享库文件:“.so 文件”)。
3、e_machine
指定该程序在什么平台上使用,比如 EM_386 表示在 Inter x86 机器上使用。
4、e_version
ELF 文件版本号。
5、e_entry
程序的入口虚拟地址。对于可执行文件来说,当 ELF 文件加载完成后,将从这个地址开始执行。对于其它文件,该值为 0。
6、e_phoff / e_shoff
分别指明 Program Header Table 和 Section Header Table 在文件中的字节偏移量,没有则为 0。
7、e_flags
处理器特定的标志位,通常不怎么关心。
8、e_ehsize
指明 ELF 文件头的字节大小(52 个字节)。
9、e_phentsize / e_phnum
e_phentsize 指明在 Program Header Table 中的每一项的字节大小,e_phnum 指明共有多少项。
10、e_shentsize / e_shnum
e_shentsize 指明在 Section Header Table 中的每一项的字节大小,e_shnum 指明共有多少项。
11、e_shstrndx
在 Section Header Table 中,存储“节名字表”的 Section(就是 .shstrtab 节)所对应的索引。
ELF Format 笔记(三)—— Section Types
ELF 文件中会包含很多 section,所有的 section 都在 section header table 中有对应的一项,每个 section header 都是一个 Elf32_Shdr 结构,用于描述相应 section 的信息。Elf32_Shdr 结构中有一个 sh_type 成员,用于指明 section 的类型。
/* Section Header */
typedef struct {
Elf32_Word sh_name; /* name - index into section header
string table section */
Elf32_Word sh_type; /* type */
Elf32_Word sh_flags; /* flags */
Elf32_Addr sh_addr; /* address */
Elf32_Off sh_offset; /* file offset */
Elf32_Word sh_size; /* section size */
Elf32_Word sh_link; /* section header table index link */
Elf32_Word sh_info; /* extra information */
Elf32_Word sh_addralign; /* address alignment */
Elf32_Word sh_entsize; /* section entry size */
} Elf32_Shdr;
typedef struct {
Elf64_Half sh_name; /* section name */
Elf64_Half sh_type; /* section type */
Elf64_Xword sh_flags; /* section flags */
Elf64_Addr sh_addr; /* virtual address */
Elf64_Off sh_offset; /* file offset */
Elf64_Xword sh_size; /* section size */
Elf64_Half sh_link; /* link to another */
Elf64_Half sh_info; /* misc info */
Elf64_Xword sh_addralign; /* memory alignment */
Elf64_Xword sh_entsize; /* table entry size */
} Elf64_Shdr;
/* Special Section Indexes */
#define SHN_UNDEF 0 /* undefined */
#define SHN_LORESERVE 0xff00 /* lower bounds of reserved indexes */
#define SHN_LOPROC 0xff00 /* reserved range for processor */
#define SHN_HIPROC 0xff1f /* specific section indexes */
#define SHN_ABS 0xfff1 /* absolute value */
#define SHN_COMMON 0xfff2 /* common symbol */
#define SHN_HIRESERVE 0xffff /* upper bounds of reserved indexes */
/* sh_type */
#define SHT_NULL 0 /* inactive */
#define SHT_PROGBITS 1 /* program defined information */
#define SHT_SYMTAB 2 /* symbol table section */
#define SHT_STRTAB 3 /* string table section */
#define SHT_RELA 4 /* relocation section with addends*/
#define SHT_HASH 5 /* symbol hash table section */
#define SHT_DYNAMIC 6 /* dynamic section */
#define SHT_NOTE 7 /* note section */
#define SHT_NOBITS 8 /* no space section */
#define SHT_REL 9 /* relation section without addends */
#define SHT_SHLIB 10 /* reserved - purpose unknown */
#define SHT_DYNSYM 11 /* dynamic symbol table section */
#define SHT_NUM 12 /* number of section types */
#define SHT_LOPROC 0x70000000 /* reserved range for processor */
#define SHT_HIPROC 0x7fffffff /* specific section header types */
#define SHT_LOUSER 0x80000000 /* reserved range for application */
#define SHT_HIUSER 0xffffffff /* specific indexes */
/* Section names */
#define ELF_BSS ".bss" /* uninitialized data */
#define ELF_DATA ".data" /* initialized data */
#define ELF_DEBUG ".debug" /* debug */
#define ELF_DYNAMIC ".dynamic" /* dynamic linking information */
#define ELF_DYNSTR ".dynstr" /* dynamic string table */
#define ELF_DYNSYM ".dynsym" /* dynamic symbol table */
#define ELF_FINI ".fini" /* termination code */
#define ELF_GOT ".got" /* global offset table */
#define ELF_HASH ".hash" /* symbol hash table */
#define ELF_INIT ".init" /* initialization code */
#define ELF_REL_DATA ".rel.data" /* relocation data */
#define ELF_REL_FINI ".rel.fini" /* relocation termination code */
#define ELF_REL_INIT ".rel.init" /* relocation initialization code */
#define ELF_REL_DYN ".rel.dyn" /* relocaltion dynamic link info */
#define ELF_REL_RODATA ".rel.rodata" /* relocation read-only data */
#define ELF_REL_TEXT ".rel.text" /* relocation code */
#define ELF_RODATA ".rodata" /* read-only data */
#define ELF_SHSTRTAB ".shstrtab" /* section header string table */
#define ELF_STRTAB ".strtab" /* string table */
#define ELF_SYMTAB ".symtab" /* symbol table */
#define ELF_TEXT ".text" /* code */
/* Section Attribute Flags - sh_flags */
#define SHF_WRITE 0x1 /* Writable */
#define SHF_ALLOC 0x2 /* occupies memory */
#define SHF_EXECINSTR 0x4 /* executable */
#define SHF_MASKPROC 0xf0000000 /* reserved bits for processor */
/* specific section attributes */
SHT_NULL:无效的 section header,没有与之对应的 section,section header 中的其他成员也没有意义。
SHT_PROGBITS:很多 section 都属于该类型,比如:.text、.data、.got、.plt、.rodata、.interp 等等。
SHT_SYMTAB / SHT_DYNSYM:这两类 section 中存放符号表。SHT_DYNSYM 中含有动态链接的符号表。
SHT_STRTAB:存放字符串表,通常一个 ELF 文件中含有不止一个字符串表。比如:.shstrtab 是专门用于存放 section name 的字符串表。
SHT_RELA / SHT_REL:包含重定位表,通常该类型的 section 也不止一个,比如:.rel.plt、.rel.dyn。
SHT_HASH:存放与符号表对应的哈希表,用于根据指定的符号名快速定位到符号表中相应的符号。
SHT_DYNAMIC:包含动态连接信息,.dynamic 属于此类型。
SHT_NOTE:包含一些注释信息。
SHT_NOBITS:该 section 在文件中不占空间,典型的是 .bss。.bss 在内存中是存在的,存放那些未初始化的全局变量。
SHT_SHLIB、SHT_LOPROC / SHT_HIPROC、SHT_LOUSER / SHT_HIUSER:基本用不到,不用关心。
ELF Format 笔记(四)—— 节(Section)
ELF 文件可以包含很多 section,所有的 section 都在 section header table 中有对应的一项,每个 section header 都是一个 Elf32_Shdr 结构,用于描述相应 section 的信息。
ELF Header 中的 e_shoff 给出了 section header table 在 ELF 文件中的字节偏移量,e_shentsize 指明在 section header table 中的每一项的字节大小,e_shnum 指明共有多少项。
Elf32_Shdr 结构:
typedef struct {
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;
sh_name:section 的名字。这里其实是一个索引,指出 section 的名字存储在 .shstrtab 的什么位置。.shstrtab是一个存储所有 section 名字的字符串表。
sh_type:section 的类型,上一篇笔记已经学习过。
sh_flags:通过 1-bit flag 定义一些属性。
- SHF_WRITE:此 section 中包含的数据,在进程运行时可写。
- SHF_ALLOC:此 section 在进程运行时要占用内存。
一些控制用的 section 在进程运行时是不占用内存的,比如动态链接器可能只把 section 中的一些信息读出来,用完就拉倒了,没必要再存储到进程镜像中。
- SHF_EXECINSTR:此 section 存储的是可执行的机器码。
- SHF_MASKPROC:保留的,不用管。
sh_addr:如果此 section 需要映射到进程空间,此成员指定映射的起始地址。如不需映射,此值为 0。
sh_offset: 此 section 相对于文件开头的字节偏移量。如果 section 类型为 SHT_NOBITS,表明该 section 在文件中不占空间,这时 sh_offset 没什么用。
sh_size:此 section 的字节大小。如果 section 类型为 SHT_NOBITS,就不用管 sh_size 了。
不同类型的 section 中 sh_link 和 sh_info 的意义不同:
基本上就是针对不同的 section 类型,分别给出字符串表、符号表等所在的 section 在 section header table 中的索引。
sh_addralign:指明此 section 的 sh_addr 向几字节对齐,sh_addralign 应该是 2 的正整数倍。如果为 0 或 1,表明此 section 没有字节对齐约束。
sh_entsize:有些节的内容是一张表,其中每个表项的大小固定,比如符号表。对于这种表,本成员指定其每个表项的大小。如果此值为 0,表明本 section 不好含 XX 表。
section header table 中的索引为 0(SHN_UNDEF) 的这一项特殊,内容为:
ELF Format 笔记(五)—— 特殊 Section
链接器把一些独立的 object files 和库文件链接起来,形成可执行文件。在这个过程中,链接器需要解决一些符号的引用以及指令的重定位。
除此之外,还有一个动态链接的过程。比如有些符号是定义在某个 so 文件中的,需要由动态链接器在装载的过程中进行一些符号查找和地址重定位的工作。要完成此工作,动态链接器需要一些信息,它们存储在一些特殊的 section 中,比如 .dynamic。
section 和 segment 是分别针对链接视图和执行视图来说的,各种资料对他们的翻译比较混乱,慢慢就会理解了。
还有一些用于支持调试功能的 section,比如 .debug、.line。下面是一张描述特殊 section 的表格:
.bss:保存未初始化的数据,比如那些未初始化的全局变量。因为是“未初始化”,所以也没必要在文件中占用任何空间去记录其初始值(所以类型为 SHT_NOBITS)。在程序开始运行时,系统会将 .bss 映射的内存区域清零。
.comment:保存版本控制信息。
.data / .data1:保存已初始化的数据。它们会在文件中占用存储空间,这与 .bss 不同。
.debug:保存调试相关的信息。
.dynamic:保存动态链接信息。
.dynstr:保存动态链接所需的字符串。比如符号表中的每个符号都有一个 st_name(符号名),他是指向字符串表的索引,这个字符串表可能就保存在 .dynstr。
.dynsym:保存需要动态连接的符号表。
.fini / .init:分别保存进程退出和初始化时要执行的指令。.init 指令会在程序入口点(main)之前被执行。
.got:保存全局偏移量表 (global offset table)。
在 Android 中,GOT 分为两部分:.got 和 .got.plt。其中 .got 表用来保存全局变量引用的地址,而 .got.plt 用来保存函数引用的地址。
.hash:保存符号哈希表,用于快速查找与其对应的符号表中的符号。
.interp:保存 ELF 程序解释器(比如 Android 下的动态链接器)的路径名。
.line:保存用于调试的行号信息。
.note:保存一些注释信息。
.plt:保存过程链接表 (procedure linkage table)。每个外部定义的函数都会在 PLT 中有对应的一项,用于定位外部函数的地址。
.relname / .relaname:保存重定位表。比如:.rel.dyn、.rel.plt。
.rodata / .rodata1:保存程序中的只读数据。
.shstrtab:保存一个字符串表,这些字符串都是 section 的名字。
.strtab:保存字符串表,类似于 .dynstr,但 .dynstr 中保存的都是那些需要动态链接的符号的名字。
.symtab:保存符号表(非动态链接)。
.text:保存可执行的指令代码。
这些以“.”为前缀的 section 名字为系统保留。应用程序可以构造自己的段,但最好不要与系统已定义 section 节重名,也不要以“.”开头,以避免潜在的冲突。
了解动态链接(六)—— 重定位表
无论是可执行文件还是 so,只要它依赖于其他 so(.dynsym 动态符号表中有导入符号存在),那么在编译链接阶段,这些符号的地址未知,所以只能在动态链接阶段对其进行地址重定位。
注意:以 PIC 编译的 so,虽然称“地址无关代码”,但也需要重定位。因为对于 PIC 的 so 来说,只不过是把代码中的绝对地址提出来,放到了数据段的 GOT 表中。所以,虽然代码段不需重定位,但数据段的 GOT 表需要重定位。(以 PIC 编译 so,是为了复用 so 的代码段)
以 android liblog.so 为例。在代码中调用 memset 时,实际上会跳转到标号 memset_ptr 指向的内存。
而标号 memset_ptr 指向的内存就定义在 GOT 表中:
那么如何为 GOT 表做重定位呢?GOT 表中的每一项应该指向哪一个符号在内存中的地址呢?这些信息就由重定位表来描述。具体到 android 来说,重定位表保存在 .rel.dyn 和 .rel.plt 中。
从表中可以看出,位于 .rel.dyn 中的主要是 R_ARM_GLOB_DAT 类型的重定位项,而位于 .rel.plt 中的主要是 R_ARM_JUMP_SLOT 类型的重定位项。前者用于对数据引用做重定位,而后者用于对函数引用做重定位。除此之外,还看到了 R_ARM_RELATIVE 类型的重定位。
在 android linker 的源码中,调用了两次 soinfo_relocate 函数,分别为 .rel.dyn 和 .rel.plt 做重定位:
在 soinfo_relocate 函数的源码中可以看到,对于不同类型的重定位,计算符号地址的方式也有所不同。
实际上对于 R_ARM_GLOB_DAT、R_ARM_JUMP_SLOT 这两种重定位类型来说,只需将符号地址填入被修正的内存即可。而 R_ARM_RELATIVE 类型看起来特殊些,它的作用是进行基址重置 (Rebasing) 。
比如指针 p 指向静态变量 a,而静态变量 a 相对于 so 基址的偏移为 A。在编译时,so 的基址为 0,此时 p 的值为 A。而当 so 被装载到内存中时,p 的值就需要加上一个 so 在内存中的基址 base。R_ARM_RELATIVE 类型的重定位就是用来干这个的。
对于 android linker 的重定位细节,以及其他重定位类型,在后面写 android linker 源码分析笔记时再描述。
学习资料: 《程序员的自我修养——链接、装载和库》
ELF Format 笔记(七)—— 符号表
符号表 (symbol table) 中保存着符号的定义或者引用信息。对于 android so 文件来说,.dynsym 符号表保存着库文件的导入和导出符号。
用 readelf 看一下 android liblog.so 的 .dynsym 动态符号表:
符号 __cxa_finalize 的 Ndx(st_shndx) 为 UND,表明该符号在本 so 中未定义,需要去 liblog.so 的依赖库中去找。那 liblog.so 依赖哪些库?可以从 .dynamic 中找到。
符号 __cxa_finalize 的定义应该在 libc.so 中。
符号 __android_log_vprint 的 Ndx(st_shndx) 为 8,表示该符号在本 so 中有定义,并且定义该符号的 section 在 section header table 中的索引是 8。看一下 section header table:
可以看到,符号 __android_log_vprint 定义在 .text section 中,这也是理所当然。所以 __android_log_vprint 是一个导出符号,由那些需要打印日志的程序调用。
因为符号 __android_log_vprint 是在本 so 中定义的,所以看到其 Value(st_value) 不为 0。android linker 在做符号重定位工作时,就是由这个 st_value 找到该符号定义在内存中的地址。不过 0x00001539 当然不是符号的最终的内存地址,linker 会再加上 liblog.so 在内存中的基址,所以 st_value 相当于一个地址偏移。
回过头再看一下,符号表其实就是一个结构体数组,每个结构体描述一个符号的信息。结构体的定义如下:
其中 st_value 和 st_shndx ,上面已经有所介绍。
st_name 是符号的名字,这里保存的只是一个引用字符串表的索引值。
st_size 对于导入符号来说,根本不知道人家有多大,所以为 0。对于导出符号,则会明确标明大小。
st_info 包含了符号的类型和绑定属性等信息,后续笔记再具体写。
st_other 保留未用,不必关心。
ELF Format 笔记(八)—— 符号的类型和属性(st_info)
上篇笔记中说过,Elf32_Sym 结构的 st_info 成员包含了符号的类型和绑定属性等信息。
在 android linker 做重定位时,如果遇到未定义的符号,会判断该符号是否是一个弱引用符号,如果不是,则出错返回。具体是通过 ELF32_ST_BIND 宏来判断的:
ELF32_ST_BIND 宏就是从 st_info 中提取一些 bits 作为绑定属性:
符号绑定属性的宏定义:
- STB_LOCAL:本地符号,在本文件外不可见。
- STB_GLOBAL:全局符号,在所有要链接在一起的 object files 中都可见(全局符号可以被其他文件引用)。
- STB_WEAK:弱符号,类似于全局符号,但(定义的)优先级低于全局符号。
如果有同名的弱符号和全局符号,则优先使用全局符号的定义。
在符号表中,通常是本地符号排在前面,全局和弱符号排在后面。
还有一个宏 ELF32_ST_TYPE,用于判断符号的类型,同样也是从 st_info 中提取一些 bits 作为类型信息:
在 android linker 的源码中,并没有用到该宏,也没有用到符号的类型信息。
符号类型信息的宏定义:
- STT_NOTYPE:符号类型未指定。
- STT_OBJECT:数据对象,比如变量、数组等。
- STT_FUNC:函数或其它可执行代码。
- STT_SECTION:表示一个 section,主要用于重定位,通常具有 STB_LOCAL 属性。
- STT_FILE:文件符号,具有 STB_LOCAL 属性,st_shndx 的值为 SHN_ABS。在 ELF 文件的符号表中位于其他 STB_LOCAL 符号的前面。
ELF Format 笔记(九)—— Elf32_Sym 结构的 st_value 和 st_shndx 成员
前面的笔记中提到过 Elf32_Sym 结构,本篇笔记再写一下其中的 st_value 和 st_shndx 成员。
对于不同类型的 object file,st_value 的含义略有不同:
· 对于 relocatable (.o) 文件,如果相应的 st_shndx 的值为 SHN_COMMON,则 st_value 保存的是对齐字节数。
· 对于 relocatable (.o) 文件,如果该符号已定义,则 st_value 保存的是该符号在其所定义的 section (由 st_shndx 指定) 中的偏移量。
· 对于可执行和 shared object (.so) 文件,st_value 保存的是一个虚拟地址。android linker 通过将 st_value 与该文件加载到内存的基址相加,从而得到该符号的定义地址。
st_shndx 是定义该符号的 section 在 section header table 中的索引 (section number)。这里列出几个特殊值:
· SHN_UNDEF:通常表示该符号在本文件中未定义 (外部符号),android linker 会为该符号做地址重定位。
· SHN_ABS:表示该符号包含一个绝对的 (absolute) 值 (往往是一个地址),不受重定位影响。例如:
又如,表示文件名的符号:
· SHN_COMMON:表示该符号是一个 common 符号,通常未初始化的全局变量就是该类型的符号。关于 common 符号可以去了解一下链接器的“common block”机制。
ELF Format 笔记(十)—— 重定位(relocation)
重定位就是把符号引用与符号定义链接起来的过程,这也是 android linker 的主要工作之一。
当程序中调用一个函数时,相关的 call 指令必须在执行期将控制流转到正确的目标地址。所以,so 文件中必须包含一些重定位相关的信息,linker 据此完成重定位的工作。
这些重定位信息保存在一系列的重定位项中,重定位项的结构如下:
这些重定位项位于 .rel.plt section 中。
r_offset:对于可执行文件或 so 文件来说,该值是要进行重定位的存储单元的虚拟地址。
r_info:该值给出两个重要信息,一个是要重定位的符号在符号表中的索引,另一个是要施行的重定位类型。这两个信息分别由两个宏来提取。
对于 32 位的 so 来说,ELFW(R_TYPE) 就是 ELF32_R_SYM,ELFW(R_SYM) 就是 ELF32_R_SYM。
程序头表 (program header table) 是一个结构体数组,数组中的每个结构体元素是一个程序头 (program header),每个程序头描述一个段 (segment)。
一个 so 通常有两个可加载段 (LOAD) 段,android linker 在加载 so 时会根据 LOAD 段在内存中的最大虚拟地址减去最小虚拟地址,来计算要申请的内存大小。
除了两个 LOAD 段,其他段中的数据也是执行程序所需的,如:动态链接需要 DYNAMIC 段,c++ 异常栈展开需要 EXIDX 段。所以其他段也是需要加载到内存的,它们实际就位于两个 LOAD 段内部。
节 (section) 和段 (segment) 分别是从链接视图和执行视图两个不同的角度的来划分的,它们有一个对应关系 (每个段“包含”一个或者多个节):
程序头结构:
typedef struct {
Elf32_Word p_type;
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;
p_type:段类型,如:PT_LOAD (可加载段)、PT_DYNAMIC (动态段)。
p_offset:段数据的第一个字节相对于文件开头的偏移量。
p_vaddr:段数据的第一个字节在内存中的虚拟地址 (只是一个偏移)。
p_paddr:暂时不用关心。
p_filesz:段数据在文件中的的字节大小,可以是 0。
p_memsz:段数据在内存映像中的字节大小,可以是 0。
p_flags:段内存映像的访问权限:PF_X (可执行)、PF_W (可写)、PF_R (可读)。
p_align:如果为 0 或 1,表示不需要对齐。否则,p_align 应该是 2 的正整数幂,p_vaddr 和 p_offset 在对 p_align 取模后应相等。
ELF Format 笔记(十二)—— 段类型(segment types)
- PT_NULL:如果段类型是 PT_NULL,那相应程序头结构体的其它成员都无意义,该程序头项可被忽略。
暂时还没遇到过 PT_NULL 类型的段,android linker 也没有针对 PT_NULL 类型的段做什么特殊处理。
- PT_LOAD:可加载段。段数据由文件映射到内存,如果 p_memsz 大于 p_filesz,则额外部分填充为 0。
- PT_DYNAMIC:动态段。包含动态链接所需的信息。
- PT_INTERP:本段包含一个路径字符串,该路径存放解释器。
android 5 linker 没有用到 PT_INTERP 段,android 6 linker 在初始化默认的库加载路径时,从 PT_INTERP 段读取了解释器的名字:
- PT_NOTE:注释段,包含一些辅助信息。
android linker 没有用到。
- PT_SHLIB:保留的段类型,暂不关心。
- PT_PHDR:程序头段。指明程序头表在文件和内存映像中的位置和大小。
如果存在此类型段,则对应的程序头项必须出现在所有可加载段项的前面。
- PT_LOPROC ~ PT_HIPROC:为特定处理器保留,暂不关心。
一个可被系统加载的程序至少拥有一个可加载段。当系统创建可加载段的内存映像时,会根据 p_flags 赋予一定的访问权限。
不过实际的内存访问权限还要取决于系统的 MMU (内存管理单元),系统给予的访问权限可能比指定的权限要大。
不过如果没有指定 PF_W,系统一定不会赋予写权限。
段权限列表:
可以看出:
具备可执行权限,就具备可读权限。反之亦成立,具备可读权限,即具备可执行权限。
具备可写权限,就肯定具备可读、可执行权限。
个段 (segment) 由一个或多个节 (section) 组成,但这对 android linker 是透明的,linker 在加载程序时没有用到节信息。所以对于一个 so 文件,修改 ELF Header 中和节有关的成员 (e_shoff、e_shentsize、e_shnum) 不会影响程序运行。
文本段 (也叫代码段) 包含只读指令和数据,通常包含如下这些节:
通常 android so 的第一个 LOAD 段就是文本段:
数据段包含可写的数据和指令,通常包括如下这些节:
通常 android so 的第二个 LOAD 段就是数据段:
.got 和 .plt 节保存的信息用于支援“位置无关代码”,在动态链接的过程中会修改 .got 节。
.bss 节的类型是 SHT_NOBITS,表示在文件中不占空间。但 .bss 在段的内存映像中是占用空间的,通常位于段的末尾,用于存放未初始化的全局变量,所以数据段的 p_memsz 会比 p_filesz 大一些。
符号哈希表用于支援符号表的访问,能够提高符号搜索速度。
下表用于解释该哈希表的组织,但该格式并不属于 ELF 规范。
bucket 和 chain 数组中都保存有符号表的索引,数组大小分别为 nbucket 和 nchain。
先来看 android linker 中的 hash 函数:
给定一个符号名字,返回一个哈希值 x,然后由 bucket[x%nbucket] 得到一个符号表索引 y,如果索引 y 对应的符号表项不是想要的符号,则由 chain[y] 得到下一个符号表索引 z,如果仍不是想要的符号,继续 chain[z]…。
之所以这样使用符号哈希表,应该是因为它是采用链地址法来解决哈希冲突。