trace系列4 - kprobe学习笔记

Linux Trace总结 Utrace 论文地址 1. 暂未合入kernel主线,最终Ptrace底层可由utrace实现,Ptrace可作为utrace的客户端 2. uprobe是utrace的客户端 Ptrace 系统调用,主要用来实现debug工具,也可用来实现trace的功能。 ?如何实现的断点 Ptrace应用场景: gdb:跟踪多线程时,一旦某一个线程命中一个断点,gdb会停止所有的线程; strace... 阅读详情

0.前言

本文主要是根据阅码场 《Linux内核tracers的实现原理与应用》视频课程在aarch64上的实践。通过观察钩子函数的创建过程以及替换过程,理解trace的原理。本文同样以blk_update_request函数为例进行说明kprobe的工作原理,此处的kprobe是基于trace event来实现,同时使用了ftrace的框架。
首先让我们大致了解一下trace point、trace event、kprobe之间的关联:

  1. tracepoint会被放到代码中,需要提供一个probe函数与之关联,当tracepoint被打开时,当tracepoint执行时,提供的probe函数会被调用,probe函数返回时会继续原来函数的执行。使用trace point有3个步骤:
    (1) 在头文件include/trace/events/subsys.h中通过DECLARE_TRACE宏添加tracepoint声明
    (2) 在系统文件subsys/file.c中通过DEFINE_TRACE创建trace point
    (3)通过register_trace_subsys_eventname将tracepoint与probe关联

  2. trace event是建立在tracepoint的基础之上,它可以通过一个宏来实现如上前两步骤,并定义注册和注销函数,通过echo 1 > events/subsys_event来执行注册。从内核4.0及以后都将鼓励使用trace event,不再提倡直接使用trace point。这个可以从 tracepoint的相关sample示例从3.9移除可以得出。使用Tracing event不用像tracingpoint那样需要自己定义probe函数,而且这些probe函数往往要通过模块的方式进行定义,然后加载,而Tracing event提供了TRACE_EVENT宏,可以通过复杂宏帮助定义统一格式的probe函数,而Tracing event需要用户指定trace 信息以何种格式存放到ring buffer中,trace信息将以何种格式打印。

  3. kprobe可以理解为动态的trace event,可以在除了__kprobes/nokprobe_inline annotation 和那些标记为 NOKPROBE_SYMBOL的任何函数设置trace event。使用前需要打开内核选项:CONFIG_KPROBE_EVENTS=y.。

kprobe主要有两种使用方法,一是通过模块加载;二是通过debugfs接口。
(1)模块加载的方式:以内核的kprobe_example为例。首先声明一个kprobe结构体,然后定义其中几个关键成员变量,包括symbol_name,pre_handler,post_handler。然后通过register_kprobe函数注册kprobe即可。将kprobe_example.ko inmod进内核之后,每当系统新启动一个进程,比如执行ls,cat等,都会执行pre_handler和post_handler回调。
(2) 通过debugfs接口的方式:可以通过/sys/kernel/debug/tracing/kprobe_events来增加kprobe跟踪点,然后通过写入/sys/kernel/debug/tracing/events/kprobes//enabled使能。

kernel版本:5.10
平台:arm64

1. kprobe的总体原理

注:如下参考自kprobe原理解析(二)

kprobe的工作过程大致如下:

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

2. kprobe领域模型

在这里插入图片描述

