深入理解Linux 内核追踪机制

深入ftrace uprobe原理和功能介绍 上一章我们学习了,kprobe 可以实现动态内核的注入,基于中断的方法在任意指令中插入追踪代码,并且通过 pre_handler/post_handler去接收回调。另一个 kprobe 的同族是,只不过是针对函数级别的内核监控,根据用户注册时提供的和来分别在函数进入时和返回前进行回调。本章的我们来学习uprobe ,顾名思义,相对于内核函数/地址的监控,主要用于用户态函数/地址的监控。听起来是不是有点神奇,内核怎么监控用户态函数的调用呢? 阅读详情

Linux 存在众多 tracing tools,比如 ftrace、perf,他们可用于内核的调试、提高内核的可观测性。众多的工具也意味着繁杂的概念,诸如 tracepoint、trace events、kprobe、eBPF 等,甚至让人搞不清楚他们到底是干什么的。本文尝试理清这些概念。

 

注入 Probe 的机制

Probe Handler

如果我们想要追踪内核的一个函数或者某一行代码,查看执行的上下文和执行情况,通用的做法是在代码或函数的执行前后 printk 打印日志,然后通过日志来查看追踪信息。但是这种方式需要重新编译内核并重启,非常麻烦。如果是在生产环境排查问题,这种方式也是无法接受的。

一种比较合理的方式是在内核正常运行时,自定义一个函数,注入到我们想要追踪的内核函数执行前后,当内核函数执行时触发我们定义的函数,我们在函数中实现获取我们想要的上下文信息并保存下来。同时因为增加了内核函数的执行流程,我们定义的函数最好是需要的时候开启,不需要的时候关闭,避免对内核函数造成影响。

这个自定义的函数就是 probe handler,注入 probe handler 的地方被称为探测点或者 Hook 点,在探测点前执行的 probe handler 叫 pre handler, 执行后的叫 post handler,注入 probe handler 的方式被称为“插桩”,内核提供了多种 probe handler 注入机制。接下来我们聊一聊他们是如何实现在内核运行时注入 probe handler。

Kprobes 机制

Kprobes 是一个动态 tracing 机制,能够动态的注入到内核的任意函数中的任意地方,采集调试信息和性能信息,并且不影响内核的运行。Kprobes 有两种类型:kprobes、kretprobes。kprobes 用于在内核函数的任意位置注入 probe handler,kretprobes 用于在函数返回位置注入 probe handler。出于安全性考虑,在内核代码中,并非所有的函数都能“插桩”,kprobe 维护了一个黑名单记录了不允许插桩的的函数,比如 kprobe 自身,防止递归调用。

kprobes 机制如何实现注入 probe handler

内核提供了一个 krpobe 注册接口,当我们调用接口注册一个 kprobe 在指定探测点注入 probe handler 时,内核会把探测点对应的指令复制一份,记录下来,并且把探测点的指令的首字节替换为「断点」指令,在 x86 平台上也就是 int3 指令。

cpu 执行断点指令时,会触发内核的断点处理函数「do_int3」,它判断是否为 kprobe 引起的断点,如果是 kprobe 机制触发的断点,会保存这个程序的状态,比如寄存器、堆栈等信息,并通过 Linux 的「notifier_call_chain」机制,将 cpu 的使用权交给之前 kprobe 的 probe handler,同时会把内核所保存的寄存器、堆栈信息传递给 probe handler。

前面已经提到了,probe handler 分两种类型,一种是 pre handler、一种是 post handler。pre handler 将首先被调用(如果有的话),pre handler 执行完成后,内核会将 cpu 的 flag 寄存器的值设置为 1,开始单步执行原指令,单步执行是 cpu 的一个 debug 特性,当 cpu 执行完一个指令后便会产生一个 int1 异常,触发中断处理函数「do_debug」执行,do_debug 函数会检查本次中断是否为 kprobe 引起,如果是的话,执行 post handler,执行完毕后关闭单步,恢复原始执行流。

kretprobe 探针很有意思,Kprobe 会在函数的入口处注册一个 kprobe,当函数执行时,这个 krpobe 会把函数的返回地址暂存下来,并把它替换为 trampoline 地址。

Kprobe 也会在 trampoline 注册一个 kprobe,函数执行返回时,cpu 控制权转移到 trampoline,此时又会触发 trampoline 上的 kprobe 探针,继续陷入中断,并执行 probe handler。

