然后使用内核提供的方法编写代码,并且将代码编译成内核模块,载入到内核中。代码如下:
//kprobe_example.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/kprobes.h>
//统计do_fork()总共执行了几次
static int total_count = 0 ;
//前置方法,这里可以拿到方法入参和栈,每次执行do_fork() total_count++
static int handler_pre ( struct kprobe * p , struct pt_regs * regs )
{
total_count ++ ;
//printk 列印的日志 可以通过dmesg 命令查看
printk ( KERN_INFO "累计调用do_fork[%d]次
" , total_count );
return 0 ;
}
//后置方法,这里可以拿到方法返回值
static void handler_post ( struct kprobe * p , struct pt_regs * regs ,
unsigned long flags )
{
}
//方法执行失败的回调函数
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 ;
}
//通过kprobe这个数据结构,定义要hook的内核方法名称
static struct kprobe kp = {
. symbol_name = "do_fork" ,
};
//通过register_kprobe 方法更改内核对应方法的指令
static int 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 %d
" , ret );
return ret ;
}
printk ( KERN_INFO "Planted kprobe at %p
" , kp . addr );
return 0 ;
}
//通过unregister_kprobe卸载hook
static void kprobe_exit ( void ){
unregister_kprobe ( & kp );
printk ( KERN_INFO "kprobe at %p unregistered
" , kp . addr );
}
//构造内核模块
module_init ( kprobe_init );
module_exit ( kprobe_exit );
MODULE_LICENSE ( "GPL" );
编写Makefile 文件,并执行make命令,将kprobe_example.c编译成kprobe_example.ko, Makefile 内容如下:
// Makefile
obj-m +=kprobe_example.o
CURRENT_PATH:=$(shell pwd)
LINUX_KERNEL_PATH:=/lib/modules/$(shell uname -r)/build
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
然后执行sudo insmod kprobe_example.ko
装载内核模块,然后使用dmesg查看内核日志:
最后记得sudo rmmod kprobe_example.ko
卸载模块。
至此,linux能够获取内核代码执行信息的原理就搞清楚了,使用kprobe ,每次只要装载一个内核模块就能进行调试,卸载模块就能停止调试。
大概是觉得每次调试装载卸载太过繁琐,如果可以装载一个通用的模块,每次调试通过配置这个模块来实现不同的功能,调试就更方便了。于是就引出了第二个关键词eBPF
二、eBPF
eBPF 简史 这篇文章,除了没有给个例子告诉读者eBPF代码怎么执行 ,其他关于是什么、为什么都讲的非常好理解。就是这篇文章给我打开了新世界的大门。我这边不再赘述eBPF相关的东西,记录一下我的一些新的发现。
首先这篇文章提到BPF实际上是运行在内核中的一个虚拟机 。每次看到虚拟机 这三个字,我都觉得特别高深,这篇文章最棒的地方就是,直接把BPF这个虚拟机的代码给出来了filter.c ,居然只需要600行就能实现一个虚拟机?!
之前一直很好奇,虚拟机是怎么把虚拟机指令对应到机器指令的。看了这个代码恍然大悟。BPF定义了两个寄存器A和X,对应到代码中就是定义两个变数A和X。然后定义一个byte数组里边放上虚拟机指令。通过一个for循环模拟cpu取指令,通过switch模拟cpu执行不同指令,对A和X进行指令对应的加减乘除操作。虚拟机的本质居然是这么简单的东西。。。
接著文章趁热打铁,又提到了JIT ,实时把虚拟机指令编译成机器指令。并给出了代码bpf_jit_comp.c ,打开一看,居然还是一个for 循环+switch,JIT的本质其实就是把一个byte数组转化成另一个byte数组。。。 说实话看到这里真的特别开心,以前一直觉得高深莫测的东西,原来除去各种细节以后,其实就这么简单。之前一直看书上说学习内核代码很有用 ,第一次真正认同这句话。
我的理解是eBPF 就是那个装载到内核的通用模块,通过把eBPF 代码发送给内核来实现不同的调试功能,这样就不用每次装载卸载了。
但是问题的关键是,文章给出了eBPF的例子,但是却并没有详细解释怎么执行这些例子的代码。所以我继续百度,找到了这篇文章 7 个使用 bcc/BPF 的性能分析神器 ,里边提到bcc 提供了使用ebpf的方法,并且给出了官方教程:bcc Python Developer Tutorial
三、bcc
至此,终于找到了能在linux下使用的动态追踪工具,并且也从原理上证明这个是可以使用的。接下来就是安装使用了。
centos默认的内核版本是3.10,太低了,接下来就需要升级内核如何在 CentOS 7 中安装或升级最新的内核
然后按照INSTALL.md 的说明安装。话说这个make 已经执行了一个多小时 了。。。几十兆的东西编译怎么这么慢。。。
最终这次能不能找到一个能够在我们生产环境使用的动态追踪工具还不清楚,但是这次学习真的是收获满满。等待安装的时候,我开始整理这篇文章,点开了一切的起点动态追踪技术漫谈 ,发现所有之前看不懂的东西,都能看懂了。而且再次看到作者的这句话:
有的工程师在线上出问题的时候,非常慌乱,会去胡乱猜测可能的原因,但又缺乏任何证据去支持或者否证他的猜测与假设。他甚至会在线上反复地试错,反复地折腾,搞得一团乱麻,毫无头绪,让自己和身边的同事都很痛苦,白白浪费了宝贵的排错时间。
但是当我们有了动态追踪技术之后,排查问题本身就可能会变成一个非常有趣的过程,让我们遇到线上的诡异问题就感到兴奋,就仿佛好不容易又逮著机会,可以去解一道迷人的谜题。
如果线上问题真的能成为迷人的谜题,那该有多棒啊~~
结论:
如果这个bcc真的能用的话,下篇文章就简单介绍一下使用心得,希望给力一点吧~~
万一不行,还有个SystemTap可以尝试。
面对这么一个难以理解的东西,真的感谢本篇文章引用到的所有帖子的作者。电影末尾都有个鸣谢,这里我也模仿搞一下:
---------------鸣谢 ---------------
---------------动态追踪技术漫谈 ---------------
---------------Linux Extended BPF (eBPF) Tracing Tools ---------------
---------------eBPF 简史 ---------------
---------------Linux内核调试技术——kprobe使用与实现 - (醍醐灌顶,失眠绝佳之读物) ---------------
---------------如何在 CentOS 7 中安装或升级最新的内核 ---------------
---------------bcc Python Developer Tutorial ---------------
最后,让我们保持独立思考,不卑不亢。长成自己想要的样子! (引用自 我非常喜欢的B站up主 」独立菌儿「->猛戳链接<-的口头禅)
推荐阅读: