kprobe原理解析

简介: 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宝库”。

相关文章
|
1月前
|
关系型数据库 MySQL Shell
CMake构建Makefile深度解析:从底层原理到复杂项目(三)
CMake构建Makefile深度解析:从底层原理到复杂项目
32 0
|
1月前
|
编译器 vr&ar C++
CMake构建Makefile深度解析:从底层原理到复杂项目(二)
CMake构建Makefile深度解析:从底层原理到复杂项目
35 0
|
29天前
|
存储 安全 编译器
【C++ 17 新功能 std::visit 】深入解析 C++17 中的 std::visit:从原理到实践
【C++ 17 新功能 std::visit 】深入解析 C++17 中的 std::visit:从原理到实践
70 0
|
25天前
|
安全 Java 数据安全/隐私保护
【深入浅出Spring原理及实战】「EL表达式开发系列」深入解析SpringEL表达式理论详解与实际应用
【深入浅出Spring原理及实战】「EL表达式开发系列」深入解析SpringEL表达式理论详解与实际应用
55 1
|
21小时前
|
缓存 JavaScript 前端开发
|
1天前
|
SQL 分布式计算 资源调度
一文解析 ODPS SQL 任务优化方法原理
本文重点尝试从ODPS SQL的逻辑执行计划和Logview中的执行计划出发,分析日常数据研发过程中各种优化方法背后的原理,覆盖了部分调优方法的分析,从知道怎么优化,到为什么这样优化,以及还能怎样优化。
|
1天前
|
Java
并发编程之线程池的底层原理的详细解析
并发编程之线程池的底层原理的详细解析
9 0
|
1天前
|
JSON Java Maven
Javaweb之SpringBootWeb案例之 SpringBoot原理的详细解析
Javaweb之SpringBootWeb案例之 SpringBoot原理的详细解析
5 0
Javaweb之SpringBootWeb案例之 SpringBoot原理的详细解析
|
1天前
|
前端开发 JavaScript 编译器
深入解析JavaScript中的异步编程:Promises与async/await的使用与原理
【4月更文挑战第22天】本文深入解析JavaScript异步编程,重点讨论Promises和async/await。Promises用于管理异步操作,有pending、fulfilled和rejected三种状态。通过.then()和.catch()处理结果,但可能导致回调地狱。async/await是ES2017的语法糖,使异步编程更直观,类似同步代码,通过事件循环和微任务队列实现。两者各有优势,适用于不同场景,能有效提升代码可读性和维护性。
|
12天前
|
机器学习/深度学习 分布式计算 BI
Flink实时流处理框架原理与应用:面试经验与必备知识点解析
【4月更文挑战第9天】本文详尽探讨了Flink实时流处理框架的原理,包括运行时架构、数据流模型、状态管理和容错机制、资源调度与优化以及与外部系统的集成。此外,还介绍了Flink在实时数据管道、分析、数仓与BI、机器学习等领域的应用实践。同时,文章提供了面试经验与常见问题解析,如Flink与其他系统的对比、实际项目挑战及解决方案,并展望了Flink的未来发展趋势。附带Java DataStream API代码样例,为学习和面试准备提供了实用素材。
33 0

推荐镜像

更多