当kprobe创建后,形成如上的领域模型

  • trace_kprobe:既包含了kretprobe,又包含了trace_probe

  • kretprobe:用于描述kretprobe

  • trace_probe:用于描述kprobe, 它将与kretprobe公用kprobe结构,其中的args数组就保存了echo到节点的参数

  • kprobe: 用于描述kprobe的核心结构体,会连入全局哈希表kprobe_table

  • trace_probe_event:描述kprobe的trace event,包含了核心结构体trace_event_class和trace_event_call

  • trace_event_class:用于描述trace event的类

  • trace_event_call:是trace_event的封装,会连入全局ftrace_events链表

  • trace_event:主要关联了trace_event_functions结构体, trace_event_functions定义了trace_event的回调,trace_event会连入全局的ftrace_event_list

  • trace_event_functions:定义了trace_event的回调

  • trace_array:用于描述trace的最顶层的结构体,目前ftrace_trace_arrays只有一个全局的trace_array即global_trace,可以看出每个trace_event_call对应一个trace_array,trace_array->event_dir指向/sys/kernel/debug/tracing/events目录

  • trace_event_file: 管理kprobe trace event下所有的文件,通过event_call指向trace_event_call,通过system指向trace_subsystem_dir,通过tr指向trace_array,可见trace_event_file, trace_event_call,trace_array是一一对应的,trace_event_file通过list连入trace_array的events链表

  • trace_subsystem_dir: 管理kprobe trace event的目录,通过entry指向管理的目录节点(/sys/kernel/debug/tracing/events/kprobe),通过tr指向trace_array,通过list连入trace_array的systems链表。从上述图示可以看出,trace_subsystem_dir:本例中就表示events/kprobes目录

trace_event_file,trace_array, trace_event_call,trace_subsystem_dir一一对应

3. kprobe创建

kprobe的工作过程与前述function trace和function graph trace有所区别,但是kprobe仍然复用了ftrace的框架,在执行如下操作后,将执行probes_write

ubuntu@VM-0-9-ubuntu:~$ echo 'p:blk_update blk_update_request request=$arg1 status=$arg2:u8 bytes=$arg3:u32' > /sys/kernel/debug/tracing/kprobe_events

如上p表示kprobe,对于retkprobe则需要改成r;
blk_update为本次trace的名称,可以自己设置;
arg1表示需要跟踪的第一个参数,与其它几个参数一起保存到trace_probe.args数组中

static ssize_t probes_write(struct file *file, const char __user *buffer,size_t count, loff_t *ppos)
    \--trace_parse_run_command(file, buffer, count, ppos,create_or_delete_trace_kprobe);
           |--char *kbuf, *buf, *tmp
           |  size_t done = 0
           |  //分配空间用于存放event命令,本例中为:
           |  //'p:blk_update blk_update_request request=$arg1 status=$arg2:u8 bytes=$arg3:u32'
           |--kbuf = kmalloc(WRITE_BUFSIZE, GFP_KERNEL)
           |  //将event命令从用户空间拷贝到内核空间
           |--copy_from_user(kbuf, buffer + done, size))
           |--buf = kbuf;
           |  //对event命令进行解析
           \--trace_run_command(buf, create_or_delete_trace_kprobe)
                 |  //对event参数以空格为分割符,存放到argv数组,此时每个argv一维数组就保存了一个命令参数
                 |  //argv[0]:"p:blk_update",    argv[1]:"blk_update_request", argv[2]:"request=$arg1",
                 |  //argv[3]: "status=$arg2:u8",argv[4]:"bytes=$arg3:u32"
                 |--argv = argv_split(GFP_KERNEL, buf, &argc)
                 \--create_or_delete_trace_kprobe(argc, argv)

trace_parse_run_command主要用于解析kprobe命令,

