linux内核调试工具之kprobe

Posted 为了维护世界和平_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux内核调试工具之kprobe相关的知识,希望对你有一定的参考价值。

目录

一、内核调试的痛点

二、kprobe的优点

三、kprobe探测点的要点

四、探测点的开销与优化

五、内核配置

六、API

七、程序架构

八、实例


一、内核调试的痛点


        内核调试,添加打印信息。在运行过程中想看某个函数的变量,需要重新编译内核。这样破坏了执行的过程。

二、kprobe的优点


        kprobe 可以在系统运行期间,自定义回调函数,动态插入探测点。当内核执行到探测函数时,会调用回调函数。同时,也可以动态移除探测函数。

   探测点的类型

  •  pre_handler:在被探测函数执行前回调
  •  post_handler:在被探测函数执行
  •  falut_handler:探测期间发生的错误,但在新内核中被删除

三、kprobe探测点的要点

1、探测的特点

  • 可以探测的函数,一般来说,可以探测任意函数。
  • 回调函数,通过修改内核的数据结构或pt_regs结构体中的数据,可以修改被探测函数的环境。如在测试中安装bug修复信息或者注入错误代码。
  • kprobe会避免在处理探测点函数时 再次调用另一个探测点的回调函数。如在printk上注册的探测点,在它的回调函数中可能再次调用printk函数,此时将不再触发printk探测点的回调。
  • 在注册和销毁的过程中,不要使用mutexs或allocate memory函数
  • 多个回调函数可以同时在不同的CPU上触发。
  • 探测函数运行在禁用抢占或禁用中断的情况下运行,在这种情况下中断函数不能调用会放弃CPU的函数。
  • 如何函数的调用次数和返回次数不等,则在类似的函数上注册kretprobe不会达到预期的效果。如do_exit()
  • 在进入和退出一个函数时,CPU运行再非当前任务所有的栈上,往该函数上注册kretprobe可能会导致不可预料的后果。因此X86_64结构下为__switch_to()注册kretprobe会返回-EINVAL

