Linux 用户和内核空间中的动态追踪

您是否忘记在代码中插入探针点?没问题。了解如何使用 uprobe 和 kprobe 动态插入它们。
348 位读者喜欢这篇文章。
Colorful deployments: An introduction to blue-green, canary, and rolling deployments

Internet Archive Book Images。由 Opensource.com 修改。CC BY-SA 4.0

您是否曾遇到过这种情况:您意识到在代码的某些位置没有插入 debug print,因此现在您将不知道您的 CPU 是否执行了某行代码,除非您使用调试语句重新编译代码?别担心,有一个更简单的解决方案。基本上,您需要在源代码汇编指令的不同位置插入动态探针点。

对于高级用户,内核 documentation/traceman perf 提供了关于不同类型内核和用户空间追踪机制的很多信息;但是,普通用户只想了解几个简单的步骤和一个示例来快速入门。本文将在这方面提供帮助。

让我们从定义开始。

探针点

探针点是一种调试语句,用于帮助探索软件的执行特性(即,探针语句执行时软件数据结构的执行流程和状态)。printk 是最简单的探针语句形式,也是开发人员用于内核 hacking 的基本工具之一。

静态与动态探测

由于 printk 插入需要重新编译源代码,因此它是一种静态探测方法。内核代码中的许多重要位置还有许多其他静态跟踪点,可以动态启用或禁用。Linux 内核有一些框架可以帮助开发人员探测内核或用户空间应用程序,而无需重新编译源代码。Kprobe 是一种在内核代码中插入探针点的动态方法,而 uprobe 则在用户应用程序中执行此操作。

使用 uprobe 追踪用户空间

可以使用 sysfs 接口或 perf 工具 将 uprobe 跟踪点插入到任何用户空间代码中。

使用 sysfs 接口插入 uprobe

考虑以下简单的测试代码,其中没有 print 语句,我们想在某些指令处插入一个探针

[[app-listing]]
[source,c]
.test.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static int func_1_cnt;
static int func_2_cnt;

static void func_1(void)
{
	func_1_cnt++;
}

static void func_2(void)
{
	func_2_cnt++;
}

int main(int argc, void **argv)
{
	int number;

	while(1) {
		sleep(1);
		number = rand() % 10;
		if (number < 5)
			func_2();
		else
			func_1();
	}
}

编译代码并找到您要探测的指令地址

# gcc -o test test.c
# objdump -d test

假设我们在 ARM64 平台上具有以下目标代码

0000000000400620 <func_1>:
  400620:	90000080 	adrp	x0, 410000 <__FRAME_END__+0xf6f8>
  400624:	912d4000 	add	x0, x0, #0xb50
  400628:	b9400000 	ldr	w0, [x0]
  40062c:	11000401 	add	w1, w0, #0x1
  400630:	90000080 	adrp	x0, 410000 <__FRAME_END__+0xf6f8>
  400634:	912d4000 	add	x0, x0, #0xb50
  400638:	b9000001 	str	w1, [x0]
  40063c:	d65f03c0 	ret

0000000000400640 <func_2>:
  400640:	90000080 	adrp	x0, 410000 <__FRAME_END__+0xf6f8>
  400644:	912d5000 	add	x0, x0, #0xb54
  400648:	b9400000 	ldr	w0, [x0]
  40064c:	11000401 	add	w1, w0, #0x1
  400650:	90000080 	adrp	x0, 410000 <__FRAME_END__+0xf6f8>
  400654:	912d5000 	add	x0, x0, #0xb54
  400658:	b9000001 	str	w1, [x0]
  40065c:	d65f03c0 	ret

并且我们想在偏移量 0x6200x644 处插入探针。执行以下命令

# echo 'p:func_2_entry test:0x620' > /sys/kernel/debug/tracing/uprobe_events
# echo 'p:func_1_entry test:0x644' >> /sys/kernel/debug/tracing/uprobe_events
# echo 1 > /sys/kernel/debug/tracing/events/uprobes/enable
# ./test&