(gdb) p *argv@20
$6 = {
   
   0xffff000007467700 "p:blk_update", 0xffff00000746770d "blk_update_request", 
      0xffff000007467720 "request=$arg1", 0xffff00000746772e "status=$arg2:u8",
      0xffff00000746773e "bytes=$arg3:u32", 0x0, 0
.....

3.1 create_or_delete_trace_kprobe

create_or_delete_trace_kprobe(argc, argv)
   |--trace_kprobe_create(argc, (const char **)argv)
          |--struct trace_kprobe *tk = NULL;
          |  const char *event = NULL
          |  //初始化全局trace_probe_log结构体,包含子系统名、trace参数格式、参数个数
          |--trace_probe_log_init("trace_kprobe", argc, argv)
          |  //获取到event名,本例为blk_update
          |--event = strchr(&argv[0][1], ':');
          |  //将字符串blk_update_request转换为unsigned long
          |--if (kstrtoul(argv[1], 0, (unsigned long *)&addr))
          |      trace_probe_log_set_index(1)
          |      //本例symbol为blk_update_request
          |      symbol = kstrdup(argv[1], GFP_KERNEL)
          |      //查询symbol入口地址偏移offset是否在符号表存在,此处是offset为0,检查blk_update_request符号是否存在
          |      if (kprobe_on_func_entry(NULL, symbol, offset))
          |          flags |= TPARG_FL_FENTRY
          |--trace_probe_log_set_index(0);
          |  //解析event名称
          |--traceprobe_parse_event_name(&event, &group, buf,event - argv[0])
          |  //分配trace_kprobe结构体
          |--tk = alloc_trace_kprobe(group, event, addr, symbol, offset, maxactive,	
          |             argc - 2, is_return);
          |      |--if (is_return)
          |             tk->rp.handler = kretprobe_dispatcher;
          |         else
          |             tk->rp.kp.pre_handler = kprobe_dispatcher
          |  //解析event命令行参数,结果保存在tk->tp->args数组中,其中:
          |  //args[i].name为“=”左侧部分,args[i].comm为“=”右侧部分
          |--for (i = 0; i < argc && i < MAX_TRACE_ARGS; i++)
          |      traceprobe_parse_probe_arg(&tk->tp, i, tmp, flags)
          |  // 设置trace event打印格式,执行完tk->tp->event->call->print_fmt格式为:
          |  //"(%lx) request=0x%Lx status=%u bytes=%u\", REC->__probe_ip, REC->request, REC->status, REC->bytes
          |--traceprobe_set_print_fmt(&tk->tp, is_return);
          |  //注册kprobe event
          \--register_trace_kprobe(tk)       

create_or_delete_trace_kprobe中通过gdb可以查看到argv就是上述echo的部分,注册kprobe_event,注册trace_event_call,注册kprobe,关于这三者的关系,可参考 1.kprobe领域模型 部分。其中最主要的pre_handler为kprobe_dispatcher,同时设置了打印格式,并完成trace_kprobe的注册

  • alloc_trace_kprobe:为trace_kprobe分配空间,主要初始化了kprobe的pre_handler为kprobe_dispatcher,和post_handler

  • traceprobe_set_print_fmt:设置kprobe的打印格式

  • register_trace_kprobe:初始化trace_kprobe.,trace_probe., trace_probe_event, trace_event_call,并注册了trace_event, trace_event_call, kprobe,

3.1.1 register_trace_kprobe

register_trace_kprobe(tk)
     |  //注册kprobe event
     |--register_kprobe_event(tk)
     |      |  //初始化trace_event_call
     |      |--init_trace_event_call(tk)
     |      |      |--if (trace_kprobe_is_return(tk))
     |      |      |      call->event.funcs = &kretprobe_funcs	
     |      |      |      call->class->fields_array = kretprobe_fields_array;
     |      |      |  else
     |      |      |      call->event.funcs = &kprobe_funcs;
     |      |      |      call->class->fields_array = kprobe_fields_array
     |      |      |--call->flags = TRACE_EVENT_FL_KPROBE
     |      |      |  //此函数将作为kprobe trace event使能/禁用的回调
     |      |      \--call->class->reg = kprobe_register
     |      \--trace_probe_register_event_call(&tk->tp)
     |            |--struct trace_event_call *call = trace_probe_event_call(tp)
     |            |  //注册trace_event
     |            |--register_trace_event(&call->event)
     |            |     |--INIT_LIST_HEAD(&event->list)
     |            |     |--event->funcs->raw = trace_nop_print;
     |            |     |  event->funcs->hex = trace_nop_print
     |            |     |  event->funcs->binary = trace_nop_print
     |            |     \--hlist_add_head(&event->node, &event_hash[key]);
     |            |  //注册trace_event_call
     |            \--trace_add_event_call(call)
     |  //注册kprobe
     |-- __register_trace_kprobe(tk)
     |        |--if (trace_kprobe_is_return(tk))
     |               register_kretprobe(&tk->rp)
     |           else
     |               register_kprobe(&tk->rp.kp)
     |                   |  //通过kprobe的名字(blk_update_request)查符号表,得到符号地址,p为kprobe
     |                   |--addr = kprobe_addr(p)
     |                   |  //此addr为blk_update_request函数
     |                   |--p->addr = addr;
     |                   |  p->flags &= KPROBE_FLAG_DISABLED;
     |                   |  //因为在使能kprobe event时会在探测函数入口用brk替换原有指令,因此要保存原有指令
     |                   |  //首次不会注册kprobe,因此old_p 为空
     |                   |--old_p = get_kprobe(p->addr)
     |                   |--prepare_kprobe(p)
     |                   |--INIT_HLIST_NODE(&p->hlist)
     |                   |--hlist_add_head_rcu(&p->hlist, &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]);
     \--dyn_event_add(&tk->devent)
           \--list_add_tail(&ev->list, &dyn_event_list)       

