函式追蹤器設計¶
- 作者:
Mike Frysinger
注意
本文件已過時。 以下某些描述與當前的實現不符。
簡介¶
在這裡,我們將介紹通用函式追蹤程式碼賴以正常執行的架構元件。 這些內容被分解為逐漸增加的複雜性,因此您可以從簡單開始,並至少獲得基本功能。
請注意,這僅側重於架構實現細節。 如果您想更詳細地瞭解通用程式碼中的某個功能,請檢視通用的 ftrace - 函式追蹤器 檔案。
理想情況下,希望在核心中保持效能並支援追蹤的每個人都應一直支援到動態 ftrace。
先決條件¶
- Ftrace 依賴於以下功能的實現
STACKTRACE_SUPPORT - 實現 save_stack_trace()
TRACE_IRQFLAGS_SUPPORT - 實現 include/asm/irqflags.h
HAVE_FUNCTION_TRACER¶
您將需要實現 mcount 和 ftrace_stub 函式。
確切的 mcount 符號名稱將取決於您的工具鏈。 有些將其稱為 “mcount”,“_mcount”,甚至 “__mcount”。 您可以透過執行類似下面的命令來解決這個問題
$ echo 'main(){}' | gcc -x c -S -o - - -pg | grep mcount
call mcount
在下面的示例中,我們將假設該符號為“mcount”,以使事情變得簡單明瞭。
請記住,mcount 函式中生效的 ABI 是 高度 架構/工具鏈特定的。 我們無法在這方面為您提供幫助,抱歉。 查詢一些舊文件和/或找到比您更熟悉的人來一起討論想法。 通常,暫存器使用(引數/暫存/等等...)是此時的主要問題,尤其是在 mcount 呼叫的位置(函式序言之前/之後)。 您可能還想看看 glibc 如何為您的體系結構實現 mcount 函式。 它可能是(半)相關的。
mcount 函式應檢查函式指標 ftrace_trace_function,以檢視它是否設定為 ftrace_stub。 如果是,則您無需執行任何操作,請立即返回。 如果不是,則以與 mcount 函式通常呼叫 __mcount_internal 相同的方式呼叫該函式 - 第一個引數是“frompc”,而第二個引數是“selfpc”(經過調整以刪除嵌入在函式中的 mcount 呼叫的長度)。
例如,如果函式 foo() 呼叫 bar(),則當 bar() 函式呼叫 mcount() 時,mcount() 將傳遞給 tracer 的引數為
“frompc” - bar() 將用於返回到 foo() 的地址
“selfpc” - bar() 的地址(經過 mcount() 大小調整)
還要記住,此 mcount 函式將被呼叫 很多 次,因此,在停用跟蹤時,針對沒有跟蹤器的預設情況進行最佳化將有助於您的系統平穩執行。 因此,mcount 函式的開頭通常是檢查事物之前的最小裸程式碼。 這也意味著程式碼流通常應保持線性(即,在 nop 情況下沒有分支)。 這當然是一種最佳化,而不是硬性要求。
以下是一些虛擬碼,應該有所幫助(這些函式實際上應該用匯編語言實現)
void ftrace_stub(void)
{
return;
}
void mcount(void)
{
/* save any bare state needed in order to do initial checking */
extern void (*ftrace_trace_function)(unsigned long, unsigned long);
if (ftrace_trace_function != ftrace_stub)
goto do_trace;
/* restore any bare state */
return;
do_trace:
/* save all state needed by the ABI (see paragraph above) */
unsigned long frompc = ...;
unsigned long selfpc = <return address> - MCOUNT_INSN_SIZE;
ftrace_trace_function(frompc, selfpc);
/* restore all state needed by the ABI */
}
不要忘記為模組匯出 mcount!
extern void mcount(void);
EXPORT_SYMBOL(mcount);
HAVE_FUNCTION_GRAPH_TRACER¶
深呼吸... 是時候做一些真正的工作了。 在這裡,您需要更新 mcount 函式以檢查 ftrace 圖函式指標,以及實現一些函式來儲存(劫持)和恢復返回地址。
mcount 函式應檢查函式指標 ftrace_graph_return(與 ftrace_stub 比較)和 ftrace_graph_entry(與 ftrace_graph_entry_stub 比較)。 如果這兩個指標都沒有設定為相關的存根函式,則呼叫特定於架構的函式 ftrace_graph_caller,該函式反過來又呼叫特定於架構的函式 prepare_ftrace_return。 這些函式名稱都不是嚴格要求的,但是您仍然應該使用它們以保持跨架構埠的一致性 - 更容易比較和對比事物。
傳遞給 prepare_ftrace_return 的引數與傳遞給 ftrace_trace_function 的引數略有不同。 第二個引數“selfpc”是相同的,但是第一個引數應該是指向“frompc”的指標。 通常,它位於堆疊上。 這樣,該函式可以臨時劫持返回地址,以使其指向特定於架構的函式 return_to_handler。 該函式將僅呼叫通用 ftrace_return_to_handler 函式,該函式將返回原始的返回地址,您可以使用該地址返回到原始呼叫站點。
這是更新後的 mcount 虛擬碼
void mcount(void)
{
...
if (ftrace_trace_function != ftrace_stub)
goto do_trace;
+#ifdef CONFIG_FUNCTION_GRAPH_TRACER
+ extern void (*ftrace_graph_return)(...);
+ extern void (*ftrace_graph_entry)(...);
+ if (ftrace_graph_return != ftrace_stub ||
+ ftrace_graph_entry != ftrace_graph_entry_stub)
+ ftrace_graph_caller();
+#endif
/* restore any bare state */
...
這是新的 ftrace_graph_caller 彙編函式的虛擬碼
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
void ftrace_graph_caller(void)
{
/* save all state needed by the ABI */
unsigned long *frompc = &...;
unsigned long selfpc = <return address> - MCOUNT_INSN_SIZE;
/* passing frame pointer up is optional -- see below */
prepare_ftrace_return(frompc, selfpc, frame_pointer);
/* restore all state needed by the ABI */
}
#endif
有關如何實現 prepare_ftrace_return() 的資訊,只需檢視 x86 版本(幀指標傳遞是可選的;有關更多資訊,請參見下一節)。 其中唯一特定於體系結構的部分是故障恢復表的設定(asm(...) 程式碼)。 其餘程式碼在不同的體系結構中應相同。
這是新的 return_to_handler 彙編函式的虛擬碼。 請注意,此處應用的 ABI 與應用於 mcount 程式碼的 ABI 不同。 由於您是從一個函式返回(在函式尾聲之後),因此您可能會跳過儲存/恢復的內容(通常只是用於傳遞返回值的暫存器)。
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
void return_to_handler(void)
{
/* save all state needed by the ABI (see paragraph above) */
void (*original_return_point)(void) = ftrace_return_to_handler();
/* restore all state needed by the ABI */
/* this is usually either a return or a jump */
original_return_point();
}
#endif
HAVE_FUNCTION_GRAPH_FP_TEST¶
架構可以將一個唯一值(幀指標)傳遞到函式的進入和退出。 在退出時,將比較該值,如果該值不匹配,則核心將發生 panic。 這很大程度上是對 gcc 生成的錯誤程式碼的健全性檢查。 如果您的埠的 gcc 可以在不同的最佳化級別下正確地更新幀指標,請忽略此選項。
但是,新增對其的支援並不十分困難。 在呼叫 prepare_ftrace_return() 的彙編程式碼中,將幀指標作為第三個引數傳遞。 然後,在該函式的 C 版本中,執行 x86 埠所做的事情,並將其傳遞給 ftrace_push_return_trace(),而不是傳遞存根值 0。
同樣,當您呼叫 ftrace_return_to_handler() 時,請將幀指標傳遞給它。
HAVE_SYSCALL_TRACEPOINTS¶
您只需要很少的東西就可以在架構中進行系統呼叫追蹤。
支援 HAVE_ARCH_TRACEHOOK(請參見 arch/Kconfig)。
在 <asm/unistd.h> 中具有 NR_syscalls 變數,該變數提供了架構支援的系統呼叫數量。
支援 TIF_SYSCALL_TRACEPOINT 執行緒標誌。
將來自 ptrace 的 trace_sys_enter() 和 trace_sys_exit() 追蹤點呼叫放入 ptrace 系統呼叫追蹤路徑中。
如果此架構上的系統呼叫表比系統呼叫的簡單地址陣列更復雜,請實現 arch_syscall_addr 以返回給定系統呼叫的地址。
如果此架構上的系統呼叫的符號名稱與函式名稱不匹配,請在 asm/ftrace.h 中定義 ARCH_HAS_SYSCALL_MATCH_SYM_NAME,並實現 arch_syscall_match_sym_name,並使用適當的邏輯來返回 true(如果函式名稱與符號名稱相對應)。
將此架構標記為 HAVE_SYSCALL_TRACEPOINTS。
HAVE_FTRACE_MCOUNT_RECORD¶
有關更多資訊,請參見 scripts/recordmcount.pl。 只需填寫特定於架構的詳細資訊,以瞭解如何透過 objdump 查詢 mcount 呼叫站點的地址。 如果沒有同時實現動態 ftrace,則此選項沒有多大意義。
HAVE_DYNAMIC_FTRACE¶
您將首先需要 HAVE_FTRACE_MCOUNT_RECORD 和 HAVE_FUNCTION_TRACER,因此,如果您過於渴望,請向上滾動您的閱讀器。
- 完成這些之後,您將需要實現
- asm/ftrace.h
MCOUNT_ADDR
ftrace_call_adjust()
struct dyn_arch_ftrace{}
- 彙編程式碼
mcount() (新的存根)
ftrace_caller()
ftrace_call()
ftrace_stub()
- C 程式碼
ftrace_dyn_arch_init()
ftrace_make_nop()
ftrace_make_call()
ftrace_update_ftrace_func()
首先,您需要在 asm/ftrace.h 中填寫一些架構詳細資訊。
將 MCOUNT_ADDR 定義為 mcount 符號的地址,類似於
#define MCOUNT_ADDR ((unsigned long)mcount)
由於沒有人會擁有該函式的宣告,因此您需要
extern void mcount(void);
您還將需要輔助函式 ftrace_call_adjust()。 大多數人都可以像這樣將其存根輸出
static inline unsigned long ftrace_call_adjust(unsigned long addr)
{
return addr;
}
<要填寫的詳細資訊>
最後,您將需要自定義的 dyn_arch_ftrace 結構。 如果您在執行時修補任意呼叫站點時需要一些額外的狀態,那麼這裡就是存放的地方。 但是,現在,建立一個空的結構
struct dyn_arch_ftrace {
/* No extra data needed */
};
在標頭檔案完成之後,我們可以填寫彙編程式碼。 雖然我們之前已經建立了一個 mcount() 函式,但是動態 ftrace 只需要一個存根函式。 這是因為 mcount() 僅在啟動期間使用,然後所有對它的引用都將被修補掉,永遠不會返回。 相反,舊的 mcount() 的內部結構將用於建立一個新的 ftrace_caller() 函式。 由於兩者很難合併,因此最好透過 #ifdef 將兩個單獨的定義分開。 ftrace_stub() 也是如此,因為它現在將內聯在 ftrace_caller() 中。
在我們更加困惑之前,讓我們看一下一些虛擬碼,以便您可以用匯編語言實現自己的東西
void mcount(void)
{
return;
}
void ftrace_caller(void)
{
/* save all state needed by the ABI (see paragraph above) */
unsigned long frompc = ...;
unsigned long selfpc = <return address> - MCOUNT_INSN_SIZE;
ftrace_call:
ftrace_stub(frompc, selfpc);
/* restore all state needed by the ABI */
ftrace_stub:
return;
}
乍一看,這可能有點奇怪,但請記住,我們將執行時修補多件事。 首先,只有我們實際想要跟蹤的函式才會被修補以呼叫 ftrace_caller()。 其次,由於我們一次只有一個 tracer 處於活動狀態,因此我們將修補 ftrace_caller() 函式本身以呼叫有問題的特定 tracer。 這就是 ftrace_call 標籤的意義所在。
考慮到這一點,讓我們轉到實際將進行執行時修補的 C 程式碼。 為了能夠透過下一節,您需要了解您架構的指令程式碼。
每個架構都有一個 init 回撥函式。 如果您需要在早期執行某些操作來初始化某些狀態,那麼這就是時候了。 否則,對於大多數人來說,以下簡單函式應該足夠了
int __init ftrace_dyn_arch_init(void)
{
return 0;
}
有兩個函式用於對任意函式進行執行時修補。 第一個用於將 mcount 呼叫站點轉換為 nop(這有助於我們在不跟蹤時保持執行時效能)。 第二個用於將 mcount 呼叫站點轉換為對任意位置的呼叫(但通常是 ftracer_caller())。 有關這些函式的常規函式定義,請參見 linux/ftrace.h
ftrace_make_nop()
ftrace_make_call()
rec->ip 值是 mcount 呼叫站點的地址,該地址由 build time 期間的 scripts/recordmcount.pl 收集。
最後一個函式用於對活動 tracer 進行執行時修補。 這將修改 ftrace_caller() 函式內 ftrace_call 符號位置處的彙編程式碼。 因此,您應該在該位置具有足夠的填充,以支援您將插入的新的函式呼叫。 有些人將使用“call”型別的指令,而另一些人將使用“branch”型別的指令。 具體來說,該函式是
ftrace_update_ftrace_func()
HAVE_DYNAMIC_FTRACE + HAVE_FUNCTION_GRAPH_TRACER¶
函式圖需要進行一些調整才能與動態 ftrace 一起使用。 基本上,您將需要
- 更新
ftrace_caller()
ftrace_graph_call()
ftrace_graph_caller()
- 實現
ftrace_enable_ftrace_graph_caller()
ftrace_disable_ftrace_graph_caller()
<要填寫的詳細資訊>
快速筆記
在名為 ftrace_graph_call 的 ftrace_call 位置之後新增一個 nop 存根; 存根需要足夠大以支援對 ftrace_graph_caller() 的呼叫
更新 ftrace_graph_caller() 以使其能夠透過新的 ftrace_caller() 進行呼叫,因為某些語義可能已更改
ftrace_enable_ftrace_graph_caller() 將使用對 ftrace_graph_caller() 的呼叫執行時修補 ftrace_graph_call 位置
ftrace_disable_ftrace_graph_caller() 將使用 nops 執行時修補 ftrace_graph_call 位置