kprobe原理解析

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: kprobe的出现就很有必要,它可以在运行的内核中动态插入探测点,执行你预定义的操作。

kprobe是什么?

    如何高效的调试内核?printk是一种方法,但是printk终归是毫无选择的全量输出,某些场景下不适用,于是你可以试一下tracepoint,我使能tracepoint机制的时候才输出。对于傻傻的放置printk来输出信息的方式,tracepoint是个进步,但是tracepoint只是内核某些特定行为(比如进程切换)上部署的一些静态锚点,这些锚点并不一定是你需要的,所以你仍需要自己部署tracepoint,重现编译内核。那么kprobe的出现就很有必要了,它可以在运行的内核中动态插入探测点,执行你预定义的操作。

kprobe怎么使用?

    kprobe主要有两种使用方法,一是通过模块加载,二是通过debugfs接口。
    模块加载的方式:内核源码下有目录下sample/kprobes,该目录下有许多kprobes的例子,可以仿照这些例子写自己的kprobe模块。以kprobe_example.c为例,首先声明一个kprobe结构体,然后定义其中几个关键成员变量,包括symbol_name,pre_handler,post_handler。其中,symbol_name是函数名(kprobe_example.c中该项为do_fork),告诉内核我的探测点放置在了函数do_fork处,pre_handler和post_handler分别表示在执行探测点之前和之后执行的钩子函数。然后通过register_kprobe函数注册kprobe即可。将kprobe_example.ko inmod进内核之后,每当系统新启动一个进程,比如执行ls,cat等,都会输出:

pre_handler: p->addr = 0x, ip = *.
post_handler: p->addr = 0x, pc = *.

    第一行是执行pre_handler钩子函数的输出,第二行是执行post_handler钩子函数的输出,当然这些都是内核中案例的写法,你可以写自己的钩子函数。
    通过debugfs接口注册kprobe:模块加载的终究不是很方便,尤其对于一些不带gcc的嵌入式系统,需要交叉编译ko,将ko拷贝到单板,然后insmod,不便。debugfs下(确切地说,应该是ftrace)提供了一套注册、使能、注销kprobe的接口,可以很方便的操作kprobe。

用法如下:

    1)cd /sys/kernel/debug/tracing[有些系统没有挂载debugfs,需要先挂载下mount -t debugfs nodev /sys/kernel/debug]
    2)进入tracing目录,这里就是传说中ftrace的天下了,执行:

echo "p:sys_write_event sys_write" > kprobe_events

    向kprobe_events写入“p:sys_write_event sys_write”,注册kprobe事件。你会发现,当前目录下的events下,新增一个kprobes目录,该目录下:

root@station:/sys/kernle/debug/tracing/events/kprobes# ls
enable filter sys_write_event

    即,我们注册的kprobe时间生效了。那么“p:sys_write_event sys_write”是什么意思呢?首先p表示我们要注册一个kprobe,如果要注册retprobe,此处应为r;sys_write_event表示这个kprobe叫什么名字;sys_write表示我们的插入点在哪里。那么,“p:sys_write_event sys_write”的语义就很明显了:在函数sys_write处插入了一个kprobe点,这个点的名字叫做sys_write_event。
    3)使能kprobe。执行:

cd /sys/kernel/debug/tracing/events/kprobes/events/sys_write_event
echo 1 > enable
cd ../../..[退回到/sys/kernel/debug/tracing,查看trace文件的输出]
cat trace
trace文件的输出是如下的:
......
bash-808 [003] d... 42715.347565:sys_write_event:(SyS_write+0x0/0xb0)
......
解释下置红的这条输出:pid为808的进程bash,在自本次开机42715.347565秒的时候,调用了一次函数sys_write。

    4)撤销kprobe。执行

cd /sys/krenel/debug/tracing/events/sys_write_event
echo 0 > enable[首先先关闭kprobe]
cd ../../..
echo "-:kprobes/sys_write_event" >> kprobe_events[注销kprobe]

    以上就是kprobe的两种注册机使用方式:通过模块加载以及通过debugfs注册。
    使用模块加载的方式,是kprobe的一种原始用法:在kprobe结构体里定义插入点、钩子函数,然后通过register_kprobe注册上这个kprobe即可。ftrace接口是kprobe的一种应用,它是一套trace的框架,下面的trace机制包括tracepoint、function trace等,kprobe仅仅是这些trace机制中的一员。上面的讲述我们也已经看出来了,通过ftrace注册的kprobe的输出是在ftrace的输出:trace文件。模块加载模式中我们可以定义kprobe的钩子函数pre_handler和post_handler,但是在ftrace下注册的kprobe的钩子是ftrace接口默认的,我们设置不了,但是具体输出什么,我们可以在echo "p:sys_write_event sys_write"时指定,比如指定x1寄存器的内容等,所以ftrace下注册的kprobe功能同样很强大。同时,由于ftrace下kprobe的输出基于ftrace的输出框架,所以输出信息包含当前进程、CPU、时间戳等信息,对于trace来说非常有用。
    上面和大家简要说明了下kprobe到底应该怎样用,那么现在我们就揭开kprobe神秘的面纱。

kprobe的工作过程大致如下:

1)注册kprobe。注册的每个kprobe对应一个kprobe结构体,该结构体记录着插入点(位置),以及该插入点本来对应的指令original_opcode;
2)替换原有指令。使能kprobe的时候,将插入点位置的指令替换为一条异常(BRK)指令,这样当CPU执行到插入点位置时会陷入到异常态;
3)执行pre_handler。进入异常态后,首先执行pre_handler,然后利用CPU提供的单步调试(single-step)功能,设置好相应的寄存器,将下一条指令设置为插入点处本来的指令,从异常态返回;
4)再次陷入异常态。上一步骤中设置了single-step相关的寄存器,所以original_opcode刚一执行,便会二进宫:再次陷入异常态,此时将signle-step清楚,并且执行post_handler,然后从异常态安全返回。
    步骤2),3),4)便是一次kprobe工作的过程,它的一个基本思路就是将本来执行一条指令扩展成执行kprobe->pre_handler--->指令--->kprobe-->post_handler这样三个过程。下面详细解释每个过程:

image

    指令替换过程:上图中蓝色区域表示内存,红色标明了地址,绿色部分代表一条指令,上图的意思是,内存0xffffffc000162914处存放一条指令是0xa9bd7bfd。那么,现在我注册了一个kprobe,探测点是sys_write函数,该函数的起始位置就是0xffffffc000162914,现在我要使能kprobe了,那么我要做的就是把0xffffffc000162914处原来的指令0xa9bd7bfd替换成一条BRK指令,即上图所表示的一个一花节目过程。你可能会好奇原来的指令0xa9bd7bfd存在哪里?存在kprobe结构体的opcode域!这样当不再使能kprobe的时候,我再恢复回去。
触发BRK指令:
    上面把人家指令给改了,那么CPU执行到BRK必然会引发内核陷入BRK异常状态:

image

    蓝色部分依旧表示内存,绿色部分表示指令,红色表示CPU,上图表示CPU执行到0xffffffc000162914(sys_write)处,该处指令为BRK,于是内核陷入异常态。在异常态中,内核通过BRK指令的错误码判断这是一个kprobe异常,于是进入了kprobe处理函数。kprobe异常处理函数会根据发生异常的地址来找到对应的kprobe(kprobe的addr域记录着地址),执行kprobe的pre_handler函数,然后设置single-step相关的寄存器,为下一步执行原指令时发生single-step异常做准备。那么紧接着就是设置元之灵的地址了,我们知道0xffffffc000162914处已经被替换成了BRK指令,原指令保存在kprobe结构体中,怎么保证下一步执行到原指令呢?最简单的做法是申请一块内存,然后将原指令复制到这块内存开始处,设置PC寄存器位该内存的首地址,这样当代码从异常态返回时,执行的第一条指令便是原指令了!

image

