ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

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

2022-05-24 16:33:51  阅读:187  来源: 互联网

标签:ply open pid kpid 嵌入式 comm 内核 Linux proc


转自: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

标签:ply,open,pid,kpid,嵌入式,comm,内核,Linux,proc
来源: https://www.cnblogs.com/sky-heaven/p/16305992.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有