函式追蹤器設計

作者:

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 位置