“tcp丢包分析”实验解析(二)–kprobe和tracepoint

tcp丢包分析系列文章代码来自谢宝友老师,由西邮陈莉君教授研一学生进行解析,本文由戴君毅整理,梁金荣编辑,贺东升校对。

上次我们分析了 《“tcp丢包分析”实验解析(一)–proc文件系统》

继续顺下实验代码,前面说过,我们需要为我们(在proc文件系统中)加入的条目编写操作集接口:

实验的运行逻辑重点落在了write操作的实现中:

它的逻辑很简单,我们把用户写入的字符串通过 copy_from_user 传入到内核 buf 中,然后调用 sscanf 格式化 bufcmd ,然后跟“activate”和“deactiavate”比较,换句话说,如果用户输入“activate”,那么就可以开启丢包检测了。在加载模块后,用如下命令可以查看proc内容,由 drop_packet_show 实现:

cat /proc/mooc/net/drop-packet

结果如下:(省略了settings:)

activated:N

执行如下命令,可以激活丢包检测功能:

echo activate > /proc/mooc/net/drop-packet

再次查看proc文件内容,结果如下:

activated:y

那么接下来自然要看下 activate_drop_packet 干了些什么事:

可以看到,这段代码在 net_dev_xmit 中挂接一个 tracepoint 钩子,在 dev_queue_xmit / eth_type_trans / napi_gro_receive / __netif_receive_skb_core / tcp_v4_rcv 等函数的入口处挂接一个kprobe钩子。

那么到底什么是tracepoint?kprobe又是什么?说它们之前不得不说一下ftrace。ftrace(function trace)是利用gcc 编译器在编译时在每个函数的入口地址放置一个 probe 点,这个 probe 点会调用一个 probe 函数,这样这个probe 函数会对每个执行的内核函数进行跟踪并打印日志到 ring buffer 中,而用户可以通过 debugfs 来访问 ring buffer 中的内容。

kprobe 是很早前就存在于内核中的一种动态 trace 工具。kprobe 本身利用了 int 3(在 x86 中)实现了 probe 点(对应图中的A)。使用 kprobe 需要用户自己实现 kernel module 来注册 probe 函数。kprobe 并没有统一的B、C 和 D。使用起来用户需要自己实现很多东西,不是很灵活。而在 functiontrace 出现后,kprobe 借用了它的一部分设计模式,实现了统一的 probe 函数(对应于图中的 B),并利用了 functiontrace 的环形缓存和用户接口部分,也就是 C 和 D 部分功能。

而tracepoint是静态的trace,说白了它就是内核开发人员提前设置好的跟踪点,也提供了管理桩函数的接口,它们已经编译进了内核,这样做既有优点也有缺点。优点是使用tracepoint的开销较小,并且它的API相对稳定;缺点也很明显,第一,默认的点明显不够多,可能覆盖不到你的需求;第二,你加一个点就需要重新编译内核,而kprobe不需要;第三,由于是静态的,不使用它也会造成开销(从内核文档可以看出这个问题已经被优化,现在基本忽略)。

实验代码中,挂接kprobe和tracepoint钩子的方法分别为 hook_kprobehook_tracepointhook_kprobe 代码如下:

由于是动态追踪,需要先调用 kallsyms_lookup_name 检查是否有这个函数。如果有这个函数,那么就把kprobe的 pre_handlerpost_handler 赋值给他,这两个都是钩子,会在其他地方定义好,它才是真正要干的事情,这是kprobe机制规定的,不用觉得奇怪。最后调用 register_kprobe 注册这个kprobe。

register_kprobe 函数非常复杂,我这里截取最为核心的部分:

prepare_kprobe 保存当前的指令, arm_kprobe 将当前指令替换为int3。然后就会执行 kprobe_int3_handler ,此时如果你的handler实现了并且注册了,那么就会执行你的handler了,这里应该就是“插桩”的本质了。

在设置单步调试 setup_singlestep 时,还会把 post_handler 的地址设置为下一个执行指令,以便 pre_handler 返回时可以“reenter”到kprobe异常,顺利执行 post_handler ,最后才执行原本执行的代码,它存放在kprobe结构体中。虽然这个实验并没有实现 post_handler ,但机制就是这样的。

Tracepoint相对简单一点,实验里直接调用了 tracepoint_probe_register ,就不展开说了,通常情况都是用 DECLARE_TRACEDEFINE_TRACE 这两个宏。可以阅读内核文档查阅一些高级的用法。

值得一提的是,实验代码为tracepoint设置了条件宏以便适应不同的内核版本,比较有意思,这里给出结构大家感受一下: