elf文件格式
android是建立在linux的基础上,其底层代码是安装linux可执行文件——elf的格式来组装的。本文结合android中的so文件来了解elf格式,资料大多收集于网上;elf格式位于android源码:elf.h(下面涉及到的结构体和宏定义都可以在此头文件中找到)。
elf大致可分为三部分:elf头、程序头表、节区头表;当然还有上图没标出的动态符号表,
elf头:
#define EI_NIDENT 16 typedef struct { unsigned char e_ident[EI_NIDENT]; //magic Elf32_Half e_type; //type 1:重定位文件;2:可执行文件;3:共享文件 Elf32_Half e_machine; //cpu结构 Elf32_Word e_version; //版本 Elf32_Addr e_entry; //程序进入点 可执行:main;so:无用 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;
我们会发现 e_ehsize = e_phoff(为什么?看第一幅图)。在elf头中我们很容易发现其实主要分三部分:info相关,程序头相关,节区头相关;刚好对应着链接器和装载器所需内容。e_phoff、e_phentsize、e_phnum装载器必须;e_shoff、e_shentsize、e_shnum、e_shstrndx是链接器必须;即section是供给linker使用,而segment是供给loader使用。注意下e_shstrndx是字符串表头在section头中的索引,例如e_shstrndx=2,则section头中的第3个旧市字符串表头。
phdr头:
typedef struct elf32_phdr{ Elf32_Word p_type; //segment类型 Elf32_Off p_offset; //该segment在文件的偏移地址 Elf32_Addr p_vaddr; //segment映射到内存中的地址 Elf32_Addr p_paddr; //segment物理地址,现代操作系统基本无法触及,基本无效 Elf32_Word p_filesz; //segment在文件中所占大小,有些segment在文件中不存在却占据一定的内存大小,则为0 Elf32_Word p_memsz; //segment在内存中所占的地址空间大小 Elf32_Word p_flags; //segment可操作的读写权限 Elf32_Word p_align; //按几个字节对齐 } Elf32_Phdr;
重点是p_offset和p_filesz,它们是segment的起始地址和大小;p_type为segment类型详见elf.h中的PT开头的宏定义;p_flags为segment的可操作权限详见elf.h中的PF开头宏定义(跟linux的文件权限rwx是一样的)。
shdr头:
typedef struct { Elf32_Word sh_name; //section name:.data、.dynamic、.got、.init...... Elf32_Word sh_type; //section类型 Elf32_Word sh_flags; //section权限 Elf32_Addr sh_addr; //section映射到内存中起始地址 Elf32_Off sh_offset; //该section在文件中的偏移 Elf32_Word sh_size; //section大小 Elf32_Word sh_link; //一般来说是该section所用的string table在section header table中的索引,见参考资料3 Elf32_Word sh_info; // Elf32_Word sh_addralign;//section按几字节对齐 Elf32_Word sh_entsize; //section内容中表项所占大小,例如.dynamic为8下面解释 } Elf32_Shdr;
动态符号表(dynamic_symbol_table):
介绍完elf格式的整体框架后,来深入了解内在的联系和一些section。
.shstrtab:字符串表保存着一系列以NULL结尾的的字符串
.dynstr:该section包含了用于动态链接的字符串,通常是符号表项名称字符串;
.dynamic:该section包含了动态链接信息,该section属性将包含SHF_ALLOC比特位,而SHF_WRITE比特位是否为1取决于处理器(通常.dynamic会独占一个segment叫dynamic);简单来说它包含着一连串的dynamic结构
typedef struct dynamic{ Elf32_Sword d_tag; union{ Elf32_Sword d_val; Elf32_Addr d_ptr; } d_un; } Elf32_Dyn;
d_tag控制d_un是d_val还是d_ptr;可通过d_tag来识别是属于哪个section(elf.h中DT开头的宏定义),d_un为d_tag在文件中的偏移量(不完全正确!,以后再补充)。例如d_tag为6则是DT_SYMTAB为.dynsym,则d_un为.dynsym为偏移量。值得一提的是在该section中,sh_addralign为4,sh_entsize为8(为什么看dynamic结构体)。这个节区非常重要,android linker就是通过它解析出其他section,看源码:http://androidxref.com/4.4_r1/xref/bionic/linker/linker.cpp#1339。注意d_tag = NEEDED表示为共享库,而其共享库的name是strtab[dun]为首字母的字符串。
.hash:包含了符号hash表,hash表内容的组合形式如下:
Symbol Hash Table nbucket nchain bucket[0] ... bucket[nbucket - 1] / chain[0] ... chain[nchain - 1]
由此可以看出hash表的长度为(nbucket+nchain+2)*4。假设函数hash值为funHash,在.hash中得到值funIndex=bucket[funHash]或chain[funHash]。
.dynsym:该section包含了动态链接符号表;其实该section是elf32_sym结构体数组
typedef struct elf32_sym{ Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; //不是很理解,看源码http://androidxref.com/4.4_r1/xref/bionic/libc/include/sys/exec_elf.h STT_FUNC unsigned char st_other; Elf32_Half st_shndx; } Elf32_Sym;
sh_name值是以.dynstr为基址的索引,st_value为fun指令偏移地址,st_size为fun指令长度。funInfo = sym[funIndex],可得到fun的信息。st_shndx为STN_UNDEF表示该函数为外部引用需要被重定位,即st_value为0。这里提下st_info这个字符,它由类型和绑定属性组成,可以源码
.rel:重定位表
typedef struct elf32_rel { Elf32_Addr r_offset; // Elf32_Word r_info; // } Elf32_Rel;
r_offset为需要重定位的内容地址,而r_info分为2部分:elf.h中定义了宏定义,ELF32_R_SYM是在dynsym的索引,ELF32_R_TYPE是重定位的类型:
#define R_ARM_ABS32 2 //外部函数局部指针函数调用方式 位于.rl.dyn
/* 20-31 are reserved for ARM Linux. */ 位于源码/bionic/libc/arch-arm/include/machine/elf_machdep.h #define R_ARM_COPY 20 #define R_ARM_GLOB_DAT 21 //外部函数全局指针函数调用方式 位于.rl.dyn #define R_ARM_JUMP_SLOT 22 //外部函数直接调用方式 位于.rl.plt #define R_ARM_RELATIVE 23 #define R_ARM_GOTOFF 24 #define R_ARM_GOTPC 25 #define R_ARM_GOT32 26 #define R_ARM_PLT32 27
r_offset是该函数的指令或者数据的值在内存中的指针,比如r_offset = addr1,在地址addr1中存放addr2,则addr2为函数指令地址
Tips:
1 字符串符号表.shstrtab后跟着section_header_table;节区表头分布在elf文件最后,而字符串符号表往往是在最靠后的内容
2 section name需要在shstr table找;而segment 没有name只有type,只需比较就能确定类型
3 根据函数名找到函数指令:函数名hash值funHash,在hash表得到索引值funIndex,在dynsym表索引得到funInfo,funIndo.st_name为.dynstr的索引(这可以判断是否为我们要找到的函数),funInfo.st_value为函数指令偏移地址
4 .dynsym的项数=.hash中nchain
资料: