1.前言
本文主要是根据阅码场 《Linux内核tracers的实现原理与应用》视频课程在aarch64上的实践。通过观察钩子函数的创建过程以及替换过程,理解trace的原理。本文同样以blk_update_request函数为例进行说明function graph trace的工作原理。
kernel版本:5.10
平台:arm64
2. function graph trace钩子函数替换过程
2.1 编译阶段
同Linux ftrace学习笔记中编译阶段部分的描述。在使能内核配置项CONFIG_FTRACE时,可以看到blk_update_request函数增加了如下部分:
3f3c: 94000000 bl 0 <_mcount>
2.2 链接阶段
同Linux ftrace学习笔记中编译阶段部分的描述。在使能内核配置项CONFIG_FTRACE时,可以看到在链接阶段blk_update_request函数编译阶增加的部分,已经被替换为如下:
bl ffff80001002c36c <_mcount>
其中_mcount为:
ffff80001002c36c <_mcount>:
ffff80001002c36c: d65f03c0 ret
2.3 运行阶段
同Linux ftrace学习笔记中编译阶段部分的描述。在使能内核配置项CONFIG_FTRACE时,内核在start_kernel执行时,会调用ftrace_init,它会将所有可trace函数中的_mcount进行替换,
链接阶段blk_update_request中的 bl ffff80001002c36c <_mcount> 已经被替换为nop指令
0xffff8000104e43f4 <blk_update_request+44>: nop
设定trace函数blk_update_request,执行如下命令来trace函数blk_update_request
ubuntu@VM-0-9-ubuntu:~$ echo blk_update_request > /sys/kernel/debug/tracing/set_graph_function
ubuntu@VM-0-9-ubuntu:~$ echo function_graph > /sys/kernel/debug/tracing/current_tracer
(gdb) disassemble blk_update_request
//分配栈空间,此后sp指向栈顶
0xffff8000104e43c8 <+0>: sub sp, sp, #0x60
//根据ARM64栈帧结构,x29(FP)指向blk_mq_end_request函数栈顶
//x30(LR)指向blk_mq_end_request中bl blk_update_request的下条指令
//此处将x29和x30存放到栈顶偏移16字节的位置
0xffff8000104e43cc <+4>: stp x29, x30, [sp,#16]
//更新x29指向blk_update_request的栈顶+16
0xffff8000104e43d0 <+8>: add x29, sp, #0x10
0xffff8000104e43d4 <+12>: stp x19, x20, [sp,#32]
0xffff8000104e43d8 <+16>: stp x21, x22, [sp,#48]
0xffff8000104e43dc <+20>: stp x23, x24, [sp,#64]
0xffff8000104e43e0 <+24>: str x25, [sp,#80]
0xffff8000104e43e4 <+28>: mov x22, x0
0xffff8000104e43e8 <+32>: uxtb w24, w1
0xffff8000104e43ec <+36>: mov w21, w2
//x0保存了blk_mq_end_request中bl blk_update_request的下条指令
0xffff8000104e43f0 <+40>: mov x0, x30
0xffff8000104e43f4 <+44>: bl 0xffff80001002c370 <ftrace_caller>
0xffff8000104e43f8 <+48> mov w0, w24
......
同Linux ftrace学习笔记中编译阶段部分的描述。在执行如上语句后,nop已经被替换成了
bl 0xffff80001002c370 <ftrace_caller>
这点function graph trace与function trace是相同的。
前面我们说blk_update_request的nop语句,无论是function trace还是function graph trace都会被替换成 bl ftrace_caller,那么两者有何分别呢?答案就是从ftrace_caller开始产生了区别。
同Linux ftrace学习笔记中编译阶段部分的描述,通过 gdb可以看到ftrace_caller在替换之前的反汇编代码如下:
(gdb) disassemble ftrace_caller
Dump of assembler code for function ftrace_caller:
0xffff80001002c370 <+0>: stp x29, x30, [sp,#-16]!
0xffff80001002c374 <+4>: mov x29, sp
0xffff80001002c378 <+8>: sub x0, x30, #0x4
0xffff80001002c37c <+12>: ldr x1, [x29]
0xffff80001002c380 <+16>: ldr x1, [x1,#8]
0xffff80001002c384 <+20>: nop /*ftrace_call*/
0xffff80001002c388 <+24>: nop /*ftrace_graph_call*/
0xffff80001002c38c <+28>: ldp x29, x30, [sp],#16
0xffff80001002c390 <+32>: ret
End of assembler dump.
在执行如下操作后
ubuntu@VM-0-9-ubuntu:~$echo blk_update_request > /sys/kernel/debug/tracing/set_graph_function
ubuntu@VM-0-9-ubuntu:~$echo function_graph > /sys/kernel/debug/tracing/current_tracer
当我们echo function_graph的时候,ftrace_modify_graph_caller会将这条nop指向替换成一条
"b ftrace_graph_caller"指令,注意这是不保存LR的无条件跳转。使能function_graph的时候需要disable 掉CONFIG_STRICT_MEMORY_RWX和“KERNEL_TEXT_RDONLY“这样,才会允许代码被动态修改。
替换后反汇编ftrace_caller的结果如下:
(gdb) disassemble ftrace_caller
//x29(FP)指向blk_update_request函数栈顶+16,blk_update_request函数栈顶+16存放了blk_mq_end_request的栈顶
//x30(LR)指向blk_update_request中bl ftrace_caller的下条指令
0xffff80001002c370 <+0>: stp x29, x30, [sp,#-16]!
//更新x29(FP)指向ftrace_caller函数栈顶
0xffff80001002c374 <+4>: mov x29, sp
//x0指向blk_update_request中当前指令bl ftrace_caller
0xffff80001002c378 <+8>: sub x0, x30, #0x4
//x1指向blk_mq_end_request函数栈顶
0xffff80001002c37c <+12>: ldr x1, [x29]
//x1存放blk_mq_end_request函数中bl blk_update_request的下条指令的地址
0xffff80001002c380 <+16>: ldr x1, [x1,#8]
0xffff80001002c384 <+20>: bl 0xffff800010188ffc <ftrace_ops_no_ops>
0xffff80001002c388 <+24>: b 0xffff80001002c394 <ftrace_graph_caller>
0xffff80001002c38c <+28>: ldp x29, x30, [sp],#16
0xffff80001002c390 <+32>: ret
我们可以看到原本ftrace_caller中的两条nop指令分别被替换为:
bl ftrace_ops_no_ops和b ftrace_graph_caller
在调用bl ftrace_ops_no_ops和b ftrace_graph_caller前,x0,x1分别为:
- x0指向blk_update_request中当前指令bl ftrace_caller
- x1存放blk_mq_end_request函数中bl blk_update_request的下条指令的地址
3. function graph trace钩子函数执行过程
blk_update_request的钩子函数ftrace_caller主要经历了如下的调用流程:
blk_mq_end_request
\--blk_update_request
\--ftrace_caller
|--ftrace_ops_no_ops
\--ftrace_graph_caller
\--prepare_ftrace_return
\--function_graph_enter
|--ftrace_push_return_trace
\--ftrace_graph_entry
根据ARM64函数调用规则,可形成如下的栈帧结构:
下面将详细说明钩子函数的工作流程
3.1 ftrace_ops_no_ops
先来看下ftrace_ops_no_ops,同Linux ftrace学习笔记中编译阶段部分的描述,从ftrace_ops_no_ops源码中看到它会遍历ftrace_ops_list链表,并执行这个链表上的回调函数,这里看下ftrace_ops_list上都链接了哪些func:
(gdb) p *ftrace_ops_list
$1 = {
func = 0xffff80001002c3b8 <ftrace_stub>,
next = 0xffff800011c5a438 <ftrace_list_end>,
....
},
trampoline = 0,
trampoline_size = 0,
list = {
next = 0x0,
prev = 0x0
}
}
可以看到此链表上只有一个ftrace_stub,它的反汇编如下:
(gdb) disassemble ftrace_stub
Dump of assembler code for function ftrace_stub:
0xffff80001002c3b8 <+0>: ret
End of assembler dump.
只有一个ret返回指令,这与function trace的不同,在function trace中not最终会被替换为function_trace_call函数,执行function trace操作。
3.2 ftrace_graph_caller
ftrace_caller的第二个nop被替换为ftrace_graph_caller,通过ftrace_caller函数可知调用b ftrace_graph_caller前,x0,x1分别为:
- x0指向blk_update_request中当前指令bl ftrace_caller
- x1存放blk_mq_end_request函数中bl blk_update_request的下条指令的地址
- x2指向blk_mq_end_request的栈顶
(gdb) disassemble ftrace_graph_caller
//此时x29指向ftrace_caller栈顶,x29+8指向blk_update_request中bl ftrace_caller的下条指令的地址
0xffff80001002c394 <+0>: ldr x0, [x29,#8]
//***x0=x29+8-4指向blk_update_request函数bl ftrace_caller指令的地址
0xffff80001002c398 <+4>: sub x0, x0, #0x4
//此时x1指向blk_update_request的栈顶+16(根据blk_update_request中x29)
0xffff80001002c39c <+8>: ldr x1, [x29]
//x1保存了blk_mq_end_request中 bl blk_update_request的下条指令地址的地址
0xffff80001002c3a0 <+12>: add x1, x1, #0x8
//x2指向blk_update_request的栈顶+16(根据blk_update_request中x29)
0xffff80001002c3a4 <+16>: ldr x2, [x29]
//x2指向blk_mq_end_request的栈顶
0xffff80001002c3a8 <+20>: ldr x2, [x2]
0xffff80001002c3ac <+24>: bl 0xffff80001002c280 <prepare_ftrace_return>
0xffff80001002c3b0 <+28>: ldp x29, x30, [sp],#16
0xffff80001002c3b4 <+32>: ret
3.2.1 prepare_ftrace_return
prepare_ftrace_return
\--function_graph_enter
|--ftrace_push_return_trace
\--ftrace_graph_entry
通过ftrace_graph_caller函数可知调用prepare_ftrace_return函数前,参数x0, x1, x2分别为:
- x0:指向blk_update_request函数bl ftrace_caller的地址
- x1:保存了blk_mq_end_request中 bl blk_update_request的下条指令地址的地址
- x2:指向blk_mq_end_request的栈顶
如上x0, x1, x2分别对应了prepare_ftrace_return的形参self_addr, parent,frame_pointer
void prepare_ftrace_return(unsigned long self_addr, unsigned long *parent,
unsigned long frame_pointer)
{
unsigned long return_hooker = (unsigned long)&return_to_handler;
unsigned long old;
if (unlikely(atomic_read(¤t->tracing_graph_pause)))
return;
/*
* Note:
* No protection against faulting at *parent, which may be seen
* on other archs. It's unlikely on AArch64.
*/
//保存原始的lr值,对于本例来讲,就是blk_mq_end_request中bl blk_update_request指令的下条指令地址
//它将用于恢复lr值
old = *parent;
if (!function_graph_enter(old, self_addr, frame_pointer, NULL))
//更新lr值,对于本例来讲,就是将blk_mq_end_request中bl blk_update_request指令的下条指令地址
//修改为return_to_handler
*parent = return_hooker;
}
prepare_ftrace_return,反汇编如下:
(gdb) disassemble prepare_ftrace_return
Dump of assembler code for function prepare_ftrace_return:
0xffff80001002c280 <+0>: mrs x3, sp_el0
0xffff80001002c284 <+4>: ldr w3, [x3,#2460]
0xffff80001002c288 <+8>: cbnz w3, 0xffff80001002c2c8 <prepare_ftrace_return+72>
0xffff80001002c28c <+12>: stp x29, x30, [sp,#-32]!
0xffff80001002c290 <+16>: mov x29, sp
0xffff80001002c294 <+20>: str x19, [sp,#16]
//x19存放了指向blk_mq_end_request函数bl blk_update_reques的下条指令地址的指针
0xffff80001002c298 <+24>: mov x19, x1
//x1存放了blk_update_request函数当前执行的指令bl ftrace_caller的地址;
0xffff80001002c29c <+28>: mov x1, x0
//x3为0
0xffff80001002c2a0 <+32>: mov x3, #0x0 // #0
//x0存放了blk_mq_end_request函数中bl blk_update_reques的下条指令的地址
0xffff80001002c2a4 <+36>: ldr x0, [x19]
0xffff80001002c2a8 <+40>: bl 0xffff8000101a2dfc <function_graph_enter>
//如果function_graph_enter返回值为非0,表示失败,跳转到prepare_ftrace_return+60
0xffff80001002c2ac <+44>: cbnz w0, 0xffff80001002c2bc <prepare_ftrace_return+60>
0xffff80001002c2b0 <+48>: adrp x0, 0xffff80001002c000 <ftrace_make_call>
//此时x0的值为0xffff80001002c000+0x3bc,它就是return_to_handler
0xffff80001002c2b4 <+52>: add x0, x0, #0x3bc
//根据前述,x19存放了指向blk_mq_end_request函数bl blk_update_reques的下条指令地址的指针
//此处就是通过重新赋值x19修改blk_mq_end_request函数bl blk_update_reques的下条指令地址为return_to_handler
0xffff80001002c2b8 <+56>: str x0, [x19]
//如果function_graph_enter返回值为非0,跳转至此
0xffff80001002c2bc <+60>: ldr x19, [sp,#16]
0xffff80001002c2c0 <+64>: ldp x29, x30, [sp],#32
0xffff80001002c2c4 <+68>: ret
0xffff80001002c2c8 <+72>: ret
通过prepare_ftrace_return函数可知调用function_graph_enter函数前的参数x0和x1、x2分别为:
- x0存放了blk_mq_end_request函数中bl blk_update_reques的下条指令的地址
- x1存放了blk_update_request函数当前执行的指令bl ftrace_caller的地址;
- x2指向ftrace_caller栈顶
- x3为0
如上x0, x1, x2、x3分别对应了function_graph_enter的形参ret, func,frame_pointer, retp
int function_graph_enter(unsigned long ret, unsigned long func,
unsigned long frame_pointer, unsigned long *retp)
{
struct ftrace_graph_ent trace;
trace.func = func;
trace.depth = ++current->curr_ret_depth;
if (ftrace_push_return_trace(ret, func, frame_pointer, retp))
goto out;
/* Only trace if the calling function expects to */
if (!ftrace_graph_entry(&trace))
goto out_ret;
return 0;
out_ret:
current->curr_ret_stack--;
out:
current->curr_ret_depth--;
return -EBUSY;
}
3.2.1.1 ftrace_push_return_trace
通过function_graph_enter函数可知,ftrace_push_return_trace函数与之有相同的形参,因此调用ftrace_push_return_trace函数前的参数分别为:
- ret存放了blk_mq_end_request函数中bl blk_update_reques的下条指令的地址
- func存放了blk_update_request函数当前执行的指令bl ftrace_caller的地址;
- frame_pointer指向ftrace_caller栈顶
- retp为0
/* Add a function return address to the trace stack on thread info.*/
static int
ftrace_push_return_trace(unsigned long ret, unsigned long func,
unsigned long frame_pointer, unsigned long *retp)
{
unsigned long long calltime;
int index;
if (unlikely(ftrace_graph_is_dead()))
return -EBUSY;
if (!current->ret_stack)
return -EBUSY;
/*
* We must make sure the ret_stack is tested before we read
* anything else.
*/
smp_rmb();
/* The return trace stack is full */
if (current->curr_ret_stack == FTRACE_RETFUNC_DEPTH - 1) {
atomic_inc(¤t->trace_overrun);
return -EBUSY;
}
calltime = trace_clock_local();
index = ++current->curr_ret_stack;
barrier();
//存放blk_mq_end_request函数中bl blk_update_reques的下条指令的地址
current->ret_stack[index].ret = ret;
//存放了blk_update_request函数当前执行的指令bl ftrace_caller的地址
current->ret_stack[index].func = func;
current->ret_stack[index].calltime = calltime;
#ifdef HAVE_FUNCTION_GRAPH_FP_TEST
current->ret_stack[index].fp = frame_pointer;
#endif
#ifdef HAVE_FUNCTION_GRAPH_RET_ADDR_PTR
current->ret_stack[index].retp = retp;
#endif
return 0;
}
3.2.1.2 ftrace_graph_entry
通过gdb可知ftrace_graph_entry函数指针被赋值为trace_graph_entry,trace_graph_entry最终会将被trace函数以及执行时间保存到ring buffer中。
(gdb) p ftrace_graph_entry
$1 = (trace_func_graph_ent_t) 0xffff8000101a1418 <trace_graph_entry>
3.3 return_to_handler
return_to_handler
\--ftrace_return_to_handler
|--struct ftrace_graph_ret trace
|--ftrace_pop_return_trace(&trace, &ret, frame_pointer)
|--trace.rettime = trace_clock_local()
\--ftrace_graph_return(&trace)
前面在blk_mq_end_request-> blk_update_request-> ftrace_caller-> ftrace_graph_caller-> prepare_ftrace_return调用时,
会将blk_mq_end_request的链接地址LR替换为return_to_handler,从blk_update_request返回后将执行return_to_handler,下面看下return_to_handler函数:
/*
* void return_to_handler(void)
*
* Run ftrace_return_to_handler() before going back to parent.
* @fp is checked against the value passed by ftrace_graph_caller().
*/
SYM_CODE_START(return_to_handler)
/* save return value regs */
sub sp, sp, #64
stp x0, x1, [sp]
stp x2, x3, [sp, #16]
stp x4, x5, [sp, #32]
stp x6, x7, [sp, #48]
//x0保存了blk_mq_end_request的栈顶
mov x0, x29 // parent's fp
//根据ftrace_return_to_handler分析,
//ftrace_return_to_handler返回值保存了blk_mq_end_request原有的链接地址
//即bl blk_update_request的下一条地址
bl ftrace_return_to_handler// addr = ftrace_return_to_hander(fp);
//x0保存了blk_mq_end_request的原来的链接地址,赋值给x30,这样在return_to_handler返回时,
//会执行x30保存的指令地址
mov x30, x0 // restore the original return address
/* restore return value regs */
ldp x0, x1, [sp]
ldp x2, x3, [sp, #16]
ldp x4, x5, [sp, #32]
ldp x6, x7, [sp, #48]
add sp, sp, #64
//函数返回后将执行x30保存的指令地址
ret
SYM_CODE_END(return_to_handler)
根据对return_to_handler函数,在调用ftrace_return_to_handler之前,参数frame_pointer为blk_mq_end_request的栈顶
/*
* Send the trace to the ring-buffer.
* @return the original return address.
*/
unsigned long ftrace_return_to_handler(unsigned long frame_pointer)
{
struct ftrace_graph_ret trace;
unsigned long ret;
ftrace_pop_return_trace(&trace, &ret, frame_pointer);
trace.rettime = trace_clock_local();
ftrace_graph_return(&trace);
current->curr_ret_stack--;
//ret保存了blk_mq_end_request的返回地址,保存在x0
return ret;
}
3.3.1 ftrace_pop_return_trace
ret用于保存blk_mq_end_request原始的返回地址(bl blk_update_request的下条地址)
/* Retrieve a function return address to the trace stack on thread info.*/
static void
ftrace_pop_return_trace(struct ftrace_graph_ret *trace, unsigned long *ret,
unsigned long frame_pointer)
{
int index;
index = current->curr_ret_stack;
//此处ret保存了blk_mq_end_request的返回地址
*ret = current->ret_stack[index].ret;
//trace->func拿到了blk_mq_end_request函数bl blk_update_request当前指令的地址
trace->func = current->ret_stack[index].func;
trace->calltime = current->ret_stack[index].calltime;
trace->overrun = atomic_read(¤t->trace_overrun);
trace->depth = current->curr_ret_depth--;
}
3.3.2 trace_clock_local
/*
* trace_clock_local(): the simplest and least coherent tracing clock.
*
* Useful for tracing that does not cross to other CPUs nor
* does it go through idle events.
*/
u64 notrace trace_clock_local(void)
{
u64 clock;
/*
* sched_clock() is an architecture implemented, fast, scalable,
* lockless clock. It is not guaranteed to be coherent across
* CPUs, nor across CPU idle events.
*/
preempt_disable_notrace();
clock = sched_clock();
preempt_enable_notrace();
return clock;
}
EXPORT_SYMBOL_GPL(trace_clock_local);
获取返回当前时钟,也就是被trace函数的执行结束时间
3.3.3 ftrace_graph_return
ftrace_graph_return为trace_graph_return函数
void trace_graph_return(struct ftrace_graph_ret *trace)
{
struct trace_array *tr = graph_array;
struct trace_array_cpu *data;
unsigned long flags;
long disabled;
int cpu;
int pc;
ftrace_graph_addr_finish(trace);
if (trace_recursion_test(TRACE_GRAPH_NOTRACE_BIT)) {
trace_recursion_clear(TRACE_GRAPH_NOTRACE_BIT);
return;
}
local_irq_save(flags);
cpu = raw_smp_processor_id();
data = per_cpu_ptr(tr->array_buffer.data, cpu);
disabled = atomic_inc_return(&data->disabled);
if (likely(disabled == 1)) {
pc = preempt_count();
__trace_graph_return(tr, trace, flags, pc);
}
atomic_dec(&data->disabled);
local_irq_restore(flags);
4. 总结
通过前面的分析,我们可以看到function graph trace 实际是在要跟踪函数的入口处和返回处分别放置了钩子函数,以本例中的blk_update_request函数为例:
- 首先在blk_update_request函数入口处插入钩子函数ftrace_caller;
- 钩子函数记录函数调用关系
在 ftrace_caller->ftrace_graph_caller->prepare_ftrace_return->function_graph_enter->ftrace_push_return_trace调用关系中,
ftrace_push_return_trace函数将记录当前函数执行地址和blk_mq_end_request的链接地址,以及调用时间和ftrace_caller栈帧地址。其中当前函数执行地址为blk_mq_end_request函数的bl blk_update_request指令的地址,blk_mq_end_request链接地址为bl blk_update_request的下一条指令的地址。通过记录如上的地址就可以还原函数调用的关系。 - 在blk_update_request函数返回处插入钩子函数return_to_handler
在ftrace_caller->ftrace_graph_caller->prepare_ftrace_return时会用return_to_handler替换blk_mq_end_request函数的链接地址,通过return_to_handler获取函数的执行结束时间,之后再恢复blk_mq_end_request函数的链接返回地址,blk_mq_end_request按照原来的链接返回地址继续执行
最后举例如下:
ftrace_graph.sh如下:
#!/bin/sh
debugfs=/sys/kernel/debug
echo nop > $debugfs/tracing/current_tracer
echo 0 > $debugfs/tracing/tracing_on
echo 0 > $debugfs/tracing/max_graph_depth
echo $$ > $debugfs/tracing/set_ftrace_pid
echo blk_update_request > $debugfs/tracing/set_graph_function
echo function_graph > $debugfs/tracing/current_tracer
echo 1 > $debugfs/tracing/options/funcgraph-tail
echo 1 > $debugfs/tracing/tracing_on
exec "$@"
执行如下命令:
# ./ftrace_graph.sh cat ftrace_graph.sh
通过如下命令查看结果
cat /sys/kernel/debug/tracing/trace
注:在执行前需要通过echo 3 > /proc/sys/vm/drop_caches 清下cache,否则可能不会触发blk_update_request 执行