转载-Kernel Tracing 101

日常搜索 kernel 监控,找到了 Linux 内核监控在 Android 攻防中的应用, 转载其 Kernel Tracing 101 章节.

这个章节很好总结了 内核监控的现有工具, 有删减.

Kernel Tracing 101

在内核代码中引入一次性的 trampoline,然后在后续增加或者减少系统调用监控入口时通过内核模块的方式去进行修改。这样似乎稍微合理一些,但其实内核中已经有了许多类似的监控方案,这样做纯属重复造轮子,效率低不说还可能随时引入 kernel panic。

大局观

那么,内核中都有哪些监控方案?这其实不是一个容易回答的问题,我们在日常运维时听说过 kprobe、jprobe、uprobe、eBPF、tracefs、systemtab、perf,……到底他们之间的的关系是什么,分别都有什么用呢?

这里推荐一篇文章: Linux tracing systems & how they fit together,根据其中的介绍,这些内核监控方案/工具可以分为三类:

  1. 数据: 根据监控数据的来源划分
  2. 采集: 根据内核提供给用户态的原始事件回调接口进行划分
  3. 前端: 获取和解析监控事件数据的用户工具

![[3.png]]

后面对这些监控方案分别进行简要的介绍。

数据

Kprobe

简单来说,kprobe 可以实现动态内核的注入,基于中断的方法在任意指令中插入追踪代码,并且通过 pre_handler/post_handler/fault_handler 去接收回调。

使用

参考 Linux 源码中的 samples/kprobes/kprobe_example.c,一个简单的 kprobe 内核模块实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>

#define MAX_SYMBOL_LEN 64
static char symbol[MAX_SYMBOL_LEN] = "_do_fork";
module_param_string(symbol, symbol, sizeof(symbol), 0644);

/* For each probe you need to allocate a kprobe structure */
static struct kprobe kp = {
.symbol_name = symbol,
};
/* kprobe pre_handler: called just before the probed instruction is executed */
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
pr_info("<%s> pre_handler: p->addr = 0x%p, pc = 0x%lx\n", p->symbol_name, p->addr, (long)regs->pc);
/* A dump_stack() here will give a stack backtrace */
return 0;
}
/* kprobe post_handler: called after the probed instruction is executed */
static void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags)
{
pr_info("<%s> post_handler: p->addr = 0x%p\n", p->symbol_name, p->addr);
}
/*
* fault_handler: this is called if an exception is generated for any
* instruction within the pre- or post-handler, or when Kprobes
* single-steps the probed instruction.
*/
static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
pr_info("fault_handler: p->addr = 0x%p, trap #%dn", p->addr, trapnr);
/* Return 0 because we don't handle the fault. */
return 0;
}

static int __init kprobe_init(void)
{
int ret;
kp.pre_handler = handler_pre;
kp.post_handler = handler_post;
kp.fault_handler = handler_fault;

ret = register_kprobe(&kp);
if (ret < 0) {
pr_err("register_kprobe failed, returned %d\n", ret);
return ret;
}
pr_info("Planted kprobe at %p\n", kp.addr);
return 0;
}
static void __exit kprobe_exit(void)
{
unregister_kprobe(&kp);
pr_info("kprobe at %p unregistered\n", kp.addr);
}
module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");

安装该内核模块后,每当系统中的进程调用 fork,就会触发我们的 handler,从而在 dmesg 中输出对应的日志信息。值得注意的是,kprobe 模块依赖于具体的系统架构,上述 pre_handler 中我们打印指令地址使用的是 regs->pc,这是 ARM64 的情况,如果是 X86 环境,则对应 regs->ip,可查看对应 arch 的 struct pt_regs 实现。

原理

kprobe 框架基于中断实现。当 kprobe 被注册后,内核会将对应地址的指令进行拷贝并替换为断点指令 (比如 X86 中的 int 3),随后当内核执行到对应地址时,中断会被触发从而执行流程会被重定向到我们注册的 pre_handler 函数;当对应地址的原始指令执行完后,内核会再次执行 post_handler (可选),从而实现指令级别的内核动态监控。也就是说,kprobe 不仅可以跟踪任意带有符号的内核函数,也可以跟踪函数中间的任意指令。

另一个 kprobe 的同族是 kretprobe,只不过是针对函数级别的内核监控,根据用户注册时提供的 entry_handlerret_handler 来分别在函数进入时和返回前进行回调。当然实现上和 kprobe 也有所不同,不是通过断点而是通过 trampoline 进行实现,可以略为减少运行开销。

有人可能听说过 Jprobe,那是早期 Linux 内核的的一个监控实现,现已被 Kprobe 替代。

拓展阅读:

  • An introduction to KProbes
  • Documentation/trace/kprobetrace.rst
  • samples/kprobes/kprobe_example.c
  • samples/kprobes/kretprobe_example.c

Uprobe

uprobe 顾名思义,相对于内核函数/地址的监控,主要用于用户态函数/地址的监控。听起来是不是有点神奇,内核怎么监控用户态函数的调用呢?

使用

站在用户视角,我们先看个简单的例子,假设有这么个一个用户程序:

1
2
3
4
5
6
7
8
9
// test.c
#include <stdio.h>
void foo() {
printf("hello, uprobe!\n");
}
int main() {
foo();
return 0;
}

编译好之后,查看某个符号的地址,然后告诉内核我要监控这个地址的调用:

1
2
3
4
5
6
$ gcc test.c -o test
$ readelf -s test | grep foo
87: 0000000000000764 32 FUNC GLOBAL DEFAULT 13 foo
$ echo 'p /root/test:0x764' > /sys/kernel/debug/tracing/uprobe_events
$ echo 1 > /sys/kernel/debug/tracing/events/uprobes/p_test_0x764/enable
$ echo 1 > /sys/kernel/debug/tracing/tracing_on

然后运行用户程序并检查内核的监控返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ ./test && ./test
hello, uprobe!
hello, uprobe!

$ cat /sys/kernel/debug/tracing/trace
# tracer: nop
#
# WARNING: FUNCTION TRACING IS CORRUPTED
# MAY BE MISSING FUNCTION EVENTS
# entries-in-buffer/entries-written: 3/3 #P:8
#
# _-----=> irqs-off
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
test-7958 [006] …. 34213.780750: p_test_0x764: (0x6236218764)
test-7966 [006] …. 34229.054039: p_test_0x764: (0x5f586cb764)

当然,最后别忘了关闭监控:

1
2
3
$ echo 0 > /sys/kernel/debug/tracing/tracing_on
$ echo 0 > /sys/kernel/debug/tracing/events/uprobes/p_test_0x764/enable
$ echo > /sys/kernel/debug/tracing/uprobe_events

原理

上面的接口是基于 debugfs (在较新的内核中使用 tracefs),即读写文件的方式去与内核交互实现 uprobe 监控。其中写入 uprobe_events 时会经过一系列内核调用:

  • probes_write
  • create_trace_uprobe
  • kern_path: 打开目标 ELF 文件;
  • alloc_trace_uprobe: 分配 uprobe 结构体;
  • register_trace_uprobe: 注册 uprobe;
  • regiseter_uprobe_event: 将 probe 添加到全局列表中,并创建对应的 uprobe debugfs 目录,即上文示例中的 p_test_0x764;

当已经注册了 uprobe 的 ELF 程序被执行时,可执行文件会被 mmap 映射到进程的地址空间,同时内核会将该进程虚拟地址空间中对应的 uprobe 地址替换成断点指令。当目标程序指向到对应的 uprobe 地址时,会触发断点,从而触发到 uprobe 的中断处理流程 (arch_uprobe_exception_notify),进而在内核中打印对应的信息。

与 kprobe 类似,我们可以在触发 uprobe 时候根据对应寄存器去提取当前执行的上下文信息,比如函数的调用参数等。同时 uprobe 也有类似的同族: uretprobe。使用 uprobe 的好处是我们可以获取许多对于内核态比较抽象的信息,比如 bash 中 readline 函数的返回、SSL_read/write 的明文信息等。

拓展阅读:

  • Linux uprobe: User-Level Dynamic Tracing
  • Documentation/trace/uprobetracer.rst
  • Linux tracing - kprobe, uprobe and tracepoint

Tracepoints

