文章目录
- [1. 前言](#1. 前言)
- [2. ftrace 实现](#2. ftrace 实现)
-
- [2.1 实现原理](#2.1 实现原理)
- [2.2 实现细节](#2.2 实现细节)
-
- [2.2.1 `CONFIG_DYNAMIC_FTRACE=n` 情形下的细节](#2.2.1
CONFIG_DYNAMIC_FTRACE=n情形下的细节) -
- [2.2.1.1 初始化](#2.2.1.1 初始化)
- [2.2.1.2 设置 function 为当前 tracer & 启用](#2.2.1.2 设置 function 为当前 tracer & 启用)
- [2.2.1.3 调用函数时触发 function trace 接口](#2.2.1.3 调用函数时触发 function trace 接口)
- [2.2.2 `CONFIG_DYNAMIC_FTRACE=y` 情形下的细节](#2.2.2
CONFIG_DYNAMIC_FTRACE=y情形下的细节) -
- [2.2.2.1 初始化](#2.2.2.1 初始化)
- [2.2.2.2 设置 function 为当前 tracer & 启用](#2.2.2.2 设置 function 为当前 tracer & 启用)
- [2.2.2.3 调用函数时触发 function trace 接口](#2.2.2.3 调用函数时触发 function trace 接口)
- [2.2.1 `CONFIG_DYNAMIC_FTRACE=n` 情形下的细节](#2.2.1
- [2.3 小结](#2.3 小结)
- [3. 参考资料](#3. 参考资料)
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. ftrace 实现
2.1 实现原理
本小节讲述 function tracer 功能的核心原理。
当开启了 CONFIG_FUNCTION_TRACER 配置项时,就开启了 function tracer,编译器会加入 -pg 编译选项:
c
/* linux/Makefile */
...
ifdef CONFIG_FUNCTION_TRACER
ifndef CC_FLAGS_FTRACE
CC_FLAGS_FTRACE := -pg
endif
export CC_FLAGS_FTRACE
...
KBUILD_CFLAGS += $(CC_FLAGS_FTRACE) $(CC_USING_FENTRY)
...
加入 -pg 编译选项,会发生什么?本文以 arm-linux-gcc 编译器为例来进行分析。编写一个 test.c,其内容如下:
c
void main(void) {}
使用 -pg 选项进行编译:
bash
$ arm-linux-gcc -c -pg -o test.o test.c
然后反汇编 test.o:
bash
$ arm-linux-objdump -D test.o > test.S
$ cat test.S
test.o: file format elf32-littlearm
Disassembly of section .text:
00000000 <main>:
0: e92d0808 push {r3, fp}
4: e28db004 add fp, sp, #4
8: e52de004 push {lr} ; (str lr, [sp, #-4]!)
c: ebfffffe bl 0 <__gnu_mcount_nc> // -pg 选项指示编译器加入 __gnu_mcount_nc 调用
10: e24bd004 sub sp, fp, #4
14: e8bd0808 pop {r3, fp}
18: e12fff1e bx lr
Disassembly of section .comment:
00000000 <.comment>:
0: 43434700 movtmi r4, #14080 ; 0x3700
4: 6328203a teqvs r8, #58 ; 0x3a
8: 2d676e74 stclcs 14, cr6, [r7, #-464]! ; 0xfffffe30
c: 31322e31 teqcc r2, r1, lsr lr
10: 322d302e eorcc r3, sp, #46 ; 0x2e
14: 2d673932 stclcs 9, cr3, [r7, #-200]! ; 0xffffff38
18: 20294146 eorcs r4, r9, r6, asr #2
1c: 2e392e34 mrccs 14, 1, r2, cr9, cr4, {1}
20: Address 0x0000000000000020 is out of bounds.
Disassembly of section .ARM.attributes:
00000000 <.ARM.attributes>:
0: 00003441 andeq r3, r0, r1, asr #8
4: 61656100 cmnvs r5, r0, lsl #2
8: 01006962 tsteq r0, r2, ror #18
c: 0000002a andeq r0, r0, sl, lsr #32
10: 412d3705 teqmi sp, r5, lsl #14
14: 070a0600 streq r0, [sl, -r0, lsl #12]
18: 09010841 stmdbeq r1, {r0, r6, fp}
1c: 12030a02 andne r0, r3, #8192 ; 0x2000
20: 15011404 strne r1, [r1, #-1028] ; 0xfffffbfc
24: 18031701 stmdane r3, {r0, r8, r9, sl, ip}
28: 1a011901 bne 46434 <main+0x46434>
2c: 1c031b02 stcne 11, cr1, [r3], {2}
30: 22061e01 andcs r1, r6, #1, 28
34: Address 0x0000000000000034 is out of bounds.
可以看到,反汇编代码里面有一行 c: ebfffffe bl 0 <__gnu_mcount_nc>,这就是编译时加入 -pg 选项的效果:增加了一个对 __gnu_mcount_nc 函数的调用。在 ARM32 架构下,Linux 内核实现了 __gnu_mcount_nc 函数,对于 CONFIG_FUNCTION_TRACER 启用的 function tracer 功能:
-
在
未开启配置CONFIG_DYNAMIC_FTRACE时在
未开启 function 跟踪功能时(默认)时,__gnu_mcount_nc调用ftrace_trace_function函数指针指向的、不做任何工作的ftrace_stub()函数;在通过命令echo function > /sys/kernel/debug/tracing/current_tracer开启了 function 跟踪功能时,ftrace_trace_function将指向函数function_trace_call(),__gnu_mcount_nc将调用function_trace_call()执行函数追踪,即记录调用函数信息到trace ring buffer。该配置场景下,对所有函数的__gnu_mcount_nc调用一直存在,只是在未开启 function 跟踪功能时(默认)时,调用的是什么也不做的ftrace_stub();反之,则是调用做实际跟踪工作的function_trace_call()。 -
在
开启了配置CONFIG_DYNAMIC_FTRACE时在系统
BOOT期间,ftrace_init()将所有对__gnu_mcount_nc的调用替换为nop指令,同时创建记录所有这些__gnu_mcount_nc调用地址信息的数据对象,让后续需要开启 function 追踪时,可以使用调用地址信息的数据对象,进行指令替换。ftrace_init()将所有对__gnu_mcount_nc的调用指令替换为nop指令时,需要知道这些__gnu_mcount_nc调用指令的地址信息,所有这些信息放置在地址区间[__start_mcount_loc, __stop_mcount_loc]。那地址区间[__start_mcount_loc, __stop_mcount_loc]又是来自于哪里?答案来自于scripts/recordmcount.c在编译期间创建的所有__mcount_locsection。还是以前面的test.c为例,简要的说明scripts/recordmcount.c的处理过程。在加上-pg编译选项将test.c编译成test.o后,反汇编代码可以看到添加了c: ebfffffe bl 0 <__gnu_mcount_nc>调用,我们需要这条指令的地址信息,将test.o交给scripts/recordmcount.c处理,scripts/recordmcount.c找到c: ebfffffe bl 0 <__gnu_mcount_nc>的地址0xc,然后创建一个名为__mcount_loc的 section,扩展覆写到test.o,来看这一个过程的细节:
bash
$ ./linux/scripts/recordmount test.o
$ arm-linux-objdump -D test.o > test.S
$ cat test.S
est.o: file format elf32-littlearm
Disassembly of section .text:
00000000 <main>:
0: e92d0808 push {r3, fp}
4: e28db004 add fp, sp, #4
8: e52de004 push {lr} ; (str lr, [sp, #-4]!)
c: ebfffffe bl 0 <__gnu_mcount_nc> // (1) __gnu_mcount_nc 调用指令为 0xc
10: e24bd004 sub sp, fp, #4
14: e8bd0808 pop {r3, fp}
18: e12fff1e bx lr
Disassembly of section .comment:
00000000 <.comment>:
0: 43434700 movtmi r4, #14080 ; 0x3700
4: 6328203a teqvs r8, #58 ; 0x3a
8: 2d676e74 stclcs 14, cr6, [r7, #-464]! ; 0xfffffe30
c: 31322e31 teqcc r2, r1, lsr lr
10: 322d302e eorcc r3, sp, #46 ; 0x2e
14: 2d673932 stclcs 9, cr3, [r7, #-200]! ; 0xffffff38
18: 20294146 eorcs r4, r9, r6, asr #2
1c: 2e392e34 mrccs 14, 1, r2, cr9, cr4, {1}
20: Address 0x0000000000000020 is out of bounds.
Disassembly of section __mcount_loc:
00000000 <__mcount_loc>:
0: 0000000c andeq r0, r0, ip // (2) 存储 __gnu_mcount_nc 调用指令地址 0xc
在 Linux 编译过程中,将每个 .o 文件传递工具 scripts/recordmcount.c 处理,然后在链接时将所有 .o 中的 __mcount_loc section 放置到 __start_mcount_loc 和 __stop_mcount_loc 符号之间:
c
/* include/asm-generic/vmlinux.lds.h */
#ifdef CONFIG_FTRACE_MCOUNT_RECORD
#define MCOUNT_REC() . = ALIGN(8); \
VMLINUX_SYMBOL(__start_mcount_loc) = .; \
*(__mcount_loc) \
VMLINUX_SYMBOL(__stop_mcount_loc) = .;
#else
#define MCOUNT_REC()
#endif
早期用 scripts/recordmcount.pl 来处理 __gnu_mcount_nc,但由于效率问题已经被废弃。为什么编译器期间处理 __gnu_mcount_nc,而不是直接在 BOOT 期间直接查找 __gnu_mcount_nc 调用?理论上也可以,但很显然,这种方式太过低效,因为要从所有代码里面去查找 __gnu_mcount_nc 调用。
2.2 实现细节
本小节概要的分析 function tracer 的实现细节,基于 Linux 4.14.x + ARMv7。CONFIG_FUNCTION_TRACER=y 开启 function tracer,在 CONFIG_DYNAMIC_FTRACE 配置开启和关闭两种不同的情形下,实现细节上存在较大差异。CONFIG_FUNCTION_TRACER 和 CONFIG_DYNAMIC_FTRACE 有以下可能的配置组合:
| CONFIG_DYNAMIC_FTRACE | CONFIG_FUNCTION_TRACER | 说明 |
|---|---|---|
| =n | =n | 没有 function 跟踪功能。 |
| =n | =y | 编译时加入 -pg 选项,在函数入口插入 bl __gnu_mcount_nc 调用。在 __gnu_mcount_nc 中默认调用 ftrac_stub();通过 echo function > current_tracer 后,会调用覆盖的 function_trace_call(),function_trace_call() 将函数追踪信息写入 trace ring buffer。该配置下,将启用所有函数的追踪,且无法进行过滤,因为没有 set_ftrace_filter, set_ftrace_notrace 文件接口。 |
| =y | =n | 不可能的配置,因为 CONFIG_DYNAMIC_FTRACE 依赖于 CONFIG_FUNCTION_TRACER。 |
| =y | =y | 编译时加入 -pg 选项,在函数入口插入 bl __gnu_mcount_nc 调用。系统初始化时,将所有的 bl __gnu_mcount_nc 替换成 nop 指令,并记录这些调用地址信息;通过 echo function > current_tracer 后,替换成 ftrace_caller()。ftrace_caller() -> ftrace_ops_list_func() -> function_trace_call() 调用链写函数追踪信息到 trace ring buffer。该配置下,默认启用所有函数的追踪,但可以通过设置 set_ftrace_filter, set_ftrace_notrace 进行过滤。 |
本文在 CONFIG_FUNCTION_TRACER=y 前提配置下,对 CONFIG_DYNAMIC_FTRACE=n 和 CONFIG_DYNAMIC_FTRACE=y 这两种情形逐一进行分析。先看两种情形下初始化过程中相同的部分。
首先主要是分配 trace ring buffer 和 注册 function tracer:
c
start_kernel()
ftrace_init() // CONFIG_DYNAMIC_FTRACE=n 为空操作
early_trace_init()
tracer_alloc_buffers()
c
/* kernel/trace/trace.c */
__init static int tracer_alloc_buffers(void)
{
...
if (allocate_trace_buffers(&global_trace, ring_buf_size) < 0) {
...
}
global_trace.current_trace = &nop_trace; /* 初始的 tracer 为 nop */
...
register_tracer(&nop_trace);
...
/* Function tracing may start here (via kernel command line) */
init_function_trace();
...
global_trace.flags = TRACE_ARRAY_FL_GLOBAL; /* 标记为全局的 */
...
}
c
/* kernel/trace/trace_functions.c */
__init int init_function_trace(void)
{
init_func_cmd_traceon(); /* 注册 function tracer 的命令 */
return register_tracer(&function_trace); /* 注册 function tracer:添加到 trace_types 列表供选用 */
}
接下来是创建一些用户空间接口文件:
c
/* kernel/trace/trace.c */
static __init int tracer_init_tracefs(void)
{
struct dentry *d_tracer;
...
d_tracer = tracing_init_dentry();
...
init_tracer_tracefs(&global_trace, d_tracer);
...
}
fs_initcall(tracer_init_tracefs);
c
/* kernel/trace/trace.c */
static const struct file_operations set_tracer_fops = {
.open = tracing_open_generic,
.read = tracing_set_trace_read,
.write = tracing_set_trace_write,
.llseek = generic_file_llseek,
};
static const struct file_operations tracing_fops = {
.open = tracing_open,
.read = seq_read,
.write = tracing_write_stub,
.llseek = tracing_lseek,
.release = tracing_release,
};
static const struct file_operations rb_simple_fops = {
.open = tracing_open_generic_tr,
.read = rb_simple_read,
.write = rb_simple_write,
.release = tracing_release_generic_tr,
.llseek = default_llseek,
};
static void
init_tracer_tracefs(struct trace_array *tr, struct dentry *d_tracer)
{
...
/* /sys/kernel/debug/tracing/current_tracer */
trace_create_file("current_tracer", 0644, d_tracer,
tr, &set_tracer_fops);
...
/* /sys/kernel/debug/tracing/trace */
trace_create_file("trace", 0644, d_tracer,
tr, &tracing_fops);
...
/* /sys/kernel/debug/tracing/tracing_on */
trace_create_file("tracing_on", 0644, d_tracer,
tr, &rb_simple_fops);
...
}
tracer_init_tracefs() -> init_tracer_tracefs() 进行了 frace 子系统的一些初始化,并构建了以下和本文讨论的 function tracer 相关的主要文件节点:
/sys/kernel/debug/tracing/current_tracer
/sys/kernel/debug/tracing/tracing_on
/sys/kernel/debug/tracing/trace
/sys/kernel/debug/tracing/current_tracer
用来设置当前的 tracer 类型。function tracer 写入 function。/sys/kernel/debug/tracing/tracing_on
写 1 开启 trace,写 0 关闭 trace。/sys/kernel/debug/tracing/trace
主要用来读取 trace 内容。
接下来讨论 CONFIG_DYNAMIC_FTRACE 配置开启和关闭两种不同的情形下,各自不同的实现细节概要。
2.2.1 CONFIG_DYNAMIC_FTRACE=n 情形下的细节
该场景下,对所有函数的 __gnu_mcount_nc 调用一直存在,只是在未开启 function 跟踪功能时(默认)时,调用的是什么也不做的 ftrace_stub();反之,则是调用做实际跟踪工作的 function_trace_call()。
2.2.1.1 初始化
CONFIG_DYNAMIC_FTRACE=n 时, 系统启动默认使能 function 跟踪:
c
/* kernel/trace/ftrace.c */
/* ftrace_enabled is a method to turn ftrace on or off */
int ftrace_enabled __read_mostly;
...
static int __init ftrace_nodyn_init(void)
{
ftrace_enabled = 1; /* CONFIG_DYNAMIC_FTRACE=n 时, 系统启动默认使能 function 跟踪 */
return 0;
}
core_initcall(ftrace_nodyn_init);
c
static struct ftrace_ops ftrace_list_end __read_mostly = {
.func = ftrace_stub,
.flags = FTRACE_OPS_FL_RECURSION_SAFE | FTRACE_OPS_FL_STUB,
INIT_OPS_HASH(ftrace_list_end)
};
/* Current function tracing op */
struct ftrace_ops *function_trace_op __read_mostly = &ftrace_list_end;
/* What to set function_trace_op to */
static struct ftrace_ops *set_function_trace_op;
...
static struct ftrace_ops __rcu *ftrace_ops_list __read_mostly = &ftrace_list_end;
ftrace_func_t ftrace_trace_function __read_mostly = ftrace_stub; /* 默认的接口什么也不做 */
static struct ftrace_ops global_ops;
2.2.1.2 设置 function 为当前 tracer & 启用
- 设置 function 为当前 tracer
写文件 /sys/kernel/debug/tracing/current_tracer 设置当前的 tracer 类型为 function:
cd /sys/kernel/debug/tracing
echo function > current_tracer
首先打开 /sys/kernel/debug/tracing 文件触发 sys_open() -> ... -> tracing_open_generic() 调用:
c
/* kernel/trace/trace.c */
int tracing_open_generic(struct inode *inode, struct file *filp)
{
...
filp->private_data = inode->i_private; /* filp->private_data = &global_trace */
return 0;
}
然后写文件 /sys/kernel/debug/tracing 触发 tracing_set_trace_write():
c
/* kernel/trace/trace.c */
static ssize_t
tracing_set_trace_write(struct file *filp, const char __user *ubuf,
size_t cnt, loff_t *ppos)
{
struct trace_array *tr = filp->private_data; /* tr = &global_trace */
...
if (copy_from_user(buf, ubuf, cnt)) /* buf: "function" */
return -EFAULT;
...
err = tracing_set_tracer(tr, buf);
...
}
static int tracing_set_tracer(struct trace_array *tr, const char *buf)
{
struct tracer *t;
...
/* 遍历所有已注册的 tracer (通过 register_tracer()), 找到名为 @buf 的 tracer */
for (t = trace_types; t; t = t->next) {
if (strcmp(t->name, buf) == 0)
break;
}
...
if (t->init) {
ret = tracer_init(t, tr); /* 新 tracer 初始化 */
if (ret)
goto out;
}
tr->current_trace = t; /* 设置当前的 tracer */
tr->current_trace->enabled++;
...
}
int tracer_init(struct tracer *t, struct trace_array *tr)
{
tracing_reset_online_cpus(&tr->trace_buffer);
return t->init(tr); /* function_trace_init() */
}
c
/* kernel/trace/trace_functions.c */
static int function_trace_init(struct trace_array *tr)
{
ftrace_func_t func;
...
/* Currently only the global instance can do stack tracing */
if (tr->flags & TRACE_ARRAY_FL_GLOBAL &&
func_flags.val & TRACE_FUNC_OPT_STACK)
...
else
func = function_trace_call;
ftrace_init_array_ops(tr, func);
...
tracing_start_function_trace(tr);
return 0;
}
static void tracing_start_function_trace(struct trace_array *tr)
{
tr->function_enabled = 0;
register_ftrace_function(tr->ops);
tr->function_enabled = 1;
}
register_ftrace_function()
ftrace_startup()
__register_ftrace_function()
...
add_ftrace_ops(&ftrace_ops_list, ops);
...
if (ftrace_enabled)
update_ftrace_function();
...
c
static void update_ftrace_function(void)
{
ftrace_func_t func;
set_function_trace_op = rcu_dereference_protected(ftrace_ops_list,
lockdep_is_held(&ftrace_lock));
/* If there's no ftrace_ops registered, just call the stub function */
if (set_function_trace_op == &ftrace_list_end) {
...
} else if (rcu_dereference_protected(ftrace_ops_list->next,
lockdep_is_held(&ftrace_lock)) == &ftrace_list_end) {
func = ftrace_ops_get_list_func(ftrace_ops_list); // function_trace_call()
} else {
...
}
...
ftrace_trace_function = func; // ftrace_trace_function = function_trace_call
}
这里主要是将 ftrace_trace_function 函数指针设置为 function_trace_call() 函数,function_trace_call() 用来将被追踪函数的信息写入 trace ring buffer。
- 启用 trace
写 1 到 /sys/kernel/debug/tracing/tracing_on 启用 trace:
c
/* kernel/trace/trace.c */
static int tracing_open_generic_tr(struct inode *inode, struct file *filp)
{
struct trace_array *tr = inode->i_private;
...
filp->private_data = inode->i_private; // &global_trace
return 0;
}
static ssize_t
rb_simple_write(struct file *filp, const char __user *ubuf,
size_t cnt, loff_t *ppos)
{
struct trace_array *tr = filp->private_data; /* &global_trace */
struct ring_buffer *buffer = tr->trace_buffer.buffer;
...
ret = kstrtoul_from_user(ubuf, cnt, 10, &val);
...
if (buffer) {
mutex_lock(&trace_types_lock);
if (!!val == tracer_tracing_is_on(tr)) {
...
} else if (val) { /* 使能 trace */
tracer_tracing_on(tr); /* 使能 trace ring buffer 写 */
if (tr->current_trace->start)
tr->current_trace->start(tr); /* function_trace_start() */
} else {
...
}
mutex_unlock(&trace_types_lock);
}
...
}
c
/* kernel/trace/trace_functions.c */
static void function_trace_start(struct trace_array *tr)
{
/* 重置每个 online cpu 的 trace ring buffer */
tracing_reset_online_cpus(&tr->trace_buffer);
}
2.2.1.3 调用函数时触发 function trace 接口
CONFIG_DYNAMIC_FTRACE=n 时,Linux 内核跟踪绝大多数函数(少数函数不被跟踪,如 ftrace 子系统的函数,static 函数也可能不被跟踪),在这些被跟踪函数被调用时,由于 CONFIG_FUNCTION_TRACER=y 导致编译器在函数头部插入了 __gnu_mcount_nc 函数调用,所以每个函数都会调用 __gnu_mcount_nc,ARM32 架构下的 __gnu_mcount_nc 函数如下:
c
ENTRY(__gnu_mcount_nc)
UNWIND(.fnstart)
#ifdef CONFIG_DYNAMIC_FTRACE
...
#else
__mcount
#endif
ENDPROC(__gnu_mcount_nc)
.macro __mcount suffix
//mcount_enter
stmdb sp!, {r0-r3, lr}
ldr r0, =ftrace_trace_function
ldr r2, [r0]
adr r0, .Lftrace_stub
cmp r0, r2
bne 1f // 如果 ftrace_trace_function != ftrace_stub,转去执行 function_trace_call(),否则执行 ftrace_stub
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
// 这里不关注 function graph tracer
#endif
//mcount_exit
ldr lr, [fp, #-4]
ldmia sp!, {r0-r3, pc}
//mcount_get_lr r1 @ lr of instrumented func
1: ldr r1, [fp, #-4] @ lr of instrumented func
//mcount_adjust_addr r0, lr @ instrumented function
bic r0, lr, #1 @ clear the Thumb bit if present
sub r0, r0, #MCOUNT_INSN_SIZE @ instrumented function
badr lr, 2f
mov pc, r2 // 跳转到 function_trace_call()
//mcount_exit
2: ldr lr, [fp, #-4]
ldmia sp!, {r0-r3, pc}
.endm
...
ENTRY(ftrace_stub)
.Lftrace_stub:
ret lr
ENDPROC(ftrace_stub)
触发简要流程如下图:

最后看下 function_trace_call() 写函数追踪信息到 trace ring buffer 的扼要:
c
/* kernel/trace/trace_functions.c */
static void
function_trace_call(unsigned long ip, unsigned long parent_ip,
struct ftrace_ops *op, struct pt_regs *pt_regs)
{
struct trace_array *tr = op->private; // &global_trace
struct trace_array_cpu *data;
unsigned long flags;
int bit;
int cpu;
int pc; /* 抢占计数 */
...
pc = preempt_count(); /* 读取当前 CPU 上抢占计数 */
preempt_disable_notrace(); /* CPU 禁用抢占 */
...
cpu = smp_processor_id();
data = per_cpu_ptr(tr->trace_buffer.data, cpu);
if (!atomic_read(&data->disabled)) {
local_save_flags(flags); /* 中断标志位 */
/* 写 被跟踪函数的 IP 信息到 trace ring buffer */
trace_function(tr, ip, parent_ip, flags, pc);
}
...
out:
preempt_enable_notrace(); /* CPU 重新使能抢占 */
}
c
/* kernel/trace/trace.c */
/* 写 被跟踪函数的 IP 信息到 trace ring buffer */
void
trace_function(struct trace_array *tr,
unsigned long ip, unsigned long parent_ip, unsigned long flags,
int pc)
{
struct trace_event_call *call = &event_function;
struct ring_buffer *buffer = tr->trace_buffer.buffer;
struct ring_buffer_event *event;
struct ftrace_entry *entry;
event = __trace_buffer_lock_reserve(buffer, TRACE_FN, sizeof(*entry),
flags, pc);
...
entry = ring_buffer_event_data(event);
entry->ip = ip;
entry->parent_ip = parent_ip;
if (!call_filter_check_discard(call, entry, buffer, event)) {
...
__buffer_unlock_commit(buffer, event);
}
}
到此,对 CONFIG_DYNAMIC_FTRACE=n 情形的分析已经完毕。
2.2.2 CONFIG_DYNAMIC_FTRACE=y 情形下的细节
2.2.2.1 初始化
首先,在 ftrace_init() 中将工具 scripts/recordmcount.c 组织在 [__start_mcount_loc, __stop_mcount_loc] 内对应地址的 __gnu_mcount_nc 调用指令全部替换为 nop:
c
start_kernel()
ftrace_init()
c
/* kernel/trace/ftrace.c */
void __init ftrace_init(void)
{
extern unsigned long __start_mcount_loc[];
extern unsigned long __stop_mcount_loc[];
unsigned long count, flags;
int ret;
...
count = __stop_mcount_loc - __start_mcount_loc;
...
pr_info("ftrace: allocating %ld entries in %ld pages\n",
count, count / ENTRIES_PER_PAGE + 1);
last_ftrace_enabled = ftrace_enabled = 1;
/* 为所有 call mcount 分配存储空间 ftrace_page ,然后将这些指令初始替换为 nop 指令 */
ret = ftrace_process_locs(NULL,
__start_mcount_loc,
__stop_mcount_loc);
set_ftrace_early_filters(); /* 按内核命令行参数配置 ftrace 的 filter */
return;
...
}
static int ftrace_process_locs(struct module *mod,
unsigned long *start,
unsigned long *end)
{
struct ftrace_page *start_pg;
struct ftrace_page *pg;
struct dyn_ftrace *rec;
unsigned long count;
unsigned long *p;
unsigned long addr;
unsigned long flags = 0; /* Shut up gcc */
int ret = -ENOMEM;
count = end - start;
...
/* 将所有的 mcount 地址按升序排列 */
sort(start, count, sizeof(*start),
ftrace_cmp_ips, NULL);
/* 分配页面存储 mcount 对象 (struct dyn_ftrace) */
start_pg = ftrace_allocate_pages(count);
...
mutex_lock(&ftrace_lock);
if (!mod) { /* 非 module */
...
/* First initialization */
ftrace_pages = ftrace_pages_start = start_pg;
} else {
...
}
p = start;
pg = start_pg;
while (p < end) { /* 遍历所有 mcount */
addr = ftrace_call_adjust(*p++);
/*
* Some architecture linkers will pad between
* the different mcount_loc sections of different
* object files to satisfy alignments.
* Skip any NULL pointers.
*/
if (!addr)
continue;
if (pg->index == pg->size) { /* 当前 ftrace_page 已存满 dyn_ftrace (指代 mcount) */
/* We should have allocated enough */
if (WARN_ON(!pg->next))
break;
pg = pg->next; /* 使用下一个 ftrace_page */
}
/* 为 @addr 指代的当前 mcount 分配并初始一个 dyn_ftrace 对象 */
rec = &pg->records[pg->index++];
rec->ip = addr; /* 记录 mcount 调用指令的地址 */
}
/* We should have used all pages */
WARN_ON(pg->next);
/* Assign the last page to ftrace_pages */
ftrace_pages = pg;
/*
* We only need to disable interrupts on start up
* because we are modifying code that an interrupt
* may execute, and the modification is not atomic.
* But for modules, nothing runs the code we modify
* until we are finished with it, and there's no
* reason to cause large interrupt latencies while we do it.
*/
if (!mod)
local_irq_save(flags);
ftrace_update_code(mod, start_pg); /* 将所有 ftrace_page @new_pgs 内的 call mcount 替换为 nop 指令 */
if (!mod)
local_irq_restore(flags);
ret = 0;
out:
mutex_unlock(&ftrace_lock);
return ret;
}
在这里,为所有 __gnu_mcount_nc 调用指令分配了 dyn_ftrace 对象,记录其信息,然后 ftrace_update_code() 逐条将所有 __gnu_mcount_nc 调用指令替换为 nop。
2.2.2.2 设置 function 为当前 tracer & 启用
- 设置 function 为当前 tracer
写文件 /sys/kernel/debug/tracing/current_tracer 设置当前的 tracer 类型为 function:
cd /sys/kernel/debug/tracing
echo function > current_tracer
对比 CONFIG_DYNAMIC_FTRACE=n 的情形,流程从 ftrace_startup() 开始不同:
c
tracing_set_trace_write()
tracing_set_tracer()
tracer_init()
function_trace_init()
ftrace_init_array_ops()
tracing_start_function_trace()
register_ftrace_function()
ftrace_ops_init()
ftrace_startup()
...
__register_ftrace_function()
...
ftrace_startup_enable()
c
/* kernel/trace/ftrace.c */
static int __register_ftrace_function(struct ftrace_ops *ops)
{
...
/* 注册 ftrace_ops @ops 到列表 @ftrace_ops_list 头部 */
add_ftrace_ops(&ftrace_ops_list, ops);
...
if (ftrace_enabled)
update_ftrace_function();
...
}
static void update_ftrace_function(void)
{
ftrace_func_t func;
set_function_trace_op = rcu_dereference_protected(ftrace_ops_list,
lockdep_is_held(&ftrace_lock));
if (set_function_trace_op == &ftrace_list_end) {
func = ftrace_stub;
} else if (rcu_dereference_protected(ftrace_ops_list->next,
lockdep_is_held(&ftrace_lock)) == &ftrace_list_end) { /* 只有一个注册的 ftrace_ops, 则没得选, 直接使用它即可 */
func = ftrace_ops_get_list_func(ftrace_ops_list); // function_trace_call()
} else {
/*
* 有多个注册的 ftrace_ops, 则将 trace 接口设为 ftrace_ops_list_func(),
* 让 ftrace_ops_list_func() 遍历这些 ftrace_ops.
*/
/* Just use the default ftrace_ops */
set_function_trace_op = &ftrace_list_end;
func = ftrace_ops_list_func;
}
...
ftrace_trace_function = func; // ftrace_trace_function = ftrace_ops_list_func
}
c
/* kernel/trace/ftrace.c */
static void ftrace_startup_enable(int command)
{
...
ftrace_run_update_code(command);
}
static void ftrace_run_update_code(int command)
{
...
arch_ftrace_update_code(command);
...
}
c
/* arch/arm/kernel/ftrace.c */
void arch_ftrace_update_code(int command)
{
stop_machine(__ftrace_modify_code, &command, NULL);
}
static int __ftrace_modify_code(void *data)
{
int *command = data;
set_kernel_text_rw();
ftrace_modify_all_code(*command);
set_kernel_text_ro();
return 0;
}
c
/* kernel/trace/ftrace.c */
void ftrace_modify_all_code(int command)
{
int update = command & FTRACE_UPDATE_TRACE_FUNC;
int err = 0;
if (update) {
/*
* 将 {ftrace_call,ftrace_regs_call,ftrace_call_old} 标号处的
* bl ftrace_stub 指令替换为 bl ftrace_ops_list_func.
*/
err = ftrace_update_ftrace_func(ftrace_ops_list_func);
...
}
if (command & FTRACE_UPDATE_CALLS)
ftrace_replace_code(1); /* 将 call mount 处的 nop 替换成 bl ftrace_caller */
else if (command & FTRACE_DISABLE_CALLS)
...
...
}
在 ftrace_modify_all_code() 函数中做了两件核心工作:
- 将
ftrace_caller函数中,ftrace_call符号处的bl ftrace_stub指令替换成bl ftrace_ops_list_func。 - 将
被追踪函数的call mcount处的nop替换成bl ftrace_caller。
这样被追踪函数就形成了如下的调用链:
c
xxx_function() -> ftrace_caller() -> ftrace_ops_list_func() -> function_trace_call()
可以通过 GDB 调试内核,在执行命令 echo function > /sys/kernel/debug/tracing/current_tracer 后,在任意函数下断点,然后反汇编任意没有优化掉的函数来观察确认指令替换情况,下图是一个例子:

2.2.2.3 调用函数时触发 function trace 接口
正如上一小节锁分析的,被追踪函数就形成了如下的调用链:
c
xxx_function() -> ftrace_caller() -> ftrace_ops_list_func() -> function_trace_call()
function_trace_call() 将函数追踪信息写入 trace ring buffer,这在 CONFIG_DYNAMIC_FTRACE=n 情形下的 2.2.1.3 小结已经分析过了,这里就不再赘述。
2.3 小结
CONFIG_DYNAMIC_FTRACE=n 情形下,对函数的 mcount 调用一直存在,差别在于 ftrace_stub (未开启函数追踪时) 比 function_trace_call()(未开启函数追踪时) 开销更小。无法进行函数过滤,因为没有 set_ftrace_filter, set_ftrace_notrace 文件接口。
CONFIG_DYNAMIC_FTRACE=y 实际是对 CONFIG_DYNAMIC_FTRACE=n 的优化,没有开启函数追踪时,所有的 mcount 调用都被替换成了 nop,开销几乎为 0;在开启函数追踪时,所有或过滤器()指定的 mcount 处的指令被替换成 ftrace_caller()。
3. 参考资料
1\] [Function Tracer Design](https://kernel-doc.readthedocs.io/zh-cn/latest/trace/ftrace-design.html "Function Tracer Design")