在上面的第一个和第二个 echo 语句中,p 告诉我们这是一个简单探针。(探针可以是简单的或返回的。)func_n_entry 是我们在跟踪输出中看到的名称。Name 是一个可选字段;如果未提供,我们应该期望看到像 p_test_0x644 这样的名称。test 是我们要在其中插入探针的可执行二进制文件。如果 test 不在当前目录中,我们需要指定 path_to_test/test0x6200x640 是从程序开始的指令偏移量。请注意第二个 echo 语句中的 >>,因为我们要添加一个以上的探针。因此,当我们在前两个命令中插入探针点后启用 uprobe 追踪时,当我们写入 events/uprobes/enable 时,它将启用所有 uprobe 事件。我们也可以通过写入 events 目录中创建的特定事件文件来启用单个事件。一旦探针点被插入和启用,每当执行探测指令时,我们就可以看到一个跟踪条目。

读取跟踪文件以查看输出

# cat /sys/kernel/debug/tracing/trace
# tracer: nop
#
# entries-in-buffer/entries-written: 8/8   #P:8
#
#                              _-----=> irqs-off
#                             / _----=> need-resched
#                            | / _---=> hardirq/softirq
#                            || / _--=> preempt-depth
#                            ||| /     delay
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
            test-2788  [003] ....  1740.674740: func_1_entry: (0x400644)
            test-2788  [003] ....  1741.674854: func_1_entry: (0x400644)
            test-2788  [003] ....  1742.674949: func_2_entry: (0x400620)
            test-2788  [003] ....  1743.675065: func_2_entry: (0x400620)
            test-2788  [003] ....  1744.675158: func_1_entry: (0x400644)
            test-2788  [003] ....  1745.675273: func_1_entry: (0x400644)
            test-2788  [003] ....  1746.675390: func_2_entry: (0x400620)
            test-2788  [003] ....  1747.675503: func_2_entry: (0x400620)

我们可以看到哪个 CPU 在什么时间执行了探测指令,以及完成了什么任务。

返回探针也可以插入到任何指令中。这将记录当具有该指令的函数返回时的一个条目

# echo 0 > /sys/kernel/debug/tracing/events/uprobes/enable
# echo 'r:func_2_exit test:0x620' >> /sys/kernel/debug/tracing/uprobe_events
# echo 'r:func_1_exit test:0x644' >> /sys/kernel/debug/tracing/uprobe_events
# echo 1 > /sys/kernel/debug/tracing/events/uprobes/enable

这里我们使用 r 而不是 p,所有其他参数都相同。请注意,如果我们想插入新的探针点,我们需要禁用 uprobe 事件

            test-3009  [002] ....  4813.852674: func_1_entry: (0x400644)
            test-3009  [002] ....  4813.852691: func_1_exit: (0x4006b0 <- 0x400644)
            test-3009  [002] ....  4814.852805: func_2_entry: (0x400620)
            test-3009  [002] ....  4814.852807: func_2_exit: (0x4006b8 <- 0x400620)
            test-3009  [002] ....  4815.852920: func_2_entry: (0x400620)
            test-3009  [002] ....  4815.852921: func_2_exit: (0x4006b8 <- 0x400620)

上面的日志告诉我们 func_1 在时间戳 4813.852691 返回到地址 0x4006b0

# echo 0 > /sys/kernel/debug/tracing/events/uprobes/enable
# echo 'p:func_2_entry test:0x630' > /sys/kernel/debug/tracing/uprobe_events count=%x1
# echo 1 > /sys/kernel/debug/tracing/events/uprobes/enable
# echo > /sys/kernel/debug/tracing/trace
# ./test&

这里,当执行偏移量为 0x630 的指令时,我们将 ARM64 x1 寄存器的值打印为 count=

