1. kprobe event的使用场景
kprobe主要用来跟踪内核函数的调用、入参与返回值(还能获得全局变量的值)。与内核里的静态tracepoint
不同,kprobe几乎可以动态跟踪所有的内核函数(除了内联函数、使用NOKPROBE_SYMBOL标记的函数等,可通过cat /sys/kernel/debug/kprobes/blacklist查看)
2. kprobe event的使用前提
编译内核时打开以下选项:
=y
CONFIG_HAVE_KPROBES=y
=y
3. 通过shell添加kprobe event
kprobe event有两种类型:kprobe、kretprobe
.

3.1 脚本框架
ruby
# 为了保证脚本执行前环境是干净的,执行以下命令恢复tracing的默认状态
echo 0 > /sys/kernel/debug/tracing/tracing_on
echo 0 > /proc/sys/kernel/ftrace_enabled
echo 0 > /sys/kernel/debug/tracing/events/kprobes/enable
echo '' > /sys/kernel/debug/tracing/set_ftrace_filter
【这里根据需求添加kprobe的probe点,下文详述】
# 启动kprobe
echo 1 > /sys/kernel/debug/tracing/events/kprobes/enable
echo 1 > /proc/sys/kernel/ftrace_enabled
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 查看结果信息
cat /sys/kernel/debug/tracing/trace_pipe
3.2 获得函数入参
比如想获得如下函数的tty指针

bash
echo 'p:prob3 uart_write_room tty=$arg1:x64' >> /sys/kernel/debug/tracing/kprobe_events

(如未见以上输出,可能是函数并未调用,可通过ftrace的function trace功能先确认函数是否被调用)
有些平台可能不支持将kprobe时的参数打出来,需要做部分调整,笔者实验的平台需要打上这个patch:登录 - Gitee.com

3.3 入参是结构体指针,获得其成员(5星推荐的好用功能)
仍以上面uart_write_room的入参tty为例,其结构体定义如下:

在3.2节中获得了其结构体指针,更进一步如果想获得tty_struct的magic和name应该怎么做?首先需要先获得这两个成员在结构体中的偏移,可以用gdb vmlinux获得,也可以用crash工具解析vmcore获得

然后将如下命令写入3.1节的脚本框架中,执行脚本即可获得如下图结果
bash
echo 'p:prob3 uart_write_room tty_magic=+0($arg1):x32 tty_name=+368($arg1):string' >> /sys/kernel/debug/tracing/kprobe_events

magic的值是符合预期的(如下图宏定义),name看起来也是合理的。

简单解释一下命令的含义:

(详细的用法介绍见内核文档:Documentation/trace/kprobetrace.rst)

(Documentation/trace/kprobetrace.rst部分摘录)
该功能可以非常方便的获得运行时的参数内容,在某些场景定位问题是神器。
3.4 获得函数调用栈(5星推荐的好用功能)
在3.3节的基础上增加一行stacktrace的选项,即可打印出调用栈
ruby
echo 1 > /sys/kernel/debug/tracing/options/stacktrace # echo 0就是关闭调用栈
echo 'p:prob3 uart_write_room tty_magic=+0($arg1):x32 tty_name=+368($arg1):string' >> /sys/kernel/debug/tracing/kprobe_events

3.5 获得全局变量的值
有时候我们希望查看系统里全局变量的值,一个朴素的方式是增加一行日志打印,重新编译内核运行,但这种效率较低,可以借用kprobe来实现该诉求。
bash
echo 'p:probe2 uart_write_room @pid_max:x32' >> /sys/kernel/debug/tracing/kprobe_events

pid_max是linux内核里的一个全局变量,和uart_write_room没有任何关系,但我们可以借助这个probe来打印出全局变量的内容,pid_max是0x8000是符合预期的。

3.6 获得函数返回值(5星推荐的好用功能)
bash
echo 'r:probe1 uart_write_room $retval' >> /sys/kernel/debug/tracing/kprobe_events

又是一个常用的功能,可以获得两个信息:(1)uart_write_room的返回值是0xfff;(2)uart_write_room的调用者是tty_write_room。
4 简述kprobe event原理(主要摘抄参考文献3,侵删)
ARM64架构下kprobe event的实现原理基于指令替换 和异常处理机制,通过动态修改目标指令为断点指令(BRK),并在异常处理流程中执行用户定义的回调函数。示意如下:

