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程序

#include <stdio.h>

int func1() {
printf("1\n");
} int main() {
func1();
return 0;
}

编译之

gcc main.c -O0 -o uprobe_test

我们想要trace uprobe_test启动之后,什么时候调用的func1, 什么时候从func1返回的

这时我们使用这样的命令

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的符号地址

[root@VM-0-13-centos uprobe]# nm uprobe_test | grep func1
0000000000401126 T func1

0x401126 看起来和0x1126差了一个0x400000

而官方描述里面这个对应的参数叫offset,

这下就明白设计意图了,我们告诉内核两个信息

  1. 要trace的程序的路径
  2. 程序加载到进程内存空间之后,断点距离加载起始地址的偏移

有了1,系统就可以根据进程里面的maps分布找到起始地址,再加上2中的偏移,就得到了具体断点在进程空间中的位置。

[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这个其实地址

uprobe使用的偏移地址为0x400000是因为该地址是ELF文件在内存中的加载地址。ELF文件是可执行和共享对象文件的标准格式,其中包含有关程序加载和执行的信息。加载器在将ELF文件加载到内存中时,会将文件中的各个段(如代码段、数据段)映射到相应的虚拟地址空间中。而0x400000是Linux系统默认的ELF加载地址,它提供了一个通用的起始地址,可以避免与其他系统或库冲突。因此,uprobe在使用偏移地址时选择了0x400000作为默认值。当然,根据具体情况,也可以根据需要修改偏移地址。

我们使用readelf -l 也能看到这样的信息

[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,但如果我们用地址无关的方式编译,效果会是怎样呢?

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

但是当程序加载起来,我们看看真正加载在哪

[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/下面多出两个目录

[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这两个目录和我们的命令一一对应。

通过执行

echo 1 >/sys/kernel/debug/tracing/events/uprobes/enable

开启trace

这时我们运行./uprobe_test 再cat /sys/kernel/debug/tracing/trace

[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而不用自己计算偏移量

perf probe -x ./uprobe_test func1
perf probe -x ./uprobe_test func1%return

只不过这时grp变成了probe_uprobe_test

[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

uprobe的使用浅析的更多相关文章

  1. SQL Server on Linux 理由浅析

    SQL Server on Linux 理由浅析 今天的爆炸性新闻<SQL Server on Linux>基本上在各大科技媒体上刷屏了 大家看到这个新闻都觉得非常震精,而美股,今天微软开 ...

  2. 【深入浅出jQuery】源码浅析--整体架构

    最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...

  3. 高性能IO模型浅析

    高性能IO模型浅析 服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种: (1)同步阻塞IO(Blocking IO):即传统的IO模型. (2)同步非阻塞IO(Non-blocking  ...

  4. netty5 HTTP协议栈浅析与实践

      一.说在前面的话 前段时间,工作上需要做一个针对视频质量的统计分析系统,各端(PC端.移动端和 WEB端)将视频质量数据放在一个 HTTP 请求中上报到服务器,服务器对数据进行解析.分拣后从不同的 ...

  5. Jvm 内存浅析 及 GC个人学习总结

    从诞生至今,20多年过去,Java至今仍是使用最为广泛的语言.这仰赖于Java提供的各种技术和特性,让开发人员能优雅的编写高效的程序.今天我们就来说说Java的一项基本但非常重要的技术内存管理 了解C ...

  6. 从源码浅析MVC的MvcRouteHandler、MvcHandler和MvcHttpHandler

    熟悉WebForm开发的朋友一定都知道,Page类必须实现一个接口,就是IHttpHandler.HttpHandler是一个HTTP请求的真正处理中心,在HttpHandler容器中,ASP.NET ...

  7. 【深入浅出jQuery】源码浅析2--奇技淫巧

    最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...

  8. 浅析匿名函数、lambda表达式、闭包(closure)区别与作用

    浅析匿名函数.lambda表达式.闭包(closure)区别与作用 所有的主流编程语言都对函数式编程有支持,比如c++11.python和java中有lambda表达式.lua和JavaScript中 ...

  9. word-break|overflow-wrap|word-wrap——CSS英文断句浅析

    ---恢复内容开始--- word-break|overflow-wrap|word-wrap--CSS英文断句浅析 一 问题引入 今天在再次学习 overflow 属性的时候,查看效果时,看到如下结 ...

  10. 编写轻量ajax组件02-AjaxPro浅析

    前言 上一篇介绍了在webform平台实现ajax的一些方式,并且实现一个基类.这一篇我们来看一个开源的组件:ajaxpro.虽然这是一个比较老的组件,不过实现思想和源码还是值得我们学习的.通过上一篇 ...

随机推荐

  1. Jenkins+docker 部署SpringCloud微服务

    部署需要提前准备的环境:安装好Jenkins.docker.Maven.Jdk1.8.Git 说明:由于本例只说明如何部署,所以有关项目其他服务如nacos.mysql.redis.seata等默认已 ...

  2. 测试工程师-bug的组成要素

    bug的组成要素:所属产品.所属模块.版本.指派开发.bug标题.严重程度.优先级.bug类型.重现步骤.附件等: 1. 所属产品.所属模块.版本.指派开发 根据相应的项目正确填写 2.bug标题 简 ...

  3. JavaScript 中的闭包和事件委托

    包 (Closures) 闭包是 JavaScript 中一个非常强大的特性,它允许函数访问其外部作用域中的变量,即使在该函数被调用时,外部作用域已经执行完毕.闭包可以帮助我们实现数据的私有化.封装和 ...

  4. 【RabbitMQ】08 深入部分P1 可靠性投递

    1.消息投递确认 这里的代码延用了06的东西: https://www.cnblogs.com/mindzone/p/15374684.html 删除之前的整合案例,重新写了一份案例的队列和交换机配置 ...

  5. 【Vue】Re09 Webpack 第一部分(介绍、安装、配置)

    一.Webpack的用途 webpack要解决的是统一网页资源的问题 前端工程化出现了很多问题,就是兼容性,浏览器所不能解析 所以需要一个打包,转换等方式处理 二.安装描述介绍 下载安装NodeJS, ...

  6. 寻路数据集 —— PathFinding数据集 —— Moving AI Lab. 实验室

    好几个做pathfinding的论文都是引用这个网站的数据集,不过这个网站的数据集的地图都是 .map 格式,这个类型的格式该如何打开还不知道. Moving AI Lab. 实验室的工作 地址: h ...

  7. Jax框架的jit编译是否可以使用循环结构,如果使用循环结构需要注意什么(续)

    前文: Jax框架的jit编译是否可以使用循环结构,如果使用循环结构需要注意什么 从前文我们知道,jax的jit中尽可能的不要放入循环结构,因为在jit编译时会将循环结构暂开,因而会消耗掉大量的时间进 ...

  8. 【转载】 vim中常用折叠命令

    原文地址: https://www.cnblogs.com/litifeng/p/11675547.html 个人推荐的一个视频教程地址: 上古神器Vim:从恶言相向到爱不释手 - 终极Vim教程01 ...

  9. 查看numpy中不同数据类型的表示范围

    在numpy中数据类型主要可以分为int和float两个类型,查看int类型的表示范围可以使用numpy.iinfo,查看float类型的表示范围可以使用numpy.finfo  . 例子: impo ...

  10. 武汉市委郭元强书记、盛阅春代市长会见白鲸开源CEO郭炜等嘉宾代表

    2024年6月14日,第二届软件创新发展大会在中国武汉举行.大会云集了来自全国的书数百位院士.专家.知名软件企业负责人,包括中国工程院院士倪光南.中国科学院院士陈十一.国家工业信息安全发展研究中心总工 ...