Linux内核调试技术——kprobe使用与实现

2022-09-24 15:36:09 浏览数 (2)

Linux kprobes调试技术是内核开发者们专门为了便于跟踪内核函数执行状态所设计的一种轻量级内核调试技术。利用kprobes技术,内核开发人员可以在内核的绝大多数指定函数中动态的插入探测点来收集所需的调试状态信息而基本不影响内核原有的执行流程。kprobes技术目前提供了3种探测手段:kprobe、jprobe和kretprobe,其中jprobe和kretprobe是基于kprobe实现的,他们分别应用于不同的探测场景中。本文首先简单描述这3种探测技术的原理与区别,然后主要围绕其中的kprobe技术进行分析并给出一个简单的实例介绍如何利用kprobe进行内核函数探测,最后分析kprobe的实现过程(jprobe和kretprobe会在后续的博文中进行分析)。

内核源码:Linux-4.1.15

实验环境:CentOS(x86_64)、树莓派1b

2022年嵌入式开发想进互联网大厂,你技术过硬吗?

腾讯T6-9首发“Linux内核源码嵌入式开发进阶笔记”,差距不止一点点哦

一、kprobes技术背景

开发人员在内核或者模块的调试过程中,往往会需要要知道其中的一些函数有无被调用、何时被调用、执行是否正确以及函数的入参和返回值是什么等等。比较简单的做法是在内核代码对应的函数中添加日志打印信息,但这种方式往往需要重新编译内核或模块,重新启动设备之类的,操作较为复杂甚至可能会破坏原有的代码执行过程。

而利用kprobes技术,用户可以定义自己的回调函数,然后在内核或者模块中几乎所有的函数中(有些函数是不可探测的,例如kprobes自身的相关实现函数,后文会有详细说明)动态的插入探测点,当内核执行流程执行到指定的探测函数时,会调用该回调函数,用户即可收集所需的信息了,同时内核最后还会回到原本的正常执行流程。如果用户已经收集足够的信息,不再需要继续探测,则同样可以动态地移除探测点。因此kprobes技术具有对内核执行流程影响小和操作方便的优点。

kprobes技术包括的3种探测手段分别时kprobe、jprobe和kretprobe。首先kprobe是最基本的探测方式,是实现后两种的基础,它可以在任意的位置放置探测点(就连函数内部的某条指令处也可以),它提供了探测点的调用前、调用后和内存访问出错3种回调方式,分别是pre_handler、post_handler和fault_handler,其中pre_handler函数将在被探测指令被执行前回调,post_handler会在被探测指令执行完毕后回调(注意不是被探测函数),fault_handler会在内存访问出错时被调用;jprobe基于kprobe实现,它用于获取被探测函数的入参值;最后kretprobe从名字中就可以看出其用途了,它同样基于kprobe实现,用于获取被探测函数的返回值。

kprobes的技术原理并不仅仅包含存软件的实现方案,它也需要硬件架构提供支持。其中涉及硬件架构相关的是CPU的异常处理和单步调试技术,前者用于让程序的执行流程陷入到用户注册的回调函数中去,而后者则用于单步执行被探测点指令,因此并不是所有的架构均支持,目前kprobes技术已经支持多种架构,包括i386、x86_64、ppc64、ia64、sparc64、arm、ppc和mips(有些架构实现可能并不完全,具体可参考内核的Documentation/kprobes.txt)。

kprobes的特点与使用限制: 1、kprobes允许在同一个被被探测位置注册多个kprobe,但是目前jprobe却不可以;同时也不允许以其他的jprobe回调函数和kprobe的post_handler回调函数作为被探测点。

