Record of using kprobe

Background

之前在生产环境排查了一个TCP的ACK无法被正确接收,导致发送端一直重传,最终接收端keepalive超时后关闭连接的问题([nf] netfilter: conntrack: work around exceeded receive window)。排查过程中主要用到了kprobe这个动态调试手段去分析内核代码执行的情况,这里简单记录下。

Kprobe

kprobe是一种内核的动态调试机制,可以在几乎所有内核代码上进行插桩,当执行到对应的代码时,先执行插桩函数,执行完毕后,在继续运行后续逻辑。它的实现逻辑基本上就是指令替换,将运行指令跳转到用户定义好的kprobe handler。

上面说了是几乎所有内核代码,有几种场景无法正常kprobe:

  • kprobe自身相关的代码:没法嵌套kprobe自身
  • 内联函数:内联函数在编译时会被展开到所有的调用处,kprobe实际上是根据符号对应的地址进行的,不能确认所有内联函数展开后的地址,这种情况需要计算内联函数的调用地址来进行kprobe
  • 一些被优化后的静态函数

同时kprobe有两种实现形式:

  • 基于debugfs/ftrace的简易实现:对于一些简单场景,如获取函数参数,或者函数返回值的场景,可以直接使用
  • 基于内核模块的实现:可以实现一些复杂功能和逻辑,适用于复杂的场景

kprobe based on ftrace

用户可以直接通过/sys/kernel/debug/tracing/kprobe_events添加kprobe,语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 p[:[GRP/][EVENT]] [MOD:]SYM[+offs]|MEMADDR [FETCHARGS]        : Set a probe
r[MAXACTIVE][:[GRP/][EVENT]] [MOD:]SYM[+0] [FETCHARGS] : Set a return probe
p[:[GRP/][EVENT]] [MOD:]SYM[+0]%return [FETCHARGS] : Set a return probe
-:[GRP/][EVENT] : Clear a probe

GRP : Group name. If omitted, use "kprobes" for it.
EVENT : Event name. If omitted, the event name is generated
based on SYM+offs or MEMADDR.
MOD : Module name which has given SYM.
SYM[+offs] : Symbol+offset where the probe is inserted.
SYM%return : Return address of the symbol
MEMADDR : Address where the probe is inserted.
MAXACTIVE : Maximum number of instances of the specified function that
can be probed simultaneously, or 0 for the default value
as defined in Documentation/trace/kprobes.rst section 1.3.1.

FETCHARGS : Arguments. Each probe can have up to 128 args.
%REG : Fetch register REG
@ADDR : Fetch memory at ADDR (ADDR should be in kernel)
@SYM[+|-offs] : Fetch memory at SYM +|- offs (SYM should be a data symbol)
$stackN : Fetch Nth entry of stack (N >= 0)
$stack : Fetch stack address.
$argN : Fetch the Nth function argument. (N >= 1) (\*1)
$retval : Fetch return value.(\*2)
$comm : Fetch current task comm.
+|-[u]OFFS(FETCHARG) : Fetch memory at FETCHARG +|- OFFS address.(\*3)(\*4)
\IMM : Store an immediate value to the argument.
NAME=FETCHARG : Set NAME as the argument name of FETCHARG.
FETCHARG:TYPE : Set TYPE as the type of FETCHARG. Currently, basic types
(u8/u16/u32/u64/s8/s16/s32/s64), hexadecimal types
(x8/x16/x32/x64), "string", "ustring" and bitfield
are supported.

(\*1) only for the probe on function entry (offs == 0).
(\*2) only for return probe.
(\*3) this is useful for fetching a field of data structures.
(\*4) "u" means user-space dereference. See :ref:`user_mem_access`.

当kprobe添加完成后,会生成对应的kprobe目录:/sys/kernel/debug/tracing/events/kprobes/<EVENT>,可以通过/sys/kernel/debug/tracing/events/kprobes/<EVENT>/enable去控制对应kprobe的启用禁用。

官方示例(应该是32位的x86),对do_sys_open,可通过如下kprobe获取其参数:

1
echo 'p:myprobe do_sys_open dfd=%ax filename=%dx flags=%cx mode=+4($stack)' > /sys/kernel/debug/tracing/kprobe_events

各个参数含义如下:

  • p:kprobe类型为kprobe
  • myprobe:event名称
  • do_sys_open:进行kprobe的函数名
  • dfd=%ax filename=%dx flags=%cx mode=+4($stack):获取参数并输出,%ax%dx%cx+4($stack)分别为寄存器名字和栈上偏移

可通过如下kprobe获取返回值:

