本文为看雪论坛精华文章
看雪论坛作者ID:LeadroyaL
起因
平时讨论的函数调用栈结构
push ebp
mov ebp, esp
sub esp, 0x100
arm 的栈结构
.text:000233C0 PUSH.W {R4-R8,LR}
.text:000233C4 SUB SP, SP, #8
.text:000233C6 LDR R4, [SP,#0x20+arg_8]
.text:000233C8 MOV R5, R1
...
.text:00023460 MOV R0, R4
.text:00023462 ADD SP, SP, #8
.text:00023464 POP.W {R4-R8,PC}
.text:00023A94 PUSH.W {R4-R9,LR}
.text:00023A98 SUB SP, SP, #4
.text:00023A9A MOV R8, R1
.text:00023A9C MOV R5, R0
...
.text:00023B12 ADD SP, SP, #4
.text:00023B14 POP.W {R4-R9,PC}
.text:0003477C PUSH {R7,LR}
.text:0003477E MOV R7, SP
.text:00034780 SUB SP, SP, #0x28
.text:00034782 LDR R2, =(__stack_chk_guard_ptr - 0x34788)
...
.text:000347CE MOVS R0, #0
.text:000347D0 ADD SP, SP, #0x28
.text:000347D2 POP {R7,PC}
.text:000138C4 PUSH {R4,R5,R7,LR}
.text:000138C6 ADD R7, SP, #8
.text:000138C8 SUB SP, SP, #0x20
.text:000138CA LDR R4, =(__stack_chk_guard_ptr - 0x138D0)
...
.text:000138F4 ADDEQ SP, SP, #0x20
.text:000138F6 POPEQ {R4,R5,R7,PC}
0x9a8c <__cxa_end_cleanup_impl>: @0x14f28
Compact model index: 1
0x97 vsp = r7
0x41 vsp = vsp - 8
0x84 0x0b pop {r4, r5, r7, r14}
0xb0 finish
0xb0 finish
arm ehabi
看雪有篇不错的文档:原创andorid native栈回溯原理分析与思考。
-
使用 readelf -u 可以打印相关信息,也可以使用pyelftools里的 readelf.py -au 打印出来(而且这个功能是我写的)。
-
千万不要使用 llvm-readelf -u,因为它有 bug,只支持 .o 文件。
总结
ELF .ARM.exidx 和 .ARM.extab 的位置
~ readelf -S libnative-lib.so
There are 25 section headers, starting at offset 0x1a130:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .note.android.ide NOTE 00000134 000134 000098 00 A 0 0 4
[ 2] .note.gnu.build-i NOTE 000001cc 0001cc 000024 00 A 0 0 4
.......
[14] .ARM.exidx ARM_EXIDX 00013eb8 013eb8 000e80 08 AL 13 0 4
[15] .ARM.extab PROGBITS 00014d38 014d38 001014 00 A 0 0 4
readelf -l libnative-lib.so
Elf file type is DYN (Shared object file)
Entry point 0x0
There are 8 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x00000034 0x00000034 0x00100 0x00100 R 0x4
LOAD 0x000000 0x00000000 0x00000000 0x1802e 0x1802e R E 0x1000
LOAD 0x018570 0x00019570 0x00019570 0x01aa0 0x01cb9 RW 0x1000
DYNAMIC 0x019c94 0x0001ac94 0x0001ac94 0x00110 0x00110 RW 0x4
NOTE 0x000134 0x00000134 0x00000134 0x000bc 0x000bc R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
EXIDX 0x013eb8 0x00013eb8 0x00013eb8 0x00e80 0x00e80 R 0x4
GNU_RELRO 0x018570 0x00019570 0x00019570 0x01a90 0x01a90 RW 0x4
本文参考:
-
官方文档,发现看不懂的就去读文档: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栈回溯原理分析与思考 https://bbs.pediy.com/thread-216447.htm -
网上挺火的外国人的文档 《Stack frame unwinding on ARM》 (Ken Werner)(可以在《andorid native栈回溯原理分析与思考》的附件里下载到)
.ARM.exidx 结构
struct EHEntry {
uint32_t Offset;
uint32_t World1;
};
| 31----24 | 23----16 | 15-----8 | 7------0 |
| 0XXXXXXX | XXXXXXXX | XXXXXXXX | XXXXXXXX |
| 00000000 | 00000000 | 00000000 | 00000001 |
| 31----24 | 23----16 | 15-----8 | 7------0 |
| 10000000 | XXXXXXXX | YYYYYYYY | ZZZZZZZZ |
| 31----24 | 23----16 | 15-----8 | 7------0 |
| 0XXXXXXX | XXXXXXXX | XXXXXXXX | XXXXXXXX |
prel31 解码
static uint64_t PREL31(uint32_t Address, uint32_t Place) {
uint64_t Location = Address & 0x7fffffff;
if (Location & 0x04000000)
Location |= (uint64_t) ~0x7fffffff;
return Location + Place;
}
.ARM.extab 结构
| 31----24 | 23----16 | 15-----8 | 7------0 |
| 0XXXXXXX | XXXXXXXX | XXXXXXXX | XXXXXXXX |
0:inline compact model,X、Y、Z, 3 个 byte 表示字节码。
| 31----24 | 23----16 | 15-----8 | 7------0 |
| 10000000 | XXXXXXXX | YYYYYYYY | ZZZZZZZZ |
1或者2: [23:16] 表示 more_word(uint_8),表示剩余字节码个数,后面的都是字节码。
| 31----24 | 23----16 | 15-----8 | 7------0 |
| 10000001 | MOREWORD | ........ | ........ |
| 10000010 | MOREWORD | ........ | ........ |
字节码的反汇编
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
-
加了 has_ehabi_infos 和 get_ehabi_infos 两个 API,返回 List[EHABIInfo]。 -
添加 class EHABIInfo,提供 num_entry 和 get_entry(i) 两个 API,返回 EHABIEntry。 -
添加 class EHABIEntry 及其子类,描述每个 unwind 条目,描述函数偏移和字节码,也可以反汇编
pyelftools git:(master) scripts/readelf.py -au test/testfiles_for_unittests/arm_exidx_test.so | head -n 20
Unwind section '.ARM.exidx' at offset 0x639d8 contains 1933 entries
Entry 0:
Function offset 0x34610: @0x69544
Compact model index: 1
0x97 ; vsp = r7
0x41 ; vsp = vsp - 8
0x84 0x0d ; pop {r4, r6, r7, lr}
0xb0 ; finish
0xb0 ; finish
Entry 1:
Function offset 0x34640: @0x6bef4
Compact model index: 1
0x97 ; vsp = r7
0x41 ; vsp = vsp - 8
0x84 0x0b ; pop {r4, r5, r7, lr}
0xb0 ; finish
0xb0 ; finish
总结
IDA arm unwind plugin
1. 总目标,对当前断点进行栈回溯,得到 pc 序列,进一步可以得到 crash-log 一样的栈回溯展示;
2. 检查运行环境,需要是 ARM 架构,需要处于调试中的状态;
3. 将当前 pc 加入序列;
4. 初始化VRSStatus状态,将各个寄存器的值设置正确,为 unwind 做准备;
-
根据当前 pc 找到当前 ELF 头部的大概位置,解析头部的一些字节,使用 pyelftools 得到第一个 PT_LOAD -
.ARM.exidx 和 .ARM.extab 一定在第一个 PT_LAOD 里,使用 pyelftools 解析它们的数据 -
根据当前 pc、List[EHABIEntry],找到对应的 EHABIEntry -
解释执行对应的字节码,得到最终状态 -
最终状态的 LR 就是返回地址,判断 ARM 还是 THUMB,再决定是 -2 还是 -4,修正为上一条指令的地址 -
将计算好的 pc 加入序列
6. 使用 pc 序列,寻找对应的 module、funcion,计算相对偏移,构造为 List[Frame];
7. 绑定快捷键,画 GUI,抄的 https://github.com/ChiChou/IDA-ObjCExplorer 的代码。
总结
三个部分虽然是按照开发的时间顺序写的,但发生顺序其实是反的,这个流程拖得挺长:
2. 写插件,研究栈回溯,发现 ARM 的栈回溯跟人不一样
(参考 原创andorid native栈回溯原理分析与思考 )
3. pyelftools 不提供 arm ehabi 的解析,于是自己实现数据解析
(参考 https://github.com/llvm/llvm-project/blob/master/llvm/tools/llvm-readobj/ARMEHABIPrinter.h )
4. libunwind 接入成本太高,于是自己用 python 写字节码的解释执行和 unwind(参考 https://github.com/llvm/llvm-project/blob/master/libunwind/src/Unwind-EHABI.cpp )
看雪ID:LeadroyaL
https://bbs.pediy.com/user-735539.htm
*本文由看雪论坛 LeadroyaL 原创,转载请注明来自看雪社区。
推荐文章++++
求分享
求点赞
求在看
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论