register_trace_kprobe:初始化trace_kprobe.,trace_probe., trace_probe_event, trace_event_call,并注册了trace_event, trace_event_call, kprobe

  • init_trace_event_call:初始化trace_kprobe.,trace_probe. trace_probe_event. trace_event_call

  • trace_probe_register_event_call: 分别调用了register_trace_event和trace_add_event_call。
    (1)register_trace_event将trace_event注册到全局event_hash哈希链表;
    (2)trace_add_event_call将trace_event_call注册到全局ftrace_events链表,trace_add_event_call创建trace_event_file,在sys/kernel/debug/tracing/events/kprobes目录下创建blk_update目录,同时在其下创建其它的文件节点

  • __register_trace_kprobe:注册kprobe,这里区分了retkprobe和kprobe,其中register_kprobe会将kprobe注册到全局kprobe_table哈希表中
    (1)register_kprobe:完成了kprobe向全局kprobe_table添加哈希节点,完成kprobe的注册,它以插入的位置addr作为哈希值.
    (1-1)prepare_kprobe:主要是为探测点触发以后如何返回到blk_update_request原始的指令做准备,p->opcode保存了blk_update_request原始的入口指令;p->ainsn.api.insn保存了blk_update_request原始的入口指令slot;p->ainsn.api.restore保存了原始入口指令的下一条指令的地址,这样当断点指令返回后就可以从这条指令执行,这样就可以沿着原始执行路径执行。

3.1.1.1 trace_add_event_call
trace_add_event_call(call)
	|--__register_event(call, NULL)
	|     |--event_init(call)
	|      \--list_add(&call->list, &ftrace_events)
	\--__add_event_to_tracers(call)
	       |  //本例中ftrace_trace_arrays链表只有一个全局的global_trace, 此处tr为global_trace
	       |--list_for_each_entry(tr, &ftrace_trace_arrays, list)
	              __trace_add_new_event(call, tr);
	                  |--struct trace_event_file *file
	                  |  //创建trace_event_file,初始化并连入tr->events链表
	                  |--file = trace_create_new_event(call, tr)
	                  |      |--file->event_call = call;
	                  |      |  file->tr = tr
	                  |      |  atomic_set(&file->sm_ref, 0)
	                  |      |  atomic_set(&file->tm_ref, 0)
	                  |      |  INIT_LIST_HEAD(&file->triggers)
	                  |      |--list_add(&file->list, &tr->events)
	                  \--event_create_dir(tr->event_dir, file)

trace_add_event_call创建trace_event_file,在sys/kernel/debug/tracing/events/kprobes目录下创建blk_update目录,同时在其下创建其它的文件节点

  • trace_create_new_event:创建trace_event_file,初始化并连入tr->events链表

  • event_create_dir:主要是在"sys/kernel/debug/tracing/events"目录下创建"kprobes"目录,并在sys/kernel/debug/tracing/events/kprobes目录下创建blk_update目录,同时在其下创建其它的文件节点

event_create_dir(tr->event_dir, file)
   |--
tracepoint、kprobe、ebpf、debugfs、ftrace、tracefs、systemtap之间的关系 详细理解以下tracepoint、ftrace、debugfs、tracefs、kprobe、systemtap、ebpf这些关于系统追踪的工具之间的关系和依赖。 阅读详情

