深入ftrace function graph原理

学习完了ftrace的function的基本功能,其作用主要是用来跟踪特定内核函数调用的频次,对于内核,特别是初学者,对于函数的调用关系不清晰,并且内核中有很多函数指针,会把我们弄的摸不着头脑,那么我们就需要分析内核函数的调用的子过程,即本函数调用了哪些子函数,处理的流程如何,那么我们就需要用到function_graph,本章主要是学习如下

  • function_graph的使用方法
  • function_graph的原理解析,它是如何实现,整个过程的重新梳理

本文采用的linux内核版本为Linux 5.15

1 ARM64插桩实现

首先我们来看看最新的ARM64代码是否是基于pg方式来做的,首先我们来看看CC_FLAGS_FTRACE这个宏

在这里插入图片描述

对于这个宏在arch/arm64/Makefile中被重新定义为-fpatchable-function-entry=2,我们首先要去看看对于Makefile中:=的含义,这个是覆盖之前的值,那么CC_FLAGS_FTRACE就不是等于-pg了

在这里插入图片描述

大致的意思是如果编译时指定 -fpatchable-function-entry=N[,M],会在函数入口后,函数第一个指令之前插入 N 个 nop,但是要留 M 个放在函数入口之前,同时通过一个特殊的 __patchable_function_entries 段来记录所有的函数入口。
在这里插入图片描述

链接后的 vmlinux 可以通过 __{start,stop}_mcount_loc 符号获取到所有函数入口,同时每个函数入口都会有 nop,用来做指令修改。

我们查看下各个文件的编译选项,发现已经没有使用-pg,而是使用的-fpatchable-function-entry=2
在这里插入图片描述

观察编译后的 blk-core.o 中的函数入口是否是2 个 nop,以及所有的函数入口记录在哪里?

在这里插入图片描述

其基本的原理跟_mcount类似,编译时,在所有内核函数入口前插入 2个 nop 指令,并创建 __patchable_function_entries 段用来记录所有的函数入口,链接时,把所有的函数入口归档到 __{start,stop}_mcount_loc,启动时,由 ftrace_init 把所有函数入口维护在 start_pgftrace_pages_start)链表中。就可以针对某个特定的函数,修改其 nop 指令,让其被调用时,跳转到自定义的指令中,比如跳转到 _mcount 处,继而执行 ftrace_trace_function 函数指针,来执行特定功能的 tracer(例如:function,function_graph)

2 function_grap的使用方法

对于function_grap也是满足三步法,设置tracer类型,设置tracer参数,使能tracer

oot@rlk:/sys/kernel/tracing# echo function_graph > current_tracer 
root@rlk:/sys/kernel/tracing# echo dev_attr_show > set_graph_function
root@rlk:/sys/kernel/tracing# echo 1 > tracing_on
root@rlk:/sys/kernel/tracing# echo 0 > tracing_on

在这里插入图片描述

通过简单的例子,看到了 function/function_graph tracer 的基本用法,但是在实际应用中会有比较棘手的问题和需求。比如:

  • 某个函数被谁调用?调用栈是什么?
  • 如何跟踪某个进程?如何跟踪一个命令,但是这个命令执行时间很短?
  • 用户态的行为轨迹是如何与内核中的trace联系到一起
  • 如何跟踪过滤多个进程?多个函数?
  • 如何灵活控制tracer的开关

这个就要用到ftrace的过滤控制相关文件,详细的使用方式,可以参考这个文件Ftrace 进阶用法

