Elf(可执行和可链接文件)是一个永远也绕不开的话题,只要我们还在使用安卓手机/linux服务器,我们就需要了解elf的一些方方面面,现在就让我们从一个常量值提取的小需求出发,逐步解析elf文件结构吧!
一、写作目的:
网络上关于elf文件结构描述的文章不在少数,但能具体到二进制分析的却屈指可数,总给人一种八股文的感觉,而最近恰好又遇到了一个需要通过符号表获取其表示的常量值的需求,在完成之后,我将实现的过程进行总结提炼写下这么一篇elf结构入门的文章供后续学习回顾。
二、需求:
在C++中存在许多常量赋值和使用的操作,现在我们获取到了一个由C++编译成的动态链接库.so文件,我们想要反推一下其中可能的符号及其所表示的值。
三、基础知识
①.Elf文件类型:
Elf文件类型分为三种,.o\.so\.exe,普通的.exe可执行文件相信大家并不陌生,这里主要介绍一下.o和.so文件。.o为可重定位的目标文件,.so为共享目标文件,两者的区别就是.o是静态的,.so是动态的,静态就是指它将被链接器在编译时合并到可执行文件中,而动态则是在可执行文件要使用它时才进行加载。除了用途不同,其文件结构和文件结构中各种数据类型都是相同的,elf文件中数据类型大致为图中几类有符号无符号1/2/4大小的地址偏移整数等(我们解析时只需记住char是1字节half是2字节其余4字节即可)。
②.Elf文件链接视图和执行视图
elf的文件结构就稍微复杂一些,其分为链接视图和执行视图两种视角,之所以叫视角,是因为如同人看待一个物体的不同角度,虽然看上去不一样,但本质都是同一个物体。这里的链接视图即指以链接器的角度来看elf文件,它关注的是elf文件的节区,即用头部节取表去定位各个节区然后进行链接,而执行视图则是以程序执行的角度来看elf文件,它关注的是如何使用程序头部表去定位各段然后加载到内存中去。其两种视图的对应关系也如上图所示,.text节对应代码段,.rodata/.data/.bbs等包含数据的节对应数据段,其余还有一些专门链接用的如动态符号表等,则不会被加载到内存中。
四、实现
在了解了上述的一些基础知识后,我们也知道要获取符号及其对应值,我们不能从执行视图出发,因为符号表可能都不会被程序头部表识别到,所以我们从链接视图出发,根据头部节区表定位数据节区和符号表节区,根据其索引关系完成匹配,具体实现过程如下所示。
接着,让我们一步一步梳理
①解析elf文件头部:
首先贴出elf文件头的结构定义
想必精通C/C++的各位大佬一定是一眼秒懂的,这里就不过多解释构造了,其ELF32的数据类型具体表示含义在上面已有展示,这里也不多说。这里关键数据有以下几点:
e_ident:十六字节数组
首先就是魔数了,看文件先看魔数,这里的16位比特的e_ident的前4位数据只能是0x7F454C46,转换成ascii码即0x7F ELF。然后依次表示进制(1为32位/2为64位)、大小端(1为LSB/2为MSB)、版本信息(1为当前版本)、运行所在系统(0为UNIX/3为Linux...)、操作系统ABI、7位填充数据。
e_type:两字节目标文件类型
1表示可重定位文件、2表示可执行文件、3表示共享对象文件、4表示核心转储文件
依照这个规律解读上图所示例子,即这是一个32位/LSB/当前版本/运行在UNIX上的.so文件。
在确定了文件类型之后,我们便可以依照上述的流程接着往下解析...
节区头部表格偏移、表项大小、表项数
根据elf头部结构我们可以轻松知道上面我们要的信息
e_shoff(32-35)\e_shentsize(46-47)\e_shnum(48-49)
该测试文件头部节区表偏移为2896(LSB)、表项大小为40、表项数为22
节区头部表名称字符串表索引
节区头部表中每一个表项所需使用的名称字符串,对应的字符串表,这个在解析节区头部表项时会使用到,此处为21(0X15)
验证
我们知道从链接视图来看,elf文件头部节区表结束后文件也就读取完了,故我们2896+40*22应该就是文件大小3776了(果真如此,看来上述分析工作全对)
②获取头部节区表:
在elf头部中,我们已经获取了头部节区表偏移、表项大小、表项数,现在我们就可以根据头部表项依次读取节区了,节区表表项结构如下图所示
其中我们需要关注的有以下几点:
表项序号(index)、节区名(sh_name)、节区类型(sh_type)、节区偏移(sh_offset)、节区长度(sh_size)、附加信息(sh_info)
表项序号:
加载时通过计数获得
节区偏移:
表项内第17-20个字节,表示文件内节区数据偏移
节区长度:
表项内第21-24个字节,表示文件内节区数据偏移
节区名:
节点区名为在对应(文件头部的字符串表索引)字符串表中的索引,再以\0结尾取得一个字符串。
节区类型:
位于单个表项的第5-8位比特,表示节区用途,常见的有:
SHT_PROGBITS(0x1):包含程序定义的数据,如代码、只读数据、可读写数据等。
SHT_SYMTAB(0x2):包含符号表信息,用于链接或调试。
SHT_STRTAB(0x3):包含字符串表,通常用于表示符号表或节区表中的名字。
...
SHT_DYNSYM(11):包含动态链接符号表,用于运行时的符号解析。
...
③获取节区字符串表
而上文文件头部中我们已经得到节区使用的字符串表项的索引为15,而节区表偏移为2896、表项大小为40,所以该字符串表表项的偏移为2896 + 21*40 = 3736
从字符串表的节区表项中我们可以得到其实际字符串表的偏移为2691(0xA83),长度为202(CA)
而一个表项的节区名即表项内第1-4个字节,为对应字符串表的内部索引,字符串表的节区名索引为178(0XB2),再根据\0结尾断句,即头部节区表对应字符串表名字为.shstrtab
(通过节区头部表项对应的字符串表.shstrtab我们也能够大致知道该elf文件中的成分信息了--如是否包含某些特定节区)
上述字符串表第5-8位为0x03000000,即表示它包含字符串表(其他节区也可能包含字符串表,但用法就不尽相同了)...
④获取符号表
(从上述节区名字符串表中我们可以得知存在动态符号表.dynsym,不存在静态符号表.symtab,所以在遍历节区表项的时候,我们不仅可以通过名称字符串".dynsym"也可以通过节区类型11/0x0B来定位动态符号表项)
根据符号表节区表项的信息我们可以知道符号表存放的具体位置及单个项目大小
sh_addr(节区在内存中位置):第13-16个字节,值为524(0x0C020000)
sh_offset(节区数据文件中偏移):第17-20个字节,值为524(0x0C020000)
sh_size(节区长度):第21-24个字节,值为304(0x30010000)
sh_link(节区头部表索引):第25-28个字节,值为7
sh_entsize(节区中单个项目大小):第37-40个字节,值为16(0x10000000)
在上述符号表中,实际存储19个符号结构体(304 / 16)
单个符号项如上图所示,其中有这么几个值
符号名称:
st_name,第1-4个字节,为符号表中sh_link指向的字符串表中的索引,同样通过索引+\0结尾的方式获取该符号名称字符串。
符号值:
st_value,第5-8个字节,根据具体情况取得含义,例如符号表示函数时,该值为函数在内存中的起始地址,若该符号表示全局或静态变量时,表示内存在变量中的位置。
符号值值大小:
st_size,第9-12个字节,变量长度或者函数代码所占字节数
符号类型:
st_info,第13个字节,根据1个字节的八位比特作为flag标注符号的特征,高4位表示绑定属性(Binding),低4位表示符号类型(Type)
Type:
STT_OBJECT(1):数据对象,通常是变量
STT_FUNC(2):函数或其他可执行代码
...
Binding:
STB_LOCAL(0):局部符号,只在当前模块中可见
STB_GLOBAL(1):全局符号,在所有模块中可见
...
节区头部索引:
st_shndx,第15-16个字节,根据具体情况取得含义
⑤获取符号名称字符串表
首先我们要获取符号名,符号名即变量名/函数名...
根据节区头部符号表项中的sh_link值7,我们可以计算出对应字符串表的起始地址
2896+40*7
(根据节区表项中的节区类型为3,我们也可以笃定该节区就是我们要找的字符串节区)
读取节区信息:
名称:93(0x5D),加上名称符号表偏移2691,得到该名称字符串.dynstr
偏移:1180(0x9C04)
大小:341(0x0155)
以下即符号名字字符串表
⑥遍历符号
获取符号类型
遍历符号表(与4中图重复)每一个符号(4中已简述每个符号结构),获取其符号名和符号类型,st_info的低四位为1,则符号为OBJECT变量,若4size则可能为字符串指针
如这六个符号,其符号值大小为4,st_info(0x11)为00010001即全局的数据对象
获取符号值节区
在根据st_shndx值18(0x12),定位到符号值存储节区头部表项偏移2896 + 18*40 = 3616
名称值为77+2691即.ARM.attributes
内存中地址为14620(0x1C39)
文件偏移为2332(0x1C09)
节区长度为24(0x18)
获取符号值和对应常量
再结合上述六个符号(符号名对应字符串表已在上文给出),我们可以得到以下信息
(ad_value为通过计算st_value与上述内存偏移14620获得的符号变量值-地址)(注意:此处地址值仍需LSB转换)
(value为通过地址值偏移获取的变量对应的常量值)
st_name-1: 1426 = 1180 + 246(0xF6) -> "global_var2"
st_value-1: 14628(0x2439)
ad_value-1: 1831(0x2707)
Value: "测"
st_name-2: 1385 = 1180 + 205(0xCD) -> "a"
st_value-2: 14636(0x2C39)
ad_value-2: 0xFFFFFF7F
Value-2: 0x7FFFFFFF(INT_MAX)
st_name-3: 1438 = 1180 + 258(0x0201) -> "global_var3"
st_value-3: 14632(0x2839)
ad_value-3: 1743(0xCF06)
Value: "abc"
st_name-4: 1401 = 1180 + 221(0xDD) -> "b"
st_value-4: 14640(0x3039)
ad_value-4: 0xFFFFFF7F
Value-4: 0x7FFFFFFF(INT_MAX)
st_name-5: 1403 = 1180 + 223(0xDF) -> "global_var"
st_value-5: 14620(0x1C39)
ad_value-5: 1747(0xD306)
Value: "doGlobalVarTest测试"
st_name-6: 1414 = 1180 + 234(0xEA) -> "global_var1"
st_value-6: 14624(0x2039)
ad_value-6: 1652(0x7406)
Value: "测aaa"
四、总结
通过上述步骤,我们依托定位符号常量的需求,逐步分析了elf的文件架构。并根据以下测试程序我们实验了有哪些数据会在编译成.so文件后保留符号(全局非静态变量),以及如何获取其变量值