如何获取跨系统调用的函数调用栈
在进行功能调试或者问题定位时,经常需要找一下哪里触发的系统调用,并跟踪一下系统调用过程。
一种方法是使用simpleperf :
浏览器打开https://profiler.firefox.com/将生成的add_client.perf.json.gz拖进去,就可以查看调用树、火焰图、栈图等进一步分析函数调用关系。
参考另一篇文章 尝试通过一个demo分析binder的执行流程 的使用simpleperf抓取通讯双方的函数调用栈小节
或者使用基于eBPF的工具比如stackplz 参考 :https://github.com/SeeFlowerX/stackplz
这两种方式在尝试定位某些问题时比较受限,比如kernel启动早期时。因此本文尝试一种更加直接的方式:直接在目标位置打印函数调用栈。
获取用户态和内核态的函数调用栈
首先确保
CONFIG_STACKTRACE CONFIG_KALLSYMS和CONFIG_USER_STACKTRACE_SUPPORT内核宏打开
可以通过 zcat /proc/config.gz | grep -E "STACKTRACE|KALLSYMS" 确认。
在代码中插入以下代码:
| |
可以在目标位置插入调用dump_all_stacks()。 抓内核日志。
解析用户态调用栈
准备了一个python脚本来解析用户态的堆栈,该脚本会计算堆栈中地址相对于符号文件的偏移,并用addr2line尝试解析符号。
| |
解析示例
原理分析
1. 发生系统调用时保存现场
以用户空间执行 open("/dev/binder") 的系统调用为例,指令是:
svc #0
CPU 硬件做的事情:
- 当 EL0 执行 svc #imm:
- CPU 切换异常级:EL0 → EL1;
- 用户 PSTATE 保存到 SPSR_EL1
- 用户 PC(svc 下一条指令地址)保存到 ELR_EL1
- 使用异常向量表中 EL0 同步异常的入口地址(VBAR_EL1 指向的一张表):
- 跳转到 el0_sync(或类似名字)的入口
- 切换栈指针:
- 使用 SP_EL1 作为栈指针(这时已经是内核栈)
- SP_EL0 保留的是用户态栈,暂时不会动
注意:此时 x0–x30 里的值仍然是用户态的寄存器值,CPU 没帮你保存到内存,必须靠内核汇编自己存。
将vectors填入vbar_el1寄存器中,其中vectors是一个全局标记:
vectors处通过kernel_ventry定义了多个入口:
先预留PT_REGS_SIZE大小的栈空间,然后跳转到el0t_64_sync处执行
kernel_entry \el, \regsize宏处保存寄存器信息到内核栈中
| |
之后栈布局如下
| |
2. stack_trace的实现
| |
arch_stack_walk_user是一个架构相关的函数,定义在arch/arm64/kernel/stacktrace.c中:
| |
task_pt_regs(current)
| |
其中THREAD_SIZE 是内核栈的大小,task_stack_page拿到的是栈的地址。
因此task_pt_regs(current)拿到的就是当前进程内核栈上保存的中断线程,然后 arch_stack_walk_user从中找到x29寄存器,并依次去找lr指针和fp指针,就可以抓到用户态的调用栈。