ARM的栈回溯(一):函数调用栈的简介

本系列文章共三篇。本文是第一篇,讲一些栈回溯的背景,不涉及技术细节。关键词:arm unwind,ehabi,arm stacktrace。

起因

故事要从几个月前的一个 arm crash 说起,把 crash 交给新来的小朋友看,他说 IDA 里显示的栈回溯和logcat里显示的栈回溯是不一致的,问我为什么。

我说一直是logcat里看的,是正确的;那么 ida 里只有一层栈回溯肯定是错的,但我却解释不来原因,于是有了本文和一系列研究。

平时讨论的函数调用栈结构

从小老师就教育我们,函数开头一般是这三句话,用于保存堆栈,开辟新的栈空间:

1
2
3
push ebp
mov ebp, esp
sub esp, 0x100

在这种设定下,栈回溯变得非常简单,ebp 就是栈帧,ebp 附近是上一个栈帧,再附近是返回地址。网上相关的文章一搜一大把,这里就不多讲了,找一张网图凑合一下。

arm 的栈结构

我们随便找个 /system/lib/libc.so,再随便编译一个 so,随便找几个函数看一下,发现和x86的不大一样。

1
2
3
4
5
6
7
8
.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}
1
2
3
4
5
6
7
.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}
1
2
3
4
5
6
7
8
.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}
1
2
3
4
5
6
7
.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}

观察这几组汇编,前两段 sp 的内容并没有被保存到任意一个寄存器里,但它可以被正确栈回溯,暗示栈回溯信息不在这段汇编里;后两段,把 sp 放到 r7 里,把 sp+8 放到 r7 里,有点像栈帧的感觉,并且函数内也没有覆盖掉 r7 的内容,有点 x86 的感觉。

查阅资料,随着时代发展,arm 有两种 unwind 方式:

  1. 一种是古老的,和 x86 类似的(目前没有找到样例,可能在某种编译选项下存在),使用专用的 fp 寄存器保存原先的 spfp 在函数内禁止被改写,thumb 模式下使用 r7 作为 fp,arm 模式下使用 r11 作为 fp
  2. 另一种是流行的, arm 特有的(目前绝大部分都使用这种方式),遵从 eabi 里的 ehabi 标准,即 exception handler abi,定义了一套专属的标准。简而言之就是对每个函数分配自己专用的字节码,解释执行,从而实现栈回溯。

使用readelf -u可以查看,字节码长这样:

1
2
3
4
5
6
7
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

讲了这么多,终于引出本系列的重点:arm ehabi。

官方文档,复杂但权威:https://developer.arm.com/documentation/ihi0038/b/
看雪有篇不错的文档:原创andorid native栈回溯原理分析与思考

  • 使用 readelf -u 可以打印相关信息,也可以使用pyelftools里的 readelf.py -au 打印出来(而且这个功能是我写的)。
  • 千万不要使用 llvm-readelf -u,因为它有 bug,只支持 .o 文件。

名词解释

名词 解释
stack unwind 意思就是栈回溯。
abi (application binary interface)二进制应用接口,相当于标准和规范
arm eabi arm 很多规范的合集,包括 AADWARF、AAELF等,也包括 CLIBABI、CPPABI、【EHABI】
arm ehabi arm 的 exception handler abi
exception handler 异常处理,既包括 crash 时的栈回溯,也包括 c++ 里的异常处理。
arm exception handler index table .ARM.exidx,存放函数offset、简单的handler 的数据、复杂handler的索引。
arm exception handler table .ARM.extab,存放复杂 handler 的数据。

和平时逆向相关的,有两部分内容,有个大致认知就行:

  1. 数据存放。在 ELF 文件里肯定存放了信息,指导如何进行栈回溯,.ARM.exidx.ARM.extab 就是做这件事情的;
  2. 数据使用。每个函数 unwind 时需要解析 exception handler table,需要解释执行字节码,这个功能有时会由操作系统完成(例如 crash 的时候),有时会由应用程序自己完成(例如使用写代码主动进行栈回溯)。

总结

本文讲的是背景,没什么技术细节,第二篇讲文件格式。

第一篇指路:https://leadroyal.cn/p/1125
第二篇指路:https://leadroyal.cn/p/1131
第三篇指路:https://leadroyal.cn/p/1135