2、不能探测的函数

  • 函数中有 __kprobes或nokprobe_inline 不能探测
  • 使用NOKPEOBE_SYSBOL宏的函数
  • 在黑名单中的函数 /sys/kernel/debug/kprobes/blacklist
  • 内敛函数inline,不能保证都探测,因为gcc可能会优化某些函数
  • 在kernel/kprobes.c和arch/*/kernel/kprobes.c 用于实现自身的函数不能探测
  • do_page_fault与 notifier_call_chain 函数会出现错误。在register_*probe 注册时会返回-EINVAL

3、接口稳定性

  • 内核函数API在不断的变化,有些会被弃用;内核探测模块需要不断维护
  • 在探测大量代码路径(如 调度,中断,或网络代码)避免探测。如果探测,使用printk 速率限制的API。pr_debug_ratelimited、pr_err_ratelimited

四、探测点的开销与优化


2005年使用的典型CPU,kprobe命中需要0.5到1.0微秒来处理,返回探测命中的时间通常比kprobe命中的时间长50-75%

k = kprobe; r = return probe; kr = kprobe + return probe
on same function
i386: Intel Pentium M, 1495 MHz, 2957.31 bogomips
k = 0.57 usec; r = 0.92; kr = 0.99
x86_64: AMD Opteron 246, 1994 MHz, 3971.48 bogomips
k = 0.49 usec; r = 0.80; kr = 0.82
ppc64: POWER5 (gr), 1656 MHz (SMT disabled, 1 virtual CPU per physical CPU)
k = 0.77 usec; r = 1.26; kr = 1.45


优化的kprobe命中需要0.07到0.1微秒才能处理

k = unoptimized kprobe, b = boosted (single-step skipped), o = optimized kprobe,
r = unoptimized kretprobe, rb = boosted kretprobe, ro = optimized kretprobe.
i386: Intel(R) Xeon(R) E5410, 2.33GHz, 4656.90 bogomips
k = 0.80 usec; b = 0.33; o = 0.05; r = 1.10; rb = 0.61; ro = 0.33
x86-64: Intel(R) Xeon(R) E5410, 2.33GHz, 4656.90 bogomips
k = 0.99 usec; b = 0.43; o = 0.06; r = 1.24; rb = 0.68; ro = 0.30

五、内核配置

CONFIG_KPROBES=y
CONFIG_KALLSYMS=y
CONFIG_KALLSYMS_ALL=y
CONFIG_DEBUG_INFO=y

六、API

#include <linux/kprobes.h>
#include <linux/ptrace.h>

//注册
int register_kprobe(struct kprobe *kp);

//探测前回调函数
int pre_handler(struct kprobe *p, struct pt_regs *regs);

//探测后回调函数
void post_handler(struct kprobe *p, struct pt_regs *regs,
                  unsigned long flags);

int disable_kprobe(struct kprobe *kp);

int enable_kprobe(struct kprobe *kp);


void unregister_kprobes(struct kprobe **kps, int num);

七、程序架构

#include <linux/kprobes.h>
#include <linux/ptrace.h>

static int handler_pre(struct kprobe *p, struct pt_regs *regs)




static void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags)

  


static struct kprobe kpb;
static int __init kprobe_init(void)

        kpb.pre_handler = handler_pre;
        kpb.post_handler = handler_post;
        kpb.symbol_name = kprobe_func;
        if (register_kprobe(&kpb)) 
                pr_alert("register_kprobe failed!\\n“);
                return -EINVAL;
        


static void __exit kprobe_exit(void)

        unregister_kprobe(&kpb);
        pr_info("bye, unregistering kernel probe @ '%s'\\n", kprobe_func);


module_init(kprobe_init);
module_exit(kprobe_exit);


八、实例

使用kprobe跟踪do_sys_openat2,并输出打开的文件名参数

内核中do_sys_openat2的原型

static long do_sys_openat2(int dfd, const char __user *filename,
               struct open_how *how)


1、内核模块程序

kprobe.c源码


#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/interrupt.h>
#include <linux/spinlock.h>
#include <linux/kprobes.h>
#include <linux/ptrace.h>
#include <linux/uaccess.h>

#include <linux/jiffies.h>
#include <linux/ktime.h>


MODULE_AUTHOR("wy");
MODULE_LICENSE("Dual MIT/GPL");
MODULE_VERSION("0.1");

static spinlock_t lock;
static struct kprobe kpb;
static u64 tm_start, tm_end;
static char *fname;

//接收脚本传递的参数 kprobe_func
#define MAX_FUNCNAME_LEN  64
static char kprobe_func[MAX_FUNCNAME_LEN];
module_param_string(kprobe_func, kprobe_func, sizeof(kprobe_func), 0);
MODULE_PARM_DESC(kprobe_func, "function name to attach a kprobe to");

#define pr_fmt(fmt) "%s:%s(): " fmt, KBUILD_MODNAME, __func__

//显示时间差
#define SHOW_DELTA(later, earlier)  do     \\
    if (time_after((unsigned long)later, (unsigned long)earlier))  \\
            s64 delta_ns = ktime_to_ns(ktime_sub(later, earlier));      \\
        pr_info("delta: %lld ns", delta_ns);       \\
                if (delta_ns/1000 >= 1)                    \\
                        pr_info(" %lld us", delta_ns/1000);    \\
                if (delta_ns/1000000 >= 1)                 \\
                        pr_info(" %lld ms", delta_ns/1000000); \\
     else  \\
        pr_warn("SHOW_DELTA(): *invalid* earlier > later?\\n");  \\
 while (0)


//探测前的执行
static int handler_pre(struct kprobe *p, struct pt_regs *regs)

	char *param_fname_reg;

	param_fname_reg = (char __user *)regs->si;
#if 1
    //拷贝数据
	if (!strncpy_from_user(fname, param_fname_reg, PATH_MAX + 1))
#else
	/* 使用 copy_from_user() 会产生调度 导致CPU挂掉*/
	if (!copy_from_user(fname, (const char __user *)regs->si,
			    strnlen_user((const char __user *)regs->si, PATH_MAX + 1)))
#endif
		return -EFAULT;

	pr_info("FILE being opened: reg:0x%px   fname:%s\\n",
		(void *)param_fname_reg, fname);

	spin_lock(&lock);
	tm_start = ktime_get_real_ns();
	spin_unlock(&lock);
    //内核栈信息
    dump_stack();

	return 0;


//探测之后的输出
static void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags)

	spin_lock(&lock);
	tm_end = ktime_get_real_ns();
    //计算时间
	SHOW_DELTA(tm_end, tm_start);
	spin_unlock(&lock);



static int __init kprobe_init(void)

	if (kprobe_func[0] == '\\0') 
		pr_warn("expect a valid kprobe_func=<func_name> module parameter");
		return -EINVAL;
	
    //申请内存空间,用来存储do_sys_openat2参数名
	fname = kzalloc(PATH_MAX + 1, GFP_ATOMIC);
	if (unlikely(!fname))
		return -ENOMEM;
    
	kpb.pre_handler = handler_pre;
	kpb.post_handler = handler_post;
	kpb.symbol_name = kprobe_func;
    //注册
	if (register_kprobe(&kpb)) 
		pr_alert("kernel fun register_kprobe failed!\\n", kprobe_func);
		return -EINVAL;
	
	pr_info("registering kernel probe @ '%s'\\n", kprobe_func);
	spin_lock_init(&lock);

	return 0;		/* success */


static void __exit kprobe_exit(void)

	kfree(fname);
	unregister_kprobe(&kpb);
	pr_info("bye, unregistering kernel probe @ '%s'\\n", kprobe_func);


module_init(kprobe_init);
module_exit(kprobe_exit);

Makefile

FNAME_C := kprobe

KDIR ?= /lib/modules/$(shell uname -r)/build

CC     := $(CROSS_COMPILE)gcc
PWD            := $(shell pwd)
obj-m          += $FNAME_C.o

all:
        make -C $(KDIR) M=$(PWD) modules
install:
        make
        sudo make -C $(KDIR) M=$(PWD) modules_install
clean:
        make -C $(KDIR) M=$(PWD) clean

2、用户态应用程序

测试程序打开/home/kprobe.c  测试在kprobe探测能否探测到

#include<stdio.h>
#include <unistd.h>
 
void main()

        FILE *fp = NULL;
        while(1)
        
            fp = fopen("/home/kprobe.c","r+");
            if(fp == NULL)
                return;

            fclose(fp);
            usleep(10000);
        


 gcc main.c  生成a.out

3、运行测试

运行用户程序

a.out

运行内核程序

insmod ./kprobe.ko kprobe_func=do_sys_openat2

运行输出截取,运行时很多的干扰可以使用 journalctl 

#journalctl -k >log.txt

#cat log.txt | grep "/home/kprobe.c"

将内核输出都写到log.txt中(dmesg只能写部分),使用grep检索所需要查询的信息。

Oct 24 19:23:19 ubuntu kernel: CPU: 1 PID: 380 Comm: systemd-journal Tainted: G           OE     5.13.0-52-generic #59-Ubuntu
Oct 24 19:23:19 ubuntu kernel:  ? do_syscall_64+0x6e/0xb0
Oct 24 19:23:19 ubuntu kernel: 3_kprobe:handler_pre(): FILE being opened: reg:0x000055bdf0a79010   fname:/home/wy/misc/kernel/Linux-Kernel-Debugging/ch4/kprobes/3_kprobe/3_kprobe.c
Oct 24 19:23:19 ubuntu kernel:  ? do_sys_openat2+0x5/0x150
Oct 24 19:23:19 ubuntu kernel:  ? irqentry_exit+0x19/0x30
Oct 24 19:23:19 ubuntu kernel:  ? do_sys_openat2+0x5/0x150
Oct 24 19:23:19 ubuntu kernel: RSP: 0018:ffffba4ec070be98 EFLAGS: 00000246 ORIG_RAX: 0000000000000000
Oct 24 19:23:19 ubuntu kernel: 3_kprobe:handler_post(): delta: 743566 ns
Oct 24 19:23:19 ubuntu kernel: RBP: 000055da77256800 R08: 0000000000000000 R09: ffffffffffffffff
Oct 24 19:23:19 ubuntu kernel: 3_kprobe:handler_post(): 001)  systemd-journal :380   |  ...0   /* handler_post() */
Oct 24 19:23:19 ubuntu kernel: Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, Bios 6.00 07/22/2020
Oct 24 19:23:19 ubuntu kernel:  ? __x64_sys_openat+0x55/0x90
Oct 24 19:23:19 ubuntu kernel:  show_stack+0x52/0x58
Oct 24 19:23:19 ubuntu kernel: Call Trace:
Oct 24 19:23:19 ubuntu kernel: 3_kprobe:handler_post():  313 us
Oct 24 19:23:19 ubuntu kernel:  entry_SYSCALL_64_after_hwframe+0x44/0xae
Oct 24 19:23:19 ubuntu kernel:  kprobe_ftrace_handler+0xf9/0x1c0
Oct 24 19:23:19 ubuntu kernel:  kprobe_ftrace_handler+0xf9/0x1c0
Oct 24 19:23:19 ubuntu kernel:  show_stack+0x52/0x58
Oct 24 19:23:19 ubuntu kernel: Call Trace:
Oct 24 19:23:19 ubuntu kernel: 3_kprobe:handler_post():  481 us
Oct 24 19:23:19 ubuntu kernel: 3_kprobe:handler_pre(): 001)  systemd-journal :380   |  ...0   /* handler_pre() */
Oct 24 19:23:19 ubuntu kernel: 3_kprobe:handler_post(): 001)  systemd-journal :380   |  ...0   /* handler_post() */
Oct 24 19:23:19 ubuntu kernel:  </TASK>

在输出信息中可以看到函数的地址、打开的文件名、函数执行的时间等信息

参考

Kernel Probes (Kprobes) — The Linux Kernel documentation

以上是关于linux内核调试工具之kprobe的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

当前的 Linux 内核调试技术

linux 使用kdump和crash工具调试内核

linux 使用kdump和crash工具调试内核