原指令得到执行,二进宫
    经过上面一个步骤,pre_handler得到了执行,从异常态返回之后,原指令也得到了执行,但是由于设置了single-step模式,所以执行完原指令,马上又陷入了异常态,二进宫:

    这次进入异常态后,先清一下single-step相关的寄存器,确保下次从异常返回时的指令不会由于single-step发生三进宫,然后执行post_handler,最后将地址0xffffffc000162918写入到PC寄存器,为什么是这个数值呢?它正是紧接着0xffffffc000162914的下一条指令的地址,有没有发现,至此我们已经完成了pre_handler->原指令->post_handler这样三个阶段,也就是说kprobe要做的事情都做完了,此时的工作就是收拾下残局,返回到正常的指令流程,我们的探测点在0xffffffc000162914处,下一条指令应该就是0xffffffc000162918了,所以把此值写入PC寄存器,让一切恢复正轨!
    kprobe工作结束,走上正轨

image


上面把PC设置成了0xffffffc000162918,所以从异常态返回时,CPU就走上了正轨接着朝下面执行了,一个BRK指令引发的反映再次就搞一段落了。
    但是每次当CPU执行到0xffffffc000162914处,都会触发上面的一连串操作,kprobe的机制也就是从一个BRK指令开始了。

原文发布时间为:2018-07-19
本文来自云栖社区合作伙伴“Linux宝库”,了解相关信息可以关注“Linux宝库”。

相关文章
|
19天前
|
存储 缓存 算法
HashMap深度解析:从原理到实战
HashMap,作为Java集合框架中的一个核心组件,以其高效的键值对存储和检索机制,在软件开发中扮演着举足轻重的角色。作为一名资深的AI工程师,深入理解HashMap的原理、历史、业务场景以及实战应用,对于提升数据处理和算法实现的效率至关重要。本文将通过手绘结构图、流程图,结合Java代码示例,全方位解析HashMap,帮助读者从理论到实践全面掌握这一关键技术。
64 13
|
2月前
|
运维 持续交付 云计算
深入解析云计算中的微服务架构:原理、优势与实践
深入解析云计算中的微服务架构:原理、优势与实践
73 1
|
3月前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
64 3
|
5天前
|
存储 物联网 大数据
探索阿里云 Flink 物化表:原理、优势与应用场景全解析
阿里云Flink的物化表是流批一体化平台中的关键特性,支持低延迟实时更新、灵活查询性能、无缝流批处理和高容错性。它广泛应用于电商、物联网和金融等领域,助力企业高效处理实时数据,提升业务决策能力。实践案例表明,物化表显著提高了交易欺诈损失率的控制和信贷审批效率,推动企业在数字化转型中取得竞争优势。
37 14
|
13天前
|
网络协议 安全 网络安全
探索网络模型与协议:从OSI到HTTPs的原理解析
OSI七层网络模型和TCP/IP四层模型是理解和设计计算机网络的框架。OSI模型包括物理层、数据链路层、网络层、传输层、会话层、表示层和应用层,而TCP/IP模型则简化为链路层、网络层、传输层和 HTTPS协议基于HTTP并通过TLS/SSL加密数据,确保安全传输。其连接过程涉及TCP三次握手、SSL证书验证、对称密钥交换等步骤,以保障通信的安全性和完整性。数字信封技术使用非对称加密和数字证书确保数据的机密性和身份认证。 浏览器通过Https访问网站的过程包括输入网址、DNS解析、建立TCP连接、发送HTTPS请求、接收响应、验证证书和解析网页内容等步骤,确保用户与服务器之间的安全通信。
62 1
|
2月前
|
运维 持续交付 虚拟化
深入解析Docker容器化技术的核心原理
深入解析Docker容器化技术的核心原理
56 1
|
2月前
|
存储 供应链 算法
深入解析区块链技术的核心原理与应用前景
深入解析区块链技术的核心原理与应用前景
60 0
|
2月前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
63 1
|
2月前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
67 0
|
3月前
|
数据采集 存储 编解码
一份简明的 Base64 原理解析
Base64 编码器的原理,其实很简单,花一点点时间学会它,你就又消除了一个知识盲点。
95 3

推荐镜像

更多