1
echo 'r:myretprobe do_sys_open $retval' >> /sys/kernel/debug/tracing/kprobe_events

各个参数含义如下:

  • r:kprobe类型为kretprobe
  • myretprobe:event名称
  • do_sys_open:进行kprobe的函数名
  • $retval:获取返回值并输出

启用这两个kprobe:

1
2
echo 1 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable
echo 1 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable

开启ftrace:

1
echo 1 > /sys/kernel/debug/tracing/tracing_on

查看结果:

1
2
3
4
5
6
7
8
9
10
11
cat /sys/kernel/debug/tracing/trace
# tracer: nop
#
# TASK-PID CPU# TIMESTAMP FUNCTION
# | | | | |
<...>-1447 [001] 1038282.286875: myprobe: (do_sys_open+0x0/0xd6) dfd=3 filename=7fffd1ec4440 flags=8000 mode=0
<...>-1447 [001] 1038282.286878: myretprobe: (sys_openat+0xc/0xe <- do_sys_open) $retval=fffffffffffffffe
<...>-1447 [001] 1038282.286885: myprobe: (do_sys_open+0x0/0xd6) dfd=ffffff9c filename=40413c flags=8000 mode=1b6
<...>-1447 [001] 1038282.286915: myretprobe: (sys_open+0x1b/0x1d <- do_sys_open) $retval=3
<...>-1447 [001] 1038282.286969: myprobe: (do_sys_open+0x0/0xd6) dfd=ffffff9c filename=4041c6 flags=98800 mode=10
<...>-1447 [001] 1038282.286976: myretprobe: (sys_open+0x1b/0x1d <- do_sys_open) $retval=3

详细使用方式可以参考:Kprobe-based Event Tracing

上面为直接在debugfs下操作,操作比较繁琐,也有一些现成的工具bpftraceperf-tools对debugfs下的操作进行了封装,可以简化操作步骤。

kprobe module

kprobe最原始的使用方式是编译一个内核模块,在模块内部进行kprobe的注册handler,在handler实现所需要的逻辑。

这种方式提供了很强大的自由度,缺点就是使用难度较高,在一些需要复杂一点的逻辑的地方可以使用,内核文档对这种方式进行了详细说明。

以上述排查ACK是否有被正常接收的问题为例,通过看代码可以确认,对应ACK处理函数为tcp_ack,参数中包含了skb,基于ftrace的kprobe可以获取到skb,甚至可以通过偏移获取到skb的成员,但问题在于,在一个正常的Linux环境,每秒处理的ACK太多了,这种kprobe没法精确过滤所需内容,所以需要自行编写过滤逻辑,并输出期望的内容。

排查过程中用到的部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/kprobes.h>
#include <net/tcp.h>

int pre_tcp_ack(struct kprobe *p, struct pt_regs *regs);

struct kprobe kp_tcp_ack = {
.symbol_name = "tcp_ack",
.pre_handler = pre_tcp_ack,
};

int pre_tcp_ack(struct kprobe *p, struct pt_regs *regs)
{
struct sk_buff *skb;
int flag;

skb = (struct sk_buff *)regs->si;
if (!skb) {
return 0;
}

if (tcp_hdr(skb)->source != htons(7001) && tcp_hdr(skb)->dest != htons(7001)) {
return 0;
}

flag = (int)regs->dx;
printk("Get ACK: %u(%x) in tcp_ack(flag: %d).\n", TCP_SKB_CB(skb)->ack_seq, TCP_SKB_CB(skb)->ack_seq, flag);

return 0;
}

static int init(void)
{
if (register_kprobe(&kp_tcp_ack)) {
return -1;
}

printk("trace-ack loaded.\n");

return 0;
}

static void fini(void)
{
unregister_kprobe(&kp_tcp_ack);

printk("trace-ack unloaded.\n");
}

module_init(init);
module_exit(fini);
MODULE_LICENSE("GPL");

可以看到整体逻辑非常简单,在module_init中通过register_kprobe注册kprobe,在module_exit中通过unregister_kprobe取消kprobe的注册。