为什么有了 kprobe 还需要 kretprobe?

Kprobe 在可以函数的任意位置插入 probe,理论上他也能实现 kretprobe 的功能,但是实际上会面临几个挑战。

比如当我们在函数的最后一行代码上注入探针,试图使用 kprobe 实现 kretprobe 的效果,但是实际上这种方式并不好,函数可能会存在多个返回情况,比如不满足 if 条件,发生异常等情况,此时代码完全有可能不会执行最后一行代码,而是在某个地方就返回了,也就意味着不会触发探针执行。

kretprobe 的优势就在于它可以稳定的在函数返回时触发 probe handler 执行,无论函数是基于什么情况下返回。

另外一方面 kprobe 虽然可以在函数的任意位置插入探针,但是实际情况下都是在函数入口处插入探针,因为函数入口是有一条标准的指令序列 prologue 可以进行断点替换,而函数内部的其他位置,可能会存在跳转指令、循环指令等情况,指令序列不太规则,不方便做断点替换。

Uprobes

Uprobes 也分为 uprobes 和 uretprobes,和 Kprobes 从原理上来说基本上是类似的,通过断点指令替换原指令实现注入 probe handler 的能力,并且他没有 Kprobes 的黑名单限制。Uprobes 需要我们提供「探测点的偏移量」,探测点的偏移量是指从程序的起始虚拟内存地址到探测点指令的偏移量。我们可以通过一个简单的例子来理解:

root@zfane-maxpower:~/traceing# cat hello.c
#include <stdio.h>
void test(){
printf("hello world");
}
int main() {
    test();
return 0;
}
root@zfane-maxpower:~/traceing# gcc hello.c -o hello

通过 readelf 读取程序的 ELF 信息,拿到程序的符号表、节表。符号表包含程序中所有的符号,例如全局变量、局部变量、函数、动态链接库符号,以及符号对应的虚拟内存地址。

汇编语言是按照节来编写程序的,例如.text 节、.data 节。每个节都包含程序中的特定数据或代码,节表就是程序中各个节的信息表。

通过符号表可以拿到 hello 函数的虚拟内存地址,通过节表拿到.text 节的虚拟内存地址,以及.text 节相较于 ELF 起始地址的偏移量。

root@zfane-maxpower:~/traceing# readelf -s hello|grep test
36: 0000000000001149    31 FUNC    GLOBAL DEFAULT   16 test
root@zfane-maxpower:~/traceing# readelf -S hello|grep .text
  [16] .text             PROGBITS         0000000000001060  00001060

那么 test 函数的指令在 hello 二进制文件的偏移量就可以计算出来了。

offset=test 函数的虚拟地址 -  .text 段的虚拟地址 + .text 端偏移量
offset= 0000000000001149 - 0000000000001060 + 00001060
offset= 0000000000001149

现在我们可以通过编写内核模块向二进制程序注入 probe handler 获取数据了。

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uprobes.h>
#include <linux/namei.h>
#include <linux/string.h>
#include <linux/uaccess.h>

#define DEBUGGEE_FILE "/home/zfane/hello/hello"
#define DEBUGGEE_FILE_OFFSET (0x1149)
static struct inode *debuggee_inode;

static int uprobe_sample_handler(struct uprobe_consumer *con,
                                 struct pt_regs *regs)
{
    printk("handler is executed, arg0: %s\\n",regs->di);

return 0;
}

static int uprobe_sample_ret_handler(struct uprobe_consumer *con,
unsigned long func,
                                     struct pt_regs *regs)
{
    printk("ret_handler is executed\\n");
return 0;
}

static struct uprobe_consumer uc = {
        .handler = uprobe_sample_handler,
        .ret_handler = uprobe_sample_ret_handler
};

static int __init init_uprobe_sample(void)
{
int ret;
struct path path;

    ret = kern_path(DEBUGGEE_FILE, LOOKUP_FOLLOW, &path);
if (ret) {
return -1;
    }

    debuggee_inode = igrab(path.dentry->d_inode);
    path_put(&path);

    ret = uprobe_register(debuggee_inode,
                          DEBUGGEE_FILE_OFFSET, &uc);
if (ret < 0) {
return -1;
    }

    printk(KERN_INFO "insmod uprobe_sample\\n");
return 0;
}

static void __exit exit_uprobe_sample(void)
{
    uprobe_unregister(debuggee_inode,
                      DEBUGGEE_FILE_OFFSET, &uc);
    printk(KERN_INFO "rmmod uprobe_sample\\n");
}

