Linux内核 eBPF基础:ftrace源码分析:过滤函数和开启追踪

Linux内核 eBPF基础
ftrace基础:过滤函数和开启追踪


荣涛
2021年5月12日

本文相关注释代码:https://github.com/Rtoax/linux-5.10.13
上篇文章:Linux内核 eBPF基础:ftrace基础
推荐Presentation:《Ftrace Kernel Hooks: More than just tracing》

图片链接:http://tinylab.org/ftrace-principle-and-practice/

1. 从set_ftrace_filter讲起

/sys/kernel/debug/tracing/tracing_on

su
cd /sys/kernel/debug/tracing/
echo 1 > tracing_on
echo "schedule" > set_ftrace_filter
echo function_graph > current_tracer
cat trace

部分输出:

   1) ! 66730.47 us |  }
 ------------------------------------------
   1)  contain-2264  =>  contain-2147 
 ------------------------------------------

   1) ! 66717.12 us |  } /* schedule */
   1)   3.683 us    |  schedule();
   1)               |  schedule() {
 ------------------------------------------
   1)  contain-2147  =>  contain-2175 
 ------------------------------------------

那么以上流程在内核中都经历了什么呢?让我们细细道来。

2. echo schedule > set_ftrace_filter

在内核中kernel\trace\ftrace.c代码有:

	trace_create_file("set_ftrace_filter", 0644, parent,
			  ops, &ftrace_filter_fops);

看一下文件操作符ftrace_filter_fops

static const struct file_operations ftrace_filter_fops = {
	.open = ftrace_filter_open,
	.read = seq_read,
	.write = ftrace_filter_write,
	.llseek = tracing_lseek,
	.release = ftrace_regex_release,
};

显然,这里对应ftrace_filter_write

2.1. ftrace_filter_write函数

该函数很简单:

ssize_t /* echo schedule > set_ftrace_filter */
ftrace_filter_write(struct file *file, const char __user *ubuf,
		    size_t cnt, loff_t *ppos)
{
	return ftrace_regex_write(file, ubuf, cnt, ppos, 1);
}

调用了ftrace_regex_write,首先从打开的文件私有数据中获取struct ftrace_iterator结构,

	if (file->f_mode & FMODE_READ) {
		struct seq_file *m = file->private_data;
		iter = m->private;
	} else
		iter = file->private_data;

这个结构中包含了太多有用的信息:

struct ftrace_iterator {
	loff_t				pos;
	loff_t				func_pos;
	loff_t				mod_pos;
	struct ftrace_page		*pg;
	struct dyn_ftrace		*func;
	struct ftrace_func_probe	*probe;
	struct ftrace_func_entry	*probe_entry;
	struct trace_parser		parser;
	struct ftrace_hash		*hash;
	struct ftrace_ops		*ops;
	struct trace_array		*tr;
	struct list_head		*mod_list;
	int				pidx;
	int				idx;
	unsigned			flags;
};

首先使用函数trace_get_user将用户字符串写入struct trace_parser结构中。接着执行下面代码:

	if (read >= 0 && trace_parser_loaded(parser) &&
	    !trace_parser_cont(parser)) {
		ret = ftrace_process_regex(iter, parser->buffer,
					   parser->idx, enable);
		trace_parser_clear(parser);
		if (ret < 0)
			goto out;
	}

我们直接看ftrace_process_regex,此时enable=1

2.2. ftrace_process_regex函数

函数首先将获取func,在我们的例子中,func="schedul",然后会调用下面的代码:

	func = strsep(&next, ":");

	if (!next) {
		ret = ftrace_match_records(hash, func, len);
		if (!ret)
			ret = -EINVAL;
		if (ret < 0)
			return ret;
		return 0;
	}

ftrace_match_records进一步会调用match_records,这里可以得知,match_records的入参为:

match_records(hash, "schedul", len, NULL);

2.3. match_records函数

首选调用filter_parse_regex进行正则表达式匹配,匹配的类型包括:

enum regex_type {
	MATCH_FULL = 0,
	MATCH_FRONT_ONLY,
	MATCH_MIDDLE_ONLY,
	MATCH_END_ONLY,
	MATCH_GLOB,
	MATCH_INDEX,
};

