一文搞懂 | Ftrace 的实现原理 - 墨天轮
https://www.modb.pro/db/106301
arm64 栈帧结构
arm64 有31个通用寄存器 r0-r30,用法分别如下:
寄存器 | 意义 |
---|---|
SP | Stack Pointer: 栈指针 |
r30 | Link Register: 在调用函数时候,保存下一条要执行指令的地址 |
r29 | Frame Pointer:保存函数栈的基地址 |
r28...r19 | |
r18 | |
r17 | |
r16 | |
r15...r9 | 临时寄存器 |
r8 | 在一些情况下,返回值是通过 r8 返回的 |
r7...r0 | 在函数调用过程中传递参数和返回值 |
NZCV | 状态寄存器:N(Negative)负数 Z(Zero) 零 C(Carry) 进位 V(Overflow) 溢出 |
下面以如下代码为例,说明它的栈帧结构:
int fun2(int c,int d)
{
return0;
}
int fun1(int a,int b)
{
int c = 1;
int d = 2;
fun2(c, d);
return0;
}
int main(int argc,char **argv)
{
int a = 0;
int b = 1;
fun1(a,b);
}
其反汇编后的结果和对应栈帧结构为:
其详细步骤如下:
-
每个函数在入口处首先会分配栈空间,且一次分配,确定栈顶,之后sp将不再变化;
-
每个函数的栈顶部存放的是caller的栈顶指针,即fun1的栈顶存放的是main栈顶指针;
-
对于最后一级callee函数,由于x29保存了上一级caller的栈顶sp指针,因此不在需要入栈保存,如示例中fun2执行时,此时x29指向fun1的栈顶sp
编译阶段
以 blk_update_request 为例,看下其开启 Ftrace 前后的反汇编代码:
可以看出,右图中多插入了一段【3f3c: 94000000 bl 0 <_mcount>】,那么是由谁何时插入的呢?
编译选项 -pg -mrecord-mcoun 会在编译时,在每个可 trace 的函数插入 bl 0 <_mcount>。
链接阶段
可以看出编译阶段的【3f3c: 94000000 bl 0 <_mcount>】,在链接阶段变成了【ffff8000104e43f4: 97ed1fde bl ffff80001002c36c <_mcount>】。
其中 _mcount 函数反汇编为:
ffff80001002c36c <_mcount>:
ffff80001002c36c: d65f03c0 ret
运行阶段
ftrace_init 执行后的 blk_update_request
可见,内核在 start_kernel 执行时,会调用 ftrace_init,它会将所有可 trace 函数中的 _mcount 进行替换,如上可以看出链接阶段的 【bl ffff80001002c36c <_mcount>】 已经被替换为 【nop】 指令。
设定 trace 后的 blk_update_request
先执行如下命令来 trace 函数 blk_update_request:
$echo blk_update_request > /sys/kernel/debug/tracing/set_ftrace_filter
$echo function > /sys/kernel/debug/tracing/current_tracer
然后再来查看blk_update_request反汇编代码:
可以看到之前在 blk_update_request 的 【nop】 指令被替换成 【bl 0xffff80001002c370 <ftrace_caller>】。函数 ftrace_caller 将调用用户注册的 trace 函数。
总结
-
编译阶段。通过编译选项 -pg -mrecord-mcount 在每个支持 trace 的函数中插入 bl 0 <_mcount> 指令。
-
链接阶段。会根据重定位段将 bl 0 <_mcount> 指令地址重定位为 _mcount 函数地址。
-
运行阶段。
(1). ftrace_init:会将可 trace 函数中的 【bl 0 <_mcount>】 替换为 【nop】 指令;
(2). 执行 echo blk_update_request > set_ftrace_filter:会使能 blk_update_request 的钩子函数替换标记(nop 替换为 ftrace_caller);
(3). 执行 echo function > current_tracer:将 ftrace_caller 中 ftrace_call 被替换为 ftrace_ops_no_ops,最终会调用到 function_trace_call。