一 概述

用户静态定义探针简称 USDT(User Statically Defined Tracing), 支持用户在代码中添加探针, 实现对程序的观测.

二 USDT 探针实现概述

在用户代码中使用 DTRACE_PROBE 系列宏添加探针(需要引入 sys/sdt.h 头文件, 由 systemtap-sdt-devel 提供), 代码编译后源码中添加的 DTRACE_PROBE 会编译为 nop 指令, 并在 ELF 文件中添加 .note.stapstd 段.

此时代码在执行过程中与无探针代码不同, 仅增加了一条 nop 指令(每个探针增加一个 nop 指令).

当用户需要对应用程序进行观测, 注册 USDT 探针时, USDT 工具会读取 ELF 中 nop 指令修改为调试中断(int 3). 此时, 当程序执行到 DTRACE_PROBE 代码处时会触发中断, 内核会执行与此探针关联的 eBPF 程序.

在用户对应用程序观测结束时, ELF 被修改的 nop 指令会被恢复.

三 测试验证

1. 安装依赖

测试环境是 CentOS-7, 安装依赖 USDT 软件包:

1
yum install -y systemtap systemtap-runtime systemtap-devel systemtap-sdt-devel

使用 bcc 对应用程序注册探针并进行观测, 安装 bcc 工具包:

1
yum install -y kernel-devel kernel-debug-devel bpftrace bpftrace-tools bpftrace-docs bcc-static bcc-tools

2. 代码示例

应用程序代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <sys/sdt.h>

void foo(int a, int b) {
// 添加探针
DTRACE_PROBE2(demo, foo, a, b);

printf("a:%d b:%d\n", a, b);
}

int main() {

int a = 10;
int b = 20;

int i = 0;
for (i = 0; i<10000; i++) {
foo(a, b);
sleep(5);
}
}

使用 bcc 对应用程序进行观测, 代码如下:

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

bpf_text = """
...

BPF_PERF_OUTPUT(events);

typedef struct msg_s {
int a;
int b;
} msg_t;

int do_trace(struct pt_regs *ctx) {
msg_t msg = {};
bpf_usdt_readarg(1, ctx, &msg.a);
bpf_usdt_readarg(2, ctx, &msg.b);

events.perf_submit(ctx, &msg, sizeof(msg));

return 0;
};
"""

u = USDT(path="./demo")
u.enable_probe(probe="foo", fn_name="do_trace")

b = BPF(text=bpf_text, usdt_contexts=[u])

class Msg(ctypes.Structure):
_fields_ = [("a", ctypes.c_int), ("b", ctypes.c_int)]

def print_event(cpu, data, size):
event = ctypes.cast(data, ctypes.POINTER(Msg)).contents
print("print event a:%d b:%d" % (event.a, event.b))

b["events"].open_perf_buffer(print_event)

while True:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
break

3. 测试操作

1
2
3
4
5
6
7
8
9
# 启动应用程序
./demo >/dev/null &

# 执行观测程序
python trace.py

# 使用 gdb 查看代码
gdb attach -p [pid]
disass foo

应用程序在被观测时汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) disassemble foo
Dump of assembler code for function foo:
0x000000000040057d <+0>: push %rbp
0x000000000040057e <+1>: mov %rsp,%rbp
0x0000000000400581 <+4>: sub $0x10,%rsp
0x0000000000400585 <+8>: mov %edi,-0x4(%rbp)
0x0000000000400588 <+11>: mov %esi,-0x8(%rbp)
0x000000000040058b <+14>: int3 // <--- here
0x000000000040058c <+15>: mov -0x8(%rbp),%edx
0x000000000040058f <+18>: mov -0x4(%rbp),%eax
0x0000000000400592 <+21>: mov %eax,%esi
0x0000000000400594 <+23>: mov $0x400690,%edi
0x0000000000400599 <+28>: mov $0x0,%eax
0x000000000040059e <+33>: callq 0x400450 <printf@plt>
0x00000000004005a3 <+38>: leaveq
0x00000000004005a4 <+39>: retq
End of assembler dump.

注意地址 0x000000000040058b 处指令变为 int3.

关闭观测程序, 再次进行反汇编查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) disassemble foo
Dump of assembler code for function foo:
0x000000000040057d <+0>: push %rbp
0x000000000040057e <+1>: mov %rsp,%rbp
0x0000000000400581 <+4>: sub $0x10,%rsp
0x0000000000400585 <+8>: mov %edi,-0x4(%rbp)
0x0000000000400588 <+11>: mov %esi,-0x8(%rbp)
0x000000000040058b <+14>: nop // <--- here
0x000000000040058c <+15>: mov -0x8(%rbp),%edx
0x000000000040058f <+18>: mov -0x4(%rbp),%eax
0x0000000000400592 <+21>: mov %eax,%esi
0x0000000000400594 <+23>: mov $0x400690,%edi
0x0000000000400599 <+28>: mov $0x0,%eax
0x000000000040059e <+33>: callq 0x400450 <printf@plt>
0x00000000004005a3 <+38>: leaveq
0x00000000004005a4 <+39>: retq
End of assembler dump.

此时地址 0x000000000040058b 处指令变为 nop.

在生产代码中应该使用 dtrace 生产辅助代码, 而非直接使用 DTRACE 宏, 参考代码链接.

四 参考链接