因为我们的例子是func="schedul",所以类型为MATCH_FULL。接着运行下面的代码:

	do_for_each_ftrace_rec(pg, rec) {

		if (rec->flags & FTRACE_FL_DISABLED)
			continue;

		if (ftrace_match_record(rec, &func_g, mod_match, exclude_mod)) {
			ret = enter_record(hash, rec, clear_filter);
			if (ret < 0) {
				found = ret;
				goto out_unlock;
			}
			found = 1;
		}
	} while_for_each_ftrace_rec();

首先是两个宏定义:

#define do_for_each_ftrace_rec(pg, rec)					\
	for (pg = ftrace_pages_start; pg; pg = pg->next) {		\
		int _____i;						\
		for (_____i = 0; _____i < pg->index; _____i++) {	\
			rec = &pg->records[_____i];

#define while_for_each_ftrace_rec()		\
		}				\
	}

最重要的是全局变量ftrace_pages_start,我们需要细致讲解一下:

static struct ftrace_page	*ftrace_pages_start;    /* ftrace 起始地址 */
                                                    /* 初始化位置 ftrace_process_locs() */
static struct ftrace_page	*ftrace_pages;          /* 同上,用于定位链表中最后一个 pg */

2.4. ftrace_pages_startftrace_pages

这两个全局变量是在ftrace_process_locs中初始化的,他是在ftrace_init中被调用,这在《Linux内核 eBPF基础:ftrace基础-ftrace_init初始化》有详细介绍,他的调用如下:

	ret = ftrace_process_locs(NULL,
				  __start_mcount_loc,
				  __stop_mcount_loc);

在我的系统中为:

# cat /proc/kallsyms | grep mcount_loc
ffffffffaf0d75d0 T __start_mcount_loc
ffffffffaf1110e0 T __stop_mcount_loc

page大小可以计算得出。

在遍历过程中,每一项用struct dyn_ftrace标识,在循环中调用ftrace_match_record

2.5. ftrace_match_record函数

他的入参为:

ftrace_match_record(
    (struct dyn_ftrace*)rec,
    (struct ftrace_glob*) {
        .search = "schedule",
        .type = MATCH_FULL,
        .len = strlen("schedule"),
        },
    (struct ftrace_glob*)NULL,
    0
)

该函数首先调用:

	kallsyms_lookup(rec->ip, NULL, NULL, &modname, str);

他的操作如下,即获取如下信息:

# cat /proc/kallsyms | grep " schedule"
[...]
ffffffffae97f1a0 T schedule
[...]

步骤如下:

  • 使用is_ksym_addr判断是否在内核中;
  • 使用kallsyms_expand_symbol获取ip对应的函数名(str);
  • 因为不在模块中,所以if (mod_g)分支不执行;
  • 执行ftrace_match函数;

2.6. ftrace_match函数

他的入参为:

ftrace_match(
    "schedule",
    (struct ftrace_glob*) {
        .search = "schedule",
        .type = MATCH_FULL,
        .len = strlen("schedule"),
        },
)

因为我没有使用正则表达式,所以我们的type为MATCH_FULL,也就是字符串完全匹配("schedule")。所以我匹配的代码是:

	case MATCH_FULL:
		if (strcmp(str, g->search) == 0)
			matched = 1;
		break;

也就是说匹配上了。

这时,ftrace_match_record返回匹配结果1。然后调用enter_record函数。

2.7. enter_record函数

他的入参为:

enter_record(
    (struct ftrace_hash *)hash,
    (struct dyn_ftrace *)rec,
    0
)

这里的hash对应struct ftrace_iterator *iter中的struct ftrace_hash *hash结构(iter为file文件的私有数据iter = file->private_data;)。

首先使用ftrace_lookup_ip从哈希结构中查找到这个ip对应的entry结构,他的结构为:

struct ftrace_func_entry {
	struct hlist_node hlist;
	unsigned long ip;
	unsigned long direct; /* for direct lookup only */
};

如果存在,直接退出:

	entry = ftrace_lookup_ip(hash, rec->ip);
	if (clear_filter) {
		/* Do nothing if it doesn't exist */
		if (!entry)
			return 0;

		free_hash_entry(hash, entry);
	} else {
		/* Do nothing if it exists */
		if (entry)
			return 0;

		ret = add_hash_entry(hash, rec->ip);
	}

至此,该函数返回,导致

		if (ftrace_match_record(rec, &func_g, mod_match, exclude_mod)) {
			ret = enter_record(hash, rec, clear_filter);
			if (ret < 0) {
				found = ret;
				goto out_unlock;
			}
			found = 1;
		}

在遍历所有page之后,match_records返回1,所以ftrace_match_records返回1,在函数ftrace_process_regex中,

		ret = ftrace_match_records(hash, func, len);
		if (!ret)
			ret = -EINVAL;
		if (ret < 0)
			return ret;
		return 0;

返回0

至此,ftrace_regex_write函数返回,也就是说,我们在中断中执行的

echo schedule > set_ftrace_filter

返回了。

那么,关于schedule的ftrace是如何使用的呢?

3. echo function_graph > current_tracer

为什么是function_graph?请使用下面的命令查看

cat available_tracers 
hwlat blk function_graph wakeup_dl wakeup_rt wakeup function nop

在内核中kernel\trace\trace.c代码有:

    /* /sys/kernel/debug/tracing/current_tracer */
	trace_create_file("current_tracer", 0644, d_tracer,
			tr, &set_tracer_fops);

也就是他对应的文件操作符为:

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,
};

那我们就要看tracing_set_trace_write函数了。

3.1. tracing_set_trace_write函数

函数原型为:

static ssize_t
tracing_set_trace_write(struct file *filp, const char __user *ubuf,
			size_t cnt, loff_t *ppos)

首先将用户字符串拷贝到内核copy_from_user(buf, ubuf, cnt),然后取出空格

	for (i = cnt - 1; i > 0 && isspace(buf[i]); i--)
		buf[i] = 0;

然后调用tracing_set_tracer函数。

3.2. tracing_set_tracer函数

struct trace_array *tr = filp->private_data;

他的入参为:

tracing_set_tracer(
    (struct trace_array *)tr,
    "function_graph"
)

struct trace_array可以认为是一块缓冲区,当ftrace时间发生时需要对这段ring-buffer读写。

而对于function_graph

cat available_tracers 
hwlat blk function_graph wakeup_dl wakeup_rt wakeup function nop

以上每个类型对应一个struct tracer结构,他的定义如下(部分):

struct tracer { /*  */
	const char		*name;
	int			    (*init)(struct trace_array *tr);
	void			(*reset)(struct trace_array *tr);
	void			(*start)(struct trace_array *tr);

在函数中或作这样的查找:

	for (t = trace_types; t; t = t->next) {
		if (strcmp(t->name, buf) == 0)
			break;
	}

也就是找到function_graph对应的struct tracer结构,他是在源文件kernel/trace/trace_functions_graph.c中定义的:

static struct tracer __tracer_data graph_trace  = {
	.name		= "function_graph",
	.update_thresh	= graph_trace_update_thresh,
	.open		= graph_trace_open,
	.pipe_open	= graph_trace_open,
	.close		= graph_trace_close,
	.pipe_close	= graph_trace_close,
	.init		= graph_trace_init,
	.reset		= graph_trace_reset,
	.print_line	= print_graph_function,
	.print_header	= print_graph_headers,
	.flags		= &tracer_flags,
	.set_flag	= func_graph_set_flag,
#ifdef CONFIG_FTRACE_SELFTEST
	.selftest	= trace_selftest_startup_function_graph,
#endif
};

这里我将跳过很多判断和鉴权,仅仅对函数调用感兴趣。

首先使用reset将之前的tracer重置

