代码仓: https://codechina.youkuaiyun.com/fu851523125/arm_unwind_backtrace.git
参考文章:arm上backtrace的分析与实现原理_流风回雪的博客-优快云博客
2.2 unwind
对于APCS来说,优点是分析起来比较简单,跟踪起来也可以很容易。缺点就是指令过多,栈消耗大,占用的寄存器也过多,比如每次调用 都必须将r11,r12,lr,pc入栈。为了解决这个问题,提出了第二种方案:
使用unwind就能避免这些问题,生产指令的效率要有用的多。unwind是最新的编译器(>gcc-4.5)为arm支持的新特性。它的原理是记录每个函数的入栈指令(一般比APCS的入栈要少的多)到特殊的段.ARM.unwind_idx .ARM.unwind_tab。
所以如果我们要使用unwind,就必须在链接文件中定义这个段
.ARM.exidx : {
__exidx_start = .;
*(.ARM.exidx* .gnu.linkonce.armexidx.*)
__exidx_end = .;
}
我们也可以通过arm-none-eabi-readelf -u xxxxx.elf查看其内容。
以上面两个为例,set_date的函数的地址是0xc007c4a0,而set_time的函数的地址是0xc00a0fb0。
而r11也就是fp地址在unwind_tab段中,也就是位于0xc00a0fa4地址处。
回溯时根据pc值到段中得到对应的编码,解析这些编码计算出lr在栈中的位置,进而计算得到调用者的执行地址。
一般来说,我们使用unwind优势比使用apcs更好,因为采用apcs时,会产生更多的代码指令,对性能有影响,但是使用unwind方式只会产生一个额外的段空间,并不会影响性能,所以大多数情况下,使用unwind更加有利。
unwind回溯的过程可以总结为三部分:
1.根据pc找到函数unwind的段内存地址
2.根据unwind段中信息找到指令相关的编码数据
3.根据入栈地址,分析函数上一级的栈底保存的sp和lr。
03
函数符号表
栈回溯的过程中,往往需要符号表来进行操作,此时需要开启-mpoke-function-name这个编译选项。
使用这个选项编译出的二进制程序中可以包含 C 语言函数名称的信息,以方便函数调用链回溯时记录信息的可读性。
比如在Linux中,系统死机后,可以打印出栈的地址和函数的名称,根据这个进行回溯操作就可以进行使用了。
基本原理就是加上-mpoke-function-name后,在每段代码段后面,都会附加一个函数的符号,我们需要使用的时候,就根据函数的pc指针,然后找到相关的偏移量,之后将这个代码段的符号获取到了。
参考文章: [原创] ARM栈回溯——从理论到实践,开发ida-arm-unwind-plugin-Android安全-看雪论坛-安全社区|安全招聘|bbs.pediy.com
.ARM.exidx 结构
这个 section 连续存放Entry,视为一个Entry数组。先要处理大小端问题,处理好后,每个 Entry 由两个 uint16 组成,相当于如下 struct:
1
2
3
4
struct EHEntry {
uint32_t Offset;
uint32_t World1;
};
EHEntry.Offset
意义是函数起始偏移。最高 bit 一定是 0,结合当前偏移(当前 pc )进行使用prel31
解码,得到 uint64_t。
格式为:
1
2
|
31
-
-
-
-
24
|
23
-
-
-
-
16
|
15
-
-
-
-
-
8
|
7
-
-
-
-
-
-
0
|
|
0XXXXXXX
| XXXXXXXX | XXXXXXXX | XXXXXXXX |
EHEntry.Word1
有三种情况:
EHEntry.Word1 == 1
,表示 CannotUnwind
1
|
00000000
|
00000000
|
00000000
|
00000001
|
最高 bit 为 1,则
[31:24]
必须为0b10000000
(其实是 personality 为 0,属于 inline compact model),余下 X、Y、Z, 3 个 byte 表示字节码。
1
2