首页
社区
课程
招聘
systemtap追踪自己开发的内核模块
2023-12-29 17:13 8112

systemtap追踪自己开发的内核模块

2023-12-29 17:13
8112

1. 背景

    看了动态追踪技术漫谈这篇文章之后,就想着拿systemtap追踪一下自己开发的内核模块,然而,systemtap官方文档,对如何追踪内核模块的描述中,只是拿内核源码树中的驱动举例,比如:probe module("ext3").function("*") { },实际验证确实也很顺利(我的系统使用的是xfs文件驱动,stap命令执行后,随便vi一个文件,就能看到xfs_iread()函数被调用了,并列出了参数信息)

    

    然后,我写了一个最简单的驱动,test.c:

#include <linux/module.h>


void test(int n)
{
    printk("%s(), %d: %d\n", __FUNCTION__, __LINE__, n);
}

static int __init test_init(void)
{
    test(100);
    printk("%s(), %d\n", __FUNCTION__, __LINE__);
    return 0;
}

static void __exit test_exit(void)
{
    test(100);
    printk("%s(), %d\n", __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("begin\n");
}

probe module("test").function("*").call {
  printf("%s -> %s\n", thread_indent(1), probefunc())
}

probe module("test").function("*").return {
  printf("%s <- %s\n", thread_indent(-1), probefunc())
}

    接下来,编译test.c->加载test.ko->执行x.stap(此刻,我还在一个思维陷阱里,以为加载驱动,是stap追踪该驱动的前提)

    

2. 解决"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还号称什么"利器"。

3. 未显示函数名称

    本来以为,接下来就可以尽情的畅游了。

    然而,通过"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函数地址。

4. 未追踪到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种重定位类型"):

    

    最终得出,0x1f处,原本应该填充为test()函数地址,但是却被直接填充为printk()函数的地址了,所以大致可以推测,可能由于test()除了调用一次printk(),其它什么也没干,编译时就被gcc优化成直接调用printk()了。

    为了证实想法,在Makefile添加一行:

ccflags-y += -O0

    再看重定项信息,就是test()函数了重新编译后,调用test()的call指令,位于0x09偏移

    

    并且可以看到test()可以被追踪了:

    

5. 无法获取test()函数的参数和局部变量值

    搞到这里,估计大家都不想再有什么妖蛾子了,但是systemtap才不管你想不想!

    加了-O0选项编译,可以追踪test()函数之后,我又油然而生了一个僭越的想法,便将x.stp改了,试图追踪到test()函数时,打印一下参数和局部变量值:

probe module("test").function("test").call {
  printf("%s -> %s, %s, %d\n", 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, %d\n", __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);

    最终达到了满意的效果:

    



[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

收藏
点赞6
打赏
分享
最新回复 (3)
雪    币: 19349
活跃值: (28971)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2024-1-2 10:12
2
1
感谢分享
雪    币: 508
活跃值: (4295)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
bwner 1 2024-1-3 11:59
3
0
感谢分享
雪    币: 8233
活跃值: (4941)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
大道在我 2024-1-10 00:27
4
0
systemtap能否支持追踪andorid内核调试呢
游客
登录 | 注册 方可回帖
返回