Linux ftrace:function tracer 实现简析

文章目录

  • [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.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_loc section。还是以前面的 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 + ARMv7CONFIG_FUNCTION_TRACER=y 开启 function tracer,在 CONFIG_DYNAMIC_FTRACE 配置开启关闭两种不同的情形下,实现细节上存在较大差异。CONFIG_FUNCTION_TRACERCONFIG_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=nCONFIG_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")

相关推荐
JiMoKuangXiangQu2 小时前
Linux 调度延迟案例 (1):ALSA 播放 XRUN
linux·trace-cmd·xrun·调度延迟
序属秋秋秋2 小时前
《Linux系统编程之进程控制》【进程替换】
linux·c语言·c++·操作系统·进程·系统编程·进程替换
阿拉伯柠檬2 小时前
MySQL内置函数(二)
linux·数据库·mysql·面试
jiedaodezhuti2 小时前
网络安全等级保护:合规基石与风险管理核心
linux
嵌入式@秋刀鱼2 小时前
ROS开发学习记录【一】
linux·c++·笔记·学习
Tipriest_2 小时前
Linux(debian)包管理器aptitude介绍
linux·运维·debian·aptitude
码上宝藏2 小时前
从解耦到拓展:Clapper 0.10.0 插件化架构设计与 Lua 脚本集成
linux·开发语言·lua·视频播放器·clapper
WoY20203 小时前
conda修改镜像源遇到的问题:defaults(默认镜像源)清不干净导致创建环境失败
linux·python·conda
素素.陈3 小时前
调用大模型解析图片中的文字
linux·windows·microsoft