tracepont 是内核中提供的一种轻量级代码监控方案,可以实现动态调用用户提供的监控函数,但需要子系统的维护者根据需要自行添加到自己的代码中。

使用

tracepoint 的使用和 uprobe 类似,主要基于 debugfs/tracefs 的文件读写去进行实现。一个区别在于 uprobe 使用的的用户自己定义的观察点 (event),而 tracepoint 使用的是内核代码中预置的观察点。

查看内核 (或者驱动) 中定义的所有观察点:

1
2
3
4
5
6
7
8
$ cat /sys/kernel/debug/tracing/available_events
sctp:sctp_probe
sctp:sctp_probe_path
sde:sde_perf_uidle_status
….
random:random_read
random:urandom_read

在 events 对应目录下包含了以子系统结构组织的观察点目录:

1
2
3
4
5
6
7
$ ls /sys/kernel/debug/tracing/events/random/
add_device_randomness credit_entropy_bits extract_entropy get_random_bytes mix_pool_bytes_nolock urandom_read
add_disk_randomness debit_entropy extract_entropy_user get_random_bytes_arch push_to_pool xfer_secondary_pool
add_input_randomness enable filter mix_pool_bytes random_read

$ ls /sys/kernel/debug/tracing/events/random/random_read/
enable filter format id trigger

以 urandom 为例,这是内核的伪随机数生成函数,对其开启追踪:

1
2
3
4
5
$ echo 1 > /sys/kernel/debug/tracing/events/random/urandom_read/enable
$ echo 1 > /sys/kernel/debug/tracing/tracing_on
$ head -c1 /dev/urandom
$ cat /sys/kernel/debug/tracing/trace_pipe
head-9949 [006] …. 101453.641087: urandom_read: got_bits 40 nonblocking_pool_entropy_left 0 input_entropy_left 2053

其中 trace_pipe 是输出的管道,以阻塞的方式进行读取,因此需要先开始读取再获取 /dev/urandom,然后就可以看到类似上面的输出。这里输出的格式是在内核中定义的,我们下面会看到。

当然,最后记得把 trace 关闭。

原理

根据内核文档介绍,子系统的维护者如果想在他们的内核函数中增加跟踪点,需要执行两步操作:

  1. 定义跟踪点
  2. 使用跟踪点

内核为跟踪点的定义提供了 TRACE_EVENT 宏。还是以 urandom_read 这个跟踪点为例,其在内核中的定义在 include/trace/events/random.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#undef TRACE_SYSTEM
#define TRACE_SYSTEM random

TRACE_EVENT(random_read,
TP_PROTO(int got_bits, int need_bits, int pool_left, int input_left),

TP_ARGS(got_bits, need_bits, pool_left, input_left),

TP_STRUCT__entry(
__field( int, got_bits )
__field( int, need_bits )
__field( int, pool_left )
__field( int, input_left )
),

TP_fast_assign(
__entry->got_bits = got_bits;
__entry->need_bits = need_bits;
__entry->pool_left = pool_left;
__entry->input_left = input_left;
),

TP_printk("got_bits %d still_needed_bits %d "
"blocking_pool_entropy_left %d input_entropy_left %d",
__entry->got_bits, __entry->got_bits, __entry->pool_left,
__entry->input_left)
);

其中:

  • random_read: trace 事件的名称,不一定要内核函数名称一致,但通常为了易于识别会和某个关键的内核函数相关联。隶属于 random 子系统 (由 TRACE_SYSTEM 宏定义);
  • TP_PROTO: 定义了跟踪点的原型,可以理解为入参类型;
  • TP_ARGS: 定义了 " 函数 " 的调用参数;
  • TP_STRUCT__entry: 用于 fast binary tracing,可以理解为一个本地 C 结构体的定义;
  • TP_fast_assign: 上述本地 C 结构体的初始化;
  • TP_printk: 类似于 printk 的结构化输出定义,上节中 trace_pipe 的输出结果就是这里定义的;

