uprobe是linux内核提供的一种trace用户态函数的机制
可以在不对二进制重新编译的情况下进行trace特定函数
本文描述了uprobe的基本使用方法
使用方法
- 官方的指引是这样的, 详细的可以看kernel代码中的文档Documentation/trace/uprobetracer.rst
p[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS] : Set a uprobe r[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS] : Set a return uprobe (uretprobe)
- p 代表trace函数
- r 代表trace函数的返回
先不描述其它参数,我们从例子入手,这样能更好理解其它参数的作用
比如我有一个main.c程序
代码语言:javascript复制#include <stdio.h>
int func1() {
printf("1n");
}
int main() {
func1();
return 0;
}
编译之
代码语言:javascript复制gcc main.c -O0 -o uprobe_test
我们想要trace uprobe_test启动之后,什么时候调用的func1, 什么时候从func1返回的
这时我们使用这样的命令
代码语言:javascript复制echo 'p:func1 ./uprobe_test:0x1126' >> /sys/kernel/debug/tracing/uprobe_events
echo 'r:func1_ret ./uprobe_test:0x1126' >> /sys/kernel/debug/tracing/uprobe_events
分几个部分解释这两个命令,首先,从输出的目标文件可以猜出,uprobe_events应该是一个列表,这是一个和内核沟通的通道,里面存储着我们期望trace的规则,规则描述了我们想要trace哪些函数的进入,想要trace哪些函数的返回,我们可以cat这个文件看看。
那么规则是什么样呢,之前说了p代表trace函数进入,r代表trace函数退出
从上面cat的结果可以看到p:后面跟了一个uprobes,这是这个事件的grp名称,由于我们在命令中直接指定了事件名称“func1”(注意这里的func1是紧跟在p:后面,是一个名称,与要跟踪的函数名称可以不同),如果不指定grp,系统会把我们的事件自动分配到uprobes组。
接着看命令
:func1 代表一个自定义的消息名称
./uprobe_test 就是一个文件的相对路径,用来供内核找到对应的inode节点
之后的:0x1126代表我们要在映射了这个inode对应的数据块的起始偏移0x1126处设置一个断点,一旦运行到了这个断点,就会进入我们设定好的trace逻辑(默认是向/sys/kernel/debug/tracing/trace和/sys/kernel/debug/tracing/trace_pipe中写日志)
那么疑点来了,:0x1126是哪来的呢,看起来像是某种地址,跟func1有关。
我们来看一下func1的符号地址
代码语言:javascript复制[root@VM-0-13-centos uprobe]# nm uprobe_test | grep func1
0000000000401126 T func1
0x401126 看起来和0x1126差了一个0x400000
而官方描述里面这个对应的参数叫offset, 这下就明白设计意图了,我们告诉内核两个信息
- 要trace的程序的路径
- 程序加载到进程内存空间之后,断点距离加载起始地址的偏移
有了1,系统就可以根据进程里面的maps分布找到起始地址,再加上2中的偏移,就得到了具体断点在进程空间中的位置。
代码语言:javascript复制[root@VM-0-13-centos uprobe]# cat /proc/121052/maps
00400000-00401000 r--p 00000000 fd:01 1721806 /root/linux_learn_diary/uprobe/uprobe_test
00401000-00402000 r-xp 00001000 fd:01 1721806 /root/linux_learn_diary/uprobe/uprobe_test
00402000-00403000 r--p 00002000 fd:01 1721806 /root/linux_learn_diary/uprobe/uprobe_test
00403000-00404000 r--p 00002000 fd:01 1721806 /root/linux_learn_diary/uprobe/uprobe_test
00404000-00405000 rw-p 00003000 fd:01 1721806 /root/linux_learn_diary/uprobe/uprobe_test
7ffff7a0b000-7ffff7bc7000 r-xp 00000000 fd:01 142256 /usr/lib64/libc-2.28.so
7ffff7bc7000-7ffff7dc6000 ---p 001bc000 fd:01 142256 /usr/lib64/libc-2.28.so
7ffff7dc6000-7ffff7dca000 r--p 001bb000 fd:01 142256 /usr/lib64/libc-2.28.so
7ffff7dca000-7ffff7dcc000 rw-p 001bf000 fd:01 142256 /usr/lib64/libc-2.28.so
7ffff7dcc000-7ffff7dd0000 rw-p 00000000 00:00 0
7ffff7dd0000-7ffff7dfc000 r-xp 00000000 fd:01 142249 /usr/lib64/ld-2.28.so
7ffff7fef000-7ffff7ff1000 rw-p 00000000 00:00 0
7ffff7ff8000-7ffff7ffb000 r--p 00000000 00:00 0 [vvar]
7ffff7ffb000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso]
7ffff7ffc000-7ffff7ffd000 r--p 0002c000 fd:01 142249 /usr/lib64/ld-2.28.so
7ffff7ffd000-7ffff7fff000 rw-p 0002d000 fd:01 142249 /usr/lib64/ld-2.28.so
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
这里可以看出,uprobe_test是被映射到了0x400000这个其实地址
我们使用readelf -l 也能看到这样的信息
代码语言:javascript复制[root@VM-0-13-centos uprobe]# readelf -l uprobe_test | grep LOAD
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000
LOAD 0x0000000000002e00 0x0000000000403e00 0x0000000000403e00
可以看到第一个需要被加载到内存中的段的偏移是0x400000
这是可能会有一个疑问,为什么不直接告诉内核断点的真实位置是0x401126呢,让内核去找起始地址再加上偏移,得到的不也是这个值吗,这不是多此一举吗。
的确,当前编译出来的uprobe_test的文件中的符号地址确实就是0x401126,但如果我们用地址无关的方式编译,效果会是怎样呢?
代码语言:javascript复制gcc main.c -pie -fPIE -O0 -o uprobe_test
[root@VM-0-13-centos uprobe]# readelf -l uprobe_test | grep LOAD
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
LOAD 0x0000000000002de0 0x0000000000003de0 0x0000000000003de0
可以看到这样编译之后起始偏移变成了0而不是0x400000
但是当程序加载起来,我们看看真正加载在哪
代码语言:javascript复制[root@VM-0-13-centos linux_learn_diary]# cat /proc/124222/maps
555555554000-555555555000 r--p 00000000 fd:01 1721809 /root/linux_learn_diary/uprobe/uprobe_test
555555555000-555555556000 r-xp 00001000 fd:01 1721809 /root/linux_learn_diary/uprobe/uprobe_test
555555556000-555555557000 r--p 00002000 fd:01 1721809 /root/linux_learn_diary/uprobe/uprobe_test
555555557000-555555558000 r--p 00002000 fd:01 1721809 /root/linux_learn_diary/uprobe/uprobe_test
555555558000-555555559000 rw-p 00003000 fd:01 1721809 /root/linux_learn_diary/uprobe/uprobe_test
7ffff7a0b000-7ffff7bc7000 r-xp 00000000 fd:01 142256 /usr/lib64/libc-2.28.so
7ffff7bc7000-7ffff7dc6000 ---p 001bc000 fd:01 142256 /usr/lib64/libc-2.28.so
7ffff7dc6000-7ffff7dca000 r--p 001bb000 fd:01 142256 /usr/lib64/libc-2.28.so
7ffff7dca000-7ffff7dcc000 rw-p 001bf000 fd:01 142256 /usr/lib64/libc-2.28.so
7ffff7dcc000-7ffff7dd0000 rw-p 00000000 00:00 0
7ffff7dd0000-7ffff7dfc000 r-xp 00000000 fd:01 142249 /usr/lib64/ld-2.28.so
7ffff7fef000-7ffff7ff1000 rw-p 00000000 00:00 0
7ffff7ff8000-7ffff7ffb000 r--p 00000000 00:00 0 [vvar]
7ffff7ffb000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso]
7ffff7ffc000-7ffff7ffd000 r--p 0002c000 fd:01 142249 /usr/lib64/ld-2.28.so
7ffff7ffd000-7ffff7fff000 rw-p 0002d000 fd:01 142249 /usr/lib64/ld-2.28.so
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
可以看到加载到一个我们未预见到的地址,也就是说,有时我们没法从一个二进制程序中的符号计算出它真正在地址空间中的地址,我们只能知道它距离映射起始地址处的偏移量。
所以这时inode和offset就变得缺一不可了。
这样,第一条命令解释完毕。 第二条命令,r开头,代表trace函数的退出。
写入规则后,debugfs中的目录结构也会发生变化,由于我们使用的是默认的uprobes组,所以会在tracing/events/uprobes/下面多出两个目录
代码语言:javascript复制[root@VM-0-13-centos events]# cd /sys/kernel/debug/tracing/events/
[root@VM-0-13-centos events]# tree uprobes/
uprobes/
├── enable
├── filter
├── func1
│ ├── enable
│ ├── filter
│ ├── format
│ ├── id
│ └── trigger
└── func1_ret
├── enable
├── filter
├── format
├── id
└── trigger
func1 func1_ret这两个目录和我们的命令一一对应。
通过执行
代码语言:javascript复制echo 1 >/sys/kernel/debug/tracing/events/uprobes/enable
开启trace
这时我们运行./uprobe_test 再cat /sys/kernel/debug/tracing/trace
代码语言:javascript复制[root@VM-0-13-centos ~]# cat /sys/kernel/debug/tracing/trace
# tracer: nop
#
# entries-in-buffer/entries-written: 2/2 #P:2
#
# _-----=> irqs-off
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
uprobe_test-127256 [001] d... 49487.340206: func1: (0x401126)
uprobe_test-127256 [001] d... 49487.340271: func1_ret: (0x401145 <- 0x401126)
就可以看到trace信息了
这里,perf给我们提供了一个简便的工具添加uprobe而不用自己计算偏移量
代码语言:javascript复制perf probe -x ./uprobe_test func1
perf probe -x ./uprobe_test func1%return
只不过这时grp变成了probe_uprobe_test
代码语言:javascript复制[root@VM-0-13-centos uprobe]# cat /sys/kernel/debug/tracing/uprobe_events
p:probe_uprobe_test/func1 /root/linux_learn_diary/uprobe/uprobe_test:0x0000000000001126
r:probe_uprobe_test/func1__return /root/linux_learn_diary/uprobe/uprobe_test:0x0000000000001126