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
资料:
1 【原创】简单粗暴的so加解密实现
2 【原创】手工打造ELF文件
3 ELF文件格式解析 (嵌入式很多用这个格式)
4 ELF格式文件符号表全解析及readelf命令使用方法