文件名功能
set_ftrace_filterfunction tracer 只跟踪某个函数
set_ftrace_notracefunction tracer 不跟踪某个函数
set_graph_functionfunction_graph tracer 只跟踪某个函数
set_graph_notracefunction_graph tracer 不跟踪某个函数
set_event_pidtrace event 只跟踪某个进程
set_ftrace_pidfunction/function_graph tracer 只跟踪某个进程
options这是一个目录,其中包含每个可用跟踪选项的文件(也在 trace_options 中)。 也可以通过将“1”或“0”分别写入具有选项名称的相应文件来设置或清除选项。
function_profile_enabled设置后,它将使用函数跟踪器或函数图跟踪器(如果已配置)启用所有功能。 它将保留被调用函数数量的直方图,如果配置了函数图跟踪器,它还将跟踪在这些函数中花费的时间。
funcgraph-overrun设置后,在跟踪每个函数后显示图形堆栈的“overrun”。 溢出是当调用的堆栈深度大于为每个任务保留的深度时。 每个任务都有一个固定的函数数组,可以在调用图中进行跟踪。 如
果调用的深度超过该深度,则不会跟踪该函数。 溢出是由于超出此数组而错过的函数数。
funcgraph-cpu设置后,将显示发生跟踪的 CPU 的 CPU 编号。
funcgraph-overhead设置后,如果函数花费的时间超过一定量,则显示延迟标记。 请参阅上面标题描述下的“delay”
funcgraph-proc与其他跟踪器不同,默认情况下不显示进程的命令行,而是仅在上下文切换期间跟踪进出任务时才显示。 启用此选项会在每一行显示每个进程的命令。
funcgraph-duration在每个函数(返回)结束时,函数中的持续时间以微秒为单位显示。
funcgraph-irqs禁用时,将不会跟踪中断内发生的函数。
funcgraph-tail设置后,返回事件将包括它所代表的函数。 默认情况下这是关闭的,并且只显示一个结束的大括号“}”来返回函数
graph-time使用函数图跟踪器运行函数探查器时,包括调用嵌套函数的时间。 如果未设置,则为函数报告的时间将仅包括函数本身执行的时间,而不包括它调用的函数的时间。

3 function实现原理

首先tracefs 初始化 set_ftrace_filter 文件,记录 global_opsinode->i_private,注册 ftrace_filter_fopsinode->i_fop,相关代码如下:

在这里插入图片描述

在这里插入图片描述

对于global_ops是一个全局结构体,主要用于记录跟踪函数ftrace_stub和记录目标函数的hash表func_hash

在这里插入图片描述

3.1 替换函数入口

我们会通过写入set_ftrace_filter节点,而ftrace_filter_fops 中定义打开文件,读写文件,关闭文件的函数指针,每个函数的作用和调用的关键函数介绍如下:

在这里插入图片描述

  • ftrace_filter_open:在文件打开时执行,初始化 iter 结构体实例,并存放在 file->private_data 中,关键函数:ftrace_regex_open

  • ftrace_filter_write:在对文件写入数据时执行,把目标函数更新到 iter->hash 中,关键函数:filter_parse_regex, enter_record

  • ftrace_regex_release:在文件关闭时执行,遍历函数入口表,根据目标函数在新 hash 与旧 hash 中的存在状态,更新对应的 rec->flags,再次遍历函数入口表,根据 rec->flags 按需替换目标函数入口

    关键函数:ftrace_hash_move, ftrace_run_update_code

    ftrace_filter_open

此函数在打开文件时执行,分配 iter 结构体实例,为其初始化相关成员,需要特别关注的成员是:

  • parser 用于存放用户输入,当前操作流程中为 “vfs_read” 字符串
  • hash 用于记录目标函数(vfs-read)对应的 struct dyn_ftrace * rec,当前操作流程中,hash 是 ops->func_hash->filter_hash 的一份拷贝

最后,写操作模式下,iter 记录在 file->private_data 中。 ftrace_filter_write函数对文件写时执行,解析用户态字符串ubuf,存放到parser->buffer,遍历函数入口表 ftrace_pages_start,找到其匹配项 rec,并通过 enter_record() 函数将 rec 记录到 iter->hash,关键代码如下:

在这里插入图片描述

最值得关注的是,do_for_each_ftrace_rec宏表示遍历函数入口表,其实是在遍历 ftrace_pages_start,所有的函数入口记录在 &pg->records 指针数组中,以 pg->index 为检索范围来找对应的 rec

在这里插入图片描述

rec 是一个 struct dyn_ftrace * 类型,此结构体中 ip 即函数入口,flags 用来控制对当前函数的跟踪,比如:是否开启跟踪,函数跟踪时是否保留寄存器,当前的引用计数等。在后续代码中,ftrace 是否要执行指令修改,就是通过 rec->flags 来判断。

在这里插入图片描述

ftrace_regex_release

此函数在文件关闭时执行,把当前的iter->hash移动到旧的hash(ops->func_hash->filter_hash),在移动过程中,对比两个hash中的目标函数,按需更新每个函数的rec->flags的计数以及相关功能flags,后以FTRACE_UPDATE_CALLS 命令执行 ftrace_run_update_code(),此函数会检测每个函数 rec->flags 的状态,执行 FTRACE_UPDATE_MAKE_CALL 操作,将当前函数入口替换为 ftrace_caller。代码执行流程如下:

在这里插入图片描述