相关推荐

DexProtector内存校验绕过与DEX提取实战

DEX文件是Android应用的核心执行单元,其动态加载与运行时保护机制直接关系到App安全强度。DexProtector作为主流商业加固方案,摒弃传统文件级防护,转而依托ART虚拟机下的匿名内存映射段(anonymous mmap)构建多维校验体系——涵盖字节码完整性校验、内存页属性检测及运行时环境感知。其技术价值在于将防护锚点从静态APK迁移至动态内存态,大幅提升逆向门槛。典型应用场景包括金融类App的防篡改加固、游戏客户端的反外挂保护,以及政企应用的合规性安全增强。本文聚焦DexProtector v

weixin_30315435的博客 702

STM32F407+LWIP实战:手把手教你移植mbedtls库实现Modbus TCP双向认证

本文详细介绍了如何在STM32F407微控制器上移植mbedtls库,结合LWIP协议栈实现Modbus TCP的双向认证功能。通过TLS 1.2加密通道,有效提升工业物联网通信的数据安全性,涵盖证书管理、内存优化及性能调优等关键步骤,为嵌入式系统开发者提供了一套完整的解决方案。

weixin_28721917的博客 510

STM32G474+Simulink+HIL硬件在环仿真

硬件:STM32G474RET6(官方的NUCLEO板)软件:STM32Cube_FW_G4 V1.6.1芯片包;KeilMATLAB 2023B(本文已成功)STM32嵌入式支持包。

qq_44701116的博客 1768

【OpenBMC 系列】4.启动流程 使用qume模拟ast2600-evb

1.安装openbmc适配的qume 2.启动bmc 3.使用bmc web

AiFool的博客 4452

HC-SR04测距不准?STM32F103定时器捕获模式详解与精度提升实战

本文详细解析了HC-SR04超声波测距模块在STM32F103上的精度问题,并提供了定时器输入捕获模式的实战配置方法。通过硬件级时间测量和温度补偿算法,将测距精度提升至±0.1cm,适用于工业级应用场景。文章包含完整的代码实现和测试对比数据,帮助开发者解决超声波测距不准的常见问题。

weixin_30313409的博客 347

UG NX二次开发(C#)-外部模式-批量删除某些图层中的体对象和曲线对象

在 UG NX中,有时需要批量删除某文件夹下多个.prt文件中固定一些图层中的体对象和曲线对象,这时如果采用手动逐个打开模型,并选择针对的图层删除,即耗时也容易遗漏,因此采用外部模式来处理比较方便。本文介绍批量删除某些图层中的体对象和曲线对象。

GimiGimmy 3033

基于YOLOv11的工业零件表面缺陷检测系统:从原理到实现

工业质量控制是制造业的核心环节,表面缺陷检测作为其中重要组成部分,直接关系到产品质量和企业效益。本文详细介绍了一种基于YOLOv11深度学习算法的工业零件表面缺陷检测系统,包含完整的算法原理分析、数据集构建方法、模型训练策略以及用户界面设计。通过大量实验验证,该系统能够高效准确地检测零件表面的划痕、凹陷、锈蚀等多种缺陷,检测精度达到96.2%,满足工业实时检测需求。本文还提供了完整的代码实现和详细的技术文档,为相关领域的研究者和工程师提供参考。关键词:表面缺陷检测;YOLOv11;深度学习;工业视觉;

YOLO 562

【100%通过率】2023华为od机试C卷【电脑病毒感染】C++ 实现

一个局域网内有很多台电脑,分别标注为 0 ~ N-1 的数字。相连接的电脑距离不一样,所以感染时间不一样,感染时间用t 表示。其中网络内一台电脑被病毒感染,求其感染网络内所有的电脑最少需要多长时间。如果最后有电脑不会感染,则返回-1.

MISAYAONE的博客 1717

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

上一篇: kernel相关参数文档说明
下一篇: trace系列5 - kretprobe学习笔记
HZero.chen
博客等级 码龄9年 579粉丝 176原创
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值