TRACE_EVENT 宏并不会自动插入对应函数,而是通过展开定义了一个名为 trace_urandom_read 的函数,需要内核开发者自行在代码中进行调用。 上述跟踪点实际上是在 drivers/char/random.c 文件中进行了调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static ssize_t
urandom_read_nowarn(struct file *file, char __user *buf, size_t nbytes,
loff_t *ppos)
{
int ret;

nbytes = min_t(size_t, nbytes, INT_MAX >> (ENTROPY_SHIFT + 3));
ret = extract_crng_user(buf, nbytes);
trace_urandom_read(8 * nbytes, 0, ENTROPY_BITS(&input_pool)); // <-- 这里
return ret;
}

static ssize_t
urandom_read(struct file *file, char __user *buf, size_t nbytes, loff_t *ppos)
{
unsigned long flags;
static int maxwarn = 10;

if (!crng_ready() && maxwarn > 0) {
maxwarn--;
if (__ratelimit(&urandom_warning))
pr_notice("%s: uninitialized urandom read (%zd bytes read)\n",
current->comm, nbytes);
spin_lock_irqsave(&primary_crng.lock, flags);
crng_init_cnt = 0;
spin_unlock_irqrestore(&primary_crng.lock, flags);
}

return urandom_read_nowarn(file, buf, nbytes, ppos);
}

值得注意的是实际上是在 urandom_read_nowarn 函数中而不是 urandom_read 函数中调用的,因此也可见注入点名称和实际被调用的内核函数名称没有直接关系,只需要便于识别和定位即可。

根据上面的介绍我们可以了解到,tracepoint 相对于 probe 来说各有利弊:

  • 缺点是需要开发者自己定义并且加入到内核代码中,对代码略有侵入性;
  • 优点是对于参数格式有明确定义,并且在不同内核版本中相对稳定,kprobe 跟踪的内核函数可能在下个版本就被改名或者优化掉了;

另外,tracepoint 除了在内核代码中直接定义,还可以在驱动中进行动态添加,用于方便驱动开发者进行动态调试,复用已有的 debugfs 最终架构。这里有一个简单的 自定义 tracepoint 示例,可用于加深对 tracepoint 使用的理解。

拓展阅读:

  • LWN: Using the TRACE_EVENT() macro (Part 1)
  • Documentation/trace/tracepoints.rst
  • Taming Tracepoints in the Linux Kernel

USDT

USDT 表示 Userland Statically Defined Tracing,即用户静态定义追踪 (币圈同志先退下)。最早源于 Sun 的 Dtrace 工具,因此 USDT probe 也常被称为 Dtrace probe。可以理解为 kernel tracepoint 的用户层版本,由应用开发者在自己的程序中关键函数加入自定义的跟踪点,有点类似于 printf 调试法 (误)。

下面是一个简单的示例:

1
2
3
4
5
6
#include "sys/sdt.h"
int main() {
DTRACE_PROBE("hello_usdt", "enter");
int reval = 0;
DTRACE_PROBE1("hello_usdt", "exit", reval);
}

DTRACE_PROBEn 是 UDST (systemtap) 提供的追踪点定义 + 插入辅助宏,n 表示参数个数。编译上述代码后就可以看到被注入的 USDT probe 信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ apt-get install systemtap-sdt-dev
$ gcc hello-usdt.c -o hello-usdt
$ readelf -n ./hello-usdt

Displaying notes found in: .note.stapsdt
Owner Data size Description
stapsdt 0x0000002e NT_STAPSDT (SystemTap probe descriptors)
Provider: "hello_usdt"
Name: "enter"
Location: 0x0000000000001131, Base: 0x0000000000002004, Semaphore: 0x0000000000000000
Arguments:
stapsdt 0x00000038 NT_STAPSDT (SystemTap probe descriptors)
Provider: "hello_usdt"
Name: "exit"
Location: 0x0000000000001139, Base: 0x0000000000002004, Semaphore: 0x0000000000000000
Arguments: -4@-4(%rbp)

readelf -n 表示输出 ELF 中 NOTE 段的信息。

在使用 trace 工具 (如 BCC、SystemTap、dtrace) 对该应用进行追踪时,会在启动过程中修改目标进程的对应地址,将其替换为 probe ,在触发调用时候产生对应事件,供数据收集端使用。通常添加 probe 的方式是 基于 uprobe 实现的。

