0%

iOS 函数调用栈那些事

程序员的自我修养–链接、装载与库10.2 栈与调用惯例 里面讲到了堆栈帧这块的内容,联想到同事面试时说到捕获奔溃调用栈的问题,感觉挺有意思,于是记录一下。
本文主要讲解以下内容

  1. 栈帧是什么东西?
  2. Arm64 汇编基础,Arm64 栈帧
  3. 栈帧回溯怎么玩?怎么符号化?常见的三方库又是怎么玩的?

程序员的自我修养–链接、装载与库》是这么描述栈的:

栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今能够看见的所有的计算机语言。
在经典的计算机科学中,栈被定义为一个特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO),多多少少像叠成一叠的书:先叠上去的书在最下面,因此要最后才能取出。
在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使栈减小。

栈帧

栈保存了函数调用所需的信息,而这些信息称为堆栈帧(Stack Frame)或活动记录(Activate Record),包括以下内容:

  • 函数的返回地址和参数
  • 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量
  • 保存的上下文:包括在函数调用前后需要保持不变的寄存器

栈帧

上图是 i386 中一个函数的栈帧,根据 ebp(Frame Pointer, 帧寄存器) 和 esp(Stack Pointer, 栈寄存器) 这两寄存器可以划分一个函数的堆栈。

  1. ebp 是固定的,始终指向栈底(高地址)
  2. esp 始终指向栈顶(低地址),它是动态变化的
  3. 可以通过 ebp esp 和获取栈帧中的数据,比如,ebp+4 获取函数的返回地址

调用惯例

调用惯例:函数的调用方和被调用方对于函数如何调用须要有一个明确的约定。就像两个人沟通一样的,只有使用相同的语言(普通话)才能进行友好的沟通。
而调用惯例包括以下几方面的内容:

  1. 函数参数的传递顺序和方式
    1. 函数参数的传递有很多种方式,最常见的一种是通过栈传递。函数的调用方将参数压入栈中,函数自己再从栈中将参数取出。对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序:是从左至右,还是从右至左。有些调用惯例还允许使用寄存器传递参数,以提高性能。
  2. 栈的维护方式
    1. 在函数将参数压栈之后,函数体会被调用,此后需要将被压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由函数的调用方来完成,也可以由函数本身来完成。 // PS: 如果不进行栈回退 pop 操作的话,函数调用会将程序的栈空间用完,从而出现经典的 stackoverflow 错误
  3. 名字修饰(Name-mangling)的策略
    1. 为了链接的时候对调用惯例进行区分,调用管理要对函数本身的名字进行修饰。不同的调用惯例有不同的名字修饰策略。

Arm64

有了上面书本基础知识的加持,现在到 Arm64 架构下实践一波吧。

汇编基础

寄存器

寄存器(Register)是中央处理器内用来暂存指令、数据和地址的电脑存储器。 它就是 CPU 内部用来存储数据的存储器件,容量小、访问速度快。

而 Arm64 有 31 个通用寄存器,相关用途如下。

The 64-bit ARM (AArch64) calling convention allocates the 31 general-purpose registers as:
x31 (SP): Stack pointer or a zero register, depending on context.
x30 (LR): Procedure link register, used to return from subroutines.
x29 (FP): Frame pointer.
x19 to x29: Callee-saved.
x18 (PR): Platform register. Used for some operating-system-specific special purpose, or an additional caller-saved register.
x16 (IP0) and x17 (IP1): Intra-Procedure-call scratch registers.
x9 to x15: Local variables, caller saved.
x8 (XR): Indirect return value address.
x0 to x7: Argument values passed to and results returned from a subroutine.

摘自 Calling_convention#ARM_(A64)


总结下常用的:

  1. x0-x7: 用来存放函数调用的参数和函数返回值(x0),更多参数可以使用堆栈来存放
  2. x29 (FP): 上图 i386 中的 ebp, 它里面存储的是上一个函数(该函数的调用方 caller) ebp 的地址
  3. x30 (LR): 存储函数的返回地址
  4. SP: 上图 i386 中的 esp, 指向栈顶
    1. 现在执行 lldb 命令register read x31 会报错:error: Invalid register name 'x31'.
  5. PC: 记录 CPU 当前执行的是哪条指令

