本文介绍linux内核中几种常用的动态调试手段,也都是我常用的,都是在生产环境中直接使用,不需要借助工具,依照我的经验,去客户生产环境中解决问题,很多都不会预装perf、BPF工具,有的即使有perf这样的工具,也因为工具性能影响客户几千亿美金的正常业务,也会不让你使用。


最常见的就是使用kprobe,kprobe具体的原理可以参考我以前写的这篇文章kprobe原理

使用kprobe最常用的就是查询函数调用的参数和返回值:

cd /sys/kernel/debug/tracing


echo ‘p:myprobe do_sys_open fname_str=+0(%si):string' > kprobe_events

 echo 'r:myretprobe do_sys_open $retval' >> kprobe_events


ls  ./events/kprobes/

enable  filter  myprobe  myretprobe


使能kprobe event:

echo 1 >  ./events/kprobes/myprobe/enable 

echo 1 >  ./events/kprobes/myretprobe/enable

cat  ./trace (可以看到系统中调用open的文件名和返回值)


Linux内核常用的动态调试手段_java


至于在kprobe_events文件中指定的寄存器(%si),基于具体平台的不同会不一样,可以预先使用perf工具查询到当前平台上获取函数参数的寄存器。


[root@u-jeff ~]# perf probe  'do_sys_open filename:string flags:u32'

Added new event:

       ...

[root@u-jeff ~]# cat /sys/kernel/debug/tracing/kprobe_events 

p:probe/do_sys_open _text+2391184 filename_string=+0(%si):string flags_u32=%dx:u32


第二类就是使用trace event:

比如像linux内核中最著名的函数__schedule()

static void __sched notrace __schedule(bool preempt)

{

  ...

trace_sched_switch(preempt, prev, next);

  ...

}

这种原理不用多说,是内核代码编译的时候就已经内嵌于函数中的,只需打开对应开关就可以看到固定格式的打印信息。


#pwd

/sys/kernel/debug/tracing/events/sched/sched_switch


#cat /sys/kernel/debug/tracing/trace

Linux内核常用的动态调试手段_java_02

系统中大量子系统中的函数都嵌入了这种trace event,只需针对性打开开关便可。

#pwd

/sys/kernel/debug/tracing/events

Linux内核常用的动态调试手段_java_03


还有一些函数里面没有内嵌trace event,或者有时想查看函数的调用堆栈,可以使用trace function + stack_trace

cd /sys/kernel/debug/tracing/

echo 0 > ./tracing_on

echo function > current_tracer

echo 1 > options/func_stack_trace

echo "your function" > set_ftrace_filter

echo 1 > tracing_on


比如把”your function"改成 schedule

cat ./trace   可以看到调用schedule时的函数堆栈,而不需要在函数中加上WARN_ON(1)这样的语句再重新编译内核。

Linux内核常用的动态调试手段_java_04


不管是function tracer还是 function_graph 这样的tracer,

cat ./available_tracers

hwlat blk function_graph wakeup_dl wakeup_rt wakeup function nop

我最佩服的就是实现此功能时同步代码段的方法,其实就是怎样从下面第一幅图变成第二幅图:

图一:

Linux内核常用的动态调试手段_java_05

图二:

Linux内核常用的动态调试手段_java_06


因为实现这些功能的原理就是在代码段函数头中插上预先设定好的函数调用,如果刚刚插入代码时,正好有程序跑到这块代码就好玩了,系统很可能会崩掉,在x86系统上采用的就是先在函数头中插入一个字节指令'cc',当其它进程执行此指令时,系统进入int3中断流程,

所以在do_int3中可以看到下面类似的代码:

Linux内核常用的动态调试手段_java_07

当插入‘cc'之后,把'cc'指令同步到所有的cpu,然后把’cc‘之后的代码换成函数调用代码,最后把’cc'换掉,一切都ok.