ftrace_hash_move_and_update_ops() 函数执行 ftrace_hash_move(),如果前者正常返回表示有函数入口需要替换,则执行 ftrace_ops_update_code()

在这里插入图片描述

ftrace_hash_move() 通过新旧两个 hash 对比来更新函数入口表中的 rec->flags,并最终把 new_hash 更新到 old_hash。

在这里插入图片描述

ftrace_ops_update_code() 函数,通过指定 FTRACE_UPDATE_CALLS 调用 ftrace_run_update_code() 函数,用来执行函数入口的指令替换:

在这里插入图片描述

在这里插入图片描述

此函数在接受命令后经过一连串的调用最终执行到 ftrace_modify_all_code(),这里执行 FTRACE_UPDATE_CALLS 命令,遍历函数入口表,执行 __ftrace_replace_code()

在这里插入图片描述

arch_ftrace_update_code这个函数首先保证安全,如用stop_machine机制,然后直接调用了ftrace_modify_all_code(int command);

在这里插入图片描述

command命令主要有三种:

  • FTRACE_UPDATE_CALLS 表示开启函数的调用链,至于那些函数需要开启,只有通过全局变量来传递;

  • FTRACE_DISABLE_CALLS 表示关闭函数的调用链,至于那些函数需要关闭,只有通过全局变量来传递;

  • FTRACE_UPDATE_TRACE_FUNC 表示修改这个调用链;

对于function最终会调用ftrace_replace_code,会选择对应的跳转目标,这里是 ftrace_caller,通过 ftrace_update_record() 判断目标函数的修改方式,这里是 FTRACE_UPDATE_MAKE_CALL,表示当前函数入口要从 nop 替换为对 ftrace_caller 的调用,继而执行 ftrace_make_call()。具体代码如下:

在这里插入图片描述

这个就是遍历函数入口表,其实是在遍历 ftrace_pages_start,这个之前已经讲过,所有的函数都在这个表中记录

在这里插入图片描述

检查函数是否被跟踪,即该地址处的原内容是否为nop,如果是从nop替换为bl tracer,则返回FTRACE_UPDATE_MAKE_CALL;如果原本该地址已经被其他tracer跟踪,则返回FTRACE_UPDATE_MODIFY_CALL

在这里插入图片描述

最终函数调用到了ftrace_caller函数,对于ARM64这个函数定义在arch/arm64/kernel/entry-ftrace.S文件里面。

在这里插入图片描述

所以本小姐设置 function tracer 的前提下,echo vfs_read >> set_ftrace_filter 命令在内核的执行过程,我们以 set_ftrace_filter 文件的相关操作函数为切入点开始分析,分别分析 {open,write,release} 三个接口的实现:

  • ftrace_filter_open(),在文件打开时执行,初始化 iter 结构体实例,并存放在 file->private_data
  • ftrace_filter_write(),在对文件写入数据时执行,把目标函数更新到 iter->hash,其中对 do_for_each_ftrace_rec 宏进行展开分析
  • ftrace_regex_release(),在文件关闭时执行,遍历函数入口表,根据目标函数是否在新旧 hash 中,更新对应的 rec->flags,再次遍历函数入口表,根据 rec->flags 按需替换目标函数入口

3.2 替换跟踪函数

在 ftrace 执行完内核函数入口的指令替换后,函数执行时会跳转到 ftrace_caller 调用跟踪函数,而此函数就是 function tracer 所注册的跟踪函数 function_trace_call()。并且在观察 ftrace_caller 执行的过程中,我们留了一个问题:ftrace_call 是如何被替换为对 function_trace_call() 的调用。既然 function_trace_call()function tracer 的跟踪函数,那么我们就从 echo function > current_tracer 命令是如何工作的角度进行分析,与替换类似,同样以 current_tracer 文件的相关操作函数为切入点,分析过程中重点关注跟踪函数如何变化以及最终如何实现对 ftrace_call 的替换。

在这里插入图片描述

对于init_tracer_tracefs(&global_trace, NULL)会tracefs 初始化 current_tracer 文件记录 global_traceinode->i_private,注册 set_tracer_fopsinode->i_fopglobal_trace 是一个 struct trace_array 结构体实例,用来表示 /sys/kernel/debug/tracing/ 顶级目录下的相关 tracing 配置

在这里插入图片描述

在这里插入图片描述

对于替换的路径基本跟上面的类似,set_ftrace_fops 中分别定义了对 current_tracer 文件打开、读取、写入的相关操作:

  1. tracing_open_generic() 在文件打开时执行,设置 global_tracefilp->private_data
  2. tracing_set_trace_read 在文件读取时执行,简单地把当前 tracer 的名字 tr->current_trace->name 拷贝到用户态
  3. tracing_set_trace_write() 在文件写入时执行,匹配到对应的 tracer 并执行对应的初始化工作,并最终执行对 ftrace_call 的指令替换

我们重点关注写的接口tracing_set_trace_write

在这里插入图片描述

此函数从用户态拷贝输入的字符串,调用 tracing_set_tracer() 函数在全局 tracer 表 trace_types 中匹配对应的 tracer,然后执行 tracer 的初始化,并将当前 tracer 记录到 tr->current_trace

在这里插入图片描述

trace_init() 函数会调用 t->init()t->init() 对应到的是 function tracer 通过 register_tracer() 函数注册的 struct tracer function_trace.init 定义,即 function_trace_init()function_trace 代码如下kernel/trace/trace_functions.c

在这里插入图片描述

function_trace_init() 函数执行 select_ftrace_function() 选择 function tracer 的跟踪函数,默认通过 TRACE_FUNC_NO_OPTS 选择到 function_trace_call() 函数,并将其设置到 ops->func(global_ops->func),之后执行 register_ftrace_function() 注册跟踪函数。

在这里插入图片描述

register_ftrace_function() 函数执行如下内容,详细的可以自己看代码

  1. __register_ftrace_function() 函数,添加当前 ops (global_ops) 到全局 ops 链表 ftrace_ops_list,并设置全局跟踪函数 ftrace_trace_funcion()()ops->func
  2. ftrace_hash_ipmodify_enbale() 函数,根据 ops->func_hash->filter_hash 更新函数入口表中每个函数记录 rec 的 ipmodify 标志位
  3. ftrace_hash_rec_enable() 函数,判断是否有函数入口需要更新,如果需要更新则为 command 设置 FTRACE_UPDATE_CALLS 标志
  4. ftrace_startup_enable() 函数,判断保存的跟踪函数 saved_ftrace_func 与当前跟踪函数 ftrace_trace_function() 是否相同,如果不同,则表示需要更新跟踪函数,为 command 设置 FTRACE_UPDATE_TRACE_FUNC,之后执行 ftrace_run_update_code(command)

对于该函数,首先设置此时为function_trace_function,其次判断函数入口是否需要更新,最终会以最后会以 FTRACE_UPDATE_CALLS | FTRACE_UPDATE_TRACE_FUNC 为命令执行 ftrace_run_update_code() 来进行函数入口和跟踪函数的替换,对于这个函数在替换的时候已经介绍过,暂时不重复说明。

所以本小节从function tracer 使能的工作过程,观察 ftrace_caller 中的 ftrace_call 是如何被替换成 function tracer 的跟踪函数,以及 ftrace_caller 如何为跟踪函数设置上下文

  • 运行时角度
    • 实现 ftrace_caller 为跟踪函数设置上下文,并调用跟踪函数
  • 指令替换的角度
    • 能够对指定的内核函数入口进行指令替换,使其跳转到 ftrace_caller
    • 能够对跟踪函数进行更新,使指定的跟踪函数能够被调用

4 function_graph实现原理

函数图跟踪机制,最初只是作为 function_graph tracer 的一部分出现在内核中,用来跟踪某个内核函数运行时的子函数调用及其时间消耗,并输出一个函数调用图,后来作为 ftrace 跟踪内核函数入口和返回的核心机制,独立到 kernel/trace/fgraph.c 文件中。对于这个功能,也可以从我们上面通过设置set_graph_function和current_tracer的角度去学习代码,其基本的流程与function基本类似,我们也不去看了。我们重点关注与替换跟踪函数,跟踪函数(入口/返回跟踪函数)的标准注册接口 register_ftrace_graph() 将其赋值为当前注册者指定的图跟踪函数。

在这里插入图片描述

在这里插入图片描述

function tracer 一致,function_graph 使能时会调用此 tracer 注册时指定的 .init 函数 - graph_trace_init,直接调用 register_ftrace_graph() 设置图跟踪函数。关键代码如下:

在这里插入图片描述

register_ftrace_graph()struct fgraph_ops *为入参,只注册graph_ops

register_ftrace_graph() 函数执行如下步骤:

  1. 设置全局返回跟踪函数 ftrace_graph_return()gops->retfunc
  2. 临时设置 __ftrace_graph_entry()gops->entryfunc,并把全局入口跟踪函数 ftrace_graph_entry() 设置为 ftrace_graph_entry_test(),之后执行 update_function_graph_func() 重新设置全局入口跟踪函数
  3. 执行 ftrace_startup(),注册全局图操作结构 graph_opsftrace_ops_list,并执行 FTRACE_START_FUNC_RET 指令替换命令

在这里插入图片描述

ftrace_graph_entry_test() 跟踪函数会先判断当前函数是否在 global_ops->func_hash 中,再执行 __ftrace_graph_entry()

在这里插入图片描述

update_function_graph_func() 函数中,遍历 ftrace_ops_list,如果存在不为 global_opsgraph_ops 的 ops,则继续采用 ftrace_graph_entry_test() 进行有判断的函数跟踪,避免不属于 global_ops 或者 graph_ops 需要跟踪的内核函数调用到 tracer 指定的入口跟踪函数。关键代码如下:

在这里插入图片描述

ftrace_startup() 函数也会被 register_ftrace_function() 函数调用,而在注册图跟踪函数的过程中,涉及到以下变化:

  1. __register_ftrace_function() 函数将 graph_ops 添加到全局链表中,并将全局跟踪函数设置为 graph_ops->func - ftrace_stub,全局入口跟踪函数设置为 gops->entryfunc

在这里插入图片描述

  1. ftrace_modify_all_code() 函数处理 FTRACE_START_FUNC_RET 命令,执行 ftrace_enable_ftrace_graph_caller(),对 ftrace_graph_call 进行指令替换,其代码为arch/arm64/kernel/ftrace.c

在这里插入图片描述

对于ARM64其实现arch/arm64/kernel/entry-ftrace.S

在这里插入图片描述

用户通过 register_ftrace_graph() 函数更新全局入口与返回跟踪函数,并以 prepare_ftrace_return() 函数替换 ftrace_graph_call。在内核执行时,内核函数入口会跳转到 ftrace_caller,继而执行 prepare_ftrace_return(),调用全局入口跟踪函数,并以 return_to_handler 覆盖函数返回地址,使得 return_to_handler 在函数返回时执行,调用全局返回跟踪函数,之后跳转到原函数的返回地址。

在这里插入图片描述

首先,我们来看看return_to_hander,其实现为arch/arm64/kernel/entry-ftrace.S

在这里插入图片描述

所以通过在函数的调用开始及调用结束分别调用了 prepare_ftrace_return 及 ftrace_return_to_handler 来进行 LR 的修改与恢复。这样可以统计到每一个函数的调用关系与具体执行时间(在开始与结束时分别记录了时间)。该功能可以帮助读者在性能调试的时候识别到性能瓶颈,以便于后期的进一步性能优化调优。

5 总结

我们了解了 ftrace 的两个核心机制 – 动态函数跟踪、动态函数图跟踪,二者分别向用户提供 register_ftrace_function()register_ftrace_graph() 接口来注册对函数进行跟踪以及对函数入口和返回进行跟踪的跟踪函数。最后我们以一张 ftrace 的架构图来结束本文。

在这里插入图片描述

6 参考文档

https://gitee.com/tinylab/riscv-linux/blob/master/articles/20220928-ftrace-impl-5-fgraph.md

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
ftrace和ptrace是两个不同的工具,它们在功能和用途上有所区别。 引用中提到的ftraceLinux 内核的一个内建跟踪工具,用于跟踪和分析内核函数调用、上下文切换、延迟和性能问题等。它可以通过配置内核和 debugfs 来使用,并包含多个跟踪器,可以方便地跟踪不同类型的信息。 而引用中提到的ptrace是一个系统调用,用于在用户空间中跟踪和控制进程的执行。通过ptrace,用户可以监视和修改目标进程的内存、寄存器和执行状态,实现调试和跟踪进程的功能。 因此,ftrace主要用于内核级别的跟踪和性能分析,而ptrace主要用于用户空间进程的调试和跟踪。它们各自具有不同的功能和应用场景,但都能提供有助于问题排查和性能优化的信息。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Linux内核调试方法总结之strace ,ltrace, ptrace, ftrace, sysrq](https://blog.csdn.net/zmjames2000/article/details/88410484)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [Linux内核学习(十):内核追踪必备技能--ftrace](https://blog.csdn.net/weixin_45264425/article/details/125955998)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值