38.1 前言
之前有做过一个通过截取内核信号,获取程序出错所在函数位置信息(如段错误),然后进行栈回溯的功能(之前的blog有写),那个虽然成功了,但仍有一些不合人意的地方。就是手动回溯结果显示的只是函数地址,如果要看是哪个函数,那还要用objdump或addrline工具用地址找到是哪个函数,比较麻烦。最近折腾了两天时间,终于搞定了根据地址自动获取函数名称的功能。
不管gdb还readelf或addrline工具,可以简单轻松的敲一下命令就可以把函数名及地址整齐地打印展现出来,那么它们是怎么实现的呢??这就是本blog将要讲述的。
38.2 ELF文件及数据结构
首先了解EFL可执行文件,因为我们要找的信息就在ELF文件的符号表中。ELF文件,其全称为Executable and Linking Format,中文语义就是可执行链接格式文件,属于一种文件的存储格式。其有四个部分构成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。如下图示。
相信c/c++软件工程师对程序的数据组织结构有一定的了解,如一个程序有.text段、.bss段、.data段等等,但这些只是属于程序文件中的一部分。一个可执行文件真实的数据组织远不止上述几个,其可能包含二十几个信息区甚至更多,如下图示,在一个elf可执行文件中,其包含了28个section,分别存放着不同的数据。
.bss | 未初始化的全局或局部变量 |
.comment | 可执行文件的版本及控制描述信息等 |
.dynamic | 程序动态链接信息及相关属性 |
.hash | 符号hash表 |
.shstrtab | 保存所有节的名称信息 |
.strtab | 保存程序中所有的字符串信息包括函数名 |
.symtab | 符号表 |
刚才讲到了,ELF头(ELF header)、程序头表(Program header table)、节头表(Section header table)和节(Section),下面就来看一下它们的数据结构是怎样的。
数据类型定义:
-
/* Type for a 16-bit quantity. */
-
typedef
uint16_t Elf32_Half;
-
-
/* Types for signed and unsigned 32-bit quantities. */
-
typedef
uint32_t Elf32_Word;
-
typedef
int32_t Elf32_Sword;
-
-
/* Type of addresses. */
-
typedef
uint32_t Elf32_Addr;
-
-
/* Type of file offsets. */
-
typedef
uint32_t Elf32_Off;
-
-
/* Type for section indices, which are 16-bit quantities. */
-
typedef
uint16_t Elf32_Section;
-
-
/* Type for version symbol information. */
-
typedef Elf32_Half Elf32_Versym;
ELF Header结构(52字节):
-
#define EI_NIDENT 16
-
typedef
struct {
-
unsigned
char e_ident[EI_NIDENT];
//Magic
-
Elf32_Half e_type;
//文件类型 2-可执行文件
-
Elf32_Half e_machine;
//机器类型 如arm
-
Elf32_Word e_version;
//文件版本
-
Elf32_Addr e_entry;
//程序入口地址
-
Elf32_Off e_phoff;
//程序头table偏移字节大小
-
Elf32_Off e_shoff;
//section偏移字节大小
-
Elf32_Word e_flags;
//processor-specific flags
-
Elf32_Half e_ehsize;
//elf文件头大小
-
Elf32_Half e_phentsize;
//程序头大小
-
Elf32_Half e_phnum;
//程序头数量
-
Elf32_Half e_shentsize;
//section头大小
-
Elf32_Half e_shnum;
//section数量
-
Elf32_Half e_shstrndx;
//字符串表索引节头位置
-
} Elf32_Ehdr;
Program Header结构(32字节):
-
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;
Section Header结构(40字节):
-
typedef
struct {
-
Elf32_Word sh_name;
//section名称索引(含义是在string table总的第几个字节数)
-
Elf32_Word sh_type;
//section类型
-
Elf32_Word sh_flags;
//
-
Elf32_Addr sh_addr;
//section的地址
-
Elf32_Off sh_offset;
//section偏移地址
-
Elf32_Word sh_size;
//大小
-
Elf32_Word sh_link;
-
Elf32_Word sh_info;
//节头信息
-
Elf32_Word sh_addralign;
-
Elf32_Word sh_entsize;
-
} Elf32_Shdr;
Section中的Symbol Table Entry结构(16字节):
-
typedef
struct {
-
Elf32_Word st_name;
//索引值,值为字符串符号表的偏移字节大小(如果是函数那就是函数名)
-
Elf32_Addr st_value;
//符号地址,如果是函数那就是函数地址
-
Elf32_Word st_size;
//
-
unsigned
char st_info;
//符号相关信息,如是否为函数、全局类型等等
-
unsigned
char st_other;
-
Elf32_Half st_shndx;
-
} Elf32_Sym;
-
-
//注:info转化方式如下。
-
#define ELF32_ST_BIND(i) ((info)>>4)
-
#define ELF32_ST_TYPE(i) ((info)&0xf)
-
#define ELF32_ST_INFO(b,t) (((b)<<4)+((t)&0xf))
38.3 根据地址寻找函数名称思路
对于一个elf文件,其文件内容及组织形式对于不同的文件可能不一样,但一样的是elf文件的文件头对于一种架构来说是一样的,因此若获取elf文件的符号信息等,需要从elf的文件头开始。
(1)据上图所示,节头表的起始地址为5388字节开始(即0x150C);
(2)节头大小40字节(即0x28),节头数量29,节头共大小为29*40=1160(即0x488);
(3)节头表数据位置为:0x150C – 0x1994(0x150C+0x488);
(4)由上表可知字符串表索引头:26,因此可以直接在29个节头中定位到字符串索引表头,并获取其中内容;
为什么在ELF头中将字符串索引节点给出,那是因为在节头数据结构中,其节section的名字并不是字符串,而只是一个索引值,所有的节名称统一存放在.shstrtab中,因此如果想知道这个section的名称的话,那就需要去找。
(5)根据名称匹配,可以知道那个section是.symtab符号表,然后拿到其起始地址和大小;
(6)拿到.symtab表起始地址和大小后,以Symbol Table Entry结构大小遍历全部数据。当查找的函数地址与Symbol Table Entry的value匹配时,此时找到对应函数的入口信息点,然后再根据Symbol Table Entry 的name索引值,到.strtab段找到函数名称,此时完成地址与函数名称的查找。
总结流程: