深入理解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的博客 75

strace用法说明

strace命令详解 strace 命令是一种强大的工具,它能够显示所有由用户空间程序发出的系统调用。   strace 显示这些调用的参数并返回符号形式的值。strace 从内核接收信息,而且不需要以任何特殊的方式来构建内核。   下面记录几个常用 option .   1 -f -F选项告诉strace同时跟踪fork和vfork出来的进程   2 -o xxx.txt 输出到某个文

process的专栏 1万+

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(

[Linux C]使用backtrace+addr2line追踪函数调用栈,实现类似内核中dump_stack的功能

在应用层,backtrace系列函数提供了追踪函数调用栈的功能。addr2line命令可以将代码中的地址转换成源码中的函数和行数。我们就用这两个工具,实现追踪函数调用栈并打印出函数名的功能。

qq_39667609的博客 1668

变,内核生命力之源

在lwn.net网站看到“2.6内核系列API的变化”一文,其中开始有一段中申明,内核开发者永不确保内部API是稳定的,即使在稳定版中。的确,这辛苦了内核开发者与驱动程序开发者。似乎,这与我们的常理相悖。通常,我们更愿意看到事物稳定不变,好不容易学到的一招一式,希望它尽可能地发挥其作用。但是,当我从“2.6内核系列API的变化“一文中找到这样一段文字:The ftrace

陈莉君的专栏 941

Linux内核调试利器|kprobe 原理与实现

本文主要介绍了 kprobe 的原理与实现,正如本文开始时所说,kprobe 机制的细节很多,所以本文不可能对所有细节进行分析。如果大家对 kprobe 的所有实现细节有兴趣,可以自行阅读源码。

weixin_60043341的博客 792

初步认识Linux oops 消息

oops是英语口语"糟糕"的意思,当LINUX 内核发生严重错误时,比如内存段错误时,将会提示一大段信息。 Oops提示信息相当多,包括出问题时的,各个常用寄存器的值,调用的堆栈,以及出错的可能原因。 oops 的格式 内核的文档里的详细的Oops的说明,的名字是 Documentation/oops-tracing.txt http://www.mjmwired.net/kernel/Documentation/oops-tracing.txt oops第一段出错是内存pa...

bcbobo21cn的专栏 1533

linux精髓pdf,LInux内核精髓-精通Linux内核必会的75个绝技

内容推荐经过近20年的发展,Linux操作系统已经成为当今最成功的开源软件之一,使用广泛,影响深远。随着Linux操作系统功能的不断丰富和完善,Linux内核的源代码也从最初的几万行增加到如今的数百万行,庞大无比,对于Linux内核的研究者和开发者而言,要系统研究Linux内核绝非易事。鉴于此,本书选取了资源管理(CPU、内存、进程等)、文件系统、网络、虚拟化、省电、调试、概要分析、追踪内核调整...

weixin_30711615的博客 278

bpftrace内核跟踪终极指南:5个实用技巧深入理解Linux系统运行机制

bpftrace是一款强大的Linux eBPF高级跟踪语言工具,能够帮助开发者和系统管理员深入理解Linux内核数据结构和系统运行机制。通过简单的脚本语言,你可以实时监控文件操作、系统调用、网络活动等关键系统行为,无需编写复杂的C代码或重启系统。🚀 ## bpftrace是什么?为什么如此重要? bpftrace基于Linux eBPF技术,提供了类似awk的简洁语法,让系统跟踪变得前所未

gitblog_00386的博客 439

24小时学通Linux内核之内存管理方式

  昨天分析的进程的代码让自己还在头昏目眩,脑子中这几天都是关于Linux内核的,对于自己出现的一些问题我会继续改正,希望和大家好好分享,共同进步。今天将会讲诉Linux如何追踪和管理用户空间进程的可用内存和内核的可用内存,还会讲到内核对内存分类的方式以及如何决定分配和释放内存,内存管理是应用程序通过软硬件协助来访问内存的一种方式,这里我们主要是介绍操作...

weixin_33924312的博客 88

使用pwru查看网络请求在内核中的处理细节

pwru

yeshen.org 228

Linux-C中libc函数以及系统调用函数查看

目录man基本查看系统调用查看libc库文件查看内核版本查看glibc版本查看参考 man基本查看 系统调用查看 libc库文件查看 内核版本查看 glibc版本查看 参考

legend050709的专栏 3622

uprobe试用小结

项目上需要分析用户态程序的性能,开发人员一般的方式是在程序内部实现打点函数,记录当程序运行到该点的时间戳,通过比较两点之间的时间间隔来估计两点之间的时间消耗。这样一方面增加了开发的工作量,另外这些打点也会给业务带来额外性能消耗,是否有另外的方式来解决该问题呢? 前段时间在研究ftrace,发现其还有个uprobe特性,故名思意就是用户态的探针工具,今天尝试了下uprobe的使用,小结如下:

daiq531的博客 3388

嵌入式跟踪单元ETB MTB (Micro Trace Buffer )的实现

嵌入式跟踪单元ETB MTB (Micro Trace Buffer )的实现 释义 1 ARM程序开发难免碰到BUG,如果是明显的逻辑BUG,那我们用普通调试手段就可以达到目的;在项目开发阶段,常常会遇到Hard fault错误或者程序跑飞的情况,这些bug采用正常的方法是比较难定位的。 2 调试(debug)手段不管用,或者效率低下的时候,就需要我们的追踪(trace)手段上场了,庆幸的...

u014285530的博客 8904

linux下的系统调用函数到内核函数的追踪

转自:http://blog.chinaunix.net/uid-28458801-id-3468966.html 非常感谢! 使用的 glibc : glibc-2.17 使用的 linux kernel :linux-3.2.07   系统调用是内核向用户进程提供服务的唯一方法,应用程序调用操作系统提供的功能模块(函数)。 用户程序通过系统调

missingu1314的专栏 1252

Linux】运行程序前加上strace,可以追踪到函数库调用过程

如执行结果可知: 我们的程序虽然只有一个printf函数,但是在执行过程中,我们前后调用了execve、access、open、fstat、mmap、brk、write等系统调用。其中write系统调用会把字符串:yikoulinux通过设备文件1,发送到驱动,该设备节点对应终端stdout。【注意】运行程序前加上strace,可以追踪到函数库调用过程。

smartvxworks的博客 3790

linux 内核探测kprobe 初步了解

kprobe(内核探测,kernel probe)是一个动态地收集调试和性能信息的工具。 如,收集寄存器和全局数据结构等调试信息,无需对Linux内核频繁编译和启动。 用户可以在任何内核代码地址进行陷阱,指定调试断点触发时的处理例程。 工作机制是: 用户指定一个探测点,并把用户定义的处理函数关联到该探测点,当内核执行到该探测点时,相应的关联函数被执行,然后继续执行正常的代码路径。 kprobe允许用户编写内核模块添加调试信息到内核。 用户可以编译一个内核模块,并将内核模块插入到调试的内核中,就可以..

bcbobo21cn的专栏 1196
上一篇: 真正理解红黑树,真正的(Linux内核里大量用到的数据结构
下一篇: 为什么新版内核将进程pid管理从bitmap替换成了radix-tree?
Linux内核站
博客等级 码龄5年 794粉丝 353原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值