0.前言
本文主要是根据阅码场 《Linux内核tracers的实现原理与应用》视频课程在aarch64上的实践。通过观察钩子函数的创建过程以及替换过程,理解trace的原理。本文同样以blk_update_request函数为例进行说明kprobe的工作原理,此处的kprobe是基于trace event来实现,同时使用了ftrace的框架。
首先让我们大致了解一下trace point、trace event、kprobe之间的关联:
-
tracepoint会被放到代码中,需要提供一个probe函数与之关联,当tracepoint被打开时,当tracepoint执行时,提供的probe函数会被调用,probe函数返回时会继续原来函数的执行。使用trace point有3个步骤:
(1) 在头文件include/trace/events/subsys.h中通过DECLARE_TRACE宏添加tracepoint声明
(2) 在系统文件subsys/file.c中通过DEFINE_TRACE创建trace point
(3)通过register_trace_subsys_eventname将tracepoint与probe关联 -
trace event是建立在tracepoint的基础之上,它可以通过一个宏来实现如上前两步骤,并定义注册和注销函数,通过echo 1 > events/subsys_event来执行注册。从内核4.0及以后都将鼓励使用trace event,不再提倡直接使用trace point。这个可以从 tracepoint的相关sample示例从3.9移除可以得出。使用Tracing event不用像tracingpoint那样需要自己定义probe函数,而且这些probe函数往往要通过模块的方式进行定义,然后加载,而Tracing event提供了TRACE_EVENT宏,可以通过复杂宏帮助定义统一格式的probe函数,而Tracing event需要用户指定trace 信息以何种格式存放到ring buffer中,trace信息将以何种格式打印。
-
kprobe可以理解为动态的trace event,可以在除了__kprobes/nokprobe_inline annotation 和那些标记为 NOKPROBE_SYMBOL的任何函数设置trace event。使用前需要打开内核选项:CONFIG_KPROBE_EVENTS=y.。
kprobe主要有两种使用方法,一是通过模块加载;二是通过debugfs接口。
(1)模块加载的方式:以内核的kprobe_example为例。首先声明一个kprobe结构体,然后定义其中几个关键成员变量,包括symbol_name,pre_handler,post_handler。然后通过register_kprobe函数注册kprobe即可。将kprobe_example.ko inmod进内核之后,每当系统新启动一个进程,比如执行ls,cat等,都会执行pre_handler和post_handler回调。
(2) 通过debugfs接口的方式:可以通过/sys/kernel/debug/tracing/kprobe_events来增加kprobe跟踪点,然后通过写入/sys/kernel/debug/tracing/events/kprobes//enabled使能。
kernel版本:5.10
平台:arm64
1. kprobe的总体原理
注:如下参考自kprobe原理解析(二)
kprobe的工作过程大致如下:
- 注册kprobe。 注册的每个kprobe对应一个kprobe结构体,该结构中记录着插入点(位置),以及该插入点本来对应的指令original_opcode;
- 替换原有指令。 使能kprobe的时候,将插入点位置的指令替换为一条异常(BRK)指令,这样当CPU执行到插入点位置时会陷入到异常态;
- 执行pre_handler。 进入异常态后,首先执行pre_handler,然后利用CPU提供的单步调试(single-step)功能,设置好相应的寄存器,将
下一条指令设置为插入点处本来的指令,从异常态返回;- 再次陷入异常态。 上一步骤中设置了single-step相关的寄存器,所以originnal_opcode刚一执行,便会二进宫:再次陷入异常态,此时将single-step 清除,并且执行post_handler,然后从异常态安全返回。
步骤2,3,4便是一次kprobe工作的过程,它的一个基本思路就是将本来执行一条指令扩展成执行 kprobe->pre_handler =>
指令 => kprobe–>post_hander这样三个过程。
2. kprobe领域模型