module_init(init_uprobe_sample);
module_exit(exit_uprobe_sample);

MODULE_LICENSE("GPL");

Tracepoint

Tracepoint 是一个静态的 tracing 机制,开发者在内核的代码里的固定位置声明了一些 Hook 点,通过这些 hook 点实现相应的追踪代码插入,一个 Hook 点被称为一个 tracepoint。

tracepoint 有开启和关闭两种状态,默认处于关闭状态,对内核产生的影响非常小,只是增加了极少的时间开销(一个分支条件判断),极小的空间开销(一条函数调用语句和几个数据结构)。

在 x86 环境下,内核代码编译后,关闭状态的 tracepoint 代码对应的 cpu 指令是:nop 指令,

启用 tracepoint 时,通过 Linux 内核提供的 static jump patch 静态跳转补丁机制,nop 指令会被替换为 jmp 指令,jmp 指令将 cpu 的使用权转移给 static_call 静态跳转函数,这个函数会遍历 tracepoint probe handler 数组获取当前 tracepoint 注册的 probe handler,并进一步跳转到 probe handler 执行,probe handler 执行完成后,再通过 jmp 指令跳转回原函数继续执行。

#include <linux/module.h>
#include <linux/ftrace.h>
#include <linux/tracepoint.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/hashtable.h>
#include <linux/slab.h>
#include <linux/time.h>
#include <linux/percpu.h>
#include <trace/events/sched.h>

static void probe_sched_switch(void *ignore, bool preempt,
                               struct task_struct *prev, struct task_struct *next,
unsigned int prev_state) {
    pr_info("probe_sched_switch: pid [%d] -> [%d] \\n",prev->tgid, next->tgid);
}

struct tracepoints_table {
const char *name;
void *fct;
struct tracepoint *value;
char init;
};

struct tracepoints_table interests[] = {
  
  {.name = "sched_switch", .fct = probe_sched_switch}};

#define FOR_EACH_INTEREST(i) \\
    for (i = 0; i < sizeof(interests) / sizeof(struct tracepoints_table); i++)

static void lookup_tracepoints(struct tracepoint *tp, void *ignore) {
int i;
    FOR_EACH_INTEREST(i) {
if (strcmp(interests[i].name, tp->name) == 0) interests[i].value = tp;
    }
}

static void cleanup(void) {
int i;

// Cleanup the tracepoints
    FOR_EACH_INTEREST(i) {
if (interests[i].init) {
            tracepoint_probe_unregister(interests[i].value, interests[i].fct,NULL);
        }
    }
}

static void __exit tracepoint_exit(void) { cleanup(); }

static int __init tracepoint_init(void)
{
int i;
// Install the tracepoints
    for_each_kernel_tracepoint(lookup_tracepoints, NULL);
    FOR_EACH_INTEREST(i) {
if (interests[i].value == NULL) {
            printk("Error, %s not found\\n", interests[i].name);
            cleanup();
return 1;
        }

        tracepoint_probe_register(interests[i].value, interests[i].fct, NULL);
        interests[i].init = 1;
    }

return 0;
}

module_init(tracepoint_init)
module_exit(tracepoint_exit)
MODULE_
通过实例了解uprobe及其对性能的影响 阅读详情

相关推荐

34、Linux内核追踪与内核恐慌处理全解析

本文深入解析了Linux内核追踪技术与内核恐慌的处理方法,介绍了LTTng和Trace Compass等工具的使用,以及如何通过SysRq触发和调试内核恐慌。同时,还探讨了内核锁死、挂起任务和CPU停滞的检测机制,并提供了学习与实践建议,帮助开发者更好地调试和优化Linux系统。

ttt77的博客 80

adding-kernel-tracepoints:我对Linux内核中当前跟踪点的分析的存储库

添加内核跟踪点 ##我对Linux内核中当前跟踪点的分析的存储库。 如何将静态跟踪点添加到Linux内核? 我首先从查看内核开始,以了解如何添加跟踪点。 ###如何在内核中添加跟踪点要添加跟踪点,必须使用该宏 。 下一个代码段显示了此宏的用法。 在此示例中,我们可以看到声明的事件是sched_switch,这是一个非常常见的事件。 / * * Tracepoint for task switches, performed by the scheduler: * / TRACE_EVENT(sched_switch, # 1 TP_PROTO(struct task_struct * prev, struct task_struct * next), # 2 TP_ARGS(prev, next), # 3 TP_STRUCT__entry( __array(

