ARM的栈回溯(二):ELF 文件里的 arm ehabi,使用 pyelftools 进行解析
本系列文章共三篇。本文是第二篇,讲 ELF 文件如何存放和使用 arm ehabi。关键词:.arm.exidx,.arm.extab。
ELF .ARM.exidx 和 .ARM.extab 的位置
Section角度:很久以前,readelf -S
时候一直不理解这两个 section 是做什么的,占空间,放的不是汇编,IDA 打开,里面也是一团意义不明的 data,总觉得没什么用。
1 | ~ readelf -S libnative-lib.so |
Segment角度:readelf -l
时候,EXIDX
就是 .ARM.exidx
,感觉还是有点用的,但仍然意义不明;不能直接确认 .ARM.extab
的位置。
1 | readelf -l libnative-lib.so |
好了不废话了,本文只针对 shared_library
和 executable
的 ehabi 解析,不支持 relocatable
;因为 relocatable
拥有大量的 .ARM.exidx
section 和重定位,有点复杂。
本文参考:
- 官方文档,发现看不懂的就去读文档:https://developer.arm.com/documentation/ihi0038/b/
- llvm-readelf 的实现:https://github.com/llvm/llvm-project/blob/master/llvm/tools/llvm-readobj/ARMEHABIPrinter.h
- binutils-readelf 的实现 https://github.com/bminor/binutils-gdb/blob/master/binutils/readelf.c
- 看雪有篇不错的文档:原创andorid native栈回溯原理分析与思考
- 网上挺火的外国人的文档 《Stack frame unwinding on ARM》 (Ken Werner)(可以在《andorid native栈回溯原理分析与思考》的附件里下载到)
.ARM.exidx 结构
这个 section 连续存放Entry,视为一个Entry数组。先要处理大小端问题,处理好后,每个 Entry 由两个 uint16 组成,相当于如下 struct:
1 | struct EHEntry { |
EHEntry.Offset
意义是函数起始偏移。最高 bit 一定是 0,结合当前偏移(当前 pc )进行使用 prel31
解码,得到 uint64_t。
格式为:
1 | | 31----24 | 23----16 | 15-----8 | 7------0 | |
EHEntry.Word1
有三种情况:
EHEntry.Word1 == 1
,表示 CannotUnwind1
| 00000000 | 00000000 | 00000000 | 00000001 |
最高 bit 为 1,则
[31:24]
必须为0b10000000
(其实是 personality 为 0,属于 inline compact model),余下 X、Y、Z, 3 个 byte 表示字节码。1
2| 31----24 | 23----16 | 15-----8 | 7------0 |
| 10000000 | XXXXXXXX | YYYYYYYY | ZZZZZZZZ |最高 bit 为 0,则整个 Word1 使用
prel31
解码,得到 uint64_t,指向真正的数据。必然会落在.ARM.extab
里。1
2| 31----24 | 23----16 | 15-----8 | 7------0 |
| 0XXXXXXX | XXXXXXXX | XXXXXXXX | XXXXXXXX |
prel31 解码
这个东西就是个计算方式,根据当前的绝对偏移(uint32),与当前的内容(uint32)进行运算,求出绝对偏移(uint64)。
对于 ELF 文件,我们可以假想它基址为 0,从而实现解析;
对于内存中的 ELF 片段,可以通过这个计算,根据当前位置寻找到附近的另一个位置,从而避免重定位;
下图代码中,Address 表示当前内容,Place 表示绝对偏移。
1 | static uint64_t PREL31(uint32_t Address, uint32_t Place) { |
.ARM.extab 结构
.ARM.extab
作为 .ARM.exidx
的附属存在,存放数据,但无法直接找到每段数据的入口。入口需要借助上文 Entry.Word1
,当 Entry.Word1
的最高 bit 为 0 时,prel31
解码后一定会指向 .ARM.extab
的内容,这就是入口。
名词解释:personality
——特性,可能没有中文概念。
先读出第一个 uint32_t
,进行初步解析,再根据情况进行进一步解析,有以下的情况:
最高 bit 为 0,表示
generic personality
,使用prel31
解码,使用指向的函数进行 unwind。1
2| 31----24 | 23----16 | 15-----8 | 7------0 |
| 0XXXXXXX | XXXXXXXX | XXXXXXXX | XXXXXXXX |最高 bit 为 1,表示
arm compact personality
,[31:28]
必须为0b1000
,[27:24]
有且仅有有0、1、2三种情况。0: inline compact model,X、Y、Z, 3 个 byte 表示字节码。
1
2| 31----24 | 23----16 | 15-----8 | 7------0 |
| 10000000 | XXXXXXXX | YYYYYYYY | ZZZZZZZZ |1或者2:
[23:16]
表示 more_word(uint_8),表示剩余字节码个数,后面的都是字节码1
2
3| 31----24 | 23----16 | 15-----8 | 7------0 |
| 10000001 | MOREWORD | ........ | ........ |
| 10000010 | MOREWORD | ........ | ........ |
字节码的反汇编
根据上文,我们可以得到每一处 Entry 及其 unwind方式。我们关心的是使用字节码进行 unwind (即 personality 为0、1、2)的情况,经过解析可以得到 uint8_t[N]
的字节码 ,解析方式在 "Table 4, ARM-defined frame-unwinding instructions" 文件章节。
参考 llvm-readelf
的实现,它的可读性最好,代码在 https://github.com/llvm/llvm-project/blob/master/llvm/tools/llvm-readobj/ARMEHABIPrinter.h ,其中有大量OpcodeDecoder::Decode_XXXXX
函数可以抄。
纯体力活,没什么好说的,官方表格如下:
Instruction | Explanation |
---|---|
00xxxxxx | vsp = vsp + (xxxxxx << 2) + 4. Covers range 0x04-0x100 inclusive |
01xxxxxx | vsp = vsp – (xxxxxx << 2) - 4. Covers range 0x04-0x100 inclusive |
10000000 00000000 | Refuse to unwind (for example, out of a cleanup) (see remark a) |
1000iiii iiiiiiii (i not a ll 0) | Pop up to 12 integer registers under masks {r15-r12}, {r11-r4} (see remark b) |
1001nnnn ( nnnn != 13,15) | Set vsp = r[nnnn] |
10011101 | Reserved as prefix for ARM register to register moves |
10011111 | Reserved as prefix for Intel Wireless MMX register to register moves |
10100nnn | Pop r4-r[4+nnn] |
10101nnn | Pop r4-r[4+nnn] , r14 |
10110000 | Finish (see remark c) |
10110001 00000000 | Spare (see remark f) |
10110001 0000iiii ( i not all 0) | Pop integer registers under mask {r3, r2, r1, r0} |
10110001 xxxxyyyy | Spare (xxxx != 0000) |
10110010 uleb128 | vsp = vsp + 0x204+ (uleb128 << 2) (for vsp increments of 0x104-0x200, use 00xxxxxx twice) |
10110011 sssscccc | Pop VFP double-precision registers D[ssss]-D[ssss+cccc] saved (as if) by FSTMFDX (see remark d) |
101101nn | Spare (was Pop FPA) |
10111nnn | Pop VFP double-precision registers D[8]-D[8+nnn] saved (as if) by FSTMFDX (seeremark d) |
11000nnn (nnn != 6,7) | Intel Wireless MMX pop wR[10]-wR[10+nnn] |
11000110 sssscccc | Intel Wireless MMX pop wR[ssss]-wR[ssss+cccc] (see remark e) |
11000111 00000000 | Spare |
11000111 0000iiii | Intel Wireless MMX pop wCGR registers under mask {wCGR3,2,1,0} |
11000111 xxxxyyyy | Spare (xxxx != 0000) |
11001000 sssscccc | Pop VFP double precision registers D[16+ssss]-D[16+ssss+cccc] saved (as if) by VPUSH (see remarks d,e) |
11001001 sssscccc | Pop VFP double precision registers D[ssss]-D[ssss+cccc] saved (as if) by VPUSH (see remark d) |
11001yyy | Spare (yyy != 000, 001) |
11010nnn | Pop VFP double-precision registers D[8]-D[8+nnn] saved (as if) by VPUSH (seeremark d) |
11xxxyyy | Spare (xxx != 000, 001, 010) |
实战:用 python 写一个 ehabi 的 parser
很遗憾,pyelftools 并未实现解析 arm ehabi 的功能,要有这个功能,我也懒得写本文了。。。
我为什么要写解析的功能?一方面因为 pyelftools 平时经常用,想为它做一些贡献;另一方面,我计划写一个 ida-arm-unwind 的插件,缺乏一个 python 库帮我完成解析,在 pyelftools 上补充功能是最合适的。
pull reqeust:https://github.com/eliben/pyelftools/pull/328
merge commit:https://github.com/eliben/pyelftools/commit/ee0facee32ae5fc91709c93f9a57a9a7683a3315
花了不少时间,写了将近 1000 行代码,实现得也比较优雅,大概有如下的功能:
- 加了
has_ehabi_infos
和get_ehabi_infos
两个 API,返回List[EHABIInfo]
。 - 添加
class EHABIInfo
,提供num_entry
和get_entry(i)
两个 API,返回EHABIEntry
。 - 添加
class EHABIEntry
及其子类,描述每个 unwind 条目,描述函数偏移和字节码,也可以反汇编
看一下效果:
1 | pyelftools git:(master) scripts/readelf.py -au test/testfiles_for_unittests/arm_exidx_test.so | head -n 20 |
总结
本文讲了 ELF 里 arm ehabi 的存放和使用。
第一篇指路:https://leadroyal.cn/p/1125
第二篇指路:https://leadroyal.cn/p/1131
第三篇指路:https://leadroyal.cn/p/1135