当kprobe创建后,形成如上的领域模型
-
trace_kprobe:既包含了kretprobe,又包含了trace_probe
-
kretprobe:用于描述kretprobe
-
trace_probe:用于描述kprobe, 它将与kretprobe公用kprobe结构,其中的args数组就保存了echo到节点的参数
-
kprobe: 用于描述kprobe的核心结构体,会连入全局哈希表kprobe_table
-
trace_probe_event:描述kprobe的trace event,包含了核心结构体trace_event_class和trace_event_call
-
trace_event_class:用于描述trace event的类
-
trace_event_call:是trace_event的封装,会连入全局ftrace_events链表
-
trace_event:主要关联了trace_event_functions结构体, trace_event_functions定义了trace_event的回调,trace_event会连入全局的ftrace_event_list
-
trace_event_functions:定义了trace_event的回调
-
trace_array:用于描述trace的最顶层的结构体,目前ftrace_trace_arrays只有一个全局的trace_array即global_trace,可以看出每个trace_event_call对应一个trace_array,trace_array->event_dir指向/sys/kernel/debug/tracing/events目录
-
trace_event_file: 管理kprobe trace event下所有的文件,通过event_call指向trace_event_call,通过system指向trace_subsystem_dir,通过tr指向trace_array,可见trace_event_file, trace_event_call,trace_array是一一对应的,trace_event_file通过list连入trace_array的events链表
-
trace_subsystem_dir: 管理kprobe trace event的目录,通过entry指向管理的目录节点(/sys/kernel/debug/tracing/events/kprobe),通过tr指向trace_array,通过list连入trace_array的systems链表。从上述图示可以看出,trace_subsystem_dir:本例中就表示events/kprobes目录
trace_event_file,trace_array, trace_event_call,trace_subsystem_dir一一对应
3. kprobe创建
kprobe的工作过程与前述function trace和function graph trace有所区别,但是kprobe仍然复用了ftrace的框架,在执行如下操作后,将执行probes_write
ubuntu@VM-0-9-ubuntu:~$ echo 'p:blk_update blk_update_request request=$arg1 status=$arg2:u8 bytes=$arg3:u32' > /sys/kernel/debug/tracing/kprobe_events
如上p表示kprobe,对于retkprobe则需要改成r;
blk_update为本次trace的名称,可以自己设置;
arg1表示需要跟踪的第一个参数,与其它几个参数一起保存到trace_probe.args数组中
static ssize_t probes_write(struct file *file, const char __user *buffer,size_t count, loff_t *ppos)
\--trace_parse_run_command(file, buffer, count, ppos,create_or_delete_trace_kprobe);
|--char *kbuf, *buf, *tmp
| size_t done = 0
| //分配空间用于存放event命令,本例中为:
| //'p:blk_update blk_update_request request=$arg1 status=$arg2:u8 bytes=$arg3:u32'
|--kbuf = kmalloc(WRITE_BUFSIZE, GFP_KERNEL)
| //将event命令从用户空间拷贝到内核空间
|--copy_from_user(kbuf, buffer + done, size))
|--buf = kbuf;
| //对event命令进行解析
\--trace_run_command(buf, create_or_delete_trace_kprobe)
| //对event参数以空格为分割符,存放到argv数组,此时每个argv一维数组就保存了一个命令参数
| //argv[0]:"p:blk_update", argv[1]:"blk_update_request", argv[2]:"request=$arg1",
| //argv[3]: "status=$arg2:u8",argv[4]:"bytes=$arg3:u32"
|--argv = argv_split(GFP_KERNEL, buf, &argc)
\--create_or_delete_trace_kprobe(argc, argv)
trace_parse_run_command主要用于解析kprobe命令,
(gdb) p *argv@20
$6 = {
0xffff000007467700 "p:blk_update", 0xffff00000746770d "blk_update_request",
0xffff000007467720 "request=$arg1", 0xffff00000746772e "status=$arg2:u8",
0xffff00000746773e "bytes=$arg3:u32", 0x0, 0
.....
3.1 create_or_delete_trace_kprobe
create_or_delete_trace_kprobe(argc, argv)
|--trace_kprobe_create(argc, (const char **)argv)
|--struct trace_kprobe *tk = NULL;
| const char *event = NULL
| //初始化全局trace_probe_log结构体,包含子系统名、trace参数格式、参数个数
|--trace_probe_log_init("trace_kprobe", argc, argv)
| //获取到event名,本例为blk_update
|--event = strchr(&argv[0][1], ':');
| //将字符串blk_update_request转换为unsigned long
|--if (kstrtoul(argv[1], 0, (unsigned long *)&addr))
| trace_probe_log_set_index(1)
| //本例symbol为blk_update_request
| symbol = kstrdup(argv[1], GFP_KERNEL)
| //查询symbol入口地址偏移offset是否在符号表存在,此处是offset为0,检查blk_update_request符号是否存在
| if (kprobe_on_func_entry(NULL, symbol, offset))
| flags |= TPARG_FL_FENTRY
|--trace_probe_log_set_index(0);
| //解析event名称
|--traceprobe_parse_event_name(&event, &group, buf,event - argv[0])
| //分配trace_kprobe结构体
|--tk = alloc_trace_kprobe(group, event, addr, symbol, offset, maxactive,
| argc - 2, is_return);
| |--if (is_return)
| tk->rp.handler = kretprobe_dispatcher;
| else
| tk->rp.kp.pre_handler = kprobe_dispatcher
| //解析event命令行参数,结果保存在tk->tp->args数组中,其中:
| //args[i].name为“=”左侧部分,args[i].comm为“=”右侧部分
|--for (i = 0; i < argc && i < MAX_TRACE_ARGS; i++)
| traceprobe_parse_probe_arg(&tk->tp, i, tmp, flags)
| // 设置trace event打印格式,执行完tk->tp->event->call->print_fmt格式为:
| //"(%lx) request=0x%Lx status=%u bytes=%u\", REC->__probe_ip, REC->request, REC->status, REC->bytes
|--traceprobe_set_print_fmt(&tk->tp, is_return);
| //注册kprobe event
\--register_trace_kprobe(tk)
create_or_delete_trace_kprobe中通过gdb可以查看到argv就是上述echo的部分,注册kprobe_event,注册trace_event_call,注册kprobe,关于这三者的关系,可参考 1.kprobe领域模型 部分。其中最主要的pre_handler为kprobe_dispatcher,同时设置了打印格式,并完成trace_kprobe的注册
-
alloc_trace_kprobe:为trace_kprobe分配空间,主要初始化了kprobe的pre_handler为kprobe_dispatcher,和post_handler
-
traceprobe_set_print_fmt:设置kprobe的打印格式
-
register_trace_kprobe:初始化trace_kprobe.,trace_probe., trace_probe_event, trace_event_call,并注册了trace_event, trace_event_call, kprobe,
3.1.1 register_trace_kprobe
register_trace_kprobe(tk)
| //注册kprobe event
|--register_kprobe_event(tk)
| | //初始化trace_event_call
| |--init_trace_event_call(tk)
| | |--if (trace_kprobe_is_return(tk))
| | | call->event.funcs = &kretprobe_funcs
| | | call->class->fields_array = kretprobe_fields_array;
| | | else
| | | call->event.funcs = &kprobe_funcs;
| | | call->class->fields_array = kprobe_fields_array
| | |--call->flags = TRACE_EVENT_FL_KPROBE
| | | //此函数将作为kprobe trace event使能/禁用的回调
| | \--call->class->reg = kprobe_register
| \--trace_probe_register_event_call(&tk->tp)
| |--struct trace_event_call *call = trace_probe_event_call(tp)
| | //注册trace_event
| |--register_trace_event(&call->event)
| | |--INIT_LIST_HEAD(&event->list)
| | |--event->funcs->raw = trace_nop_print;
| | | event->funcs->hex = trace_nop_print
| | | event->funcs->binary = trace_nop_print
| | \--hlist_add_head(&event->node, &event_hash[key]);
| | //注册trace_event_call
| \--trace_add_event_call(call)
| //注册kprobe
|-- __register_trace_kprobe(tk)
| |--if (trace_kprobe_is_return(tk))
| register_kretprobe(&tk->rp)
| else
| register_kprobe(&tk->rp.kp)
| | //通过kprobe的名字(blk_update_request)查符号表,得到符号地址,p为kprobe
| |--addr = kprobe_addr(p)
| | //此addr为blk_update_request函数
| |--p->addr = addr;
| | p->flags &= KPROBE_FLAG_DISABLED;
| | //因为在使能kprobe event时会在探测函数入口用brk替换原有指令,因此要保存原有指令
| | //首次不会注册kprobe,因此old_p 为空
| |--old_p = get_kprobe(p->addr)
| |--prepare_kprobe(p)
| |--INIT_HLIST_NODE(&p->hlist)
| |--hlist_add_head_rcu(&p->hlist, &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]);
\--dyn_event_add(&tk->devent)
\--list_add_tail(&ev->list, &dyn_event_list)
register_trace_kprobe:初始化trace_kprobe.,trace_probe., trace_probe_event, trace_event_call,并注册了trace_event, trace_event_call, kprobe
-
init_trace_event_call:初始化trace_kprobe.,trace_probe. trace_probe_event. trace_event_call
-
trace_probe_register_event_call: 分别调用了register_trace_event和trace_add_event_call。
(1)register_trace_event将trace_event注册到全局event_hash哈希链表;
(2)trace_add_event_call将trace_event_call注册到全局ftrace_events链表,trace_add_event_call创建trace_event_file,在sys/kernel/debug/tracing/events/kprobes目录下创建blk_update目录,同时在其下创建其它的文件节点 -
__register_trace_kprobe:注册kprobe,这里区分了retkprobe和kprobe,其中register_kprobe会将kprobe注册到全局kprobe_table哈希表中
(1)register_kprobe:完成了kprobe向全局kprobe_table添加哈希节点,完成kprobe的注册,它以插入的位置addr作为哈希值.
(1-1)prepare_kprobe:主要是为探测点触发以后如何返回到blk_update_request原始的指令做准备,p->opcode保存了blk_update_request原始的入口指令;p->ainsn.api.insn保存了blk_update_request原始的入口指令slot;p->ainsn.api.restore保存了原始入口指令的下一条指令的地址,这样当断点指令返回后就可以从这条指令执行,这样就可以沿着原始执行路径执行。
3.1.1.1 trace_add_event_call
trace_add_event_call(call)
|--__register_event(call, NULL)
| |--event_init(call)
| \--list_add(&call->list, &ftrace_events)
\--__add_event_to_tracers(call)
| //本例中ftrace_trace_arrays链表只有一个全局的global_trace, 此处tr为global_trace
|--list_for_each_entry(tr, &ftrace_trace_arrays, list)
__trace_add_new_event(call, tr);
|--struct trace_event_file *file
| //创建trace_event_file,初始化并连入tr->events链表
|--file = trace_create_new_event(call, tr)
| |--file->event_call = call;
| | file->tr = tr
| | atomic_set(&file->sm_ref, 0)
| | atomic_set(&file->tm_ref, 0)
| | INIT_LIST_HEAD(&file->triggers)
| |--list_add(&file->list, &tr->events)
\--event_create_dir(tr->event_dir, file)
trace_add_event_call创建trace_event_file,在sys/kernel/debug/tracing/events/kprobes目录下创建blk_update目录,同时在其下创建其它的文件节点
-
trace_create_new_event:创建trace_event_file,初始化并连入tr->events链表
-
event_create_dir:主要是在"sys/kernel/debug/tracing/events"目录下创建"kprobes"目录,并在sys/kernel/debug/tracing/events/kprobes目录下创建blk_update目录,同时在其下创建其它的文件节点
event_create_dir(tr->event_dir, file)
|--
702




被折叠的 条评论
为什么被折叠?