FoundationPose 4090部署 真实场景迁移

复现并应用foundationpose

m0_52835296的博客 606

RL调参侠之BipedalWalker BipedalWalkerHardcore SAC

前几天用PPO训练了个很一般的agent: RL调参侠之BipedalWalker PPO 我看SpinningUp上面说PPO和SAC是两个领域里各自的State_of_the_art的方法,所以就打算跑SAC试试看,之前因为Open AI Five的原因,一直感觉PPO很牛掰,实际上现在很多自动驾驶的算法也是用PPO在跑,毕竟有一个“必定能够提升期望”的理论保证在这里,我理解这对做项目很大的诱惑。 不过我跑出来的结果明显是SAC更好。 调参侠时间 策略熵的权重alpha用的0.2,我一开始没有看SAC的

weixin_48370148的博客 2097

UniApp实战:5分钟搞定后台持续定位与轨迹实时更新(附完整代码)

本文详细介绍了在UniApp中实现后台持续定位与动态轨迹地图系统的完整方案。针对物流配送、运动健身等场景,文章深入讲解了权限申请、坐标系选择、定位防抖优化、地图轨迹线(polyline)实时绘制等核心步骤,并提供了可复用的完整代码,帮助开发者高效解决App切后台定位停止、轨迹漂移等常见难题。

weixin_29266679的博客 473

人工智能训练师认证教程(5)使用MONAI进行医学影像分析的完整指南

本文介绍了使用MONAI框架进行医学影像分析的全流程。首先详细说明了环境搭建步骤,包括PyTorch和MONAI的安装配置。然后以肺部CT 3D分割为例,分步骤讲解了数据准备与预处理、3D UNet模型构建、训练配置(DiceLoss损失函数、Adam优化器等)以及训练验证循环的实现。文章还包含推理可视化方法和模型导出为ONNX格式的部署方案,并强调了医学影像分析中的数据格式适配、强度归一化、显存优化等关键注意事项。最后指出MONAI的医学专用组件可大幅简化开发流程,建议后续可探索SwinUNETR等进阶方

华为开发者布道师,天府英才计划,致力于大数据+AI 的应用创新,助力企业与个人AI创新赋能。 1190

51单片机AHT20-21源代码

传感器输出经过 标定的数字信号,标准 I 2 C 格式。AHT20 配有一个全新设计的 ASIC专用芯片、一 个经过改进的MEMS半导体电容式湿度传感元件和一个标准的片上温度传感元件,其 性能已经大大提升甚至超出了前一代传感器的可靠性水平,新一代温湿度传感器, 经过改进使其在恶劣环境下的性能更稳定。但是现在网上的大多数原码都是针对stm32等的这种多位单片机,而我们都知道51单片机是8位单片机,无法对浮点数进行运算,对32位的整数计算有时候也不是很准确,所以我对网上stm32的例程进行了修改。...

wd0710的博客 4538

使用ChIPSeeker进行ChIP-seq, ATAC-seq,cut&tag等富集峰的基因组注释

常规的下一代高通量测序(next generation sequencing, NGS)实验通常产生大量短片段(reads),通常我们需要将这些reads比对到参考基因组/转录组上,即将它们置于生物学上有意义的基因背景下,才能获得有意义的结果。一般我们认为会产生两种类型的数据(当然两者并无严格意义上的区分)

微生信 3306

国产FPGA高云GW1NSR-4C,集成ARM Cortex-M3硬核

从板子整体角度来看,这是一款国产化程度很高的板子,从主控FPGA,到JTAG芯片,存储芯片、电源芯片等,都是使用的国产半导体公司的产品。板子整体尺寸也比较迷你,属于小而美类型的,可以认为是一款FPGA核心板,当然也预留了排针接口,给用户足够的扩展空间。对于想了解高云小蜜蜂SoC软硬件开发,以及对云源软件、MCU开发环境做个深度评测来说足够了。JTAG调试器固件可以增加虚拟串口功能,对于板子本来来说面积不会有改动,只需要添加两根信号线即可。

whik1194的博客 6379

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

上一篇: 真正理解红黑树,真正的(Linux内核里大量用到的数据结构
下一篇: 为什么新版内核将进程pid管理从bitmap替换成了radix-tree?
Linux内核站
博客等级 码龄5年 1018粉丝 353原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值