	if (tr->current_trace->reset)
		tr->current_trace->reset(tr);   

加入上次使用function_graph,本次使用function,那么此处就会调用function_graph对应的reset函数,即graph_trace_reset,简单看下它做了什么:

static void graph_trace_reset(struct trace_array *tr)
{
	tracing_stop_cmdline_record();
	if (tracing_thresh)
		unregister_ftrace_graph(&funcgraph_thresh_ops);
	else
		unregister_ftrace_graph(&funcgraph_ops);
}

简言之,就是注销一系列的东西(断点,进程调度相关内容等)。

然后就是使用初始化函数初始化struct tracer

	if (t->init) {
		ret = tracer_init(t, tr);
		if (ret)
			goto out;
	}

3.3. tracer_init函数

入参:

tracer_init(
    (struct tracer*)t,
    (struct trace_array *)tr
)

函数实现很简单:

int tracer_init(struct tracer *t, struct trace_array *tr)
{
	tracing_reset_online_cpus(&tr->array_buffer);
	return t->init(tr);
}

首先使用tracing_reset_online_cpus重置buffer,然后调用tracer对应init函数,function_graph对应的init为graph_trace_init

3.4. graph_trace_init函数

static int graph_trace_init(struct trace_array *tr)
{
	int ret;

	set_graph_array(tr);
	if (tracing_thresh)
		ret = register_ftrace_graph(&funcgraph_thresh_ops);
	else
		ret = register_ftrace_graph(&funcgraph_ops);
	if (ret)
		return ret;
	tracing_start_cmdline_record();

	return 0;
}

首先初始化array

void set_graph_array(struct trace_array *tr)
{
	graph_array = tr;

	/* Make graph_array visible before we start tracing */

	smp_mb();
}

内存屏障smp_mb();的作用:让graph_array在开始tracing之前可见。

然后调用register_ftrace_graph,函数中设置了对应的全局函数指针:

ftrace_graph_return = trace_graph_return;
__ftrace_graph_entry = trace_graph_entry;
ftrace_graph_entry = ftrace_graph_entry_test;

使用register_pm_notifier注册电源管理同质量,这里不讨论。

调用start_graph_tracing函数。

3.5. start_graph_tracing函数

	ftrace_graph_active++;
	ret = start_graph_tracing();
	if (ret) {
		ftrace_graph_active--;
		goto out;
	}

首先申请存放栈的内存空间:

	ret_stack_list = kmalloc_array(FTRACE_RETSTACK_ALLOC_SIZE,
				       sizeof(struct ftrace_ret_stack *),
				       GFP_KERNEL);

默认大小为:

#define FTRACE_RETFUNC_DEPTH 50
#define FTRACE_RETSTACK_ALLOC_SIZE 32

然后是对idle进程的设置:

	/* The cpu_boot init_task->ret_stack will never be freed */
	for_each_online_cpu(cpu) {
		if (!idle_task(cpu)->ret_stack)
			ftrace_graph_init_idle_task(idle_task(cpu), cpu);
	}

然后尝试在FTRACE_RETSTACK_ALLOC_SIZE任务上分配一个返回堆栈数组。

	do {
		ret = alloc_retstack_tasklist(ret_stack_list);
	} while (ret == -EAGAIN);

然后激活tracepoint

		ret = register_trace_sched_switch(ftrace_graph_probe_sched_switch, NULL);
		if (ret)
			pr_info("ftrace_graph: Couldn't activate tracepoint"
				" probe to kernel_sched_switch\n");

这里可参见上面提到的tracing_set_tracer函数:

