使用 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 的函數來實現的。

以上,如果感覺哪里有問題,還請不吝賜教~

最后源碼地址:

如果感覺有幫助的話還煩請給兄弟點個星星,謝謝大佬們~


歡迎長按下面的二維碼關注雲影原生生公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM