ELF文件是Linux系统中发明的很重要的文件,Linux系统中的可执行文件、Object文件、动态库文件都是ELF格式文件,它的地位就相当于Windows中的PE格式文件,不过整体上ELF文件比PE文件要设计地精简。ELF文件中大致分为文件头、段头表、结头表,剩下的就是段和结所指向的数据。
一. elf文件头
/* ELF Header */
typedef struct elfhdr {
unsigned char e_ident[EI_NIDENT]; /* ELF Identification */
Elf32_Half e_type; /* object file type */
Elf32_Half e_machine; /* machine */
Elf32_Word e_version; /* object file version */
Elf32_Addr e_entry; /* virtual entry point */
Elf32_Off e_phoff; /* program header table offset */
Elf32_Off e_shoff; /* section header table offset */
Elf32_Word e_flags; /* processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size */
Elf32_Half e_phentsize; /* program header entry size */
Elf32_Half e_phnum; /* number of program header entries */
Elf32_Half e_shentsize; /* section header entry size */
Elf32_Half e_shnum; /* number of section header entries */
Elf32_Half e_shstrndx; /* section header table's "section
header string table" entry offset */
} Elf32_Ehdr;
文件头里面记录着elf文件类型(可执行文件、动态库文件、*.o 被链接加载的文件)、是否arm、X86、程序头和节头信息。对于可执行文件和动态库文件节表可选,程序表是一定有,*.o 被链接加载的文件是程序头表可选,节头表必须有(如下图一)。在对安卓native函数做处理时用到的都是elf格式的动态库so文件,它里面是程序头表必有,节头表可选(如下图二、图三),下面讨论的是针对这种情形。
二. elf文件的程序头表
对于可执行文件和动态库文件,如果把节表信息抹掉,该文件还是有效的,而如果将程序头表信息抹掉,则文件会失效(注意:这里说的抹掉,只是抹掉表的信息,表指向的内容并没有动)。一般elf文件的程序头表中会有以下类型表:
方框中四种类型的程序头:Interpreter Path、(R_X)Loadable Segment、(RW_)Loadable Segment、Dynamic Segment。
Interpreter Path段记录的是链接器linker的路径,这个在可执行elf文件文件才有,在so文件中不会有,因为可执行文件中一般都是会调用其他的动态库的,那么这个时候系统就需要在将执行位置转到可执行文件的入口地址前,先将各种动态库加载到内存,这个操作是有linker来完成的,之后才将执行位置转到可执行文件的入口地址。
Loadable Segment指向的位置是需要加载到内存中的,两个Loadable Segment段,分别是可读可执行权限和可读可写权限。这两个段的作用和PE文件中节的作用是一样的,都是会记录内存虚拟地址偏移,文件偏移的,通过虚拟地址计算文件偏移时对所有的 Loadable Segment 中虚拟地址范围计算即可。其他程序头(非 Loadable Segment)中记录的文件偏移是可以抹掉的,因为系统在加载文件之后会通过虚拟地址来计算出文件偏移,而不是直接用程序头中的文件偏移。当然,如果抹掉 Loadable Segment中文件偏移就会使文件执行失败。(如果想尝试,通过010工具改动对应值,然后尝试即可,010中有自带elf文件格式解析模板,很快就能找到相应位置。)
Dynamic Segment 是很重要的一个程序头,里面存储着函数名、使用过的动态库名、重定位表、函数代码偏移等重要信息,不过不是直接记录,而是通过一定的方法查询得到,这个查询过程是elf设计中巧妙且关键的核心所在。Dynamic Segment指向的文件内容是以下结构体的对象数组:
/* Dynamic structure */
typedef struct {
Elf32_Sword d_tag; /* controls meaning of d_val */
union {
Elf32_Word d_val; /* Multiple meanings - see d_tag */
Elf32_Addr d_ptr; /* program virtual address */
} d_un;
} Elf32_Dyn;
/*d_tag 的取值*/
/* Dynamic Array Tags - d_tag */
#define DT_NULL 0 /* marks end of _DYNAMIC array */
#define DT_NEEDED 1 /* string table offset of needed lib */
#define DT_PLTRELSZ 2 /* size of relocation entries in PLT */
#define DT_PLTGOT 3 /* address PLT/GOT */
#define DT_HASH 4 /* address of symbol hash table */
#define DT_STRTAB 5 /* address of string table */
#define DT_SYMTAB 6 /* address of symbol table */
#define DT_RELA 7 /* address of relocation table */
#define DT_RELASZ 8 /* size of relocation table */
#define DT_RELAENT 9 /* size of relocation entry */
#define DT_STRSZ 10 /* size of string table */
#define DT_SYMENT 11 /* size of symbol table entry */
#define DT_INIT 12 /* address of initialization func. */
#define DT_FINI 13 /* address of termination function */
#define DT_SONAME 14 /* string table offset of shared obj */
#define DT_RPATH 15 /* string table offset of library
search path */
#define DT_SYMBOLIC 16 /* start sym search in shared obj. */
#define DT_REL 17 /* address of rel. tbl. w addends */
#define DT_RELSZ 18 /* size of DT_REL relocation table */
#define DT_RELENT 19 /* size of DT_REL relocation entry */
#define DT_PLTREL 20 /* PLT referenced relocation entry */
#define DT_DEBUG 21 /* bugger */
#define DT_TEXTREL 22 /* Allow rel. mod. to unwritable seg */
#define DT_JMPREL 23 /* add. of PLT's relocation entries */
#define DT_BIND_NOW 24 /* Bind now regardless of env setting */
#define DT_NUM 25 /* Number used. */
#define DT_LOPROC 0x70000000 /* reserved range for processor */
#define DT_HIPROC 0x7fffffff /* specific dynamic array tags */
对于以上d_tag,有的记录的是一块区域,对应的d_un取d_ptr的值,有的是记录某一块区域的大小,如:DT_STRSZ,则对应的d_un取d_val的值。
安卓中对so中native函数加密时,需要通过native函数名知道该native函数名对应的函数代码所在位置以及代码所占字节数,这就得需要通过Dynamic Segment来查找的,通过节表中动态符号表也是可以查询到,但节表可以抹掉,所以最好通过Dynamic Segment查找native函数的函数体。函数名在elf中是一个动态符号,DT_SYMTAB对应的偏移处记录着所有的动态符号对应的信息,是一个同结构体的数组,想要查找的函数名信息就在这个数组中,现在需要知道的是查找的函数名所对应的数组下标,步骤如下:1. 通过Dynamic Segment找到DT_STRTAB、DT_SYMTAB、DT_HASH对应的表信息,分别记录着动态符号名称字符串、符号表信息(符号对应的代码偏移和字节数就在这个表中)、哈希表;2. 通过函数
unsigned int elf_hash(const char *_name)
{
const unsigned char *name = (const unsigned char *)_name;
unsigned h = 0, g;
while (*name) {
h = (h << 4) + *name++;
g = h & 0xf0000000;
h ^= g;
h ^= g >> 24;
}
return h;
}
计算出函数名对应的哈希值;3. 通过上一步中的哈希值在哈希表中查找对应符号在符号表中下标,方法为: 给定一个符号名字,返回一个哈希值 x,然后由 bucket[x%nbucket] 得到一个符号表索引 y,如果索引 y 对应的符号表项不是想要的符号(通过符号表项对应符号名和给定符号名比对就行),则由 chain[y] 得到下一个符号表索引 z,如果仍不是想要的符号,继续 chain[z]…,直到匹配到,或者最后得出下标是0,则说明该符号不存在。bucket、nbucket、chain参考哈希表结构:
可参考以下代码,另外解析elf的工程也会给出下载链接。
int ElfFile32::getTargetFuncInfo(const char *funcName, funcInfo32 *info) {
char flag = -1, *dynstr;
int i;
Elf32_Off dyn_vaddr;
Elf32_Word dyn_size, dyn_strsz;
Elf32_Dyn *dyn;
Elf32_Addr dyn_symtab, dyn_strtab, dyn_hash;
Elf32_Sym *funSym;
unsigned funHash, nbucket;
unsigned *bucket, *chain;
int mod;
Elf32_Phdr* phdr = pProgmHdr;
// __android_log_print(ANDROID_LOG_INFO, "JNITag", "phdr = 0x%p, size = 0x%x\n", phdr, ehdr->e_phnum);
for (i = 0; i < m_dwPhNum; ++i) {
// __android_log_print(ANDROID_LOG_INFO, "JNITag", "phdr = 0x%p\n", phdr);
if (phdr->p_type == PT_DYNAMIC) {
flag = 0;
printf("Find .dynamic segment");
break;
}
phdr++;
}
if (flag)
return -1;
int nOffset = phdr->p_offset;
nOffset = Rva2Fa(phdr->p_vaddr);
dyn_vaddr = (Elf32_Addr)(nOffset + baseAddr);
dyn_size = phdr->p_filesz;
flag = 0;
for (dyn = (Elf32_Dyn*)dyn_vaddr; (Elf32_Addr)dyn < dyn_vaddr + dyn_size; dyn++){
if (dyn->d_tag == DT_HASH){
flag += 1;
dyn_hash = dyn->d_un.d_ptr;
}
else if (dyn->d_tag == DT_STRTAB){
flag += 2;
dyn_strtab = dyn->d_un.d_ptr;
}
else if (dyn->d_tag == DT_SYMTAB){
flag += 4;
dyn_symtab = dyn->d_un.d_ptr;
}
else if (dyn->d_tag == DT_STRSZ) {
flag += 8;
dyn_strsz = dyn->d_un.d_val;
}
}
if (flag & 0x0f != 0x0f){
printf("Find needed .section failed\n");
return -1;
}
dyn_hash = Rva2Fa(dyn_hash) + (Elf32_Addr)baseAddr;
dyn_strtab = Rva2Fa(dyn_strtab) + (Elf32_Addr)baseAddr;
dyn_symtab = Rva2Fa(dyn_symtab) + (Elf32_Addr)baseAddr;
funHash = elf_hash(funcName);
funSym = (Elf32_Sym *)dyn_symtab;
dynstr = (char*)dyn_strtab;
nbucket = *(unsigned*)dyn_hash;
bucket = (unsigned*)(dyn_hash + 8);
chain = bucket + nbucket;
flag = -1;
mod = (funHash % nbucket);
for (int i = bucket[mod]; i != 0; i = chain[i]){
if (strcmp(funSym[i].st_name + dynstr, funcName) == 0) {
flag = 0;
break;
}
}
if (flag != 0) {
return -1;
}
info->st_value = funSym[i].st_value;
info->st_size = funSym[i].st_size;
return 0;
}
三. elf文件的节头表
elf格式的可执行程序文件和动态库文件里面节头表是可选的,即抹掉之后也不影响系统对该文件的执行,但是节头表中对应的内容还是存在的,并且在elf的程序头表中指向的内容或者查找的内容在节表中都会有记录,例如:在上一节中通过Dynamic Segment找到DT_STRTAB、DT_SYMTAB、DT_HASH对应的表的内容分别对应节表中.dynstr节、.dynsym节、.hash节,如下图一(elf文件中获取的节表中偏移)、图二(动态解析出的对应DT_HASH、DT_STRTAB、DT_SYMTAB的内容的在文件中偏移)。
节表中常见的符串的节:.dynstr、.shdrstr,前者是动态符号字符串所在的节,后者是节头表名称所在的节,.shdrstr所在的节在整个节表数组中下标记录在文件头中。
四.可执行文件和共享库文件中动态符号
ELF文件中动态符号包含导入函数名和导出函数名等。elf文件的函数内容都存储在.text节中。可执行文件的函数默认不导出,函数执行时,直接通过elf入口地址执行,通过动态符号可解析的函数也比较少(带有调试信息的可执行文件的导出表中也包含开发者写的函数名,但用解析动态符号的方式没有获取对应函数名的函数体)。共享库的函数默认导出,导出函数的函数体可通过查找动态符号获取。
获取elf文件信息的工具有readelf.exe、objdump.exe文件等,这些文件存在于ndk目录中,如 ..\ndk-bundle\toolchains\llvm\prebuilt\windows-x86_64\bin目录下的 x86_64-linux-android-readelf.exe、aarch64-linux-android-readelf.exe、aarch64-linux-android-objdump.exe、x86_64-linux-android-objdump.exe。readelf.exe功能是解析elf文件格式,各个版本之间课通用,objdump.exe各版本如果涉及到反编译,必须使用兼容版本(arm-64可兼容arm-32,x86-64可兼容x86)。
#获取所有信息
readelf -a elf-file
#获取段信息
readelf -l elf-file
#获取节信息
readelf -S elf-file
#获取动态符号
readelf -s elf-file
#获取反编译代码
objdump -S elf-file
#获取所有节的内容
objdump -s elf-file
#获取所有动态符号
objdump -T elf-file
#更过命令,可查看readelf和objdump帮助文档
解析elf文件的C++控制台程序链接:https://download.csdn.net/download/Denny_Chen_/12913233