您是否曾遇到过这种情况:您意识到自己在代码的几个地方没有插入debug print,因此现在您不知道 CPU 是否会执行特定代码行,除非您使用调试语句重新编译代码?别担心,有一个更简单的解决方案。基本上,您需要在源代码汇编指令的不同位置插入动态探针点。
对于高级用户,内核 documentation/trace 和 man perf 提供了关于不同类型内核和用户空间追踪机制的大量信息;然而,普通用户只想了解几个简单的步骤和一个示例,以便快速入门。本文将在这方面提供帮助。
让我们从定义开始。
探针点
探针点是一个调试语句,用于帮助探索软件的执行特性(即探针语句执行时软件数据结构的执行流程和状态)。printk 是最简单的探针语句形式,也是开发人员用于内核 hacking 的基本工具之一。
静态与动态探测
由于 printk 的插入需要重新编译源代码,因此它是一种静态探测方法。内核代码中的许多重要位置还有许多其他静态跟踪点,可以动态地启用或禁用。Linux 内核有一些框架可以帮助开发人员探测内核或用户空间应用程序,而无需重新编译源代码。Kprobe 是一种在内核代码中插入探针点的动态方法,而 uprobe 则在用户应用程序中执行此操作。
使用 uprobe 追踪用户空间
可以使用 sysfs 接口或 perf 工具将 uprobe 跟踪点插入到任何用户空间代码中。
使用 sysfs 接口插入 uprobe
考虑以下简单的测试代码,其中没有打印语句,我们想在某个指令处插入一个探针
[[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
并且我们想在偏移量 0x620 和 0x644 处插入一个探针。执行以下命令
# 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/test。0x620 或 0x640 是从程序开始的指令偏移量。请注意第二个 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 之外,还有一些其他工具,例如 SystemTap、DTrace 和 LTTng,可以用于内核和用户空间追踪;然而,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 用于创建探针点事件,然后可以在执行 ./test 可执行文件时使用 perf record 探测这些事件。当我们创建一个 perf probe 点时,我们可以有其他记录选项,如 perf stat,并且我们可以有许多后分析选项,如 perf script 或 perf 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 您的可执行代码并将几个探针点插入其中。祝您追踪愉快!
4 条评论