一
背景
看了《动态追踪技术漫谈》(https://blog.openresty.com.cn/cn/dynamic-tracing/)这篇文章之后,就想着拿systemtap追踪一下自己开发的内核模块。
然而,systemtap官方文档(https://sourceware.org/systemtap/documentation.html),对如何追踪内核模块的描述中,只是拿内核源码树中的驱动举例,比如:probe module(“ext3”).function(“*”) { },实际验证确实也很顺利(我的系统使用的是xfs文件驱动,stap命令执行后,随便vi一个文件,就能看到xfs_iread()函数被调用了,并列出了参数信息):
然后,我写了一个最简单的驱动,test.c:
#include <linux/module.h>
void test(int n)
{
printk("%s(), %d: %dn", __FUNCTION__, __LINE__, n);
}
static int __init test_init(void)
{
test(100);
printk("%s(), %dn", __FUNCTION__, __LINE__);
return 0;
}
static void __exit test_exit(void)
{
test(100);
printk("%s(), %dn", __FUNCTION__, __LINE__);
}
module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
Makefile:
ifneq ($(KERNELRELEASE),)
obj-m := test.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
endif
以及x.stap:
#!/usr/bin/env stap
probe begin {
printf("beginn");
}
probe module("test").function("*").call {
printf("%s -> %sn", thread_indent(1), probefunc())
}
probe module("test").function("*").return {
printf("%s <- %sn", thread_indent(-1), probefunc())
}
接下来,编译test.c->加载test.ko->执行x.stap(此刻,我还在一个思维陷阱里,以为加载驱动,是stap追踪该驱动的前提):
二
解决"stap x.stp"执行失败
我一开始认为,驱动都已经加载了,stap却还不认识,那问题应该出在test.ko的编译上,为了解决这个问题,兜兜绕绕了一大圈。
首先是百度、google了一遍,看了前几页的回答,都是说要安装内核debuginfo,不过我的系统,正好之前已经安装过内核debuginfo,而且我要追踪的是自己开发的驱动,按道理也不需要依赖内核debuginfo。
然后到一些微信群里询问也无果,可能大佬都很忙,没空理我。
最后只能自己再翻翻官方文档,开始各种推测与尝试。
记忆中,之前看过一款动态追踪工具的原理,提到在编译被追踪程序时,gcc必须添加-pg编译选项,这会使gcc在每个函数入口,添加5条nop指令,从而预留5个字节,可以在动态追踪时,替换成”call mcount”的机器码,才能使在追踪点注入的代码有机会执行。想到这,就在Makefile里加了一行:
ccflags-y += -pg
结果,问题仍然存在,再回过头搜一下资料,得知ftrace才依赖-pg编译选项,systemtap底层依赖的是kprobe,它可以追踪任何地址处的指令,原理是将追踪地址的第一个字节,替换成0xCC(即”int 3″指令),利用中断机制实现的。
那么,和xfs驱动相比,除了是否已加载和编译选项之外,还有什么区别?
官方文档中的这么一句话,虽然只是阐述了一个客观情况,并没有表达,.ko文件一定要放在/lib/modules/$(uname -r)/目录,才能被追踪的意思,但是test.ko和xfs驱动相比,目前能想的的区别,也就这个了,所以就侥幸的试了一下,竟然成功了:
并且,额外的惊喜是,”stat x.stp”的执行,并不依赖先”insmod test.ko”,也就是说,test_init()函数,也可以被追踪。不过想想也是,如果连内核模块加载函数都追踪不了,那systemtap还号称什么”利器”。
三
未显示函数名称
本来以为,接下来就可以尽情的畅游了。
然而,通过”insmod test.ko”和”rmmod test”分别触发test_init()和test_exit()执行,发现stap的打印内容是这样:
其中,0xffffffffc037f000是test_init()函数的加载地址,0xffffffffc0876000是test_exit()函数的加载地址,这可以在test.ko卸载之前,通过以下3种方式证实:
① 查看test.ko节区的加载地址
② 查看内核符号表中,属于test驱动的符号及其加载地址(不清楚为什么没看到”test_init”)
③ 查看这两个内存地址的内容,与test_init()/test_exit()函数反汇编的机器码对比(这种方式要求系统安装了内核debuginfo)
test_exit()函数机器码:
内存查看:
确认打印内容中,”->”之后是被追踪函数的地址之后,还存在另外3个疑问:
1. stap打印的为什么是函数地址,而不是函数名称?
这个可以通过将x.stp脚本中的probefunc(),替换成ppfunc()解决,同时也能避免以下问题3中的现象:
2. test_init()和test_exit()函数都可以追踪了,test()函数为什么没被追踪到?
第4节专门介绍。
3. “<-“与”->”后面的地址不同,又代表什么地址?
解决问题2后,让函数多调用几层,就能看出,”->”后面是callee函数地址,”<-“后面是caller函数地址。
四
未追踪到test()函数
x.stp脚本明明追踪的是所有函数(function(“*”)),test()函数却没被追踪到。
首先尝试的是,在Makefile中加一行:
ccflags-y += -g
然而,仍然不能追踪test()函数。
执行”readelf -S test.ko”可以发现,不加-g编译,test.ko就已经包含debug_info节区了,节区数量也不比加了-g编译少:
这时就想着看看,init_test()/test_exit()函数中,是怎么调用test()函数的:
可以看出,调用test()的call指令,在test_init()和test_exit()函数中的偏移,都是0x1e,那么0x1f处一定有对应的重定项(这里需要一点链接原理的知识,可以看看我写的”32位elf格式中的10种重定位类型(https://bbs.kanxue.com/thread-246373.htm)“):
最终得出,0x1f处,原本应该填充为test()函数地址,但是却被直接填充为printk()函数的地址了,所以大致可以推测,可能由于test()除了调用一次printk(),其它什么也没干,编译时就被gcc优化成直接调用printk()了。
为了证实想法,在Makefile添加一行:
ccflags-y += -O0
再看重定项信息,就是test()函数了(重新编译后,调用test()的call指令,位于0x09偏移):
并且可以看到test()可以被追踪了:
五
无法获取test()函数的参数和局部变量值
搞到这里,估计大家都不想再有什么妖蛾子了,但是systemtap才不管你想不想!
加了-O0选项编译,可以追踪test()函数之后,我又油然而生了一个僭越的想法,便将x.stp改了,试图追踪到test()函数时,打印一下参数和局部变量值:
probe module("test").function("test").call {
printf("%s -> %s, %s, %dn", thread_indent(1), ppfunc(), $$parms, $n)
}
结果却是:
n的值明显不对,n等于100才对!
由于-O0给过我惊喜,加上如果优化级别为0都有问题,更何况更高的优化级别呢,所以我并没有第一时间想到它会害我,后来无意去掉”ccflags-y += -O0″,发现获取到了”n=0x64″,才没再留恋,果断弃了它。
不过去掉”ccflags-y += -O0″,又得回去面对追踪不到test()函数的问题,但这不是systemtap的问题,而是gcc作祟,也可以理解为,test()函数确实简单到不需要追踪了,所以,只要将test()函数改”复杂”,就可以”解决”这个问题:
void test(int n)
{
int m = n/10 + 7;
printk("%s(), %d: %d, %dn", __FUNCTION__, __LINE__, n, m);
}
然而,再次被systemtap玩耍:
不过还是被我冷静的发现,开始执行”cat /proc/kallsyms”时,除了没看到”test_init”,也没看到”test”。
对于”test_init”,按常理应该和”test_exit”一样被显示才对,至于为什么没显示,我没再深究,但是可以感觉到,肯定和”test”没被显示是有区别的,因为test_init()函数,一直都是可以被追踪的。由此推测,得让test()函数,在驱动加载后,也存在于内核符号表才行。
于是尝试在test.c中,导出test()函数名称:
EXPORT_SYMBOL(test);
最终达到了满意的效果:
看雪ID:jmpcall
https://bbs.kanxue.com/user-home-815036.htm
# 往期推荐
2、在Windows平台使用VS2022的MSVC编译LLVM16
3、神挡杀神——揭开世界第一手游保护nProtect的神秘面纱
球分享
球点赞
球在看
原文始发于微信公众号(看雪学苑):SystemTap追踪自己开发的内核模块