- 功能描述
我们在开发和调试代码的过程中,经常会遇到一个场景,变量在函数之间传递的过程中,我们需要追踪这个变量的值变化的过程,比如下面这个例子中的bio:
bio_add_page(struct bio *bio ...) -->
__bio_try_merge_page(bio ...) -->
bio_flagged(bio...)
__bio_add_page(bio...)
...
通常,我们会增加很多printk,然后编译安装内核,重启。如果有的函数分支忽略掉的话,还需要再增加printk,再次编译安装内核和重启,是不是很笨重?
内核中的kprobe提供了可以不用修改代码而直接查看函数参数值的方法:
cd /sys/kernel/debug/tracing
echo 'p bio_add_page arg1=$arg1 arg2=$arg2' > ./kprobe_events
echo 1 > events/kprobes/p_bio_add_page_0/enable
cat your_file > /dev/null
cat trace
jbd2/sda5-8-220 [000] ...1. 55531.677782: p_bio_add_page_0: (bio_add_page+0x0/0x90) arg1=0xffff8ecac98aba00 arg2=0xfffff58a0122a400
jbd2/sda5-8-220 [000] ...1. 55531.677786: p_bio_add_page_0: (bio_add_page+0x0/0x90) arg1=0xffff8ecac98abc00 arg2=0xfffff58a04141140
jbd2/sda5-8-220 [000] ...1. 55531.677790: p_bio_add_page_0: (bio_add_page+0x0/0x90) arg1=0xffff8ecac98ab100 arg2=0xfffff58a02e9f880
jbd2/sda5-8-220 [000] ...1. 55531.677794: p_bio_add_page_0: (bio_add_page+0x0/0x90) arg1=0xffff8ecac98ab300 arg2=0xfffff58a041d1b00
jbd2/sda5-8-220 [000] ...1. 55531.677798: p_bio_add_page_0: (bio_add_page+0x0/0x90) arg1=0xffff8ecac98ab600 arg2=0xfffff58a04141280
...
Kprobe利用了ftrace中function tracer的架构,将代码段中__fentry__(或__mcount)替换为kprobe_ftrace_handler,kprobe_ftrace_handler将函数名称和参数值打印到了ftrace的ringbuffer之中。
但还有一个问题,kprobe只能追踪变量在bio_add_page内的值,不能跟踪变量在其之后的变化。
国内社区的jeff老师近期开发了objtrace可以实现变量持续跟踪的功能,objtrace结合使用了kprobe和function tracer的功能,持续跟踪一个或多个变量,同时还可以设置偏移量,获取变量结构内部元素的值。
目前objtrace正在评审中,很快就会进入到内核代码树的主线。
使用方法
1) 进入debugfs的目录
cd /sys/kernel/debug/tracing
2)设置kprobe
echo 'p bio_add_page arg1=$arg1 arg2=$arg2' > ./kprobe_events
查看kprobe:
ls events/kprobes/ -al
里面增加了p_bio_add_page_0
cat events/kprobes/p_bio_add_page_0/trigger
# Available triggers:
# traceon traceoff snapshot stacktrace enable_event disable_event enable_hist disable_hist hist objtrace
可以看到除了原有的traceon traceoff snapshot stacktrace之外,增加了一个新的触发器objtrace
3)设置objtrace
echo 'objtrace:add:arg1,0x28:u32:5 if comm == "cat"' > ./events/kprobes/p_bio_add_page_0/trigger
表示在kprobe:bio_add_page需要跟踪第一个参数,也就是结构bio偏移量为0x28的变量的值。
cat events/kprobes/p_bio_add_page_0/trigger
objtrace:add:arg1,0x28:u32:count=0 if comm == "cat"
4) 触发
cat your_file > /dev/null //读取一个大文件,触发bio_add_page,也就是内核需要调用到bio_add_page函数
5)查看日志
cat trace
# tracer: nop
#
# entries-in-buffer/entries-written: 309/401 #P:4
#
# _-----=> irqs-off/BH-disabled
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / _-=> migrate-disable
# |||| / delay
# TASK-PID CPU# ||||| TIMESTAMP FUNCTION
# | | | ||||| | |
kworker/3:1H-79 [003] ...1. 248.076143: wbt_issue <-__rq_qos_issue object:0xffffa0c886cc8700 value:0x1000
kworker/3:1H-79 [003] ...1. 248.076143: blk_add_timer <-blk_mq_start_request object:0xffffa0c886cc8700 value:0x1000
##### CPU 1 buffer started ####
<idle>-0 [001] .Ns2. 248.080898: bio_endio <-blk_update_request object:0xffffa0c886cc8700 value:0x0
<idle>-0 [001] .Ns2. 248.080900: __rq_qos_done_bio <-bio_endio object:0xffffa0c886cc8700 value:0x0
<idle>-0 [001] .Ns2. 248.080900: bio_uninit <-bio_endio object:0xffffa0c886cc8700 value:0x0
<idle>-0 [001] .Ns2. 248.080901: bio_uninit <-bio_endio object:0xffffa0c886cc8700 value:0x0
<idle>-0 [001] .Ns2. 248.080901: __rcu_read_lock <-bio_uninit object:0xffffa0c886cc8700 value:0x0
<idle>-0 [001] .Ns2. 248.080901: __rcu_read_lock <-bio_uninit object:0xffffa0c886cc8700 value:0x0
代码分析:
1,初始化
初始化就是让内核的ftrace架构知道有objtrace这样的一个trigger(触发器),当开发者创建一个kprobe的时候,在这个kprobe的trigger目录下,会出现objtrace的选项。代码流程如下:
start_kernel -->
trace_init -->
trace_event_init -->
event_trace_enable -->
register_trigger_cmds -->
register_trigger_object_cmd -- >
register_event_command(&trigger_object_cmd); 注册trigger_object_cmd
trigger_object_cmd包含了event_command所需要的函数,如event_object_trigger_parse就是工程是输入objtrace:add...之后,将这个字符串解析的过程。
static struct event_command trigger_object_cmd = {
.name = "objtrace",
.trigger_type = ETT_TRACE_OBJECT,
.flags = EVENT_CMD_FL_NEEDS_REC,
.parse = event_object_trigger_parse,
.reg = register_trigger,
.unreg = unregister_trigger,
.get_trigger_ops = objecttrace_get_trigger_ops,
.set_filter = set_trigger_filter,
};
此外,start_kernel还有一个路径初始化objtrace的另一部分
start_kernel -->
early_trace_init -->
tracer_alloc_buffers -->
allocate_objtrace_data
allocate_objtrace_data中申请并初始化了一个结构体objtrace_data *obj_data,其中最重要的是obj_data->fops->func = trace_object_events_call,函数trace_object_events_call负责跟踪参数值并打印到ftrace的ring buffer中,可以说trace_object_events_call是objtrace中最重要的一个函数。
注意,此时trace_object_events_call只是放到了obj_data->fops->func中,并没有开始使用。
2,设置 “echo 'objtrace:add:arg1,0x28:u32:5 if comm == "cat"' > ./events/kprobes/p_bio_add_page_0/trigger”
当开发者输入这个命令的时候,系统需要将 'objtrace:add:arg1,0x28:u32:5 if comm == "cat"' 解析出来,并调用event_trigger_register注册到ftrace中,这样当内核走到bio_add_page这个函数的时候,就会触发kprobe。函数调用过程如下:
我在event_trigger_set_filter中增加了一个WARN_ONCE,可以看到调用栈,同时在event_object_trigger_parse中增加了一个printk,看到一些变量。通常我都是使用function_grath来看代码调用的流程,但ftrace本身的函数不能使用ftrace来观测,这样会造成递归调用,很不幸不是,只能使用这种笨办法了:
[ 180.024670] chensong: event_object_trigger_parse :param:add:arg1,0x28:u32:5, objtrace_cmd:add
[ 180.024685] ------------[ cut here ]------------
[ 180.024688] chensong: set_trigger_filter, filter_str:if comm == "cat"
[ 180.024698] WARNING: CPU: 3 PID: 4332 at kernel/trace/trace_events_trigger.c:1056 set_trigger_filter+0x13d/0x170
...
[ 180.024844] CPU: 3 PID: 4332 Comm: bash Not tainted 5.18.0-rc2-objtrace-v10+ #2
[ 180.024848] Hardware name: LENOVO YangTianT4900v-00/, BIOS FCKT65AUS 01/12/2015
[ 180.024851] RIP: 0010:set_trigger_filter+0x13d/0x170
[ 180.024856] Code: 83 c4 18 44 89 e0 5b 41 5c 41 5d 5d c3 48 89 fa 48 c7 c6 e0 15 23 84 48 c7 c7 15 e4 5a 84 c6 05 27 84 94 01 01 e8 d2 f7 9c 00 <0f> 0b 48 8b 45 d0 e9 f6 fe ff ff e8 73 57 a3 00 48 8b 7b 20 41 bc
[ 180.024859] RSP: 0018:ffffa28003a77d60 EFLAGS: 00010286
...
[ 180.024879] CR2: 00007fbeb41fa020 CR3: 000000000a0f6003 CR4: 00000000001706e0
[ 180.024882] Call Trace:
[ 180.024884] <TASK>
[ 180.024888] event_trigger_set_filter+0x22/0x30
[ 180.024894] event_object_trigger_parse.cold+0x313/0x345
[ 180.024904] trigger_process_regex+0xc1/0x110
[ 180.024908] event_trigger_write+0x70/0xd0
...
[ 180.024913] vfs_write+0xc0/0x2b0
[ 180.024920] ksys_write+0x67/0xe0
[ 180.024926] __x64_sys_write+0x1a/0x20
[ 180.024931] do_syscall_64+0x3b/0x90
[ 180.024937] entry_SYSCALL_64_after_hwframe+0x44/0xae
event_object_trigger_parse首先解析’objtrace:add:arg1,0x28:u32:5’,并将结果保存到变量trigger_data中,最后调用event_trigger_register,过程如下:
static int
event_object_trigger_parse(struct event_command *cmd_ops,...
{
struct event_trigger_data *trigger_data;
struct objtrace_trigger_data *obj_data;
...
obj_data->field = field; --- (1)
obj_data->obj_offset = offset;
obj_data->obj_value_type_size = obj_value_type_size;
obj_data->tr = file->tr;
snprintf(obj_data->objtrace_cmd, OBJTRACE_CMD_LEN, objtrace_cmd);
trigger_data = event_trigger_alloc(cmd_ops, cmd, param, obj_data); ---(2)
...
ret = event_trigger_set_filter(cmd_ops, file, filter, trigger_data); ---(3)
if (ret < 0)
goto out_free;
ret = event_trigger_register(cmd_ops, file, glob, trigger_data); ---(4)
...
}
int event_trigger_register(struct event_command *cmd_ops,
{
return cmd_ops->reg(glob, trigger_data, file); ---(5)
}
int event_trigger_set_filter(struct event_command *cmd_ops,
{
if (param && cmd_ops->set_filter)
return cmd_ops->set_filter(param, trigger_data, file); ---(6)
return 0;
}
static struct event_command trigger_object_cmd = { --- (7)
.name = "objtrace",
.trigger_type = ETT_TRACE_OBJECT,
.flags = EVENT_CMD_FL_NEEDS_REC,
.parse = event_object_trigger_parse,
.reg = register_trigger,
.unreg = unregister_trigger,
.get_trigger_ops = objecttrace_get_trigger_ops,
.set_filter = set_trigger_filter,
};
- )解析的结果放到objtrace_trigger_data中。
- )同时,将objtrace_trigger_data放置到event_trigger_data中。
- )注册filter,也就是’if comm == "cat"’。
- )注册trigger。
- )event_trigger_register就是调用trigger_object_cmd中的.reg,也就是register_trigger。
- )event_trigger_set_filter就是调用trigger_object_cmd中的.set_filter,也就是set_trigger_filter。
- )trigger_object_cmd的定义。
objtrace还有一个重要的工作,就是要跟踪变量的流向,这个就超出了kprobe的功能范围,需要借助function tracer了,所以kprobe部分还有一个重要的工作,就是使能function tracer。我花了很多时间来找如何使能function tracer的,一直没有找到。但如果你了解function tracer的话,会知道在使能function tracer,必然会调用ftrace_replace_code来替换代码段,将__fentry__(或__mcount)替换为ftrace_call,那么我就在ftrace_replace_code中增加一个WARN,看一下调用栈:
增加调试代码:
arch/x86/kernel/ftrace.c +194
ftrace_replace_code
+static int counter = 0;
void ftrace_replace_code(int enable)
{
rec = ftrace_rec_iter_record(iter);
+ WARN_ONCE(1, "chensong: ip:%pS \n", rec->ip);
+ counter++;
+ if (counter%100 == 0)
+ printk("chensong:%s, ip:%pS\n", __FUNCTION__, rec->ip);
输入echo 'objtrace:add:arg1,0x28:u32:5 if comm == "cat"' > ./events/kprobes/p_bio_add_page_0/trigger后,得到调用栈
[ 168.674941] chensong: ip:__traceiter_initcall_level+0x0/0x40
[ 168.674954] WARNING: CPU: 3 PID: 3032 at arch/x86/kernel/ftrace.c:205 ftrace_replace_code+0x10b/0x1d0
...
[ 168.675108] CPU: 3 PID: 3032 Comm: bash Not tainted 5.18.0-rc2-objtrace-v10+ #6
[ 168.675112] Hardware name: LENOVO YangTianT4900v-00/, BIOS FCKT65AUS 01/12/2015
[ 168.675115] RIP: 0010:ftrace_replace_code+0x10b/0x1d0
[ 168.675121] Code: 89 fe 89 c7 e8 f7 97 b4 00 5b 41 5c 41 5d 41 5e 41 5f 5d c3 48 8b 30 48 c7 c7 78 c5 b8 a5 c6 05 f0 6e ab 01 01 e8 4f f3 b3 00 <0f> 0b eb 80 e8 bc d1 13 00 31 db 45 85 f6 49 c7 c6 30 32 49 a6 0f
[ 168.675124] RSP: 0018:ffffb8a90299fc78 EFLAGS: 00010286
[ 168.675128] RAX: 0000000000000000 RBX: ffffffffa6493230 RCX: 0000000000000000
...
[ 168.675149] Call Trace:
[ 168.675151] <TASK>
[ 168.675156] ftrace_modify_all_code+0xcc/0x160
[ 168.675165] arch_ftrace_update_code+0x9/0x10
[ 168.675170] ftrace_run_update_code+0x1a/0x70
[ 168.675176] ftrace_startup_enable+0x2e/0x50
[ 168.675181] ftrace_startup+0xa8/0x120
[ 168.675185] register_ftrace_function+0x25/0x70
[ 168.675189] event_object_trigger_init+0x32/0x50
[ 168.675194] register_trigger+0x69/0xd0
[ 168.675200] event_trigger_register+0x1c/0x20
[ 168.675204] event_object_trigger_parse+0x323/0x390
[ 168.675211] trigger_process_regex+0xc1/0x110
[ 168.675216] event_trigger_write+0x70/0xd0
[ 168.675221] vfs_write+0xc0/0x2b0
[ 168.675228] ksys_write+0x67/0xe0
[ 168.675234] __x64_sys_write+0x1a/0x20
[ 168.675240] do_syscall_64+0x3b/0x90
[ 168.675246] entry_SYSCALL_64_after_hwframe+0x44/0xae
这部分代码也是从register_trigger调用过来的,最终调用了
register_trigger -->
event_object_trigger_init -->
register_ftrace_function -->
ftrace_modify_all_code
其中,register_ftrace_function还将&tr->obj_data->fops注册到function tracer的框架中,那么函数调用function tracer时,function tracer会调用tr->obj_data->fops中的回调函数func,也就是在allocate_objtrace_data中赋值的trace_object_events_call函数,此时trace_object_events_call开始工作。
至此,function tracer被打开,以后内核中所有可以trace的函数,都将要走过function tracer这条路径,也就是每一个函数调用之后,都会运行到trace_object_events_call中。
3, 触发 “cat /home/kylin/workspace/opensource/linux-objtrace-jeff/kernel/sched/core.c > /dev/null”
本节是objtrace中最重要的一个环节,当kprobe和objtrace都设置好了,一旦内核调用到bio_add_page,就要开始跟踪bio这个变量了,objtrace是如何在原有的kprobe的基础上实现这个功能的呢?
我在阅读代码的时候,将这个过程分为两部分,一个是kprobe,另一个是function tracer,我们先来看看当kprobe被触发的时候,objtrace做了什么工作:
同样的,我还是使用了printk和WARN_ONCE来进行调试,我首先在函数set_trace_object中增加了一个WARN_ONCE,得到的调用栈如下:
[ 248.069971] chensong:set_trace_object, call stack of set trace object
[ 248.069980] WARNING: CPU: 3 PID: 4343 at kernel/trace/trace_object.c:162 trace_object_trigger+0x184/0x1a0
....
[ 248.070450] Hardware name: LENOVO YangTianT4900v-00/, BIOS FCKT65AUS 01/12/2015
[ 248.070455] RIP: 0010:trace_object_trigger+0x184/0x1a0
[ 248.070462] Code: 7a ae 48 c7 c6 2d 69 3d ad e8 28 82 fe ff eb bc 48 c7 c6 60 05 43 ae 48 c7 c7 80 ce 7a ae c6 05 2c 11 95 01 01 e8 db 84 9d 00 <0f> 0b eb 9e 48 85 db 0f 84 16 ff ff ff 48 c7 c0 80 96 bc ae e9 d1
[ 248.070467] RSP: 0018:ffffb122007d36a8 EFLAGS: 00010086
....
[ 248.070515] Call Trace:
[ 248.070520] <TASK>
[ 248.070533] trace_object_count_trigger+0x1e/0x30
[ 248.070541] event_triggers_call+0x5d/0xe0
[ 248.070556] trace_event_buffer_commit+0x1a2/0x260
[ 248.070573] kprobe_trace_func+0x1ad/0x2a0
[ 248.070602] kprobe_dispatcher+0x42/0x70
[ 248.070610] ? bio_add_page+0x1/0x90
[ 248.070622] kprobe_ftrace_handler+0x168/0x1f0
[ 248.070629] ? bio_add_page+0x5/0x90
[ 248.070636] ? __bio_try_merge_page+0x140/0x140
[ 248.070648] arch_ftrace_ops_list_func+0xe5/0x160
[ 248.070655] ? submit_bh_wbc+0xca/0x140
[ 248.070677] ftrace_regs_call+0x5/0x52
[ 248.070689] RIP: 0010:bio_add_page+0x1/0x90
....
[ 248.070781] ? bio_add_page+0x5/0x90
[ 248.070789] ? submit_bh_wbc+0xca/0x140
我们可以看到,当bio_add_page被触发之后,进入和kprobe的调用路径:
bio_add_page -->
ftrace_regs_call -->
arch_ftrace_ops_list_func-->
kprobe_ftrace_handler -->
kprobe_dispatcher -->
kprobe_trace_func -->
event_triggers_call -->
trace_object_count_trigger -->
set_trace_object
其中trace_object_count_trigger也是trigger_object_cmd中定义的回调函数,在trigger条件符合被触发的时候被调用,我在set_trace_object中还增加了一个printk,打印obj的地址,也就是需要跟踪的变量的地址。
static void set_trace_object(void *obj, int obj_offset,
int obj_value_type_size, struct trace_array *tr)
{
unsigned long flags;
struct object_instance *obj_ins;
struct objtrace_data *obj_data;
...
obj_ins = &obj_data->traced_obj[atomic_read(&obj_data->num_traced_obj)];
obj_ins->obj = obj; --- (1)
printk("chensong:%s, obj:0x%lx", __FUNCTION__, obj);
obj_ins->obj_value_type_size = obj_value_type_size;
obj_ins->obj_offset = obj_offset;
obj_ins->tr = tr;
/* make sure the num_traced_obj update always appears after traced_obj update */
smp_wmb();
atomic_inc(&obj_data->num_traced_obj);
//WARN_ONCE(1, "chensong:%s, call stack of set trace object\n", __FUNCTION__);
out:
raw_spin_unlock_irqrestore(&obj_data->obj_data_lock, flags);
}
这个时候内核已经走到了bio_add_page,所以已经知道了bio的地址了,所以需要将bio的地址保存到object_instance的obj中,object_instance中其余的元素保存偏移量,变量大小等信息。
接下来是function tracer的部分,在上一部分讲过,开发者运行命令“echo 'objtrace:add:arg1,0x28:u32:5 if comm == "cat"' > ./events/kprobes/p_bio_add_page_0/trigger”之后,function tracer被打开,以后内核中所有可以trace的函数,都将要走过function tracer这条路径,每一个函数调用之后,都会运行到trace_object_events_call中,trace_object_events_call的代码如下:
static void
trace_object_events_call(unsigned long ip, unsigned long parent_ip,
struct ftrace_ops *op, struct ftrace_regs *fregs)
{
struct pt_regs *pt_regs = ftrace_get_regs(fregs);
int cpu, n;
for (n = 0; n < max_args_num; n++) {
obj = regs_get_kernel_argument(pt_regs, n); --- (1)
if (object_exist((void *)obj, tr)) { --- (2)
...
if (get_object_value(&val, (void *)(obj + obj_offset), val_type_size)) --- (3)
continue;
submit_trace_object(ip, parent_ip, obj, val, tr); ---(4)
}
...
}
static inline unsigned long regs_get_register(struct pt_regs *regs,
unsigned int offset)
{
return *(unsigned long *)((unsigned long)regs + offset); --- (5)
}
static bool object_exist(void *obj, struct trace_array *tr)
{
...
for (i = 0; i < max; i++) {
if (obj_data->traced_obj[i].obj == obj) --- (6)
return true;
}
return false;
}
- )Function tracer使能后,每个可以trace的函数都会调用到trace_object_events_call中,调用栈如下:
Any kernel tracable function -->
ftrace_regs_call -->
arch_ftrace_ops_list_func -->
__ftrace_ops_list_func -->
op->func(ip, parent_ip, op, fregs); -->
trace_object_events_call -->
get_object_value
trace_object_events_call首先调用regs_get_kernel_argument,依次获得函数的参数,本例中为bio_add_page的参数。
2)调用object_exist来检查是不是需要跟踪的变量,这个变量的地址在开发者输入”cat file > /dev/null”之后, 内核代码走到了bio_add_page,bio就会在set_trace_object中保存到了object_instance的obj中,所以在这个时候,后续函数如果使用了bio,object_exist返回为TRUE。如果内核代码没有走到过bio_add_page或者该函数的参数中没有bio,object_exist会返回FALSE。
3)获取变量值,保存到val中。
4)调用submit_trace_object将value打印到ftrace的ringbuffer中。
5)regs_get_kernel_argument最终会调用到regs_get_register中,返回指定参数的地址。
6)object_exist通过比较地址来判断,当前函数的参数中是不是包含需要跟踪的变量。
好了,到此为止,objtrace的大部分工作都结束了,找到了变量,获得了它的值,并打印到ringbuffer中,很巧妙,避免了大量的printk和WARN这样笨重的方法。
但objtrace打开了所有函数的function tracer,每个函数都要去调用trace_object_events_call,还有调用object_exist进行变量的比对,会增加一定的系统开销,所以objtrace最好不用和性能调试的trace一起工作。
4,打印 “cat trace”
从debugfs的trace文件中打印到中断,显示给开发者,调用栈如下:
[ 1617.394055] chensong:trace_object_print
[ 1617.394073] WARNING: CPU: 2 PID: 4761 at kernel/trace/trace_output.c:1567 trace_object_print+0x73/0x90
[ 1617.394733] RIP: 0010:trace_object_print+0x73/0x90
[ 1617.394749] Code: c6 83 b5 fa b8 e8 9d 0d 00 00 4c 89 e7 be 0a 00 00 00 e8 d0 0a 00 00 48 c7 c6 30 f5 c2 b8 48 c7 c7 90 b5 fa b8 e8 ec a6 9e 00 <0f> 0b 4c 89 e7 e8 23 37 ff ff 5b 41 5c 5d c3 0f 0b eb 97 66 2e 0f
[ 1617.394841] Call Trace:
[ 1617.394850] <TASK>
[ 1617.394864] print_trace_line+0x25e/0x570
[ 1617.394878] ? ring_buffer_iter_advance+0x32/0x40
[ 1617.394902] s_show+0x4c/0x160
[ 1617.394919] seq_read_iter+0x2a3/0x450
[ 1617.394947] seq_read+0xee/0x120
[ 1617.394983] vfs_read+0xa7/0x1a0
[ 1617.395006] ksys_read+0x67/0xe0
[ 1617.395029] __x64_sys_read+0x1a/0x20
[ 1617.395042] do_syscall_64+0x3b/0x90
[ 1617.395056] entry_SYSCALL_64_after_hwframe+0x44/0xae
代码分析的大体流程如下,还有一些细节需要进一步调试,加深理解,调试ftrace不容易,因为不能用到ftrace:)
源码:https://github.com/x-lugoo/linux
感谢jeff老师的指导和帮助。