pre_handler: 在CPU执行被探测指令之前 触发, 可做上下文捕获(各种寄存器)、参数获取、逻辑干预(可修改寄存器影响执行逻辑);
post_handler: 在CPU执行被探测指令之后触发(但尚未恢复原执行流), 常用场景如 统计函数耗时、验证执行正确性。
4.1 kprobe初始化
ini
static int __init init_kprobes(void)
{
int i, err = 0;
unsigned long offset = 0, size = 0;
char *modname, namebuf[128];
const char *symbol_name;
void *addr;
struct kprobe_blackpoint *kb;
// 1) 初始化用于存储 kprobe 模块的哈希表
for (i = 0; i < KPROBE_TABLE_SIZE; i++) {
INIT_HLIST_HEAD(&kprobe_table[i]);
...
}
// 2) 初始化 kprobe 的黑名单函数列表(不能被 kprobe 跟踪的函数列表)
for (kb = kprobe_blacklist; kb->name != NULL; kb++) {
kprobe_lookup_name(kb->name, addr);
if (!addr)
continue;
kb->start_addr = (unsigned long)addr;
symbol_name = kallsyms_lookup(kb->start_addr, &size, &offset, &modname,
namebuf);
if (!symbol_name)
kb->range = 0;
else
kb->range = size;
}
...
kprobes_all_disarmed = false;
// 3) 初始化CPU架构相关的环境(arm64架构的实现为空)
err = arch_init_kprobes();
// 4) 注册die通知链
if (!err)
err = register_die_notifier(&kprobe_exceptions_nb);
// 5) 注册模块通知链
if (!err)
err = register_module_notifier(&kprobe_module_nb);
...
return err;
}
内核把被跟踪的指令地址作为键,然后将 kprobe 结构保存到哈希表中,这样就能通过指令的地址快速查找到对应的 kprobe 结构。

4.2 注册kprobe
scss
int __kprobes register_kprobe(struct kprobe *p)
{
...
// 1) 获取要跟踪的指令的内存地址
addr = kprobe_addr(p);
...
p->addr = addr;
...
// 2) 检测跟踪点是否合法
//(①kprobe只能用作内核函数的探测,所以在注册前必须检查探测点的地址是否是在内核地址空间)
// (②kprobe 与 ftrace 不能同时跟踪同一个地址)
// (③跟踪点是否在 kprobe 的黑名单中,如果是就返回错误)
ret = check_kprobe_address_safe(p, &probed_mod);
...
// 3) 保存被跟踪指令的值
ret = prepare_kprobe(p);
...
// 4) 将 kprobe 结构添加到 kprobe 模块哈希表中
hlist_add_head_rcu(&p->hlist,
&kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]);
// 5) 将要跟踪的指令替换成 BRK 指令
if (!kprobes_all_disarmed && !kprobe_disabled(p))
arm_kprobe(p);
...
return ret;
}
替换指令函数实现如下:
scss
145 /* arm kprobe: install breakpoint in text */
146 void __kprobes arch_arm_kprobe(struct kprobe *p)
147 {
148 patch_text(p->addr, BRK64_OPCODE_KPROBES);
149 }
4.3 触发kprobe
CPU执行到BRK64_OPCODE_KPROBES
时,触发同步异常(Synchronous Exception),进入ARM64的el1_sync
异常处理流程,根据ESR寄存器的内容,最终执行到el1_dbg.
el1_sync-->el1_dbg-->do_debug_exception-->brk_handler-->kprobe_handler
kprobe_handler里先是进入pre_handler,然后通过setup_singlestep设置single-step相关寄存器,为下一步执行原指令时发生single-step异常做准备。
setup_singlestep() 执行完毕后,程序继续执行保存的被探测点的指令,由于开启了单步调试模式,执行完指令后会继续触发异常,单步执行探测点的指令后,会触发单步异常,进入single_step_handler,调用kprobe_breakpoint_ss_handler,主要任务是恢复执行路径,调用用户注册的post_handler

4.4 kprobe event原理
常见的使用kprobe的方式有两种:
(1)编写内核模块 直接调用register_kprobe API
- 第一步:根据需要来编写探测函数,如 pre_handler 和 post_handler 回调函数。
- 第二步:定义 struct kprobe 结构并且填充其各个字段,如要探测的内核函数名和各个探测回调函数。
- 第三步:通过调用 register_kprobe 函数注册一个探测点。
- 第四步:编写 Makefile 文件。
- 第五步:编译并安装内核模块。
可参考samples/kprobes/kprobe_example.c
(2)使用kprobe event
通过/sys/kernel/debug/tracing/目录下的trace等属性文件来探测用户指定的函数,用户可添加kprobe支持的任意函数并设置探测格式与过滤条件,无需再编写内核模块,使用更为简便,但需要内核的debugfs和ftrace功能的支持。
sys/kernel/debug/tracing/kprobe_events文件节点的代码逻辑如下:

probes_write-->create_or_delete_trace_kprobe-->trace_kprobe_create-->register_trace_kprobe-->register_kprobe
通过这一系列调用注册kprobe event。