2、一般情况下,可以探测内核中的任何函数,包括中断处理函数。不过在kernel/kprobes.c和arch/*/kernel/kprobes.c程序中用于实现kprobes自身的函数是不允许被探测的,另外还有do_page_fault和notifier_call_chain;

3、如果以一个内联函数为探测点,则kprobes可能无法保证对该函数的所有实例都注册探测点。由于gcc可能会自动将某些函数优化为内联函数,因此可能无法达到用户预期的探测效果;

4、一个探测点的回调函数可能会修改被探测函数运行的上下文,例如通过修改内核的数据结构或者保存与struct pt_regs结构体中的触发探测器之前寄存器信息。因此kprobes可以被用来安装bug修复代码或者注入故障测试代码;

5、kprobes会避免在处理探测点函数时再次调用另一个探测点的回调函数,例如在printk()函数上注册了探测点,则在它的回调函数中可能再次调用printk函数,此时将不再触发printk探测点的回调,仅仅时增加了kprobe结构体中nmissed字段的数值;

6、在kprobes的注册和注销过程中不会使用mutex锁和动态的申请内存;

7、kprobes回调函数的运行期间是关闭内核抢占的,同时也可能在关闭中断的情况下执行,具体要视CPU架构而定。因此不论在何种情况下,在回调函数中不要调用会放弃CPU的函数(如信号量、mutex锁等);

8、kretprobe通过替换返回地址为预定义的trampoline的地址来实现,因此栈回溯和gcc内嵌函数__builtin_return_address()调用将返回trampoline的地址而不是真正的被探测函数的返回地址;

9、如果一个函数的调用次数和返回次数不相等,则在类似这样的函数上注册kretprobe将可能不会达到预期的效果,例如do_exit()函数会存在问题,而do_execve()函数和do_fork()函数不会;

10、如果当在进入和退出一个函数时,CPU运行在非当前任务所有的栈上,那么往该函数上注册kretprobe可能会导致不可预料的后果,因此,kprobes不支持在X86_64的结构下为__switch_to()函数注册kretprobe,将直接返回-EINVAL。

内核学习网站:

Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

二、kprobe原理

下面来介绍一下kprobe是如何工作的。具体流程见下图:

1、当用户注册一个探测点后,kprobe首先备份被探测点的对应指令,然后将原始指令的入口点替换为断点指令,该指令是CPU架构相关的,如i386和x86_64是int3,arm是设置一个未定义指令(目前的x86_64架构支持一种跳转优化方案Jump Optimization,内核需开启CONFIG_OPTPROBES选项,该种方案使用跳转指令来代替断点指令);

2、当CPU流程执行到探测点的断点指令时,就触发了一个trap,在trap处理流程中会保存当前CPU的寄存器信息并调用对应的trap处理函数,该处理函数会设置kprobe的调用状态并调用用户注册的pre_handler回调函数,kprobe会向该函数传递注册的struct kprobe结构地址以及保存的CPU寄存器信息;

3、随后kprobe单步执行前面所拷贝的被探测指令,具体执行方式各个架构不尽相同,arm会在异常处理流程中使用模拟函数执行,而x86_64架构则会设置单步调试flag并回到异常触发前的流程中执行;

4、在单步执行完成后,kprobe执行用户注册的post_handler回调函数;

5、最后,执行流程回到被探测指令之后的正常流程继续执行。

三、kprobe使用实例

在分析kprobe的实现之前先来看一下如何利用kprobe对函数进行探测,以便于让我们对kprobre所完成功能有一个比较清晰的认识。目前,使用kprobe可以通过两种方式,第一种是开发人员自行编写内核模块,向内核注册探测点,探测函数可根据需要自行定制,使用灵活方便;第二种方式是使用kprobes on ftrace,这种方式是kprobe和ftrace结合使用,即可以通过kprobe来优化ftrace来跟踪函数的调用。下面来分别介绍:

1、编写kprobe探测模块

内核提供了一个struct kprobe结构体以及一系列的内核API函数接口,用户可以通过这些接口自行实现探测回调函数并实现struct kprobe结构,然后将它注册到内核的kprobes子系统中来达到探测的目的。同时在内核的samples/kprobes目录下有一个例程kprobe_example.c描述了kprobe模块最简单的编写方式,开发者可以以此为模板编写自己的探测模块。

1.1、kprobe结构体与API介绍

struct kprobe结构体定义如下:

代码语言:javascript复制
struct kprobe {
	struct hlist_node hlist;
 
	/* list of kprobes for multi-handler support */
	struct list_head list;
 
	/*count the number of times this probe was temporarily disarmed */
	unsigned long nmissed;
 
	/* location of the probe point */
	kprobe_opcode_t *addr;
 
	/* Allow user to indicate symbol name of the probe point */
	const char *symbol_name;
 
	/* Offset into the symbol */
	unsigned int offset;
 
	/* Called before addr is executed. */
	kprobe_pre_handler_t pre_handler;
 
	/* Called after addr is executed, unless... */
	kprobe_post_handler_t post_handler;
 
	/*
	 * ... called if executing addr causes a fault (eg. page fault).
	 * Return 1 if it handled fault, otherwise kernel will see it.
	 */
	kprobe_fault_handler_t fault_handler;
 
	/*
	 * ... called if breakpoint trap occurs in probe handler.
	 * Return 1 if it handled break, otherwise kernel will see it.
	 */
	kprobe_break_handler_t break_handler;
 
	/* Saved opcode (which has been replaced with breakpoint) */
	kprobe_opcode_t opcode;
 
	/* copy of the original instruction */
	struct arch_specific_insn ainsn;
 
	/*
	 * Indicates various status flags.
	 * Protected by kprobe_mutex after this kprobe is registered.
	 */
	u32 flags;
};

其中各个字段的含义如下:

代码语言:javascript复制
struct hlist_node hlist:被用于kprobe全局hash,索引值为被探测点的地址;
struct list_head list:用于链接同一被探测点的不同探测kprobe;
kprobe_opcode_t *addr:被探测点的地址;
const char *symbol_name:被探测函数的名字;
unsigned int offset:被探测点在函数内部的偏移,用于探测函数内部的指令,如果该值为0表示函数的入口;
kprobe_pre_handler_t pre_handler:在被探测点指令执行之前调用的回调函数;
kprobe_post_handler_t post_handler:在被探测指令执行之后调用的回调函数;
kprobe_fault_handler_t fault_handler:在执行pre_handler、post_handler或单步执行被探测指令时出现内存异常则会调用该回调函数;
kprobe_break_handler_t break_handler:在执行某一kprobe过程中触发了断点指令后会调用该函数,用于实现jprobe;
kprobe_opcode_t opcode:保存的被探测点原始指令;
struct arch_specific_insn ainsn:被复制的被探测点的原始指令,用于单步执行,架构强相关(可能包含指令模拟函数);
u32 flags:状态标记。

涉及的API函数接口如下:

代码语言:javascript复制
int register_kprobe(struct kprobe *kp)      //向内核注册kprobe探测点
void unregister_kprobe(struct kprobe *kp)   //卸载kprobe探测点
int register_kprobes(struct kprobe **kps, int num)     //注册探测函数向量,包含多个探测点
void unregister_kprobes(struct kprobe **kps, int num)  //卸载探测函数向量,包含多个探测点
int disable_kprobe(struct kprobe *kp)       //临时暂停指定探测点的探测
int enable_kprobe(struct kprobe *kp)        //恢复指定探测点的探测

1.2、用例kprobe_example.c分析与演示

该用例函数非常简单,它实现了内核函数do_fork的探测,该函数会在fork系统调用或者内核kernel_thread函数创建进程时被调用,触发也十分的频繁。下面来分析一下用例代码:

代码语言:javascript复制
/* For each probe you need to allocate a kprobe structure */
static struct kprobe kp = {
	.symbol_name	= "do_fork",
};
 
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) {
		printk(KERN_INFO "register_kprobe failed, returned %dn", ret);
		return ret;
	}
	printk(KERN_INFO "Planted kprobe at %pn", kp.addr);
	return 0;
}
 
static void __exit kprobe_exit(void)
{
	unregister_kprobe(&kp);
	printk(KERN_INFO "kprobe at %p unregisteredn", kp.addr);
}
 
module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");

程序中定义了一个struct kprobe结构实例kp并初始化其中的symbol_name字段为“do_fork”,表明它将要探测do_fork函数。在模块的初始化函数中,注册了pre_handler、post_handler和fault_handler这3个回调函数分别为handler_pre、handler_post和handler_fault,最后调用register_kprobe注册。在模块的卸载函数中调用unregister_kprobe函数卸载kp探测点。

代码语言:javascript复制
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
#ifdef CONFIG_X86
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, ip = %lx,"
			" flags = 0x%lxn",
		p->addr, regs->ip, regs->flags);
