使用 ftrace 实现一个跟踪任意命令内核函数的小工具【转】

转自:https://zhuanlan.zhihu.com/p/457795074

ftrace 是啥

简介


ftrace 是基于 Linux 中 tracefs 实现的一种可以用来追踪内核函数执行时间、调用关系、调用堆栈等信息的文件系统。

Linux 中可通过 cat /proc/filesystems 查看系统支持的文件系统都有哪些,可以看到其中 tracefs 就是一种 nodev(基于内存) 的文件系统。

然后通过 mount 命令可以查看到 tracefs 可以查看到系统自动将 tracefs 给挂载到了 /sys/kernel/tracing 和 /sys/kerne/debug/tracing 目录下,这俩目录的作用基本上是一样的


ftrace 尝鲜使用

常用配置简介


在上面说的两个目录下有很多和 ftrace 相关的文件,这些都是 ftrace 工具在追踪内核时可能会用到的内核参数

其中比较常使用的是:

current_tracer:这个是用来决定要使用哪种 tracer 来跟踪内核。用户可以自己配置的 tracer 可在 avaliable_tracers 文件中看到

每种 tracer 跟踪函数时候的效果都不一样,比如 function 可以用来追踪系统函数调用,function_graph 可以用来追踪函数调用同时还可以把调用关系以及函数执行时间给获取到等。

set_ftrace_filter:这也是个常用的配置,它可以用来配置只追踪哪些函数,支持通配符,比如对其 echo *icmp* > set_ftrace_filter 的话,那就可以只追踪和 icmp 相关的函数。

set_ftrace_pid:该选项很好用,用来指定要追踪哪个进程,只需要将想要追踪的进程的 pid 给写进去,那就只追踪该进程相关的内核调用函数。如果不指定该配置的话,那 tracer 会把所有进程的内核调用都给抓到,最后的输出结果就会贼大。

tracing_on:该选项用来决定是否开启 tracer 功能,配置为 0 则不开启,配置为 1 则为开启。

tracer:该文件不是配置项,但是通过 fstrace 追踪到的函数调用结果会输出到这个文件中,这个文件就相当于一个缓冲区,当超过缓冲区大小后,新的结果会覆盖旧的结果。

available_filter_functions:该文件中记录了操作系统可以追踪的所有函数。

available_events:该文件中记录可以追踪哪些事件,比如像 socket 的 send 或者 recv 之类的系统调用等算是事件,都可以被追踪到。

 

ftrace 使用

我们以 function_graph 这个 tracer 为例,尝试通过该 tracer 抓取 icmp 相关的内核函数调用。

首先设置要使用的 tracer

echo function_graph > current_tracer

然后设置要过滤的相关函数

echo *icmp* > set_ftrace_filter

最后开启 tracer 功能

echo 1 > tracing_on

然后我们尝试一下

ping 8.8.8.8 -c 1

什么也没发生,这是因为上面我们说过,tracer 跟踪的结果会被输出到 trace 这个文件中,因此我们可以查看一下这个文件

由于输出可能太多了,所以我们只查看前 20 行,可以发现已经把和 icmp 相关的函数跟踪到了


使用 ftrace 追踪任意命令行

简介

其实说是“追踪任意命令”,但实际上也只能是追踪大部分命令,仍然有一些无法被追踪到。

我们在上面配置项介绍中也说过了,在 tracing 目录下有个个 avaliable_filter_functions 文件,里面记录了可以被追踪到的所有函数。那么为什么不是所有函数都能被追踪到呢,这个就和 ftrace 工具的实现有关了。

由于 ftrace 的实现还挺复杂的,真的往下深挖还会涉及到内核编译和汇编的知识,我比较菜,没办法从根儿上把 tracefs 实现原理一行行解释得特别清楚,这里就简单和大家说一下大概原理(个人理解,也可能有误,如果感觉哪儿不对还请评论指出)。

 

ftrace 原理简介

  1. 在 Linux 编译的时候,内核会给可追踪的函数头部插入一个钩子函数。
  2. 如果在开启了 tracing_on 的情况下调用了某个函数就会触发这个钩子函数。
  3. 然后这个钩子会检测 current_tracer 文件中被配置了哪个 tracer。
  4. 动态地将函数头部的钩子替换为对应的 tracer 的代码段,以此来运行对应的 tracer 函数。
  5. 对于 function 这个 tracer 来讲,只需要把被跟踪的函数头部的代码段替换成 function 这个 tracer 就好。
  6. 不过对于像类似 function_graph 这种还需要记录函数运行时间的 tracer,除了替换头部的代码段,还需要将函数结束 return 时的部分也动态替换一些代码段,以此达到计算函数运行时间的目的。

