转载-奇小葩-深入ftrace function原理
原文链接:https://blog.csdn.net/u012489236/article/details/127814059
前面我们学习了ftrace的一些基本概念和用法,本章开始我们深入学习ftrace提供了哪些机制,根据之前的学习,我们知道了ftrace可用来快速排查以下问题
- 特定的内核函数调用的频次(function)
- 内核函数在被调用的过程中的调用栈(function + stack)
- 内核函数调用的子函数流程(子调用栈)(function graph)
- 使用trace event信息可以跟踪内核的流程
对于ftrace跟踪工具由性能分析器(profiler)和跟踪器(tracer)两部分组成:
-
性能分析器: 用来提供统计和直方图数据(需要 CONFIG_FUNCTION_PROFILER=y),可以提供函数性能分析或直方图
-
**跟踪器:**提供跟踪事件的详情
- 函数跟踪(function):追踪所有的内核函数
- 函数调用关系(function_grap):与function比较类似,但它除了探测函数的入口还探测函数的出口,它可以画出一个图形化的函数调用,类似于c源代码风格。
- 跟踪点(tracepoint):是内核提供的静态跟踪点,为稳定的跟踪点,需要研发人员代码编写,数量有限
- kprobe/kretprobe,为内核提供动态跟踪机制,/proc/kallsyms中的函数几乎都可以被用于跟踪,但是内核函数可能随着版本演进而发生变化,为非稳定跟踪机制,数量比较多
- uprobe
本系列用主要以what->why->how为原则,对上面几个跟踪问题进行详细的学习,本章主要是function和function_grap为开篇,本文主要是以ARM64为主。
1 function 使用方法
首先我们来回顾下,对于function也是满足三步法,设置tracer类型,设置tracer参数,使能tracer
默认情况下,当前nop,即不输出任何信息
对于function,可以输入以下的命令,function默认会记录当前运行过程中的所有函数,可以看到上述一个普通的系统,function tracer的插桩点为51505个
这么庞大的数量的插桩点,如果独立控制会消耗大量的内存,同时会导致很大的开销。
-
如果只想跟踪某个进程,可以使用set_ftrace_pid参数即可
-
如果只想跟踪某个函数,可以使用set_ftrace_filter参数即可
如果开启动态配置选项,可以设置过滤函数,或指定跟踪函数,例如设置trace的过滤函数,即值跟踪blk_update_request
设置不跟踪指定函数,我们以调度器中使用很频繁的update_rq_clock为例
还可以用通配符”
#'*match*':匹配所有包含match的函数;
echo 'hrtimer_*' >> set_ftrace_filter #过滤所有以"hrtimer_"开头的函数
- 1
- 2
还可以做基于模块的过滤:
例如,过滤 ext4 module 的 write* 函数:·
#控制范式:<function>:<command>:<parameter>
echo 'write*:mod:ext4' > set_ftrace_filter
- 1
- 2
感叹号用来移除某个函数,把多个组合过滤条件的某一个去掉:
echo '!ip_rcv' >> set_ftrace_filter
- 1
遇到 __schedule_bug 函数后关闭 trace
# echo '__schedule_bug:traceoff' > set_ftrace_filter
- 1
详细的使用方法见Documentation/ftrace.txt
2 Function Trace的实现
2.1 基础知识
我们用 C 语言实现一个非常简单程序进行简单验证:
在默认参数编译后的代码如下gcc -o hello hello.c ,objdump -S hello可见函数头部没有特殊定义。
使用 -pg
参数编译后,我们可以看到在函数头部增加了对 mcount
函数的调用,这种机制常用用于运行程序性能分析:gcc -pg -o hello.pg hello.c
function是通过在编译阶段在函数入口插入空指令,运行是对于制定的函数,将空指令替换为要执行的钩子回调,在钩子回调函数中记录函数调用关系,并写入ftrace的ring buffer。
对于ftrace实现是基于编译器选项-pg和-mfentry,这个内核选项在每个函数的开头插入一个特殊跟踪函数的调用–mcount()或fentry()。这部分依赖于处理器架构,文档 Documentation/trace/ftrace-design.txt 中详细介绍了kernel中实现一个处理器架构支持mcount
需要的接口。
如果没有使用ftrace,它几乎不会影响系统,因为内核知道调用mcount()或fentry()的位置,并在早期阶段将机器码替换为nop,当linux内核跟踪打开时,ftrace调用会被添加到必要的函数中。
mcount
在不同处理器的gcc
实现名字可能略有差异,可能是mcount
,_mcount
或者__mcount
,可以通过下面命令查看,arm64中是_mcount
本文统一使用mcount。
echo 'main(){}' | gcc -x c -S -o - - -pg | grep mcount
call mcount
- 1
- 2
ARM64的实现主要有下面三个文件
arch/arm64/kernel/entry-ftrace.S // mcount的核心实现
arch/arm64/kernel/ftrace.c // CONFIG_DYNAMIC_FTRACE 的支持接口
arch/arm64/include/asm/ftrace.h // 声明和定义arm64为ftrace核心模块提供的接口
- 1
- 2
- 3
mcount的主要任务是根据是否打开了ftrace,跳转到对应的注册函数中,做进步一处理。
2.2 静态mcount实现
Kernel中打开CONFIG_FUNCTION_TRACER
后,会增加-pg
编译选项,这样在每个函数入口处都会插入bl mcount
跳转指令,函数运行时会进入mcount
函数。mcount
会判断函数指针ftrace_trace_function
是否被注册,默认注册的是空函数ftrace_stub
,只有打开function tracer后才会注册具体的处理函数ftrace_trace_function
。在根目录的Makefile文件
# The arch Makefiles can override CC_FLAGS_FTRACE. We may also append it later.
ifdef CONFIG_FUNCTION_TRACER
CC_FLAGS_FTRACE := -pg
endif
- 1
- 2
- 3
- 4
对于tracer自身而言,是不需要-pg
选项的,因此我们可以看到在kernel/tracing/Makefile
中将-pg
从该模块的CFLAGS
中剔除了kernel/tracing/Makefile
# Do not instrument the tracer itself:
ccflags-remove-$(CONFIG_FUNCTION_TRACER) += $(CC_FLAGS_FTRACE)
- 1
- 2
- 3
所以对于不希望被 ftrace 跟踪的模块也可以如法炮制。
- 用
notrace
标记的 函数 不会被跟踪。 - 用
CFLAGS_REMOVE_file.o = -pg
禁止对 某个文件用-pg
编译
对于内核中使用说明,当gcc使用-pg,也就是在函数的添加了这么两句指令
/*
* Gcc with -pg will put the following code in the beginning of each function:
* mov x0, x30
* bl _mcount
* [function's body ...]
* "bl _mcount" may be replaced to "bl ftrace_caller" or NOP if dynamic
* ftrace is enabled.
* /
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
为什么要有mov x0,x30,是因为bl需要一个参数,而x30是lr的寄存器
我们已内核的schedule函数为例,编译好后
对于_mcount的实现,代码在arch/arm64/kernel/entry-ftrace.S
,mcount
会判断函数指针ftrace_trace_function
是否被注册,默认注册的是空函数ftrace_stub
,只有打开function tracer后才会注册具体的处理函数ftrace_trace_function
。
#ifndef CONFIG_DYNAMIC_FTRACE
/*
* void _mcount(unsigned long return_address)
* @return_address: return address to instrumented function
*
* This function makes calls, if enabled, to:
* - tracer function to probe instrumented function's entry,
* - ftrace_graph_caller to set up an exit hook
*/
SYM_FUNC_START(_mcount)
mcount_enter
ldr_l x2, ftrace_trace_function
adr x0, ftrace_stub
cmp x0, x2 // if (ftrace_trace_function
b.eq skip_ftrace_call // != ftrace_stub) {
mcount_get_pc x0 // function's pc
mcount_get_lr x1 // function's lr (= parent's pc)
blr x2 // (*ftrace_trace_function)(pc, lr);
skip_ftrace_call: // }
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
ldr_l x2, ftrace_graph_return
cmp x0, x2 // if ((ftrace_graph_return
b.ne ftrace_graph_caller // != ftrace_stub)
ldr_l x2, ftrace_graph_entry // || (ftrace_graph_entry
adr_l x0, ftrace_graph_entry_stub // != ftrace_graph_entry_stub))
cmp x0, x2
b.ne ftrace_graph_caller // ftrace_graph_caller();
#endif /* CONFIG_FUNCTION_GRAPH_TRACER */
mcount_exit
SYM_FUNC_END(_mcount)
EXPORT_SYMBOL(_mcount)
NOKPROBE(_mcount)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 这种是静态的ftrace,ftrace是有一个全局的ftrace_trace_function变量,每当我们设置了一个trace,这个ftrace_trace_function就指向我们设置的trace的回调函数,如果ftrace_trace_function不等于默认的桩函数ftrace_stub,则调用调用function tracer的回调函数:ftrace_trace_function()
- 另外,如果我们没有配置任何的trace,如果配置了
CONFIG_FUNCTION_GRAPH_TRACER
,就不再是注册ftrace_trace_function
,而是注册ftrace_graph_entry
和ftrace_graph_return
。function graph tracer是function trace的增强版,可以打印出完整的函数调用关系。
很明显,这样做有很大的副作用,其主要表现为
- 一旦使能了function trace,会对kernel中所有的函数(inline/notrace修饰除外)进行trace,性能开销会很大,并且trace日志太多,会冲刷感兴趣的日志
- 不使能的时,也会有_mcount的调用,这个开销也会很大
2.3 dynamic ftrace
static ftrace一旦使能,对kernel中所有的函数(除开notrace、online、其他特殊函数)进行插桩,这带来的性能开销是惊人的,有可能导致人们弃用ftrace功能。
为了解决这个问题,内核开发者推出了dynamic ftrace,因为实际上调试者一般不需要对所有函数进行跟踪,只会对感兴趣的一部分函数进行追踪。
- dynamic ftrace把不需要追踪的函数入口处指令“bl _mcount”替换成“nop”,这样基本对性能无影响
- 对需要追踪的函数替换入口处的“bl _mcount”为需要调用的函数
所有需要对插桩点进行收集和动态管理,根据编译时“scripts/recordmcount.pl”脚本记录的所有函数入口处插桩位置的“bl _mcount”,将其替换成“nop”指令
在trace enable的时候,把需要跟踪的函数的插桩位置"nop"替换成"bl ftrace_caller"
SYM_FUNC_START(ftrace_caller)
mcount_enter
mcount_get_pc0 x0 // function's pc
mcount_get_lr x1 // function's lr
SYM_INNER_LABEL(ftrace_call, SYM_L_GLOBAL) // tracer(pc, lr);
nop // This will be replaced with "bl xxx"
// where xxx can be any kind of tracer.
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
SYM_INNER_LABEL(ftrace_graph_call, SYM_L_GLOBAL) // ftrace_graph_caller();
nop // If enabled, this will be replaced
// "b ftrace_graph_caller"
#endif
mcount_exit
SYM_FUNC_END(ftrace_caller)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 放弃了函数指针,使用nop指令占位,在运行时动态修改指令到"bl xxx"
- 放弃了函数指针,使用nop指令占位,在运行时动态修改指令到"b ftrace_graph_call
2.4 插桩初始化
在编译的时候调用recordmcount.pl搜集所有_mcount()函数的调用点,并且所有的调用点地址保存到section __mcount_loc。
最终内核的链接脚本include/asm-generic/vmlinux.lds.h将__mcount_loc段的内容放在.init.data段中,并且通过__start_mcount_loc和__stop_mcount_loc两个全局符号来访问。
最终会将所有的会链接到mcount_loc段中,在内核启动阶段会将bl _mcount替换成nop指令
初始化时,遍历section __mcount_loc的调用点地址,默认给所有“bl _mcount”替换成“nop”。在kernel/trace/ftrace.c中调用ftrace_init
void __init ftrace_init(void)
{
extern unsigned long __start_mcount_loc[];
extern unsigned long __stop_mcount_loc[];
unsigned long count, flags;
int ret;
local_irq_save(flags);
ret = ftrace_dyn_arch_init();
local_irq_restore(flags);
if (ret)
goto failed;
count = __stop_mcount_loc - __start_mcount_loc;
if (!count) {
pr_info("ftrace: No functions to be traced?\n");
goto failed;
}
pr_info("ftrace: allocating %ld entries in %ld pages\n",
count, count / ENTRIES_PER_PAGE + 1);
last_ftrace_enabled = ftrace_enabled = 1;
/* 遍历section __mcount_loc,处理其中保存的调用地址 */
ret = ftrace_process_locs(NULL,
__start_mcount_loc,
__stop_mcount_loc);
pr_info("ftrace: allocated %ld pages with %ld groups\n",
ftrace_number_of_pages, ftrace_number_of_groups);
set_ftrace_early_filters();
return;
failed:
ftrace_disabled = 1;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
在ftrace_process_locs函数中,内核为__start_mcount_loc和__stop_mcount_loc之间的每个地址都创建一个struct dyn_ftrace结构,其中ip记录着函数开始的stub地址,ftrace_code_disable函数会将这个地址的内容替换为nop指令,这样在没有trace时,系统的性能几乎没有影响。所以将bl _mcount替换成nop的函数调用过程如下:
ftrace_init
ftrace_process_locs(NULL, __start_mcount_loc, __stop_mcount_loc);
ftrace_allocate_pages(count); //分配dyn_ftrace表占据的空间
ftrace_update_code(mod, start_pg);//对表中的每一项地址进行code修改
ftrace_code_disable
ftrace_make_nop(mod, rec, MCOUNT_ADDR);//将bl _mcount对应的指令码替换掉
ftrace_check_current_call(rec->ip, call);//校验地址中的内容的合法性
ftrace_modify_code(pc, old, new, validate);//这里的new就是nop对应的指令码,old是根据MCOUNT_ADDR计算出来的bl addr的指令码
aarch64_insn_patch_text_nosync
aarch64_insn_write(tp, insn);//更改指令码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
[ 0.000000] .text : 0xffffff8008080000 - 0xffffff8008930000 ( 8896 KB)
[ 0.000000] .rodata : 0xffffff8008930000 - 0xffffff8008c50000 ( 3200 KB)
[ 0.000000] .init : 0xffffff8008c50000 - 0xffffff8008d50000 ( 1024 KB)
[ 0.000000] .data : 0xffffff8008d50000 - 0xffffff8008eb2008 ( 1417 KB)
[ 0.000000] .bss : 0xffffff8008eb2008 - 0xffffff8009a49e00 ( 11872 KB)
//这个替换大概用了14ms
[3.281092 0.001971] [ 0.000000] ---ftrace_init start---
[3.281930 0.000838] [ 0.000000] ftrace: allocating 30444 entries in 119 pages
[3.295482 0.013552] [ 0.000000] ---ftrace_init end---
//在ftrace_replace_code中做计数,总共替换了31726个函数。
[ 136.262494] --ftrace_replace_code---count:31726
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
对于ftrace_make_nop其实现如下,找到旧的,然后以新的nop作为替换就可以了
ftrace_make_nop(mod, rec, MCOUNT_ADDR)
old = ftrace_call_replace(ip, MCOUNT_ADDR)
text_gen_insn(CALL_INSN_OPCODE, (void *)ip, (void *)MCOUNT_ADDR);
new = ftrace_nop_replace
ftrace_modify_code_direct(ip, old, new)
ftrace_verify_code(ip, old)
text_poke_early(ip, new, MCOUNT_INSN_SIZE)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
这是在不使能的情况下,如果要使能,内核是如何做的呢?首先我们需要考虑以下几点
- 需要确定想要trace的函数的插桩点位置-------这个已经保存,遍历section __mcount_loc的调用点地址
- 我们需要记录某个trace点的状态,例如我们之前的schedule,我们在有些情况是使能的,某些情况是不使能,那么我们就需要记录这个状态
所以内核使用了一个新的结构来完成这个工作, 每个“bl _mcount”的插桩点ip对应一个每个插桩点对应一个dyn_ftrace结构
struct dyn_ftrace {
unsigned long ip; /* address of mcount call-site */
unsigned long flags; /* 这个用于存放插桩点的相关状态 */
struct dyn_arch_ftrace arch; /* 大部分架构为空 */
};
- 1
- 2
- 3
- 4
- 5
内核使用ftrace_pages来保存dyn_ftrace的数据,将_mcount_loc数据迁移到ftrace_pages后,将数据释放掉,ftrace_pages中的数据是有序的,便于查找,通过dmes可以看到ftrace_pages占用的内存
- 有44478个dyn_ftrace
- 申请了174个4K pages
ftrace_pages根据mcount_loc中数据,填好数据如下:
我们通过用户态获取avilable_filter_functions的数据,其实也是通过ftrace_pages中经过转换得到对应的函数,顺序也是跟ftrace_pages的存储顺序相同
接下来,我们来看看flag这个标志位
enum {
FTRACE_FL_ENABLED = (1UL << 31),
FTRACE_FL_REGS = (1UL << 30),
FTRACE_FL_REGS_EN = (1UL << 29),
FTRACE_FL_TRAMP = (1UL << 28),
FTRACE_FL_TRAMP_EN = (1UL << 27),
FTRACE_FL_IPMODIFY = (1UL << 26),
FTRACE_FL_DISABLED = (1UL << 25),
FTRACE_FL_DIRECT = (1UL << 24),
FTRACE_FL_DIRECT_EN = (1UL << 23),
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 低22 bit用来表示引用计数,如果有ftrace_ops会操作该ip,引用计数会增加1,例如我们的function trace已经trace这个函数,这个时候其他的用户,例如perf也想用这个trace框架,这个低22位就存放这个计数
- ENABLED - the function is being traced
- REGS - the record wants the function to save regs
- REGS_EN - the function is set up to save regs.
- IPMODIFY - the record allows for the IP address to be changed.
- DISABLED - the record is not ready to be touched yet
- DIRECT - there is a direct function to call
对于其过程如下:
ftrace_pages_start
|
v
ftrace_page
+-----------------------------+
|index |
|size |
| (int) | array of dyn_ftrace
|records | +----------+----------+ +----------+----------+
| (struct dyn_ftrace*) |---->|ip | | ... | | |
| | |flags | | | | |
| | |arch | | | | |
|next | +----------+----------+ +----------+----------+
| (struct ftrace_page*) |
+-----------------------------+
|
|
v
ftrace_page
+-----------------------------+
|index |
|size |
| (int) | array of dyn_ftrace
|records | +----------+----------+ +----------+----------+
| (struct dyn_ftrace*) |---->|ip | | ... | | |
| | |flags | | | | |
|next | |arch | | | | |
| (struct ftrace_page*) | +----------+----------+ +----------+----------+
+-----------------------------+
|
|
v
ftrace_page
+-----------------------------+
|index |
|size |
| (int) | array of dyn_ftrace
|records | +----------+----------+ +----------+----------+
| (struct dyn_ftrace*) |---->|ip | | ... | | |
| | |flags | | | | |
|next | |arch | | | | |
| (struct ftrace_page*) | +----------+----------+ +----------+----------+
+-----------------------------+
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
从此所有的探针就都在ftrace_pages_start为起始的一张表中。为了遍历这张特殊的表,访问到其中的每一个entry,就引入了这么一个宏定义:
其中有两个for循环,一个是遍历ftrace_pages_start为首的链表,一个是编译每个ftrace_page的records。
2.4 tracer函数探针动态配置
当系统完成初始化后,所有的_mcount_loc中的地址处的内容均被修改成nop指令,如果需要对函数进行跟踪记录,则需要进一步将nop指令修改为函数探针的跳转指令。
ftrace框架中提供了多种的函数探针,通过函数register_tracer进行注册,并通过过/sys/kernel/debug/tracing/current_tracer节点来进行配置。根据该节点的操作函数可知,由函数tracing_set_trace_write对函数探针进行配置:
tracer可以通过register_tracer()进行注册。以function tracer为例,kernel/trace/trace_functions.c
static struct tracer function_trace __tracer_data =
{
.name = "function",
.init = function_trace_init,
.reset = function_trace_reset,
.start = function_trace_start,
.flags = &func_flags,
.set_flag = func_set_flag,
.allow_instances = true,
#ifdef CONFIG_FTRACE_SELFTEST
.selftest = trace_selftest_startup_function,
#endif
};
__init int init_function_trace(void)
{
init_func_cmd_traceon();
return register_tracer(&function_trace);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
初始化完成之后,我们关心的是怎么样能够再次打开探针。首先我们得找到这种操作的入口,比如使用”echo xxx > set_ftrace_filter“命令来配置function
首先我们找到这个文件的定义函数:
顺着这个看,中主要的就是match_recors,这个通过do_for_each_ftrace_rec遍历整个探针的记录,根据index找到究竟是要操作那个探针,然后通过enter_record()这个探针添加到ftrace_hash这个哈希表中
这样我们就知道了修改时,需要修改哪些探针地址,但是我们还没有真正的去修改这些代码,这个就是通过我们去tracer使能完成。
我们知道可以使用“echo function > current_tracer”命令来使能或者切换tracer,来选择不同的tracer,输出不同的记录,。来具体看看其代码实现:
对于tracing_set_trace_write其实现
- 当判断当前设置的tracer是否被注册过,race_types是一个全局变量,当使能了某个tracer时,调用register_tracer将tracer挂接到trace_types的链表中
- 需要设置成为的tracer和当前正在使用中的tracer不同时,先要调用正在使用的tracer的reset函数
- 调用需要设置成为的tracer的init函数
以function tracer为例,对tracer的init过程进行解析:
对于function tracer的开启过程,如下图
对于__register_ftrace_function对当前的function tracer进行注册,将其挂到tracer链表中
最终会调用ftrace_startup_enable替换了不同的函数,对于ARM64其调用流程如下:
ftrace_startup_enable
ftrace_run_update_code
arch_ftrace_update_code
ftrace_modify_all_code
- 1
- 2
- 3
- 4
首先更新tracer function
到这里,我们基本能看出一些端倪。
- 找到旧的指令 – old
- 找到新的指令 – new
- 用ftrace_modify_code()将old替换成new
对于ftrace_call出的内容为
ftrace_replace_code替换所有_mcount_loc中地址的内容
对于每一个表项,都对其进行判断,对于需要跟踪的函数,调用__ftrace_replace_code将nop替换为ftrace_caller,对于不需要跟踪的函数,则不做任何操作,是否需要根据FTRACE_FL_DISABLED 标志来判断,该标志由set_ftrace_filter来添加。
3 总结
到此我们已经分析完了ftrace的各个阶段的行为,以及钩子函数的替换过程,基本上包含如下过程:
-
编译阶段。通过编译选项 -pg -mrecord-mcount 在每个支持ftrace的函数中插入bl 0 <_mcount>指令
-
链接阶段。会根据重定位段将bl 0 <_mcount>指令地址重定位为_mcount函数地址。
-
运行阶段
(1)ftrace_init:会将可trace函数中的bl _mcount替换为nop指令;调用 ftrace_process_locs 把所有 mcount 调用点替换为 nop 指令:ftrace_make_nop()
(2)执行echo blk_update_request >set_ftrace_filter:会使能blk_update_request的钩子函数替换标记(nop替换为ftrace_caller);
(3)执行echofunction > current_tracer:触发两步替换:调用 ftrace_run_update_code,替换回 mcount 调用点:ftrace_make_call()
第一步,ftrace_caller中ftrace_call被替换为ftrace_ops_no_ops;
第二步,blk_update_request中的nop被替换为ftrace_caller。ftrace_caller最终会调用到function_trace_call,它会记录函数调用堆栈信息,并将结果写入 ring buffer,用户可以通过/sys/kernel/debug/tracing/trace文件读取该 ring buffer 中的内容。
到此我们已经分析完了ftrace的各个阶段的行为,以及钩子函数的替换过程,基本上包含如下过程:-
- 编译阶段。通过编译选项 -pg -mrecord-mcount 在每个支持ftrace的函数中插入bl 0 <_mcount>指令
- 链接阶段。会根据重定位段将bl 0 <mcount>指令地址重定位为mcount函数地址。
- 运行阶段
(1)ftrace_init:会将可trace函数中的bl _mcount替换为nop指令;调用 ftrace_process_locs 把所有 mcount 调用点替换为 nop 指令:ftrace_make_nop()
compile time __fentry__
+------------->+---------------+
| |retq |
| | |
kernel._text | +---------------+
+--------------+ |
| | |
| | |boot up
ip -> | ---|---+-------------> = nop
| | |
| | |
+--------------+ |
|
|
|enabled ftrace_caller
+------------->+---------------+
| |
|ftrace_call |
| |
+---------------+
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
(2)执行echo blk_update_request >set_ftrace_filter:会使能blk_update_request的钩子函数替换标记(nop替换为ftrace_caller);
(3)执行echofunction > current_tracer:触发两步替换:调用 ftrace_run_update_code,替换回 mcount 调用点:ftrace_make_call()
4 参考文档
https://www.ebpf.top/post/ftrace_tools/
https://blog.arstercz.com/introduction_to_linux_dynamic_tracing/
https://www.apriorit.com/dev-blog/546-hooking-linux-functions-2
【原创】Kernel调试追踪技术之 Ftrace on ARM64
Kernel Recipes 2019 - ftrace: Where modifying a running kernel all started
Kernel Recipes 2017 - Understanding the Linux Kernel via Ftrace - Steven Rostedt
LF Live Mentorship Session: Tracing with Ftrace: Critical Tooling for Linux Development
openEuler kernel技术分享-第13期-ftrace框架及指令修改机制_哔哩哔哩_bilibili
https://www.ebpf.top/post/ftrace_kernel_dynamic/
https://richardweiyang-2.gitbook.io/kernel-exploring/00-index-3/04-ftrace_internal
https://blog.linuxplumbersconf.org/2014/ocw/system/presentations/1773/original/ftrace-kernel-hooks-2014.pdf
本文来自博客园,作者:LiYanbin,转载请注明原文链接:https://www.cnblogs.com/stellar-liyanbin/p/17440951.html