#endif
#ifdef CONFIG_PPC
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, nip = 0x%lx,"
			" msr = 0x%lxn",
		p->addr, regs->nip, regs->msr);
#endif
#ifdef CONFIG_MIPS
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, epc = 0x%lx,"
			" status = 0x%lxn",
		p->addr, regs->cp0_epc, regs->cp0_status);
#endif
#ifdef CONFIG_TILEGX
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, pc = 0x%lx,"
			" ex1 = 0x%lxn",
		p->addr, regs->pc, regs->ex1);
#endif
 
	/* A dump_stack() here will give a stack backtrace */
	return 0;
}

handler_pre回调函数的第一个入口是注册的struct kprobe探测实例,第二个参数是保存的触发断点前的寄存器状态,它在do_fork函数被调用之前被调用,该函数仅仅是打印了被探测点的地址,保存的个别寄存器参数。由于受CPU架构影响,这里对不同的架构进行了宏区分(虽然没有实现arm架构的,但是支持的,可以自行添加);

代码语言:javascript复制
/* kprobe post_handler: called after the probed instruction is executed */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
				unsigned long flags)
{
#ifdef CONFIG_X86
	printk(KERN_INFO "post_handler: p->addr = 0x%p, flags = 0x%lxn",
		p->addr, regs->flags);
#endif
#ifdef CONFIG_PPC
	printk(KERN_INFO "post_handler: p->addr = 0x%p, msr = 0x%lxn",
		p->addr, regs->msr);
#endif
#ifdef CONFIG_MIPS
	printk(KERN_INFO "post_handler: p->addr = 0x%p, status = 0x%lxn",
		p->addr, regs->cp0_status);
#endif
#ifdef CONFIG_TILEGX
	printk(KERN_INFO "post_handler: p->addr = 0x%p, ex1 = 0x%lxn",
		p->addr, regs->ex1);
#endif
}

handler_post回调函数的前两个入参同handler_pre,第三个参数目前尚未使用,全部为0;该函数在do_fork函数调用之后被调用,这里打印的内容同handler_pre类似。

代码语言:javascript复制
/*
 * 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)
{
	printk(KERN_INFO "fault_handler: p->addr = 0x%p, trap #%dn",
		p->addr, trapnr);
	/* Return 0 because we don't handle the fault. */
	return 0;
}

handler_fault回调函数会在执行handler_pre、handler_post或单步执行do_fork时出现错误时调用,这里第三个参数时具体发生错误的trap number,与架构相关,例如i386的page fault为14。

下面将它编译成模块在我的x86(CentOS 3.10)环境下进行演示,首先确保架构和内核已经支持kprobes,开启以下选项(一般都是默认开启的):

代码语言:javascript复制
Symbol: KPROBES [=y]                            
Type  : boolean                                 
Prompt: Kprobes                                 
  Location:                                     
(3) -> General setup                            
  Defined at arch/Kconfig:37                    
  Depends on: MODULES [=y] && HAVE_KPROBES [=y] 
  Selects: KALLSYMS [=y]                        
 
Symbol: HAVE_KPROBES [=y]                       
Type  : boolean                                 
  Defined at arch/Kconfig:174                   
  Selected by: X86 [=y]

然后使用以下Makefile单独编译kprobe_example.ko模块:

代码语言:javascript复制
obj-m := kprobe_example.o
 
CROSS_COMPILE=''
KDIR := /lib/modules/$(shell uname -r)/build
all:
        make -C $(KDIR) M=$(PWD) modules 
clean:
        rm -f *.ko *.o *.mod.o *.mod.c .*.cmd *.symvers  modul*

加载到内核中后,随便在终端上敲一个命令,可以看到dmesg中打印如下信息:

代码语言:javascript复制
<6>pre_handler: p->addr = 0xc0439cc0, ip = c0439cc1, flags = 0x246
<6>post_handler: p->addr = 0xc0439cc0, flags = 0x246
<6>pre_handler: p->addr = 0xc0439cc0, ip = c0439cc1, flags = 0x246
<6>post_handler: p->addr = 0xc0439cc0, flags = 0x246
<6>pre_handler: p->addr = 0xc0439cc0, ip = c0439cc1, flags = 0x246
<6>post_handler: p->addr = 0xc0439cc0, flags = 0x246