	if (tr->current_trace->reset)
		tr->current_trace->reset(tr);

然后就进入了 ftrace_startup函数。

3.6. ftrace_startup函数

register_ftrace_function函数内部会调用这个函数。

他的入参为:

ftrace_startup(&graph_ops, FTRACE_START_FUNC_RET)

其中graph_ops为:

static struct ftrace_ops graph_ops = {
	.func			= ftrace_stub,
	.flags			= FTRACE_OPS_FL_RECURSION_SAFE |
				   FTRACE_OPS_FL_INITIALIZED |
				   FTRACE_OPS_FL_PID |
				   FTRACE_OPS_FL_STUB,
#ifdef FTRACE_GRAPH_TRAMP_ADDR
	.trampoline		= FTRACE_GRAPH_TRAMP_ADDR,
	/* trampoline_size is only needed for dynamically allocated tramps */
#endif
	ASSIGN_OPS_HASH(graph_ops, &global_ops.local_hash)
};

该函数首先调用__register_ftrace_function函数。

3.6.1. __register_ftrace_function函数

函数入参为graph_ops

首先调用函数add_ftrace_ops(&ftrace_ops_list, ops)ftrace_ops_list

struct ftrace_ops __rcu __read_mostly *ftrace_ops_list  = &ftrace_list_end;

ftrace_list_end为:

struct ftrace_ops __read_mostly ftrace_list_end  = {
	.func		= ftrace_stub,
	.flags		= FTRACE_OPS_FL_RECURSION_SAFE | FTRACE_OPS_FL_STUB,
	INIT_OPS_HASH(ftrace_list_end)
};

最终,生成的链表为(graph_opsftrace_ops_list是一个节点):

ftrace_ops_list->graph_ops->ftrace_list_end

add_ftrace_ops函数刚刚运行结束后,func是这样的:

  链表头
    |
graph_ops->func = ftrace_stub
    |
ftrace_list_end->func = ftrace_stub
    |
  链表尾

也就是目前他们的func均为ftrace_stub

3.6.2. ftrace_update_trampoline函数

大致流程为

  • 调用arch_ftrace_update_trampoline架构相关的函数创建蹦床结构;
    • 调用create_trampoline创建蹦床并赋值;
    • 调用calc_trampoline_call_offset计算指令偏移;
    • 调用ftrace_call_replace生成并替换指令;
    • 调用text_poke_bp进行指令替换

下面对arch_ftrace_update_trampoline函数内部流程进行详述。

3.6.2.1. create_trampoline函数

为了简化流程,我们默认用这段代码分支:

	start_offset = (unsigned long)ftrace_caller;
	end_offset = (unsigned long)ftrace_caller_end;
	op_offset = (unsigned long)ftrace_caller_op_ptr;
	call_offset = (unsigned long)ftrace_call;
	jmp_offset = 0;

在我的系统中为:

ffffffffae98f840 T ftrace_caller
ffffffffae98f89c T ftrace_call

然后申请蹦床需要的内存:

trampoline = alloc_tramp(size + RET_SIZE + sizeof(void *));

这里的大小非常讲究size + RET_SIZE + sizeof(void *),我将在下文中解释:

+--------+    start_offset=ftrace_caller
|        |
|        |
|  size  |
|        |
+--------+    end_offset=ftrace_caller_end
|RET_SIZE|
+--------+
| void * |
+--------+

然后将蹦床函数拷贝过去:

ret = copy_from_kernel_nofault(trampoline, (void *)start_offset, size);

用ip指向end之后:

ip = trampoline + size;

也就是:

+--------+    start_offset=ftrace_caller
|        |
|        |
|  size  |
|        |
+--------+    end_offset=ftrace_caller_end 
|RET_SIZE|    <--ip
+--------+
| void * |
+--------+

然后将ftrace_stub拷贝到ip

	retq = (unsigned long)ftrace_stub;
	ret = copy_from_kernel_nofault(ip, (void *)retq, RET_SIZE);
+--------+    start_offset=ftrace_caller
|        |
|        |    
|  size  |
|        |
+--------+    end_offset=ftrace_caller_end 
|RET_SIZE|    ftrace_stub    <--ip
+--------+
| void * | ---> graph_ops
+--------+

接着,计算void *位置,并将ops赋值,

	ptr = (unsigned long *)(trampoline + size + RET_SIZE);
	*ptr = (unsigned long)ops;

已知在源文件中:

SYM_FUNC_START(ftrace_caller)
	/* save_mcount_regs fills in first two parameters */
	save_mcount_regs

SYM_INNER_LABEL(ftrace_caller_op_ptr, SYM_L_GLOBAL)
	/* Load the ftrace_ops into the 3rd parameter */
	movq function_trace_op(%rip), %rdx

	/* regs go into 4th parameter (but make it NULL) */
	movq $0, %rcx

SYM_INNER_LABEL(ftrace_call, SYM_L_GLOBAL)
	call ftrace_stub

