嵌入式Linux环境下的内核探测工具【转】

转自:https://codeleading.com/article/50686270686/

简单Linux系统环境下的内核探测

在笔者之前的文章中提到,基于内核eBPF探针的常用工具主要bpftracebcc,二者复杂的依赖库使得其在嵌入式Linux系统环境下常常是不可用的。截止目前,一些嵌入式SDK(例如buildrootopenwrt等)未提供这两个性能分析工具的自动化构建功能。一种可行的方案是参考Linux内核源码samples/bpf下的示例编写基于eBPF的C代码,并编译生成BTF目柡文件和可执行应用,用于嵌入式设备上的性能分析。这种方案可行但实施的效率较低。幸运的是,同属于iovisor的开源软件PLY很好地填补了这一空缺,它可以使用eBPF子系统对Linux内核进行监测,而且没有复杂的依赖库(仅依赖libc库)。其用法接近bpftrace,尽管功能较弱,但一定程度上能够满足要求。其最大的缺憾是缺少对局部变量和uprobe功能的支持。本文主要对ply的内核探测做相关的演示说明。

监测文件的打开

bpftracebcc工具都提供了一个名为opensnoop的脚本工具,用于监测系统上所有打开的文件。笔者编写了ply版本的opensnoop.ply,其实现基于Linux内核的tracepoint探测,脚本内容如下:

#!/usr/sbin/ply -k

tracepoint:syscalls/sys_enter_open
{
	opentab[kpid] = data->filename;
}
tracepoint:syscalls/sys_enter_openat
{
	opentab[kpid] = data->filename;
}
tracepoint:syscalls/sys_exit_open /opentab[kpid] != 0/
{
	printf("[%d.%06d] pid: %d, kpid: %d, comm: %s, open(%s): %d\n",
		time / 1000000000, (time % 1000000000) / 1000000,
		pid, kpid, comm, str(opentab[kpid]), data->ret);
	delete opentab[kpid];
}
tracepoint:syscalls/sys_exit_openat /opentab[kpid] != 0/
{
	printf("[%d.%06d] pid: %d, kpid: %d, comm: %s, open(%s): %d\n",
		time / 1000000000, (time % 1000000000) / 1000000,
		pid, kpid, comm, str(opentab[kpid]), data->ret);
	delete opentab[kpid];
}

 

以上脚本中,使用到了ply多个内置的变量和函数,如timestr等。data变量仅针对tracepoint有效,它类似于C语言中的结构体指针,其能指向的成员由内核确定。例如对于syscalls/sys_enter_open这个跟踪点,data能够指向的成员名称由内核文件/sys/kernel/tracing/events/syscalls/sys_enter_open/format确定,可以打开该文件查看:

# cat /sys/kernel/tracing/events/syscalls/sys_enter_open/format
name: sys_enter_open
ID: 635
format:
	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
	field:int common_pid;	offset:4;	size:4;	signed:1;

	field:int __syscall_nr;	offset:8;	size:4;	signed:1;
	field:const char * filename;	offset:16;	size:8;	signed:0;
	field:int flags;	offset:24;	size:8;	signed:0;
	field:umode_t mode;	offset:32;	size:8;	signed:0;

print fmt: "filename: 0x%08lx, flags: 0x%08lx, mode: 0x%08lx", ((unsigned long)(REC->filename)), ((unsigned long)(REC->flags)), ((unsigned long)(REC->mode))

 

pidkpidcomm等变量由ply自动提供,分别对应进程的pid、线程的pid、及进程的名称。该脚本不建议在系统繁忙的系统中使用。在负载较低的系统环境下运行,可得到以下结果:

# ply -k trace-open.ply
[89137.000250] pid: 1, kpid: 1, comm: systemd, open(/proc/979/cgroup): 114
[89137.000251] pid: 1, kpid: 1, comm: systemd, open(/proc/912/cgroup): 114
[89138.000858] pid: 32010, kpid: 32276, comm: MemoryPoller, open(/proc/meminfo): 27
[89139.000240] pid: 14985, kpid: 33553, comm: ThreadPoolForeg, open(/etc/chromium-browser/policies/managed): -2
[89139.000240] pid: 14985, kpid: 33553, comm: ThreadPoolForeg, open(/etc/chromium-browser/policies/recommended): -2
[89140.000008] pid: 970, kpid: 970, comm: irqbalance, open(/proc/interrupts): 6
[89140.000009] pid: 970, kpid: 970, comm: irqbalance, open(/proc/stat): 6
[89140.000009] pid: 970, kpid: 970, comm: irqbalance, open(/proc/irq/49/smp_affinity): 6
[89140.000009] pid: 970, kpid: 970, comm: irqbalance, open(/proc/irq/51/smp_affinity): 6
[89140.000009] pid: 970, kpid: 970, comm: irqbalance, open(/proc/irq/56/smp_affinity): 6
[89140.000009] pid: 970, kpid: 970, comm: irqbalance, open(/proc/irq/55/smp_affinity): 6
[89140.000009] pid: 970, kpid: 970, comm: irqbalance, open(/proc/irq/0/smp_affinity): 6
[89140.000009] pid: 970, kpid: 970, comm: irqbalance, open(/proc/irq/1/smp_affinity): 6
[89140.000009] pid: 970, kpid: 970, comm: irqbalance, open(/proc/irq/8/smp_affinity): 6
[89140.000009] pid: 970, kpid: 970, comm: irqbalance, open(/proc/irq/9/smp_affinity): 6
[89140.000009] pid: 970, kpid: 970, comm: irqbalance, open(/proc/irq/12/smp_affinity): 6
[89140.000941] pid: 2198, kpid: 2198, comm: gnome-shell, open(/proc/self/stat): 46