可以看到被探测点的地址为0xc0439cc0,用以下命令确定这个地址就是do_fork的入口地址。 [root@apple kprobes]# cat /proc/kallsyms | grep do_fork c0439cc0 T do_fork

2、使用kprobe on ftrace来跟踪函数和调用栈

这种方式用户通过 /sys/kernel/debug/tracing/目录下的trace等属性文件来探测用户指定的函数,用户可添加kprobe支持的任意函数并设置探测格式与过滤条件,无需再编写内核模块,使用更为简便,但需要内核的debugfs和ftrace功能的支持。

首先,在使用前需要保证开启以下内核选项:

代码语言:javascript复制
Symbol: FTRACE [=y]                                                                                            
Type  : boolean                                                                                                
Prompt: Tracers                                                                                                
  Location:                                                                                                    
(5) -> Kernel hacking                                                                                          
  Defined at kernel/trace/Kconfig:132                                                                          
  Depends on: TRACING_SUPPORT [=y] 

Symbol: KPROBE_EVENT [=y]                                                                                      
Type  : boolean                                                                                                
Prompt: Enable kprobes-based dynamic events                                                                    
  Location:                                                                                                    
    -> Kernel hacking                                                                                          
(1)   -> Tracers (FTRACE [=y])                                                                                 
  Defined at kernel/trace/Kconfig:405                                                                          
  Depends on: TRACING_SUPPORT [=y] && FTRACE [=y] && KPROBES [=y] && HAVE_REGS_AND_STACK_ACCESS_API [=y]       
  Selects: TRACING [=y] && PROBE_EVENTS [=y]  

Symbol: HAVE_KPROBES_ON_FTRACE [=y]                                                                            
Type  : boolean                                                                                                
  Defined at arch/Kconfig:183                                                                                  
  Selected by: X86 [=y]                                                                                        
 
Symbol: KPROBES_ON_FTRACE [=y]                                                                                 
Type  : boolean                                                                                                
  Defined at arch/Kconfig:79                                                                                   
  Depends on: KPROBES [=y] && HAVE_KPROBES_ON_FTRACE [=y] && DYNAMIC_FTRACE_WITH_REGS [=y]

然后需要将debugfs文件系统挂在到/sys/kernel/debug/目录下:

代码语言:javascript复制
# mount -t debugfs nodev /sys/kernel/debug/

此时/sys/kernel/debug/tracing目录下就出现了若干个文件和目录用于用户设置要跟踪的函数以及过滤条件等等,这里我主要关注以下几个文件:

1、配置属性文件:kprobe_events 2、查询属性文件:trace和trace_pipe 3、使能属性文件:events/kprobes///enabled 4、过滤属性文件:events/kprobes///filter 5、格式查询属性文件:events/kprobes///format 6、事件统计属性文件:kprobe_profile

其中配置属性文件用于用户配置要探测的函数以及探测的方式与参数,在配置完成后,会在events/kprobes/目录下生成对应的目录;其中会生成enabled、format、filter和id这4个文件,其中的enable属性文件用于控制探测的开启或关闭,filter用于设置过滤条件,format可以查看当前的输出格式,最后id可以查看当前probe event的ID号。然后若被探测函数被执行流程触发调用,用户可以通过trace属性文件进行查看。最后通过kprobe_profile属性文件可以查看探测命中次数和丢失次数(probe hits and probe miss-hits)。

下面来看看各个属性文件的常用操作方式(其中具体格式和参数方面的细节可以查看内核的 Documentation/trace/kprobetrace.txt文件,描述非常详细):

1、kprobe_events

该属性文件支持3中格式的输入:

p[:[GRP/]EVENT] [MOD:]SYM[ offs]|MEMADDR [FETCHARGS]——设置一个probe探测点 r[:[GRP/]EVENT] [MOD:]SYM[ 0] [FETCHARGS] ——设置一个return probe探测点 -:[GRP/]EVENT ——删除一个探测点 各个字段的含义如下:

GRP : Group name. If omitted, use "kprobes" for it. ——指定后会在events/kprobes目录下生成对应名字的目录,一般不设 EVENT : Event name. If omitted, the event name is generated based on SYM offs or MEMADDR. ——指定后会在events/kprobes/目录下生成对应名字的目录 MOD : Module name which has given SYM. ——模块名,一般不设 SYM[ offs] : Symbol offset where the probe is inserted. ——指定被探测函数和偏移 MEMADDR : Address where the probe is inserted. ——指定被探测的内存绝对地址

FETCHARGS : Arguments. Each probe can have up to 128 args. ——指定要获取的参数信息 %REG : Fetch register REG ——获取指定寄存器值 @ADDR : Fetch memory at ADDR (ADDR should be in kernel) ——获取指定内存地址的值 @SYM[ |-offs] : Fetch memory at SYM |- offs (SYM should be a data symbol) ——获取全局变量的值 stackN:FetchNthentryofstack(N>=0)——获取指定栈空间值,即sp寄存器 N后的位置值

2、events/kprobes///enabled

开启探测:echo 1 > events/kprobes///enabled 暂停探测:echo 0 > events/kprobes///enabled

3、events/kprobes///filter

该属性文件用于设置过滤条件,可以减少trace中输出的信息,它支持的格式和c语言的表达式类似,支持 ==,!=,>,<,>=,<=判断,并且支持与&&,或||,还有()。

下面还是以do_fork()函数为例来举例看一下具体如何使用(实验环境:树莓派1b):

1、设置配置属性

首先添加配置探测点:

代码语言:javascript复制
root@apple:~# echo 'p:myprobe do_fork clone_flags=%r0 stack_start=%r1 stack_size=%r2 parent_tidptr=%r3 child_tidptr= 0($stack)' > /sys/kernel/debug/tracing/kprobe_events

root@apple:~# echo 'r:myretprobe do_fork $retval' >> /sys/kernel/debug/tracing/kprobe_events

这里注册probe和retprobe,其中probe中设定了获取do_fork()函数的入参值(注意这里的参数信息根据不同CPU架构的函数参数传递规则强相关,根据ARM遵守的ATPCS规则,函数入参14通过r0r3寄存器传递,多余的参数通过栈传递),由于入参为5个,所以前4个通过寄存器获取,最后一个通过栈获取。

现可通过format文件查看探测的输出格式:

代码语言:javascript复制
root@apple:/sys/kernel/debug/tracing# cat events/kprobes/myprobe/format 
name: myprobe
ID: 1211
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:unsigned long __probe_ip; offset:8;       size:4; signed:0;
        field:u32 clone_flags;  offset:12;      size:4; signed:0;
        field:u32 stack_start;  offset:16;      size:4; signed:0;
        field:u32 stack_size;   offset:20;      size:4; signed:0;
        field:u32 parent_tidptr;        offset:24;      size:4; signed:0;
        field:u32 child_tidptr; offset:28;      size:4; signed:0;

print fmt: "(%lx) clone_flags=0x%x stack_start=0x%x stack_size=0x%x parent_tidptr=0x%x child_tidptr=0x%x", REC->__probe_ip, REC->clone_flags, REC->stack_start, REC->stack_size, REC->parent_tidptr, REC->child_tidptr
root@apple:/sys/kernel/debug/tracing# cat events/kprobes/myretprobe/format     
name: myretprobe
ID: 1212
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:unsigned long __probe_func;       offset:8;       size:4; signed:0;
        field:unsigned long __probe_ret_ip;     offset:12;      size:4; signed:0;
        field:u32 arg1; offset:16;      size:4; signed:0;

print fmt: "(%lx <- %lx) arg1=0x%x", REC->__probe_func, REC->__probe_ret_ip, REC->arg1

2、开启探测并触发函数调用

往对应的enable函数中写入1用以开启探测功能:

代码语言:javascript复制
root@apple:/sys/kernel/debug/tracing# echo 1 > events/kprobes/myprobe/enable 

root@apple:/sys/kernel/debug/tracing# echo 1 > events/kprobes/myretprobe/enable

然后在终端上敲几条命令和建立一个ssh链接触发进程创建do_fork函数调用,并通过trace属性文件获取函数调用时的探测情况

代码语言:javascript复制
root@apple:/sys/kernel/debug/tracing# cat trace


# tracer: nop
......
            bash-513   [000] d... 15726.746135: myprobe: (do_fork 0x0/0x380) clone_flags=0x1200011 stack_start=0x0 stack_size=0x0 parent_tidptr=0x0 child_tidptr=0xb6f43278
            bash-513   [000] d... 15726.746691: myretprobe: (SyS_clone 0x2c/0x34 <- do_fork) arg1=0x226
            bash-513   [000] d... 15727.296153: myprobe: (do_fork 0x0/0x380) clone_flags=0x1200011 stack_start=0x0 stack_size=0x0 parent_tidptr=0x0 child_tidptr=0xb6f43278
            bash-513   [000] d... 15727.296713: myretprobe: (SyS_clone 0x2c/0x34 <- do_fork) arg1=0x227
            bash-513   [000] d... 15728.356149: myprobe: (do_fork 0x0/0x380) clone_flags=0x1200011 stack_start=0x0 stack_size=0x0 parent_tidptr=0x0 child_tidptr=0xb6f43278
            bash-513   [000] d... 15728.356705: myretprobe: (SyS_clone 0x2c/0x34 <- do_fork) arg1=0x228
            bash-513   [000] d... 15731.596195: myprobe: (do_fork 0x0/0x380) clone_flags=0x1200011 stack_start=0x0 stack_size=0x0 parent_tidptr=0x0 child_tidptr=0xb6f43278
            bash-513   [000] d... 15731.596756: myretprobe: (SyS_clone 0x2c/0x34 <- do_fork) arg1=0x229
            sshd-520   [000] d... 17755.999223: myprobe: (do_fork 0x0/0x380) clone_flags=0x1200011 stack_start=0x0 stack_size=0x0 parent_tidptr=0x0 child_tidptr=0xb6fac068
            sshd-520   [000] d... 17755.999943: myretprobe: (SyS_clone 0x2c/0x34 <- do_fork) arg1=0x22d

从输出中可以看到do_fork函数由bash(PID=513) 和sshd(PID=520)进程调用,同时执行的CPU为0,调用do_fork函数是入参值分别是stack_start=0x0 stack_size=0x0 parent_tidptr=0x0 child_tidptr=0xbxxxxxxx,同时输出函数返回上层SyS_clone系统调用的nr值。

如果输出太多了,想要清除就向trace中写0即可

代码语言:javascript复制
root@apple:/sys/kernel/debug/tracing# echo 0 > trace

3、使用filter进行过滤

例如想要把前面列出的PID为513调用信息的给过滤掉,则向filter中写入如下的命令即可:

代码语言:javascript复制
root@apple:/sys/kernel/debug/tracing# echo common_pid!=513 > events/kprobes/myprobe/filter 
root@apple:/sys/kernel/debug/tracing# cat trace
# tracer: nop
......
            bash-513   [000] d... 24456.536804: myretprobe: (SyS_clone 0x2c/0x34 <- do_fork) arg1=0x245
        kthreadd-2     [000] d... 24598.655935: myprobe: (do_fork 0x0/0x380) clone_flags=0x800711 stack_start=0xc003d69c stack_size=0xc58982a0 parent_tidptr=0x0 child_tidptr=0x0
        kthreadd-2     [000] d... 24598.656133: myretprobe: (kernel_thread 0x38/0x40 <- do_fork) arg1=0x246
            bash-513   [000] d... 24667.676717: myretprobe: (SyS_clone 0x2c/0x34 <- do_fork) arg1=0x247

如此就不会在打印PID为513的进程调用信息了,这里的参数可以参考前面的format中输出的,例如想指定输出特定clone_flags值,则可以输入clone_flags=xxx即可。

最后补充一点,若此时需要查看函数调用的栈信息(stacktrace),可以使用如下命令激活stacktrace输出:

代码语言:javascript复制
root@apple:/sys/kernel/debug/tracing# echo stacktrace > trace_options

root@apple:/sys/kernel/debug/tracing# cat trace                                
......
            bash-508   [000] d...   449.276093: myprobe: (do_fork 0x0/0x380) clone_flags=0x1200011 stack_start=0x0 stack_size=0x0 parent_tidptr=0x0 child_tidptr=0xb6f86278
            bash-508   [000] d...   449.276126: <stack trace>
 => do_fork

0 人点赞