systemtap-探秘五-编译和运行

经过前面几篇文章,我们已经走完了 systemtap 运行的前三个流程,只差最后的编译和运行了。 编译编译阶段没有什么要说的,唯一要说明的是 stap 生成的内核模块编译起来很耗时。一般来说,整个编译阶段会花上十几二十秒。所以在生成火焰图时,我通常会让 stap 空跑一遍,让它把内核模块编译出来,完成编译阶段后 Ctrl+C 中断掉它。等到正式压测时,再跑一遍。第二次跑的时候,由于可以用上第一次编译出来的内核,花在前四个阶段的时间会减少很多。 来说下最后的运行阶段。 加载stap 的最后一个阶段,其实是通过运行一个独立的 staprun 二进制文件实现的。这么设计的目的在于把对内核模块的准备和运行分离开来。我们之前执行的 stap 二进制文件可以只负责生成内核模块,然后在目标机器上通过 staprun 运行。这样做的一个好处在于目标机器上可以不用装许多依赖(比如 kernel debuginfo)。另外,在有些公司,服务器只能运行签过名的内核模块,这时候就能先通过 stap 生成内核模块,签了名之后再通过 staprun 运行。具体怎么操作,烦请参考 man staprun。 在 staprun/staprun_funcs.c 文件下,insert_module 函数调用了系统 API init_module,把生成的内核模块加载上去。感兴趣的可以 man init_module 了解下这个 API。 通讯加载的内核模块会创建 /sys/kernel/debug/systemtap/$module_name 这个目录, __stp_module_dir = debugfs_create_dir(module_name, root_dir);然后创建 /sys/kernel/debug/systemtap/$module_name/.cmd 这个文件。 /* create [debugfs]/systemtap/module_name/.cmd */ _stp_cmd_file = debugfs_create_file(".cmd", 0600, module_dir, NULL, &_stp_ctl_fops_cmd);debugfs_create_dir 和 debugfs_create_file 两者是内核模块提供的 API,用来创建一个 debugfs 下的“伪文件”。本着“一切皆文件”的传统,用户态程序可以通过读写这些“伪文件”来调用内核模块指定函数。 _stp_ctl_fops_cmd 这个结构体顾名思义,就是用在发送控制指令并接收响应的。我们可以看下它的定义: ...

October 6, 2019 · 1 min · jiezi

systemtap-探秘二-由-probe-生成的-C-代码

上一篇文章,我简单地介绍了 systemtap 的工作流程,以及第一、第二个阶段的内容。从这篇文章开始,我们将步入本系列的重头戏 - 负责生成 C 代码的第三阶段。 我们可以通过 stap -v test.stp -p3 > out.c 这样的命令,让 stap 把生成的 C 代码重定向到 out.c 去。 hello, world按照惯例,先从一个 ”hello world“ 示例开始。 probe begin { printf("hello")}probe oneshot { printf(" wor")}probe end { printf("ld\n")}出于本人的趣味,这里把一个完整的 hello world 断成三截。通过查找特定的字符串,我们可以很快地从生成的 C 代码里找到这三个 probe 对应生成的代码。 static void probe_3646 (struct context * __restrict__ c) { __label__ deref_fault; __label__ out; struct probe_3646_locals * __restrict__ l = & c->probe_locals.probe_3646; (void) l; if (c->actionremaining < 1) { c->last_error = "MAXACTION exceeded"; goto out; } (void) ({ _stp_print ("hello"); });deref_fault: __attribute__((unused));out: _stp_print_flush();}上面就是 probe begin 对应的代码。 ...

June 27, 2019 · 3 min · jiezi

systemtap-探秘一

Linux 内核(以下简称内核)提供了 kprobe 和 uprobe 的机制,允许用户通过编写自己的内核模块,挂载特定的事件来执行自己的函数。比如我们可以在 accept 系统调用结束时记录下新创建的 fd;或者在 VFS 读写操作前后记录时间戳,统计它们的耗时。 直接裸写内核模块费时费力,其中有部分工作还是可以套模板的(比如从挂载 accept 系统调用改成挂载 write 调用)。而且更大的问题是它不够安全。要是内核态的程序崩溃了,那就是 kernel panic 的事了,准备去重启服务器吧。于是乎像是 systemtap 这样的项目就应运而生了。这一类项目提供了自己的 DSL,然后编译成内核模块。用户不用直接跟 C 代码和内核接口打交道,而是改用能力受限但安全得多的 DSL 编写自己的小工具。打个比方,就像写 SQL 查询数据库和直接编写数据库 C 插件的区别。 需要强调的是,systemtap DSL(以下简称 stp)并非绝对地安全。首先,stp 允许我们嵌入 C 代码,而这部分自然是不安全的。其次,stp 允许我们通过宏来调整编译出来的内核模块的行为,而有些宏是有副作用的。举个例子,stp 里面的数组大小是预先分配好的,大小取决于 MAXMAPENTRIES这个宏(默认 2048)。有些时候,由于要插入很多的数据,你需要调大这个宏。如果简单粗暴地随便在后面加若干个 0,可能会导致内核分配内存失败,进而产生一系列问题(包括 kernel panic)。 对于 4.x 高版本的内核,我们可以使用 ebpf 机制而不是编写或者间接编写内核模块。ebpf 是个在内核态运行的,解释执行字节码的虚拟机。它从底子上提供了更多的安全限制,要比内核模块安全得多。而且编译成 ebpf 字节码要比编译内核模块快很多,这也是基于 ebpf 的内核 profile 的一大优势。 systemtap 除了可以生成内核模块,它还有生成 ebpf 字节码和生成 ptrace 代码的后端。2018 年,我曾尝试过 systmtap 的 ebpf 后端,发现该后端由于正在开发中,支持的 stp 语法特性较少,只有一部分 stp 文件能成功跑起来。相比较而言,BCC 支持的语言特性倒是足够多,然而其对 debuginfo 的支持几乎等于没有,以致于用户态 profile 只能基于 USDT (刻薄一点,就是几乎不能用)。不知道它们俩现在发展得怎样了。 ...

June 24, 2019 · 1 min · jiezi