了解了大概原理,我们就大概可以知道哪些函数不能被追踪了。

由于是在函数头部做动态替换代码段,所以使用 inline 的内联函数是无法被追踪的,因为它已经被嵌入到函数体中了。

另外 tracefs 自己实现的 trace 相关函数也不能被追踪,要是他们也在头部插入钩子的话,那不就死循环了么。

 

实现追踪命令

行了,关于 ftrace 的实现原理就点到为止吧,再深入的话兄弟我也就黔驴技穷了。接下来我们尝试实现一个脚本,该脚本的作用就是可以用来追踪某个命令行内核底层函数的执行过程。

首先我们这个脚本主要通过 function_graph 这个 tracer 来跟踪,当然你也可以自己把这个脚本做成通过参数来使用任意 tracer。

#!/bin/bash

# 获取时间戳
CURRENT_TIMESTAMP=`date +%s`
DPATH="/sys/kernel/debug/tracing"
TEMP="$( cd "$( dirname "$0"  )" && pwd  )"
TEMP_TRACE_PATH=$TEMP/temp-log.txt
TEMP_CMD_PATH=$TEMP/trace-tmp-$CURRENT_TIMESTAMP

rm -rf $TEMP_TRACE_PATH

# 获取要执行的命令
CURRENT_CMD=$@

# 设置 trace
# 先把之前的日志缓存清掉
echo /dev/null > $DPATH/trace
# 先设置 current_tracer 为 nop, nop 就是不使用任何 tracer
echo nop > $DPATH/current_tracer
# 先关掉 tracer 功能
echo 0 > $DPATH/tracing_on
# 设置要使用哪种 trace
echo function_graph > $DPATH/current_tracer

# 创建一个新的临时脚本用来执行命令
# 把要执行的脚本的 PID 设置给 tracer
echo "echo \$\$ > $DPATH/set_ftrace_pid" > $TEMP_CMD_PATH
echo "echo \"当前进程是 \$\$\"" >> $TEMP_CMD_PATH
# 启动 tracer
echo "echo 1 > $DPATH/tracing_on" >> $TEMP_CMD_PATH
# 把参数当成命令执行
echo "exec \"\$@\"" >> $TEMP_CMD_PATH

# 加权
chmod u+x $TEMP_CMD_PATH
# 执行脚本并把命令传进去
$TEMP_CMD_PATH $CURRENT_CMD

# 输出 trace 日志
`cat /$DPATH/trace > $TEMP_TRACE_PATH`
rm -rf $TEMP_CMD_PATH
echo -e "\033[32m 输出 trace 日志路径是 $TEMP_TRACE_PATH \033[0m"
# echo "输出 trace 日志路径是 $TEMP_TRACE_PATH"
echo 0 > $DPATH/tracing_on
echo nop > $DPATH/current_tracer

脚本非常简单,大体流程就是先把 tracer 功能关闭,以防在执行脚本过程中抓了一堆不需要的日志。

然后将 function_graph 给配置进 current_tracer

之后创建一个新的文件,在这个新的文件中把 "$$",也就是当前进程的 pid 号给设置到 set_ftrace_pid 中,这样就可以只抓取当前进程的相关函数调用了。

之后启动 tracer,然后执行命令,$@ 表示执行该脚本时传进来的参数。

当执行完命令之后,读取 trace 文件,把日志输出到某个路径下,最后关掉 tracer。

大概就是这么简单。

这里我们说一下为啥一定要先创建一个临时文件,然后在这个临时文件中执行命令。

这是因为 shell 脚本中大概有三种方式可以执行命令,第一种通过 exec,第二种通过 source,第三种通过 fork 执行。

其中通过 exec "$@" 这种方式执行的话,当执行完 $@ 该进程就会马上退出了,因此不会给我们留下读取 trace 文件的机会。

使用 source 虽然不会直接退出当前进程,但是 source 执行时的 $$ 也就是进程 pid 是和主进程共享的,也就是说,就算通过 source 一个文件,在这个文件里头执行 exec "$@" 的话,也会直接退出,没有读取 trace 文件的机会。

所以我们只能通过 fork 的方式执行。fork 的方式也很简单,直接创建个新的文件,在这个文件中执行 exec 就好了,然后通过点儿 ".${文件名儿}" 的方式执行该临时文件。

我们来试用一下

然后查看输出的文件

尝试搜索 icmp 相关函数,可以看到确实已经获取到了 icmp 相关的函数了。

不过令人汗颜的是,该日志文件贼大,可能会有好几万行

这也说明了 linux 的内核实现确实真的很负责。因为虽然只是个简单的 ping 命令,底层却要从完整的网络协议栈中走一个来回儿。

