《程序员的自我修养–链接、装载与库 》 10.2 栈与调用惯例
里面讲到了堆栈帧这块的内容,联想到同事面试时说到捕获奔溃调用栈的问题,感觉挺有意思,于是记录一下。 本文主要讲解以下内容
栈帧是什么东西?
Arm64 汇编基础,Arm64 栈帧
栈帧回溯怎么玩?怎么符号化?常见的三方库又是怎么玩的?
栈 《程序员的自我修养–链接、装载与库 》是这么描述栈的:
栈(stack)是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今能够看见的所有的计算机语言。 在经典的计算机科学中,栈被定义为一个特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入栈中的数据弹出(出栈,pop),但栈这个容器必须遵守一条规则:先入栈的数据后出栈(First In Last Out, FIFO),多多少少像叠成一叠的书:先叠上去的书在最下面,因此要最后才能取出。 在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使栈减小。
栈帧 栈保存了函数调用所需的信息,而这些信息称为堆栈帧(Stack Frame)或活动记录(Activate Record),包括以下内容:
函数的返回地址和参数
临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量
保存的上下文:包括在函数调用前后需要保持不变的寄存器
上图是 i386 中一个函数的栈帧,根据 ebp(Frame Pointer, 帧寄存器) 和 esp(Stack Pointer, 栈寄存器) 这两寄存器可以划分一个函数的堆栈。
ebp 是固定的,始终指向栈底(高地址)
esp 始终指向栈顶(低地址),它是动态变化的
可以通过 ebp esp 和获取栈帧中的数据,比如,ebp+4 获取函数的返回地址
调用惯例 调用惯例:函数的调用方和被调用方对于函数如何调用须要有一个明确的约定。就像两个人沟通一样的,只有使用相同的语言(普通话)才能进行友好的沟通。 而调用惯例包括以下几方面的内容:
函数参数的传递顺序和方式
函数参数的传递有很多种方式,最常见的一种是通过栈传递。函数的调用方将参数压入栈中,函数自己再从栈中将参数取出。对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序:是从左至右,还是从右至左。有些调用惯例还允许使用寄存器传递参数,以提高性能。
栈的维护方式
在函数将参数压栈之后,函数体会被调用,此后需要将被压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由函数的调用方来完成,也可以由函数本身来完成。 // PS: 如果不进行栈回退 pop 操作的话,函数调用会将程序的栈空间用完,从而出现经典的 stackoverflow 错误
名字修饰(Name-mangling)的策略
为了链接的时候对调用惯例进行区分,调用管理要对函数本身的名字进行修饰。不同的调用惯例有不同的名字修饰策略。
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) 。
总结下常用的:
x0-x7: 用来存放函数调用的参数和函数返回值(x0),更多参数可以使用堆栈来存放
x29 (FP): 上图 i386 中的 ebp, 它里面存储的是上一个函数(该函数的调用方 caller) ebp 的地址
x30 (LR): 存储函数的返回地址
SP: 上图 i386 中的 esp, 指向栈顶
现在执行 lldb 命令register read x31
会报错:error: Invalid register name 'x31'.
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
栈帧 跟《程序员的自我修养–链接、装载与库 》里面的栈帧差不多,图片来自 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
这里大概做了下面几件事
开辟栈空间,存储 x29 x30 的值到栈,因为后面会修改 x29 x30 的值
给 x29 赋值,即固定当前函数 x29 的位置
存储和读取 x0 x1 w2test2:b:
函数入参的值
给 x3 赋值为 2, 相当于 test2:b:
函数入参 b 的值
bl
调用 test2:b:
函数
从栈中取出 x29 x30 的值,回退栈空间
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(即函数跳转处的下一条指令)
总结
lr 寄存器存储的是函数返回值,根据 lr 可以回溯到上个函数
test2 没有存储 x29 x30 到栈中, 而 test1 则有存储;因为 test2 是叶子函数
,它里面没有调用其他函数。函数调用会用到 bl, 而 bl 则会把下一条指令的地址存入 x30 寄存器,会改变 x30 的值,所以出于保护现场的目的需要提前保存 x30 的值。而这里没有函数调用,意味着不会改变 x30, 所以就没有存储 x30 的意义了。
关于 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 指针。
获取当前函数的 pc, 得到当前函数的地址
根据 lr 得到上个函数的地址
循环进行 2 步,直到 lr 为空
拿到相关地址后符号化
具体实现 从易后难,先看符号化的逻辑。
符号化 看上面的 Arm64/Demo
, 如果在 test2 获取的话,相关寄存器的值如下
pc: 0x00000001020672ec
lr: 0x00000001020672c4
而 test2 和 test1 的函数地址分别是
0x1020672d8
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:
对比发现两者变化的仅仅是 lr
和 pc
。
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
用汇编实现的原因之一吗?
参考链接