struct kprobe定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
struct kprobe {
struct hlist_node hlist;

/* list of kprobes for multi-handler support */
struct list_head list;

/*count the number of times this probe was temporarily disarmed */
unsigned long nmissed;

/* location of the probe point */
kprobe_opcode_t *addr;

/* Allow user to indicate symbol name of the probe point */
const char *symbol_name;

/* Offset into the symbol */
unsigned int offset;

/* Called before addr is executed. */
kprobe_pre_handler_t pre_handler;

/* Called after addr is executed, unless... */
kprobe_post_handler_t post_handler;

/*
* ... called if executing addr causes a fault (eg. page fault).
* Return 1 if it handled fault, otherwise kernel will see it.
*/
kprobe_fault_handler_t fault_handler;

/*
* ... called if breakpoint trap occurs in probe handler.
* Return 1 if it handled break, otherwise kernel will see it.
*/
kprobe_break_handler_t break_handler;

/* Saved opcode (which has been replaced with breakpoint) */
kprobe_opcode_t opcode;

/* copy of the original instruction */
struct arch_specific_insn ainsn;

/*
* Indicates various status flags.
* Protected by kprobe_mutex after this kprobe is registered.
*/
u32 flags;
};

成员很多,通常只需关注handlersymbol

  • handler:对应事件发生时,所执行的函数
  • symbol:需要probe的符号名,即函数名

根据symbol在完成对应类型的handler编写,最后将kprobe注册到内核,即可正常工作。

可以看到上面的例子中的pre_handlerpre_tcp_ack,在其内部先是获取到了skb,再根据端口号过滤掉不需要的skb,再输出TCP的seqack

使用symbol注册的kprobe只能在特定函数的入口/出口执行,struct kprobe还有成员addroffset,可以用于精确指定probe的地址,但这种使用方式难度很大,获取所需参数时需要通过汇编代码确认对应的寄存器,再获取。而在函数入口的kprobe可以根据对应架构的传参规范通过对应寄存器直接获取,关于通过寄存器获取函数参数的方式,也可以参考bpf_tracing.h中的PT_REGS_PARAM1类似宏。

Systemtap

systemtap是一个基于kprobe开发的动态调试工具,功能非常强大,有自己的脚本语言(完整语法规范参考SystemTap Language Reference),可以通过编写systemtap脚本的方式,由systemtap生成对应的kprobe代码并加载到内核运行。这样用户的开发难度就大大降低了,不再需要懂内核编程,也不需要有内核编译环境。

systemtap的安装使用在大多数的Linux发行版上都比较容易,直接通过对应的包管理工具安装即可,需要注意的是同时需要安装对应内核debuginfo,因为systemtap是通过vmlinux和debuginfo去确定probe点的。

无debuginfo使用systemtap

对于没有debuginfo的生产环境是否可以使用systemtap呢?答案是可以的,但是要有目标环境对应的内核编译环境。

因为systemtap的工作原理就是先生成内核模块再加载执行,我们可以把这两步分开,在编译环境上生成对应的内核模块,在目标环境上加载模块进行调试。

以内核模块xt_state中的函数state_mt为例,首先查看对应的probe点,以及局部变量,注意使用-r指定内核代码路径:

1
2
[root@docker <bca7e9fa6017> /home ]#stap -r /home/kernel.rh8/ -L 'module("xt_state").function("state_mt@net/netfilter/xt_state.c")'
module("xt_state").function("state_mt@net/netfilter/xt_state.c:24") $skb:struct sk_buff const* $par:struct xt_action_param* $sinfo:struct xt_state_info const*

可以将function换成statement,以将probe点指定到具体行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@docker <bca7e9fa6017> /home ]#sed -n '23, 39p' /home/kernel.rh8/net/netfilter/xt_state.c
static bool
state_mt(const struct sk_buff *skb, struct xt_action_param *par)
{
const struct xt_state_info *sinfo = par->matchinfo;
enum ip_conntrack_info ctinfo;
unsigned int statebit;
struct nf_conn *ct = nf_ct_get(skb, &ctinfo);

if (ct) /* line 31 */
statebit = XT_STATE_BIT(ctinfo);
else if (ctinfo == IP_CT_UNTRACKED)
statebit = XT_STATE_UNTRACKED;
else
statebit = XT_STATE_INVALID;

return (sinfo->statemask & statebit);
}
[root@docker <bca7e9fa6017> /home ]#stap -r /home/kernel.rh8/ -L 'module("xt_state").statement("*@net/netfilter/xt_state.c:31")'
module("xt_state").statement("state_mt@net/netfilter/xt_state.c:31") $skb:struct sk_buff const* $par:struct xt_action_param* $sinfo:struct xt_state_info const* $ctinfo:enum ip_conntrack_info
[root@docker <bca7e9fa6017> /home ]#stap -r /home/kernel.rh8/ -L 'module("xt_state").statement("*@net/netfilter/xt_state.c:38")'
module("xt_state").statement("state_mt@net/netfilter/xt_state.c:38") $skb:struct sk_buff const* $par:struct xt_action_param* $sinfo:struct xt_state_info const* $statebit:unsigned int