指令

一些常见的指令,内容摘自 10分钟入门arm64汇编,具体的可以参加 ARM 操作手册。

1
2
3
4
5
6
7
8
9
10
11
add x0,x0,#1            ;x0 <==x0+1 ,把x0的内容加1。
add x0,x0,#0x30 ;x0 <==x0+0x30,把x0的内容加 0x30。
add x0,x1,x3 ;x0 <==x1+x3, 把x1的内容加上x3的内容放入x0
add x0,x1,x3,lsl #3 ;x0 <==x0+x3*8 ,x3的值左移3位就是乘以8,结果与x1的值相, 放入x0.
add x0,x1,[x2] ;x0 <==x1+[x2], 把x1的内容加上x2的内容作为地址取内存内容放入x0
ldr x0,[x1] ;x0 <==[x1], 把x1的内容作为地址取内存内容放入x0
str x0,[x1] ;[x1] <== x0, 把x0的内容放入x1的内容作为地址的内存中
ldr x0,[x1,#4] ;x0 <==[x1+4], 把x1的内容加上4, 作为内存地址, 取其内容放入x0
ldr x0,[x1,#4]! ;x0 <==[x1+4]、 x1<==x1+4, 把x1的内容加上4, 作为内存地址, 取其内容放入x0, 然后把x1的内容加上4放入x1
ldr x0,[x1],#4 ;x0 <==[x1] 、x1 <==x1+4, 把x1的内容作为内存地址取内存内容放入x0, 并把x1的内容加上4放入x1
ldr x0,[x1,x2] ;x0 <==[x1+x2], 把x1和x2的内容相加, 作为内存地址取内存内容放入x0

栈帧

aapcs64-variadic-stack
跟《程序员的自我修养–链接、装载与库》里面的栈帧差不多,图片来自 Procedure Call Standard for the Arm® 64-bit Architecture (AArch64)

Demo

用一个 Demo 将上面的内容串联起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (int)test1:(int)a {
int res = [self test2:a b:2];
return res;
}

- (int)test2:(int)a b:(int)b {
int res = a + b;
return res;
}

- (void)viewDidLoad {
[super viewDidLoad];

int res = [self test1:1];
NSLog(@"res: %d", res);
}

在函数调用方 test1 和被调方 test2 的第一行代码处下断点,并用汇编查看。

caller - test1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Demo_fishhook`-[ViewController test1:]:
0x102067294 <+0>: sub sp, sp, #0x30 ; sp = sp - 0x30(48), 开辟栈空间
0x102067298 <+4>: stp x29, x30, [sp, #0x20] ; 将 x29(fp) 里面的内容存入 sp+0x20(32)的位置,占 8B(x29 共 64bit, 8B) 长度;将 x30(lr) 的内容存入 sp+0x20+8=sp+40 的位置,占 8B
0x10206729c <+8>: add x29, sp, #0x20 ; x29 = sp + 0x20
0x1020672a0 <+12>: stur x0, [x29, #-0x8] ; 将 x0 的内容,x29-0x8 的位置
0x1020672a4 <+16>: str x1, [sp, #0x10] ; 将 x1 的内容放入 sp+0x10 的位置
0x1020672a8 <+20>: str w2, [sp, #0xc] ; w2 是 x2 的低32位,占 4B
-> 0x1020672ac <+24>: ldur x0, [x29, #-0x8] ; 数据读取,就是将上面的数据再读出来
0x1020672b0 <+28>: ldr w2, [sp, #0xc] ; 数据读取
0x1020672b4 <+32>: adrp x8, 87
0x1020672b8 <+36>: ldr x1, [x8, #0xb80] ; 数据读取放入 x1 寄存器中
0x1020672bc <+40>: mov w3, #0x2 ; x3 的低32位存的值是 0x2
0x1020672c0 <+44>: bl 0x102d2f0e4 ; symbol stub for: objc_msgSend ; 函数跳转
0x1020672c4 <+48>: str w0, [sp, #0x8] ; 存储 test2 的结果
0x1020672c8 <+52>: ldr w0, [sp, #0x8] ; 读取 w0
0x1020672cc <+56>: ldp x29, x30, [sp, #0x20] ; 将 [sp, #0x20] 的内容放入 x29 x30 寄存器
0x1020672d0 <+60>: add sp, sp, #0x30 ; sp = sp + 0x30
0x1020672d4 <+64>: ret

这里大概做了下面几件事

  1. 开辟栈空间,存储 x29 x30 的值到栈,因为后面会修改 x29 x30 的值
  2. 给 x29 赋值,即固定当前函数 x29 的位置
  3. 存储和读取 x0 x1 w2test2:b: 函数入参的值
  4. 给 x3 赋值为 2, 相当于 test2:b: 函数入参 b 的值
  5. bl 调用 test2:b: 函数
  6. 从栈中取出 x29 x30 的值,回退栈空间
  7. ret: 函数返回到 x30 所指向的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
(lldb) register read
General Purpose Registers:
x0 = 0x0000000102709160
x1 = 0x00000001020ab733 "test1:"
x2 = 0x0000000000000001
x3 = 0x000000016dd9dbf0
x4 = 0x0000000000000010
x5 = 0x0000000000000020
x6 = 0x000000016dd9d8f0
x7 = 0x0000000000000000
x8 = 0x00000001020be000 (void *)0x00000001020ab182: initWithCodeType:baseAddress:size:name:uuid:.hex + 7651
x9 = 0x0000000000000000
x10 = 0x000000000000005d
x11 = 0x00000001030221d8
x12 = 0x000000000000005d
x13 = 0x0000000000000000
x14 = 0x0000000180964000
x15 = 0x000000020b12c000
x16 = 0x00000001020bf9b2 (void *)0xe12800000001020b
x17 = 0x0000000102067294 Demo_fishhook`-[ViewController test1:] at ViewController.m:144
x18 = 0x0000000000000000
x19 = 0x0000000102709160
x20 = 0x0000000000000000
x21 = 0x00000001f59e3000 UIKitCore`_UIInternalPreference_IdleSchedulerTargetDeadlineFraction
x22 = 0x000000019b10ec13
x23 = 0x000000019b7043f5
x24 = 0x0000000000000000
x25 = 0x00000001f630c000 UIKitCore`_UIPreviewPresentationAnimator._startMediaTime
x26 = 0x00000001027083f0
x27 = 0x000000019bb48a24
x28 = 0x00000001fab5f4e8 CoreFoundation`__NSArray0__struct
fp = 0x000000016dd9da20
lr = 0x000000010206718c Demo_fishhook`-[ViewController viewDidLoad] + 76 at ViewController.m:84:9
sp = 0x000000016dd9da00
pc = 0x00000001020672ac Demo_fishhook`-[ViewController test1:] + 24 at ViewController.m:145:16
cpsr = 0x40000000

现在来 debug 看下相关寄存器里面的值,分别看.

  • x0-x7, 存放参数和返回值
    • x0 和 x1 存储的是 OC 方法的前两个隐藏入参: self 和 _cmd.
      1
      2
      3
      4
      5
      (lldb) po 0x0000000102709160
      <ViewController: 0x102709160>

      (lldb) po (char *)0x00000001020ab733
      "test2:b:"
    • x2 = 0x0000000000000001 存储的值是 1, 主上面的汇编代码使用的 w2, 即 x2 低 32 位(即 000000001), 就是值 1
    • 断点指向到 0x1020672c0 处后,x3 = 0x0000000000000002,同 x2, 值 2
  • pc, 当前断点指向的地址

1
2
(lldb) p/x 0x000000016f351a20 - 0x000000016f351a00
(long) $13 = 0x0000000000000020

fp-sp=0x20, 因为上面开始开辟栈空间 0x30, 但存储 x29 和 x30 用了 0x10, 所以还剩 0x20 大小的空间可用。

callee - test2

1
2
3
4
5
6
7
8
9
10
11
12
13
Demo_fishhook`-[ViewController test2:b:]:
0x1020672d8 <+0>: sub sp, sp, #0x20
0x1020672dc <+4>: str x0, [sp, #0x18]
0x1020672e0 <+8>: str x1, [sp, #0x10]
0x1020672e4 <+12>: str w2, [sp, #0xc]
0x1020672e8 <+16>: str w3, [sp, #0x8]
-> 0x1020672ec <+20>: ldr w8, [sp, #0xc]
0x1020672f0 <+24>: ldr w9, [sp, #0x8]
0x1020672f4 <+28>: add w8, w8, w9
0x1020672f8 <+32>: str w8, [sp, #0x4]
0x1020672fc <+36>: ldr w0, [sp, #0x4]
0x102067300 <+40>: add sp, sp, #0x20
0x102067304 <+44>: ret
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
(lldb) register read
General Purpose Registers:
x0 = 0x0000000102709160
x1 = 0x00000001020ab740 "test2:b:"
x2 = 0x0000000000000001
x3 = 0x0000000000000002
x4 = 0x0000000000000010
x5 = 0x0000000000000020
x6 = 0x000000016dd9d8f0
x7 = 0x0000000000000000
x8 = 0x00000001020be000 (void *)0x00000001020ab182: initWithCodeType:baseAddress:size:name:uuid:.hex + 7651
x9 = 0x0000000000000000
x10 = 0x000000000000002e
x11 = 0x0000000103021ee8
x12 = 0x000000000000002e
x13 = 0x0000000000000000
x14 = 0x0000000180964000
x15 = 0x000000020b12c000
x16 = 0x00000001020bf9b2 (void *)0xe12800000001020b
x17 = 0x00000001020672d8 Demo_fishhook`-[ViewController test2:b:] at ViewController.m:149
x18 = 0x0000000000000000
x19 = 0x0000000102709160
x20 = 0x0000000000000000
x21 = 0x00000001f59e3000 UIKitCore`_UIInternalPreference_IdleSchedulerTargetDeadlineFraction
x22 = 0x000000019b10ec13
x23 = 0x000000019b7043f5
x24 = 0x0000000000000000
x25 = 0x00000001f630c000 UIKitCore`_UIPreviewPresentationAnimator._startMediaTime
x26 = 0x00000001027083f0
x27 = 0x000000019bb48a24
x28 = 0x00000001fab5f4e8 CoreFoundation`__NSArray0__struct
fp = 0x000000016dd9da20
lr = 0x00000001020672c4 Demo_fishhook`-[ViewController test1:] + 48 at ViewController.m:145:9
sp = 0x000000016dd9d9e0
pc = 0x00000001020672ec Demo_fishhook`-[ViewController test2:b:] + 20 at ViewController.m:151:15
cpsr = 0x40000000

(lldb)

继续来看寄存器的值

  • x0-x7, 存放参数和返回值
    • x0 x1 跟 test1 一样,它们两个的 x0 值都是 0x0000000102709160
    • x2 = 0x0000000000000001, 入参 a 的值为 1
    • x3 = 0x0000000000000002, 入参 b 的值为 2
    • 返回值,当断点在 0x102067304 的时候,变化的寄存器值如下,x0 就是返回值
      1
      2
      3
      4
      5
      x0 = 0x0000000100f0ac70
      x8 = 0x0000000100a26000 (void *)0x0000000100a13182: initWithCodeType:baseAddress:size:name:uuid:.hex + 7651
      x9 = 0x0000000000000000
      sp = 0x000000016f4359e0
      pc = 0x00000001009cf2ec Demo_fishhook`-[ViewController test2:b:] + 20 at ViewController.m:151:15
      sp 和 pc 变化是可以理解的,因为回退栈空间了;但 x8 和 x9 也发生变化就不知道原因了
  • fp = 0x000000016dd9da20, 跟 test1 的 fp 值相同
  • lr = 0x00000001020672c4, 就是 test1 处的 0x1020672c4(即函数跳转处的下一条指令)

总结

  1. lr 寄存器存储的是函数返回值,根据 lr 可以回溯到上个函数
  2. test2 没有存储 x29 x30 到栈中, 而 test1 则有存储;因为 test2 是叶子函数,它里面没有调用其他函数。函数调用会用到 bl, 而 bl 则会把下一条指令的地址存入 x30 寄存器,会改变 x30 的值,所以出于保护现场的目的需要提前保存 x30 的值。而这里没有函数调用,意味着不会改变 x30, 所以就没有存储 x30 的意义了。
  3. 关于 fp 也是一个有趣的点,分别在 viewDidLoad 和 test1 处下断点,见下图
  • viewDidLoad.fp = 0x000000016f705a60
  • test1.fp = 0x000000016f705a20
  • 由于 fp 是指针,那就看它所指向地址存储的数据(x 0x000000016f705a20):0x16f705a20: 60 5a 70 6f 01 00 00 00, 因为是小端,所有读取出来就是 0x016f705a60(viewDidLoad.fp),从而验证了该函数的 fp 指向上个函数的 fp
  • 0x16f705a8: 8c f1 6f 00 01 00 00 00 就是 0x01006ff18c (test1.lr),因为在 x29 x30 在 test1 里面就是连续存储的,见 stp x29, x30, [sp, #0x20]

获取函数调用栈

思路

根据前面的 Arm64/Demo/总结 可知,如果要获取函数调用栈的话,首先要找到当前函数的 pc 和 lr 指针。

  1. 获取当前函数的 pc, 得到当前函数的地址
  2. 根据 lr 得到上个函数的地址
  3. 循环进行 2 步,直到 lr 为空
  4. 拿到相关地址后符号化

具体实现

从易后难,先看符号化的逻辑。

符号化

看上面的 Arm64/Demo, 如果在 test2 获取的话,相关寄存器的值如下

  1. pc: 0x00000001020672ec
  2. lr: 0x00000001020672c4

而 test2 和 test1 的函数地址分别是

  1. 0x1020672d8
  2. 0x102067294

也就是说获取的地址值是大于实际地址值的,那这要怎么处理呢?带着这个问题,看下经典库 KSCrash 是怎么处理的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
bool ksdl_dladdr(const uintptr_t address, Dl_info* const info)
{
info->dli_fname = NULL;
info->dli_fbase = NULL;
info->dli_sname = NULL;
info->dli_saddr = NULL;
// 在哪个 image
const uint32_t idx = imageIndexContainingAddress(address);
if(idx == UINT_MAX)
{
return false;
}
const struct mach_header* header = _dyld_get_image_header(idx);
// ALSR 值
const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);

// 就是没有 ALSR 的 VMaddress
const uintptr_t addressWithSlide = address - imageVMAddrSlide;
// 虚拟基地址+ALSR值, 就是包含 ALSR 的虚拟内存地址, 其实就是 header, 验证结果见下面 segmentBase fishhook
const uintptr_t segmentBase = segmentBaseOfImageIndex(idx) + imageVMAddrSlide;
if(segmentBase == 0)
{
return false;
}

info->dli_fname = _dyld_get_image_name(idx);
info->dli_fbase = (void*)header;

// Find symbol tables and get whichever symbol is closest to the address.
const nlist_t* bestMatch = NULL;
uintptr_t bestDistance = ULONG_MAX;
uintptr_t cmdPtr = firstCmdAfterHeader(header);
if(cmdPtr == 0)
{
return false;
}
for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++)
{
const struct load_command* loadCmd = (struct load_command*)cmdPtr;
// 符号表查询
if(loadCmd->cmd == LC_SYMTAB)
{
const struct symtab_command* symtabCmd = (struct symtab_command*)cmdPtr;
const nlist_t* symbolTable = (nlist_t*)(segmentBase + symtabCmd->symoff);
const uintptr_t stringTable = segmentBase + symtabCmd->stroff;

for(uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++)
{
// If n_value is 0, the symbol refers to an external object.
if(symbolTable[iSym].n_value != 0)
{
// 符号表的值,是不包含 ASLR 值的,所以上面 addressWithSlide 是要减去 ASLR
uintptr_t symbolBase = symbolTable[iSym].n_value;
// 两种相减,找距离最近的
uintptr_t currentDistance = addressWithSlide - symbolBase;
/*
`(addressWithSlide >= symbolBase)` : 因为 symbolBase 是具体符号值;而 addressWithSlide 则是需要查找的地址值, 二者可能想到,所以是 >= symbolBase
(currentDistance <= bestDistance) : 寻找最匹配的,距离越近越好
*/
if((addressWithSlide >= symbolBase) &&
(currentDistance <= bestDistance))
{
// iSym 符号表的下标,跟 fishhook 里面的字符串计算是一样的道理
bestMatch = symbolTable + iSym;
bestDistance = currentDistance;
}
}
}
if(bestMatch != NULL)
{
// + imageVMAddrSlide(ASLR值),因为前面 addressWithSlide 有减去 imageVMAddrSlide ASLR 值
info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide);
if(bestMatch->n_desc == 16)
{
// This image has been stripped. The name is meaningless, and
// almost certainly resolves to "_mh_execute_header"
info->dli_sname = NULL;
}
else
{
// 过掉符号修饰的前面的 `_`。
info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
if(*info->dli_sname ==addressWithSlide - '_')
{
info->dli_sname++;
}
}
break;
}
}
// 继续匹配下一个符号
cmdPtr += loadCmd->cmdsize;
}

return true;
}

从上面代码可知,跟 fishhook 查找符号表原理是类似的,就是用 lr 的地址值到符号表里一一匹配,找到比 lr 地址值小并离 lr 地址值最近的那个符号就是该函数的符号。

获取线程信息

因为 KSCrash 这块的代码比较多,现在还没捋顺,于是就找到了《iOS 开发高手课》作者的 SMCallStack.m


大致逻辑(只针对 Arm64 架构)如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
typedef struct SMStackFrame {
const struct SMStackFrame *const previous;
const uintptr_t return_address;
} SMStackFrame;

NSString *smStackOfThread(thread_t thread) {
uintptr_t buffer[100];
int i = 0;
_STRUCT_MCONTEXT64 machineContext;
mach_msg_type_number_t state_count = ARM_THREAD_STATE64_COUNT;
kern_return_t kr = thread_get_state(thread, ARM_THREAD_STATE64, (thread_state_t)&machineContext.__ss, &state_count);
if (kr != KERN_SUCCESS) {
return [NSString stringWithFormat:@"Fail get thread: %u", thread];
}
const uintptr_t instructionAddress = machineContext.__ss.__pc;
buffer[i++] = instructionAddress;
uintptr_t linkRegisterPointer = machineContext.__ss.__lr;
if (linkRegisterPointer) {
buffer[i++] = linkRegisterPointer;
}
SMStackFrame stackFrame = {0};
const uintptr_t framePointer = machineContext.__ss.__fp;

vm_size_t bytesCopied = 0;
if (framePointer == 0 || vm_read_overwrite(mach_task_self(), (vm_address_t)(void *)framePointer, (vm_size_t)sizeof(stackFrame), (vm_address_t)&stackFrame, &bytesCopied) != KERN_SUCCESS) {
return @"Fail frame pointer";
}

bytesCopied = 0;

for (; ; i++) {
buffer[i] = stackFrame.return_address;
if (buffer[i] == 0 || stackFrame.previous == 0 || vm_read_overwrite(mach_task_self(), (vm_address_t)(void *)stackFrame.previous, (vm_size_t)sizeof(stackFrame), (vm_address_t)&stackFrame, &bytesCopied) != KERN_SUCCESS) {
break;
}
}
// xxxxx
return @"";
}

代码大致逻辑能看懂:根据系统函数获取 pc fp lr, 再循环获取 lr 加入数组。
但不知道为什么要这样写,只能找到根据大佬的代码去搜索相关资料,基本都是操作系统领域的相关知识。

_STRUCT_MCONTEXT64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define _STRUCT_MCONTEXT64      struct __darwin_mcontext64
_STRUCT_MCONTEXT64
{
_STRUCT_ARM_EXCEPTION_STATE64 __es;
_STRUCT_ARM_THREAD_STATE64 __ss;
_STRUCT_ARM_NEON_STATE64 __ns;
};

_STRUCT_ARM_THREAD_STATE64
{
__uint64_t __x[29]; /* General purpose registers x0-x28 */
__uint64_t __fp; /* Frame pointer x29 */
__uint64_t __lr; /* Link register x30 */
__uint64_t __sp; /* Stack pointer x31 */
__uint64_t __pc; /* Program counter */
__uint32_t __cpsr; /* Current program status register */
__uint32_t __pad; /* Same size for 32-bit or 64-bit clients */
};

获取线程寄存器相关信息。

thread_get_state

xnu 源码中找到了搜索到了蛛丝马迹,获取到 state 内容后使用相关寄存器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#if defined(__i386__)
i386_thread_state_t state = {};
thread_state_flavor_t flavor = x86_THREAD_STATE32;
mach_msg_type_number_t count = i386_THREAD_STATE_COUNT;
#elif defined(__x86_64__)
x86_thread_state64_t state = {};
thread_state_flavor_t flavor = x86_THREAD_STATE64;
mach_msg_type_number_t count = x86_THREAD_STATE64_COUNT;
#elif defined(__arm__)
arm_thread_state_t state = {};
thread_state_flavor_t flavor = ARM_THREAD_STATE;
mach_msg_type_number_t count = ARM_THREAD_STATE_COUNT;
#elif defined(__arm64__)
arm_thread_state64_t state = {};
thread_state_flavor_t flavor = ARM_THREAD_STATE64;
mach_msg_type_number_t count = ARM_THREAD_STATE64_COUNT;
#else
#error thread_get_register_pointer_values not defined for this architecture
#endif

kern_return_t ret = thread_get_state(thread, flavor, (thread_state_t)&state, &count);

// xxxx
if (sp) {
uintptr_t __sp = arm_thread_state64_get_sp(state);
if (__sp > 128) {
*sp = __sp - 128 /* redzone */;
} else {
*sp = 0;
}
}

push_register_value(arm_thread_state64_get_lr(state));

for (int i = 0; i < 29; i++) {
push_register_value(state.__x[i]);
}

vm_read_overwrite

vm_read_overwrite

The vm_read and vm_read_overwrite functions read a portion of a task’s virtual memory (they enable tasks to read other tasks’ memory). The vm_read function returns the data in a dynamically allocated array of bytes; the vm_read_overwrite function places the data into a caller-specified buffer (the data_in parameter).

上面的代码就是调用系统函数 vm_read_overwrite 从 fp 的位置开始读取内存,给 SMStackFrame 结构体赋值,从而得到 lr, 再根据 lr 递归调用获取整个函数的调用链。

扩展

ksdl_dladdr

从上面的调试可知,dladdr 就是 lldb 命令 image lookup -a 0x00xxx 的代码实现,二者功能是类似的。

NAME
dladdr – find the image containing a given address

上面是在终端使用 man dladdr 得到的内容。

有一个问题:为什么不使用系统的 dladdr 函数?而要自己实现 ksdl_dladdr 呢?
找到了相关问题 BSBacktraceLogger - 请问为什么不用系统提供的dladdr方法,而需要自己写一个fl_dladdr呢? #8,但作者没有给出有用的答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** async-safe version of dladdr.
*
* This method searches the dynamic loader for information about any image
* containing the specified address. It may not be entirely successful in
* finding information, in which case any fields it could not find will be set
* to NULL.
*
* Unlike dladdr(), this method does not make use of locks, and does not call
* async-unsafe functions.
*
* @param address The address to search for.
* @param info Gets filled out by this function.
* @return true if at least some information was found.
*/
bool ksdl_dladdr(const uintptr_t address, Dl_info* const info);

ksdl_dladdr 的声明可知,从侧面反应系统的 dladdr 是同步的,会使用锁,可能会导致耗时。

有兴趣的可以查看 dyld 里面关于 dladdr 的实现。

lldb backtrace thread

不管是 SMCallStack 还是 BSBacktraceLogger 输出结果跟 lldb bt 的结果还是有些差异的,可能跟着两个库代码很久没更新有关系,plcrashreporter 的输出结果最接近(因为不知道怎么根据 KSCrash 直接获取调用堆栈),代码如下:

1
2
3
4
5
6
PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
NSData *data = [crashReporter generateLiveReport];
PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter
withTextFormat:PLCrashReportTextFormatiOS];

所以,就想找到 lldb backtrace thread 的源码实现。
下载 lldb 工程,然后搜索 Backtrace thread 找到如下相关代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
lldb::ThreadSP
GetExtendedBacktraceThread (ConstString type);

// ----
uint32_t
SBThread::GetExtendedBacktraceOriginatingIndexID ()
{
ThreadSP thread_sp(m_opaque_sp->GetThreadSP());
if (thread_sp)
return thread_sp->GetExtendedBacktraceOriginatingIndexID();
return LLDB_INVALID_INDEX32;
}

// ----
typedef std::shared_ptr<lldb_private::Thread> ThreadSP;

坑爹,看到 lldb_private 就知道凉凉了,因为它是私有的,只在 llvm 官网找到了相关定义 lldb_private::Thread Class Reference

后面又想过是否可以通过查看 GDB 的源码来查看 backtrace thread 的源码实现,嗯,是个好想法!!!

objc_msgSend

我们知道,OC 的方法调用最终都会走 objc_msgSend 这个汇编实现的函数,那为什么它没有出现在调用堆栈里面呢?
可以猜测下答案:因为 objc_msgSend 不会使用栈空间。
下面来 debug 调试下,还是前面 viewDidLoad 调用 test1 的例子,分别对比在这两个函数汇编下寄存器的值。


call test1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Demo_fishhook`-[ViewController viewDidLoad]:
0x104e32f58 <+0>: sub sp, sp, #0x40
0x104e32f5c <+4>: stp x29, x30, [sp, #0x30]
0x104e32f60 <+8>: add x29, sp, #0x30
0x104e32f64 <+12>: stur x0, [x29, #-0x8]
0x104e32f68 <+16>: stur x1, [x29, #-0x10]
0x104e32f6c <+20>: ldur x8, [x29, #-0x8]
0x104e32f70 <+24>: add x0, sp, #0x10
0x104e32f74 <+28>: str x8, [sp, #0x10]
0x104e32f78 <+32>: adrp x8, 89
0x104e32f7c <+36>: ldr x8, [x8, #0x6c0]
0x104e32f80 <+40>: str x8, [sp, #0x18]
0x104e32f84 <+44>: adrp x8, 88
0x104e32f88 <+48>: ldr x1, [x8, #0xbe8]
0x104e32f8c <+52>: bl 0x104e730a4 ; symbol stub for: objc_msgSendSuper2
0x104e32f90 <+56>: ldur x0, [x29, #-0x8]
0x104e32f94 <+60>: adrp x8, 88
0x104e32f98 <+64>: ldr x1, [x8, #0xbf0]
0x104e32f9c <+68>: mov w2, #0x1
-> 0x104e32fa0 <+72>: bl 0x104e73098 ; symbol stub for: objc_msgSend
0x104e32fa4 <+76>: str w0, [sp, #0xc]
0x104e32fa8 <+80>: ldr w9, [sp, #0xc]
0x104e32fac <+84>: mov x8, x9
0x104e32fb0 <+88>: adrp x0, 81
0x104e32fb4 <+92>: add x0, x0, #0x440 ; @"res: %d"
0x104e32fb8 <+96>: mov x9, sp
0x104e32fbc <+100>: str x8, [x9]
0x104e32fc0 <+104>: bl 0x104e72720 ; symbol stub for: NSLog
0x104e32fc4 <+108>: ldp x29, x30, [sp, #0x30]
0x104e32fc8 <+112>: add sp, sp, #0x40
0x104e32fcc <+116>: ret
1
2
(lldb) register read
General Purpose Registers:

objc_msgSend

1
2
3
4
Demo_fishhook`objc_msgSend:
-> 0x104e73098 <+0>: nop
0x104e7309c <+4>: ldr x16, #0xd6e4 ; (void *)0x0000000198569ce0: objc_msgSend
0x104e730a0 <+8>: br x16
1
2
3
(lldb) si
(lldb) register read
General Purpose Registers:

对比发现两者变化的仅仅是 lrpc

1
2
3
4
5
6
7
// test1
lr = 0x0000000104e32f90 Demo_fishhook`-[ViewController viewDidLoad] + 56 at ViewController.m:84:16
pc = 0x0000000104e32fa0 Demo_fishhook`-[ViewController viewDidLoad] + 72 at ViewController.m:84:15

// objc_msgSend
lr = 0x0000000104e32fa4 Demo_fishhook`-[ViewController viewDidLoad] + 76 at ViewController.m:84:9
pc = 0x0000000104e73098 Demo_fishhook`symbol stub for: objc_msgSend

经过上面的输出可知,objc_msgSend 共用当前函数的栈空间,没有产生新的栈空间,所以就不会出现在函数调用栈里面了,所以,这是 objc_msgSend 用汇编实现的原因之一吗?

参考链接