使用 USDT 的一个好处是应用开发者可以在自己的程序中定义更加上层的追踪点,方便对于功能级别监控和分析,比如 node.js server 就自带了 USDT probe 点可用于追踪 HTTP 请求,并输出请求的路径等信息。由于 USDT 需要开发者配合使用,不符合我们最初的逆向分析需要,因此就不过多介绍了。(其实是懒得搭环境)

拓展阅读:

  • Exploring USDT Probes on Linux
  • LWN: Using user-space tracepoints with BPF

小结

上述介绍的四种常见内核监控方案,根据静态/动态类型以及面向内核还是用户应用来划分的话,可以用下表进行概况:

监控方案静态动态内核用户
Kprobes
Uprobes
Tracepoints
USDT

准确来说 USDT 不算是一种独立的内核监控数据源,因为其实现还是依赖于 uprobe,不过为了对称还是放在这里,而且这样目录比较好看。

采集 & 前端

上面我们介绍了几种当今内核中主要的监控数据来源,基本上可以涵盖所有的监控需求。不过从易用性上来看,只是实现了基本的架构,使用上有的是基于内核提供的系统调用/驱动接口,有的是基于 debugfs/tracefs,对用户而言不太友好,因此就有了许多封装再封装的监控前端,本节对这些主要的工具进行简要介绍。

Ftrace

ftrace 是内核中用于实现内部追踪的一套框架,这么说有点抽象,但实际上我们前面已经用过了,就是 tracefs 中的使用的方法。

在旧版本中内核中 (4.1 之前) 使用 debugfs,一般挂载到 /sys/kernel/debug/tracing;在新版本中使用独立的 tracefs,挂载到 /sys/kernel/tracing。但出于兼容性原因,原来的路径仍然保留,所以我们将其统一称为 tracefs。

ftrace 通常被叫做 function tracer,但除了函数跟踪,还支持许多其他事件信息的追踪:

  • hwlat: 硬件延时追踪
  • irqsoff: 中断延时追踪
  • preemptoff: 追踪指定时间片内的 CPU 抢占事件
  • wakeup: 追踪最高优先级的任务唤醒的延时
  • branch: 追踪内核中的 likely/unlikely 调用
  • mmiotrace: 追踪某个二进制模块所有对硬件的读写事件
  • ……

Android 中提供了一个简略的文档指导如何为内核增加 ftrace 支持,详见: Using ftrace

Perf

perf 是 Linux 发行版中提供的一个性能监控程序,基于内核提供的 perf_event_open 系统调用来对进程进行采样并获取信息。Linux 中的 perf 子系统可以实现对 CPU 指令进行追踪和计数,以及收集 kprobe、uprobe 和 tracepoints 的信息,实现对系统性能的分析。

在 Android 中提供了一个简单版的 perf 程序 simpleperf,接口和 perf 类似。

虽然可以监测到系统调用,但缺点是无法获取系统调用的参数,更不可以动态地修改内核。因此对于安全测试而言作用不大,更多是给 APP 开发者和手机厂商用于性能热点分析。值得一提的是,perf 子系统曾经出过不少漏洞,在 Android 内核提权史中也曾经留下过一点足迹 😄

eBPF

eBPF 为 extended Berkeley Packet Filter 的缩写,BPF 最早是用于包过滤的精简虚拟机,拥有自己的一套指令集,我们常用的 tcpdump 工具内部就会将输入的过滤规则转换为 BPF 指令,比如:

1
$ tcpdump -i lo0 'src 1.2.3.4' -d (000) ld [0] (001) jeq #0x2000000 jt 2 jf 5 (002) ld [16] (003) jeq #0x1020304 jt 4 jf 5 (004) ret #262144 (005) ret #0

该汇编指令表示令过滤器只接受 IP 包,并且来源 IP 地址为 1.2.3.4。其中的指令集可以参考 Linux Socket Filtering aka Berkeley Packet Filter (BPF)。eBPF 在 BPF 指令集上做了许多增强 (extend):

  • 寄存器个数从 2 个增加为 10 个 (R0 - R9);
  • 寄存器大小从 32 位增加为 64 位;
  • 条件指令 jt/jf 的目标替换为 jt/fall-through,简单来说就是 else 分支可以默认忽略;
  • 增加了 bpf_call 指令以及对应的调用约定,减少内核调用的开销;
  • ……