输出将如下所示

            test-3095  [003] ....  7918.629728: func_2_entry: (0x400630) count=0x1
            test-3095  [003] ....  7919.629884: func_2_entry: (0x400630) count=0x2
            test-3095  [003] ....  7920.629988: func_2_entry: (0x400630) count=0x3
            test-3095  [003] ....  7922.630272: func_2_entry: (0x400630) count=0x4
            test-3095  [003] ....  7923.630429: func_2_entry: (0x400630) count=0x5

使用 perf 插入 uprobe

总是找到需要插入探针的指令或函数的偏移量是很繁琐的,而且更复杂的是知道分配给任何局部变量的 CPU 寄存器的名称。perf 是一个有用的工具,可以帮助准备和将 uprobe 插入到源代码中的任何行。

除了 perf 之外,还有一些其他工具,例如 SystemTapDTraceLTTng,可以用于内核和用户空间追踪;但是,perf 与内核完全耦合,因此受到内核开发人员的青睐。

# gcc -g -o test test.c
# perf probe -x ./test func_2_entry=func_2
# perf probe -x ./test func_2_exit=func_2%return
# perf probe -x ./test test_15=test.c:15
# perf probe -x ./test test_25=test.c:25 number
# perf record -e probe_test:func_2_entry -e probe_test:func_2_exit -e probe_test:test_15  -e probe_test:test_25 ./test

如上例所示,我们可以直接在函数开始和返回处、源文件的特定行号等位置插入探针点。您可以打印局部变量。您还可以有许多其他选项,例如函数调用的所有实例(详情请参阅 man perf probe)。perf probe 用于创建探针点事件,然后可以使用 perf record 在执行 ./test 可执行文件时探测这些事件。当我们创建一个 perf probe 点时,我们可以有其他记录选项,例如 perf stat,并且我们可以有许多后分析选项,例如 perf scriptperf report

使用 perf script,我们上面示例的输出如下所示

# perf script
            test  2741 [002]   427.584681: probe_test:test_25: (4006a0) number=3
            test  2741 [002]   427.584717: probe_test:test_15: (400640)
            test  2741 [002]   428.584861: probe_test:test_25: (4006a0) number=6
            test  2741 [002]   428.584872: probe_test:func_2_entry: (400620)
            test  2741 [002]   428.584881: probe_test:func_2_exit: (400620 <- 4006b8)
            test  2741 [002]   429.585012: probe_test:test_25: (4006a0) number=7
            test  2741 [002]   429.585021: probe_test:func_2_entry: (400620)
            test  2741 [002]   429.585025: probe_test:func_2_exit: (400620 <- 4006b8)

使用 kprobe 追踪内核空间

与 uprobe 一样,可以使用 sysfs 接口或 perf 工具将 kprobe 跟踪点插入到内核代码中。

使用 sysfs 接口插入 kprobe

我们可以在 /proc/kallsyms大多数符号内插入 kprobe;其他符号已在内核中被列入黑名单。对于与 kprobe 插入不兼容的符号,将 kprobe 插入到 kprobe_events 文件中应导致写入错误。探针也可以从符号基址的某个偏移量处插入。与 uprobe 一样,我们也可以使用 kretprobe 跟踪函数的返回。局部变量的值也可以在跟踪输出中打印。

此示例解释了如何做到这一点

; disable all events, just to insure that we see only kprobe output in trace.
# echo 0 > /sys/kernel/debug/tracing/events/enable
; disable kprobe events until probe points are inseted.
# echo 0 > /sys/kernel/debug/tracing/events/kprobes/enable
; clear out all the events from kprobe_events, to insure that we see output for
; only those for which we have enabled 
# echo > /sys/kernel/debug/tracing/kprobe_events
; insert probe point at kfree
# echo "p kfree" >> /sys/kernel/debug/tracing/kprobe_events
; insert probe point at kfree+0x10 with name kfree_probe_10
# echo "p:kree_probe_10 kfree+0x10" >> /sys/kernel/debug/tracing/kprobe_events
; insert probe point at kfree return
# echo "r:kfree_probe kfree" >> /sys/kernel/debug/tracing/kprobe_events
; enable kprobe events until probe points are inseted.
# echo 1 > /sys/kernel/debug/tracing/events/kprobes/enable
[root@pratyush ~]# more /sys/kernel/debug/tracing/trace
# tracer: nop
#
# entries-in-buffer/entries-written: 9037/9037   #P:8
#
#                              _-----=> irqs-off
#                             / _----=> need-resched
#                            | / _---=> hardirq/softirq
#                            || / _--=> preempt-depth
#                            ||| /     delay
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
            sshd-2189  [002] dn..  1908.930731: kree_probe: (__audit_syscall_exit+0x194/0x218 <- kfree)
            sshd-2189  [002] d...  1908.930744: p_kfree_0: (kfree+0x0/0x214)
            sshd-2189  [002] d...  1908.930746: kree_probe_10: (kfree+0x10/0x214)

