一、寄存器
要想了解调用栈的获取,需要先了解寄存器和 CPU 的关系(这里不展开介绍,可参考这篇文章)。只介绍用到的几个关键寄存器:
| 寄存器 | 别名 | 作用 |
|---|---|---|
| x29 | FP(帧指针) | 指向当前函数的栈帧起始位置 |
| x30 | LR(链接寄存器) | 保存函数返回地址 |
| SP | 栈指针 | 始终指向当前栈顶 |
| PC | 程序计数器 | 当前正在执行的指令地址 |
ARM64 的调用栈是"向下生长"的,即新的栈帧从低地址方向扩展。
二、函数调用时寄存器的变化
通过一段汇编代码来理解寄存器是如何被更新的:
// 调用者(Caller)
int caller(void) {
int ret = myFunction(); // 对应汇编:bl _myFunction
int x = 1; // bl 返回后执行此处
return x;
}
// 被调用者(Callee)
void myFunction(void) {
// 编译器自动插入函数序言(stp / mov)
// ... 函数体 ...
// 编译器自动插入函数结尾(ldp / ret)
}
执行 bl(Branch with Link)指令时发生以下事情:
; 调用者代码
0x100001000: bl _myFunction ; 调用函数,同时将下一条指令地址写入 LR
0x100001004: mov x0, #1 ; ← 返回地址(LR = 0x100001004)
; 被调用函数
_myFunction:
0x100002000: stp x29, x30, [sp, #-16]! ; ① sp -= 16;② [sp+0] = FP;③ [sp+8] = LR
0x100002004: mov x29, sp ; 更新 FP 指向新栈帧
; ... 函数体 ...
0x100002100: ldp x29, x30, [sp], #16 ; 恢复 FP 和 LR
0x100002104: ret ; 跳转回 LR 指向的地址
三条关键指令的作用:
bl:自动将下一条指令地址(0x100001004)写入 LR,然后跳转stp x29, x30, [sp, #-16]!:在栈上保存旧的 FP 和 LR,并下移 SPmov x29, sp:将 FP 更新为当前 SP,即新栈帧的起始位置
三、ARM64 栈帧结构
每个函数的栈帧头部固定为 16 字节,布局如下:
┌────────────────┐ ← FP + 8
│ 返回地址 (LR) │ 调用者调用本函数时的下一条指令地址
├────────────────┤ ← FP + 0(FP 寄存器指向此处)
│ 上一个 FP │ 调用者的帧指针(形成链表)
└────────────────┘
完整的多级调用栈布局(高地址 → 低地址):
High Address(栈底)
├─ main() 的栈帧
│ ┌────────────────┐
│ │ 0x100004000 │ ← [FP₀+8] main 的返回地址
│ ├────────────────┤
│ │ NULL │ ← FP₀(链表终点)
│ └────────────────┘
│
├─ funcA() 的栈帧
│ ┌────────────────┐
│ │ 0x100001234 │ ← [FP₁+8] 在 main 中调用 funcA 的位置
│ ├────────────────┤
│ │ FP₀ │ ← FP₁(指向 main 的栈帧)
│ └────────────────┘
│
├─ funcB() 的栈帧
│ ┌────────────────┐
│ │ 0x100002456 │ ← [FP₂+8] 在 funcA 中调用 funcB 的位置
│ ├────────────────┤
│ │ FP₁ │ ← FP₂(指向 funcA 的栈帧)
│ └────────────────┘
↓
Low Address(栈顶,当前执行位置)
四、栈回溯原理
两条核心信息链
| 信息链 | 作用 | 来源 |
|---|---|---|
| FP 链 | 遍历栈帧,向上追溯调用者 | [FP+0] → Previous FP |
| LR 链 | 定位每一层的调用位置 | [FP+8] → 返回地址 |
逐步回溯过程
假设调用链为:main() → funcA() → funcB() → funcC()
步骤 0:读取当前执行位置(第 0 帧)
从寄存器读取:
PC = 0x100003000 ← funcC 内部正在执行的位置
栈帧 #0: 0x100003000(funcC 内部)
步骤 1:回溯到 funcB(第 1 帧)
从寄存器读取:
FP = 0x16fdff0a0 ← funcC 的栈帧起始地址
读取内存 [0x16fdff0a0]:
┌─────────────────┐
│ 0x100002456 │ ← [FP+8] 返回地址(funcB 调用 funcC 后的位置)
├─────────────────┤
│ 0x16fdff0b0 │ ← [FP+0] Previous FP(funcB 的栈帧)
└─────────────────┘
栈帧 #1: 0x100002456(funcB 中调用 funcC 的位置)
下一个 FP = 0x16fdff0b0
步骤 2:回溯到 funcA(第 2 帧)
读取内存 [0x16fdff0b0]:
┌─────────────────┐
│ 0x100001234 │ ← [FP+8] 返回地址(funcA 调用 funcB 后的位置)
├─────────────────┤
│ 0x16fdff0c0 │ ← [FP+0] Previous FP(funcA 的栈帧)
└─────────────────┘
栈帧 #2: 0x100001234(funcA 中调用 funcB 的位置)
下一个 FP = 0x16fdff0c0
步骤 3:回溯到 main(第 3 帧)
读取内存 [0x16fdff0c0]:
┌─────────────────┐
│ 0x100000f00 │ ← [FP+8] 返回地址(main 调用 funcA 后的位置)
├─────────────────┤
│ 0x16fdff0d0 │ ← [FP+0] Previous FP(main 的栈帧)
└─────────────────┘
栈帧 #3: 0x100000f00(main 中调用 funcA 的位置)
下一个 FP = 0x16fdff0d0
步骤 4:到达栈底
读取内存 [0x16fdff0d0]:
┌─────────────────┐
│ 0x100004000 │ ← [FP+8] 系统入口点
├─────────────────┤
│ NULL │ ← [FP+0] Previous FP = NULL → 回溯结束
└─────────────────┘
五、代码实现
// 第一帧:从寄存器直接读取当前 PC
buffer[0] = threadState.__pc;
// 后续帧:遍历 FP 链表
uintptr_t currentFP = threadState.__fp;
while (index < maxSize && currentFP != 0) {
FrameEntry frame;
memcpy(&frame, (void *)currentFP, sizeof(frame));
// 栈帧结构(16 字节):
// [FP+0] frame.previous ← 上一个 FP,用于继续遍历
// [FP+8] frame.returnAddress ← 返回地址,即调用位置
if (frame.previous == NULL || frame.returnAddress == 0) {
break; // 到达栈底
}
buffer[index++] = frame.returnAddress; // 记录调用位置
currentFP = (uintptr_t)frame.previous; // 移动到上一个栈帧
}
六、地址符号化
回溯得到的是原始内存地址,需要通过 dladdr() 转换为可读的函数名:
// 回溯得到的地址数组:
// [0x100003000, 0x100002456, 0x100001234, 0x100000f00]
for (uintptr_t addr : addresses) {
Dl_info info;
dladdr((void *)addr, &info);
// info.dli_fname 镜像路径(如 "/path/to/MyApp")
// info.dli_sname 符号名(如 "funcC")
// info.dli_saddr 符号起始地址
printf("%s + %ld\n", info.dli_sname, addr - info.dli_saddr);
// ↑ 函数内的字节偏移量
}
偏移量的含义
funcA:
0x100001000: ...
0x100001230: bl funcB ; ← 调用 funcB
0x100001234: mov x0, #1 ; ← 返回地址(LR = 0x100001234)
回溯时看到的 0x100001234 是 bl funcB 的下一条指令地址,而非 funcA 的入口地址。
因此输出的 funcA + 52 表示:在 funcA 入口偏移 52 字节处,调用了下一个函数。
七、最终输出效果
📚 堆栈信息:
═══════════════════════════════════════
0 0x100003000 MyApp funcC + 0
1 0x100002456 MyApp funcB + 86
2 0x100001234 MyApp funcA + 52
3 0x100000f00 MyApp main + 240
4 0x100004000 dyld start + 0