所以为了让输出文件能够更精简一些,或者说只让其输出我们感兴趣的函数,这样在追踪的时候更加利于我们平时排查问题。

所以我们给脚本加一个通过参数指定要过滤哪些函数的功能。

#!/bin/bash

# 获取时间戳
CURRENT_TIMESTAMP=`date +%s`
DPATH="/sys/kernel/tracing"
TEMP="$( cd "$( dirname "$0"  )" && pwd  )"
TEMP_TRACE_PATH=$TEMP/temp-log.txt
TEMP_CMD_PATH=$TEMP/trace-tmp-$CURRENT_TIMESTAMP
CMD_OPERATIONS="--"

rm -rf $TEMP_TRACE_PATH

if [[ ! $* =~ $CMD_OPERATIONS ]]
then
  echo "需要通过 $CMD_OPERATIONS 指定命令参数"
  exit 0
fi

getFilterParams() {
  for arg in $*                   
  do
    array=(${arg//=/ })
    _TEMP_KEY=${array[@]:0:1}
    if [ $_TEMP_KEY = "filter" ]; then
      echo ${array[@]:1:2}
    fi
  done
}

getCommand() {
  _TEMP_PARAMS=$*
  _TEMP_CMD=${_TEMP_PARAMS#*$CMD_OPERATIONS}
  echo $_TEMP_CMD
}

# 尝试获取过滤用的参数
FILTER_PARAMS=$(getFilterParams $*)
if [ "$FILTER_PARAMS" ]; then
  echo "tracer 过滤条件是: $FILTER_PARAMS"
fi

# 获取要执行的命令
CURRENT_CMD=$(getCommand $*)
echo "tracer 要执行的命令是: $CURRENT_CMD"
if [ ! -n "$CURRENT_CMD" ]; then
  echo "至少需要一个 cmd 参数"
  exit 0
fi

# 设置 trace
echo "" > $DPATH/trace
echo nop > $DPATH/current_tracer
echo 0 > $DPATH/tracing_on
# echo "" > $DPATH/set_ftrace_filter
# 设置要使用哪种 trace
echo function_graph > $DPATH/current_tracer
# 设置过滤条件, 如果有的话
if [ "$FILTER_PARAMS" ]; then
  echo $FILTER_PARAMS > $DPATH/set_ftrace_filter
fi

# 创建一个新的临时脚本用来执行命令
# 把要执行的脚本的 PID 设置给 tracer
echo "echo \$\$ > $DPATH/set_ftrace_pid" > $TEMP_CMD_PATH
echo "echo \"当前进程是 \$\$\"" >> $TEMP_CMD_PATH
# 启动 tracer
echo "echo 1 > $DPATH/tracing_on" >> $TEMP_CMD_PATH
echo "exec \"\$@\"" >> $TEMP_CMD_PATH

# 加权
chmod u+x $TEMP_CMD_PATH
# 执行脚本并把命令传进去
$TEMP_CMD_PATH $CURRENT_CMD

# 输出 trace 日志
`cat $DPATH/trace > $TEMP_TRACE_PATH`
rm -rf $TEMP_CMD_PATH
echo -e "\033[32m 输出 trace 日志路径是 $TEMP_TRACE_PATH \033[0m"
# echo "输出 trace 日志路径是 $TEMP_TRACE_PATH"
echo "" > $DPATH/set_ftrace_filter
echo 0 > $DPATH/tracing_on
echo nop > $DPATH/current_tracer

其实主要增加的代码就是在真正执行 exec "$@" 之前增加一条给 set_ftrace_filter 配置成用户希望 filter 出来的函数。

然后我们来尝试一下

为了区分出命令和参数,我们将命令行加在 "--" 后面,然后通过 filter 参数指定要过滤哪些相关函数

然后来看下输出结果

现在结果中就只包含了和 icmp 相关的函数输出了。


总结

ftrace 提供了追踪内核函数的能力,可以在 /sys/kernel/tracing 目录或者 /sys/kernel/debug/tracing 目录下找到 tracefs 的一些可配置文件。

通过对这些可配置文件进行读写就能了解以及影响到内核参数,从而对内核中支持追踪的函数进行追踪。

然后我们自己实现的追踪命令行工具的主要原理就是通过设置 ftrace 要跟踪的 pid 以及要 filter 的函数来实现的。

以上,如果感觉哪里有问题,还请不吝赐教~

最后源码地址:

如果感觉有帮助的话还烦请给兄弟点个星星,谢谢大佬们~


欢迎长按下面的二维码关注云影原生生公众号

posted @ 2022-01-24 23:34  Sky&Zhang  阅读(1112)  评论(0编辑  收藏  举报