使用 perf 插入 kprobe

与 uprobe 一样,我们可以使用 perf 在内核代码中插入 kprobe。我们可以直接在函数开始和返回处、源文件的特定行号等位置插入探针点。我们可以将 vmlinux 提供给 -k 选项,或者我们可以将内核源代码路径提供给 -s 选项

# perf probe -k vmlinux kfree_entry=kfree
# perf probe -k vmlinux kfree_exit=kfree%return
# perf probe -s ./  kfree_mid=mm/slub.c:3408 x
# perf record -e probe:kfree_entry -e probe:kfree_exit -e probe:kfree_mid sleep 10

使用 perf script,我们看到上面示例的输出如下所示

# perf script
           sleep  2379 [001]  2702.291224: probe:kfree_entry: (fffffe0000201944)
           sleep  2379 [001]  2702.291229: probe:kfree_mid: (fffffe0000201978) x=0x0
           sleep  2379 [001]  2702.291231: probe:kfree_exit: (fffffe0000201944 <- fffffe000019f67c)
           sleep  2379 [001]  2702.291241: probe:kfree_entry: (fffffe0000201944)
           sleep  2379 [001]  2702.291242: probe:kfree_mid: (fffffe0000201978) x=0xfffffe01db8f6000
           sleep  2379 [001]  2702.291243: probe:kfree_exit: (fffffe0000201944 <- fffffe000019f67c)
           sleep  2379 [001]  2702.291249: probe:kfree_entry: (fffffe0000201944)
           sleep  2379 [001]  2702.291250: probe:kfree_mid: (fffffe0000201978) x=0xfffffe01db8f6000
           sleep  2379 [001]  2702.291251: probe:kfree_exit: (fffffe0000201944 <- fffffe000019f67c)

我希望本教程已经解释了如何 hack 您的可执行代码并在其中插入几个探针点。追踪愉快!

标签
User profile image.
Pratyush 在 Red Hat 担任 Linux 内核专家。他主要负责处理 Red Hat 产品和上游面临的多个 kexec/kdump 问题。他还处理 Red Hat 支持的 ARM64 平台周围的其他内核调试/追踪/性能问题。除了 Linux 内核之外,他还为上游 kexec-tools 和 makedumpfile 项目做出了贡献。

4 条评论

有用的信息。

稍微使用 OpenOffice Draw 可以改进插图。

感谢您的建议,下次文章中我会尝试使用一些图表。

回复 作者:SureshKumarShukla (未验证)

您知道 kprobe(和/或 uprobe)与 system tap 之间有什么区别吗?我不是专家,但它们感觉很相似

System tap 测试用例使用 kprobe 和 uprobe。本文讨论了使用 sysfs 或 perf 接口来检测 kprobe 和 uprobe。但是,这些接口具有固定的回调函数,因此您将只会看到固定的跟踪输出方式。但是,您始终可以编写自己的内核模块来拥有自己的回调函数,该函数将在命中跟踪指令时被调用。Systemtap 就是这样做的。它基于用户脚本创建一个内核模块。将来,ebpf 追踪可能会取代 systemtap。

回复 作者:kikofernandez

Creative Commons License本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.