内核存在一个 eBPF 解释器,同时也支持实时编译 (JIT) 增加其执行速度,但很重要的一个限制是 eBPF 程序不能影响内核正常运行,在 内核加载 eBPF 程序前会对其进行一次语义检查,确保代码的安全性,主要限制为:

  • 不能包含循环,这是为了防止 eBPF 程序过度消耗系统资源 (5.3 中增加了部分循环支持);
  • 不能反向跳转,其实也就是不能包含循环;
  • BPF 程序的栈大小限制为 512 字节;
  • ……

具体的限制策略都在内核的 eBPF verifier 中,不同版本略有差异。值得一提的是,最近几年 Linux 内核出过很多 eBPF 的漏洞,大多是 verifier 的验证逻辑错误,其中不少还上了 Pwn2Own,但是由于权限的限制在 Android 中普通应用无法执行 bpf(2) 系统调用,因此并不受影响。

eBPF 和 perf_event 类似,通过内核虚拟机的方式实现监控代码过滤的动态插拔,这在许多场景下十分奏效。对于普通用户而言,基本上不会直接编写 eBPF 的指令去进行监控,虽然内核提供了一些宏来辅助 eBPF 程序的编写,但实际上更多的是使用上层的封装框架去调用,其中最著名的一个就是 BCC。

BCC

BCC (BPF Compiler Collection) 包含了一系列工具来协助运维人员编写监控代码,其中使用较多的是其 Python 绑定。一个简单的示例程序如下:

1
2
3
4
5
6
7
8
from bcc import BPF
prog="""
int kprobe__sys_clone(void *ctx) {
bpf_trace_printk("Hello, World!\\n");
return 0;
}
"""
BPF(text=prog).trace_print()

执行该 python 代码后,每当系统中的进程调用 clone 系统调用,该程序就会打印 “Hello World” 输出信息。 可以看到这对于动态监控代码非常有用,比如我们可以通过 python 传入参数指定打印感兴趣的系统调用及其参数,而无需频繁修改代码。

eBPF 可以获取到内核中几乎所有的监控数据源,包括 kprobes、uprobes、tracepoints 等等,官方 repo 中给出了许多示例程序,比如 opensnoop 监控文件打开行为、execsnoop 监控程序的执行。

![[6.png]]

Bpftrace

bpftrace 是 eBPF 框架的另一个上层封装,与 BCC 不同的是 bpftrace 定义了一套自己的 DSL 脚本语言,语法 (也) 类似于 awk,从而可以方便用户直接通过命令行实现丰富的功能,截取几条官方给出的示例:

1
2
3
4
5
# 监控系统所有的打开文件调用(open/openat),并打印打开文件的进程以及被打开的文件路径
bpftrace -e 'tracepoint:syscalls:sys_enter_open { printf("%s %s\n", comm, str(args->filename)); }'

# 统计系统中每个进程执行的系统调用总数
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'

官方同样也给出了许多 .bt 脚本示例,可以通过其代码进行学习和编写。

拓展阅读:

  • LWN: A thorough introduction to eBPF
  • Extending the Kernel with eBPF
  • https://www.opersys.com/downloads/cc-slides/android-debug/slides-main-211122.html#/
  • http://www.caveman.work/2019/01/29/eBPF-on-Android/

SystemTap

SystemTap(stab) 是 Linux 中的一个命令行工具,可以对各种内核监控源信息进行结构化输出。同时也实现了自己的一套 DSL 脚本,语法类似于 awk,可实现系统监控命令的快速编程。

使用 systemtap 需要包含内核源代码,因为需要动态编译和加载内核模块。在 Android 中还没有官方的支持,不过有一些 开源的 systemtap 移植

拓展阅读: Comparing SystemTap and bpftrace

其他

除了上面介绍的这些,还有许多开源的内核监控前端,比如 LTTng、trace-cmdkernelshark 等,内核监控输出以结构化的方式进行保存、处理和可视化,对于大量数据而言是非常实用的。限于篇幅不再对这些工具进行一一介绍,而且笔者使用的也不多,后续有机会再进行研究。