对打开文件进行统计

当系统频繁打开文件时,上面的脚本会造成系统负载增加。为避免给系统带来不必要的负荷,可以使用count()特殊函数对打开的文件进行统计,并隔一段时间周期性地输出统计结果。这里使用到了interval定时器,具体用法可参考官方文档。笔者编写的统计脚本open-count.ply内容如下:

#!/usr/sbin/ply -k
kprobe:do_sys_open
{
	@openfreq[str(arg1)] = count();
}
interval:10s
{
	printf("--------------------------------------------------------\n");
	printf("[%d.%06d] dumping opened files in the last 10 seconds:\n",
		time / 1000000000, (time % 1000000000) / 1000000);
	print(@openfreq);
	clear(@openfreq);
}

 

上面的定时器每10秒执行一次,输出统计信息后会清空hash表openfreq以重新计数。笔者的观测结果如下(部分):

# ply -k open-count.ply
--------------------------------------------------------
[90150.000678] dumping opened files in the last 10 seconds:

@openfreq:
{ /proc/interrupts               }: 10
{ /proc/irq/0/smp_affinity       }: 1
{ /proc/irq/1/smp_affinity       }: 1
{ /proc/irq/12/smp_affinity      }: 1
{ /proc/irq/50/smp_affinity      }: 1
{ /proc/irq/51/smp_affinity      }: 1
{ /proc/stat                     }: 1
{ /proc/meminfo                  }: 5

--------------------------------------------------------
[90160.000678] dumping opened files in the last 10 seconds:

@openfreq:

{ /proc/979/cgroup               }: 1
{ /proc/interrupts               }: 1
{ /proc/irq/0/smp_affinity       }: 1
{ /proc/irq/1/smp_affinity       }: 1
{ /proc/irq/12/smp_affinity      }: 1
{ /proc/irq/51/smp_affinity      }: 1
{ /proc/stat                     }: 1
{ /proc/meminfo                  }: 2

过滤以只读方式打开的文件

某些情况下,我们只想跟踪探测以可写方式打开的文件,忽略以只读方式打开的文件。这样可以极大地减少跟踪探测的输出结果,从而一定程度上降低ply探测对系统负载的影响。当使用kprobe探测一些函数的入口时,通过arg0arg1等变量可以访问到函数的入参,这些入参的数据类型为整数,据此可以实现探测的过滤。笔者编写的probe-open.ply内容如下:

#!/usr/sbin/ply -k
/* fs/open.c:
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
*/
kprobe:do_sys_open
{
	if (arg1 != 0 && (arg2 & 0x3) != 0) {
		opentab[kpid] = arg1;
		openflags[kpid] = arg2;
	}
}
kretprobe:do_sys_open /opentab[kpid] != 0/
{
	printf("[%d.%06d] pid: %d, kpid: %d, comm: %s, open(%s): %d, trunc: %d\n",
		time / 1000000000, (time % 1000000000) / 1000000,
		pid, kpid, comm, str(opentab[kpid]), retval,
		(openflags[kpid] & 0x200) >> 9);
	delete opentab[kpid];
	delete openflags[kpid];
}

arg2对应函数do_sys_open的第三个参数flags,当其低2位比特不为0时,表明以O_WRONLYO_RDWR可写方式打开了文件,据此就实现了探测结果的过滤。同样的,kretprobe探针加入了opentab[kpid] != 0的限定条件,它不会输出以只读方式打开文件的结果。特殊变量retval仅对kretprobe有效,它表示函数的返回值。笔者探测结果如下:

# ply -k probe-open.ply
[91220.000009] pid: 970, kpid: 970, comm: irqbalance, open(/proc/irq/49/smp_affinity): 6, trunc: 1
[91242.000803] pid: 22765, kpid: 22765, comm: bash, open(/dev/null): 3, trunc: 1

 

打开/dev/null文件,是笔者在另一个终端上执行echo 'Hello World' > /dev/null触发的结果。