	restore_mcount_regs

	/*
	 * The code up to this label is copied into trampolines so
	 * think twice before adding any new code or changing the
	 * layout here.
	 */
SYM_INNER_LABEL(ftrace_caller_end, SYM_L_GLOBAL)

	jmp ftrace_epilogue
SYM_FUNC_END(ftrace_caller);

也就是说现在是这样的:

+--------+    ftrace_caller(start_offset)
|        |
|        |    ftrace_caller_op_ptr(op_offset)
|        |
|  size  |    ftrace_call(call_offset)
|        |
+--------+    ftrace_caller_end(end_offset)
|RET_SIZE|    ftrace_stub    <--ip
+--------+
| void * | ---> graph_ops
+--------+

也就是说,对于本文的schedule是这样的:

<schedule>:
push %rbp
mov %rsp,%rbp
call ftrace_caller
pop %rbp
[…]
ftrace_caller:
    save regs
    load args
ftrace_call:
    call func
    restore regs
ftrace_stub:
    retq

关于函数ftrace_update_trampoline我们就讲到这,需要说明的是,如果要细讲,还有提多的东西需要说明。

3.6.3. update_ftrace_function函数

该函数会替换func函数,这里我们姑且认为它是func = ftrace_ops_list_func;
同时,该函数中最终会调用追踪函数(链表):

op->func(ip, parent_ip, op, regs);

至此,__register_ftrace_function返回。

接下来就是一系列的使能:

  • ftrace_hash_ipmodify_enable
  • ftrace_hash_rec_enable
  • ftrace_startup_enable

至此,开始返回到用户空间:

  • ftrace_startup返回,
  • register_ftrace_graph返回,
  • graph_trace_init返回,
  • tracer_init返回,
  • tracing_set_tracer返回,
  • tracing_set_trace_write返回,
  • 指令echo function_graph > current_tracer返回。

4. cat trace命令

在内核中kernel\trace\trace.c代码有:

	trace_create_file("trace", 0644, d_tracer,
			  tr, &tracing_fops);

文件操作符为:

static const struct file_operations tracing_fops = {
	.open		= tracing_open,
	.read		= seq_read,
	.write		= tracing_write_stub,
	.llseek		= tracing_lseek,
	.release	= tracing_release,
};

2021年5月14日19:02:35,不要意思,要下班了,我要早点下班去,怕有长进着急。有时间在继续完成下面的部分,会在另一篇文章中继续讨论。

5. 蹦床trampoline函数

TODO

5.1. ftrace_stub函数

5.2. ftrace_caller函数

5.3. ftrace_caller_end函数

5.4. ftrace_caller_op_ptr函数

5.5. ftrace_call函数

5.6. ftrace_ops_list_func函数

6. 参考和相关链接

附录

参考路径

  • kernel/include/asm-generic/vmlinux.lds.h
  • /sys/kernel/debug/tracing/

register_ftrace_function内核路径

//kernel/kprobes.c
enable_kprobe
    arm_kprobe
        arm_kprobe_ftrace
            __arm_kprobe_ftrace
                register_ftrace_function
//kernel/trace/trace_events.c
// late_initcall(event_trace_self_tests_init);
event_trace_self_tests_init
    event_trace_self_test_with_function
        register_ftrace_function(&trace_ops)
//kernel/trace/trace_event_perf.c
perf_ftrace_event_register
    perf_ftrace_function_register
        register_ftrace_function
function_trace_init
    tracing_start_function_trace
        register_ftrace_function
func_set_flag
    register_ftrace_function
init_irqsoff_tracer() {
	register_tracer(&irqsoff_tracer);
}
core_initcall(init_irqsoff_tracer);
static struct tracer irqsoff_tracer  = {
	...
	.init		= irqsoff_tracer_init,
    ...
};
irqsoff_tracer_init
    __irqsoff_tracer_init
        start_irqsoff_tracer
            register_irqsoff_function
                register_ftrace_function
stack_trace_sysctl
    register_ftrace_function
stack_trace_init
    register_ftrace_function

device_initcall(stack_trace_init);
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值