可以看到在不同的probe点,可以访问的局部变量也是不一样的,但是关于局部变量还有一点疑惑:以31行为例,其实应该是还能访问statebitct这两个变量的,但probe显示出来的变量并不包含这两个,可能是对变量做了一些优化,一些还没赋值的变量(statebit)和一些变量可以间接通过其他变量获取(ct)被优化掉了。

编写如下systemtap脚本用于在state_mt内部输出ctinfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/* Part 1: Global embedded C include */
%{
#include <linux/netfilter/x_tables.h>
#include <linux/netfilter/xt_state.h>
#include <net/tcp.h>
#include <net/netfilter/nf_conntrack_core.h>
%}
/* Part 1 end */

/* Part2: Embedded C function */
function print_ctinfo (skb, ctinfo)
%{
struct sk_buff *skb;
struct iphdr *iph;
struct tcphdr *th;
enum ip_conntrack_info ctinfo;

skb = (struct sk_buff *)STAP_ARG_skb;
if (!skb) {
STAP_RETURN();
}

if (skb->protocol != htons(ETH_P_IP)) {
STAP_RETURN();
}

iph = ip_hdr(skb);
if (!iph) {
STAP_RETURN();
}

if (iph->protocol != IPPROTO_TCP) {
STAP_RETURN();
}

th = tcp_hdr(skb);
if (!th) {
STAP_RETURN();
}

if (th->dest != htons(22)) {
STAP_RETURN();
}

ctinfo = STAP_ARG_ctinfo;
if (printk_ratelimit()) {
printk("[%s] Capture TCP packet: %hu.%hu.%hu.%hu:%hu->%hu.%hu.%hu.%hu:%hu with ctinfo: %d.\n",
__func__,
iph->saddr & 0xff, (iph->saddr & 0xff00) >> 8, (iph->saddr & 0xff0000) >> 16, (iph->saddr & 0xff000000) >> 24,
ntohs(th->source),
iph->daddr & 0xff, (iph->daddr & 0xff00) >> 8, (iph->daddr & 0xff0000) >> 16, (iph->daddr & 0xff000000) >> 24,
ntohs(th->dest),
ctinfo);
}

STAP_RETURN();
%}
/* Part 2 end */

/* Part 3: Probes */
probe module("xt_state").statement("state_mt@net/netfilter/xt_state.c:31")
{
print_ctinfo($skb, $ctinfo)
}

probe begin
{
print("probe start.\n")
}


probe end
{
print("probe end.\n")
exit()
}
/* Part3 end */

脚本分为3部分:

  • Part 1:嵌入的C代码的全局include,因为后续嵌入的C函数需要一些头文件,所以在开头定义好。
  • Part 2:功能函数print_ctinfo,接受两个参数skbctinfo
  • Part 3:声明探测点probe

Part 1和Part 2使用了嵌入C代码,关于嵌入C代码,有如下几个特点:

  • 最外层使用%{...%}这样的括号
  • 获取函数参数通过STAP_ARG_前缀加上参数名获取,如STAP_ARG_skb
  • 函数内部使用STAP_RETURN()进行返回,参数不为空时可以为字符串或整数,和函数定义时的返回类型相同。
  • 函数内部使用STAP_RETVALUE作为返回值,可以为整数或字符串,字符串返回时需使用snprintf类似的函数为其赋值。

Part 2中print_ctinfo内部通过参数获取到skbctinfo,并进行合法性检查,通过合法性检查后,直接通过printk输出skbctinfo相关的信息。

Part 3中定义了3个probe,分别为xt_state.c第31行对应地址的probe,以及两个内建probe start 和end,分别在运行的开始和结束处调用。

systemtap脚本编写完成后,即可使用stap命令编译对应的模块:

1
2
3
4
5
6
7
8
9
[root@docker <bca7e9fa6017> /home ]#stap -r /home/kernel.rh8 -DSTP_NO_OVERLOAD -DSTP_NO_VERREL_CHECK  -DSTP_NO_BUILDID_CHECK -v -p4 -m print_ctinfo.ko -g print_ctinfo.stp 
Truncating module name to 'print_ctinfo'
Pass 1: parsed user script and 484 library scripts using 155264virt/96900res/2956shr/93944data kb, in 290usr/50sys/337real ms.
Pass 2: analyzed script: 3 probes, 4 functions, 1 embed, 0 globals using 157656virt/100236res/3944shr/96336data kb, in 50usr/380sys/431real ms.
Pass 3: translated to C into "/tmp/stap7rcpFZ/print_ctinfo_src.c" using 157920virt/100756res/4216shr/96600data kb, in 30usr/380sys/414real ms.
print_ctinfo.ko
Pass 4: compiled C into "print_ctinfo.ko" in 2270usr/760sys/2612real ms.
[root@docker <bca7e9fa6017> /home ]#file print_ctinfo.ko
print_ctinfo.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=0xd1741342db5acb8c31f82e5684273aae24125c82, not stripped