内核调用栈的回溯

ply提供了stack变量,它是多行的字符串类型,可以得到探测点的内核函数的调用栈;该功能对于调试内核非常有帮助。以笔者上一篇博客为例,Linux内核为进程加载vdso动态库之后,实际上并没有映射vvar只读内存段,而是仅当进程实际去访问该内存了,才会触发实际的内存映射操作。通过ply可以得到该映射函数的调用栈回溯。脚本func-backtrace.ply内容如下:

#!/usr/sbin/ply -k
kprobe:vvar_fault
{
	printf("PID: %d, TID: %d, comm: %s, accessing vdso memory:\n",
		pid, kpid, comm);
	print(stack);
}

跟踪探测结果如下:

# ply -k func-backtrace.ply
PID: 35486, TID: 35486, comm: clock_gettime, accessing vdso memory:

	vvar_fault+1
	__do_fault+62
	do_fault+486
	__handle_mm_fault+1561
	handle_mm_fault+218
	pgtable_bad+571
	msr_save_cpuid_features+15669
	_raw_write_lock_irqsave+2064046

 

访问内核数据

通过ply加载的内核探针kprobe,可以在函数后面加上一个偏移量,这样探针不会在函数入口处触发。不过并不是在函数的任意一个偏移量都可以成功加载内核探针的,eBPF对探针所在的代码段有一定的要求。此时带有偏移量的kprobe下的arg0arg1很可能会失去意义,不过可以通过regs变量访问探针处的寄存器。笔者编写了offset.ply脚本(该偏量的计算仅限于内核版本:Linux ubuntu 5.13.0-39-generic #44~20.04.1-Ubuntu),演示如何通过带偏移量的探针,确定一个脚本的解析器:

#!/usr/sbin/ply -k

/*
fs/binfmt_script.c
static int load_script(struct linux_binprm *bprm)
{
	...
    file = open_exec(i_name);
    if (IS_ERR(file))
        return PTR_ERR(file);

    bprm->interpreter = file;
    return 0;
}
(gdb) disassemble load_script
Dump of assembler code for function load_script:
   0xffffffff813b8270 <+0>:	callq  0xffffffff81077840 <__fentry__>
   0xffffffff813b8275 <+5>:	cmpw   $0x2123,0xa0(%rdi)
   ......
   0xffffffff813b83f9 <+393>:	mov    %r12,%rdi
   0xffffffff813b83fc <+396>:	callq  0xffffffff8132f1c0 <open_exec>
*/

tracepoint:syscalls/sys_enter_execve
{
	newapp[kpid] = data->filename;
}

tracepoint:syscalls/sys_enter_execveat
{
	newapp[kpid] = data->filename;
}

tracepoint:syscalls/sys_exit_execve
{
	if (newapp[kpid] != 0) {
		delete newapp[kpid];
	}
}

tracepoint:syscalls/sys_exit_execveat
{
	if (newapp[kpid] != 0) {
		delete newapp[kpid];
	}
}

kprobe:load_script+393 /newapp[kpid]/
{
	if (regs->r12 != 0) {
		printf("PID: %d, invoker: %s, file: %s, interpreter: %s\n",
			pid, comm, str(newapp[kpid]), str(regs->r12));
		print(stack);
	}
}

 

笔者在load_script的393字节偏移处加入内核探针,该处的寄存器r12指向了脚本的解析器路径,通常为/bin/sh等。ply对内核数据的访问是有限的,不能像bpftrace那样实现C语言层面的结构体解引用;以上脚本仅仅是将r12寄存器转化为一个字符串并输出。笔者用ply加载该脚本后,在另一个终端分别执行which -a perldocperldoc perl,可得到以下跟踪信息:

# ply -k offset.ply
PID: 35873, invoker: bash, file: /usr/bin/which, interpreter: /bin/sh

	load_script+394
	exec_binprm+314
	bprm_execve+365
	do_execveat_common.isra.0+393
	__x64_sys_execve+55
	msr_save_cpuid_features+425
	_raw_write_lock_irqsave+2061404
	
PID: 35874, invoker: bash, file: /usr/bin/perldoc, interpreter: /usr/bin/perl

	load_script+394
	exec_binprm+314
	bprm_execve+365
	do_execveat_common.isra.0+393
	__x64_sys_execve+55
	msr_save_cpuid_features+425
	_raw_write_lock_irqsave+2061404

 

可见在ubuntu系统上,可执行文件/usr/bin/which是一个shell脚本,其解析器为/bin/sh;而/usr/bin/perldoc也是一个脚本,其解析器为/usr/bin/perl

版权声明:本文为yeholmes原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/yeholmes/article/details/124224662
posted @ 2022-05-24 16:21  Sky&Zhang  阅读(355)  评论(0编辑  收藏  举报