SystemTap追踪自己开发的内核模块
2024-1-9 18:1:26 Author: 看雪学苑(查看原文) 阅读量:5 收藏


背景

看了动态追踪技术漫谈(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: %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追踪该驱动的前提)

    


解决"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, %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);

最终达到了满意的效果:

    

看雪ID:jmpcall

https://bbs.kanxue.com/user-home-815036.htm

*本文为看雪论坛优秀文章,由 jmpcall 原创,转载请注明来自看雪社区

# 往期推荐

1、区块链智能合约逆向-合约创建-调用执行流程分析

2、在Windows平台使用VS2022的MSVC编译LLVM16

3、神挡杀神——揭开世界第一手游保护nProtect的神秘面纱

4、为什么在ASLR机制下DLL文件在不同进程中加载的基址相同

5、2022QWB final RDP

6、华为杯研究生国赛 adv_lua

球分享

球点赞

球在看


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458534371&idx=1&sn=64c528cff7b50a35d1a7b168280a494a&chksm=b0ac90eb56f958ba7061a20560c60f51e859498a501afd267c7b8679e1ceb65d042d5004e671&scene=0&xtrack=1#rd
如有侵权请联系:admin#unsafe.sh