stap的执行一共有5个阶段,分别为:parseelaboratetranslatecompilerun,可以通过-p指定执行阶段。上述指定了-p4的命令最终编译出了可用的内核模块print_ctinfo.ko

在运行环境上,则可以直接通过staprun命令进行probe,该命令会自动加载模块,命令执行完成后,会自动卸载模块:

1
2
# ./staprun print_ctinfo.ko
probe start.

此时观察内核日志,可以找到期望的输出,可以看到成功过滤了经过state_mt的SSH报文并且输出了相关信息:

1
2
3
2022-12-08 17:37:07.614013 warning [kernel:] [171938.274058] [function___global_print_ctinfo__overload_0] Capture TCP packet: 172.23.9.13:7037->10.103.240.223:22 with ctinfo: 0.
2022-12-08 17:37:07.623029 warning [kernel:] [171938.283066] [function___global_print_ctinfo__overload_0] Capture TCP packet: 10.103.241.85:51515->10.103.240.223:22 with ctinfo: 0.
2022-12-08 17:37:07.623997 warning [kernel:] [171938.284053] [function___global_print_ctinfo__overload_0] Capture TCP packet: 172.23.9.13:7037->10.103.240.223:22 with ctinfo: 0.

stap在运行过程中,会将生成的C代码放在临时目录,可以通过-k选项保留临时目录,然后查看对应的C代码。上述自定义函数print_ctinfo生成的目标C代码部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static void function___global_print_ctinfo__overload_0 (struct context* __restrict__ c) {
__label__ deref_fault;
__label__ out;
struct function___global_print_ctinfo__overload_0_locals * __restrict__ l = & c->locals[c->nesting+1].function___global_print_ctinfo__overload_0;
(void) l;
#define CONTEXT c
#define THIS l
#define STAP_ARG_skb THIS->l_skb
#define STAP_ARG_ctinfo THIS->l_ctinfo
c->last_stmt = "identifier 'print_ctinfo' at print_ctinfo.stp:8:10";
if (unlikely (c->nesting+1 >= MAXNESTING)) {
c->last_error = "MAXNESTING exceeded";
return;
} else {
c->nesting ++;
}
c->next = 0;
#define STAP_NEXT do { c->next = 1; goto out; } while(0)
#define STAP_RETURN() do { goto out; } while(0)
#define STAP_PRINTF(fmt, ...) do { _stp_printf(fmt, ##__VA_ARGS__); } while (0)
#define STAP_ERROR(...) do { snprintf(CONTEXT->error_buffer, MAXSTRINGLEN, __VA_ARGS__); CONTEXT->last_error = CONTEXT->error_buffer; goto out; } while (0)
#define return goto out
if (c->actionremaining < 0) { c->last_error = "MAXACTION exceeded";goto out; }
{

struct sk_buff *skb;
struct iphdr *iph;
struct tcphdr *th;
enum ip_conntrack_info ctinfo;

skb = (struct sk_buff *)STAP_ARG_skb;
if (!skb) {
STAP_RETURN();

可以看到一些在函数体内部使用的宏,如STAP_RETURN,在函数开头都有定义,生成的函数名function___global_print_ctinfo__overload_0也和内核日志中一致。

除了内核函数的probe,systemtap还支持许多其他类型的probe,可通过stap --dump-probe-types查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@docker <bca7e9fa6017> /home ]#stap -r /home/kernel.rh8 --dump-probe-types
......
netfilter.hook(string).pf(string)
netfilter.hook(string).pf(string).priority(string)
netfilter.pf(string).hook(string)
netfilter.pf(string).hook(string).priority(string)
......
process(string).begin
process(string).end
......
procfs(string).read
procfs(string).read.maxsize(number)
......
python2.module(string).function(string)
python2.module(string).function(string).call
python2.module(string).function(string).return
python3.module(string).function(string)
python3.module(string).function(string).call
python3.module(string).function(string).return
......
timer.hz(number)
timer.jiffies(number)
timer.jiffies(number).randomize(number)

包含了许多针对特定场景的probe,包括对用户态的进程、python模块等。

  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2021-2023 Martzki
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信