首頁 > 軟體

ARMv8 上的 kprobes 事件跟蹤

2020-06-16 17:00:09

介紹

kprobes 是一種核心功能,它允許通過在執行(或模擬)斷點指令之前和之後,設定呼叫開發者提供例程的任意斷點來檢測核心。可參見 kprobes 文件註1 獲取更多資訊。基本的 kprobes 功能可使用 CONFIG_KPROBEES 來選擇。在 arm64 的 v4.8 核心發行版中, kprobes 支援被新增到主線。

在這篇文章中,我們將介紹 kprobes 在 arm64 上的使用,通過在命令列中使用 debugfs 事件追蹤介面來收集動態追蹤事件。這個功能在一些架構(包括 arm32)上可用已經有段時間,現在在 arm64 上也能使用了。這個功能可以無需編寫任何程式碼就能使用 kprobes。

探針型別

kprobes 子系統提供了三種不同型別的動態探針,如下所述。

kprobes

基本探針是 kprobes 插入的一個軟體斷點,用以替代你正在探測的指令,當探測點被命中時,它為最終的單步執行(或模擬)儲存下原始指令。

kretprobes

kretprobes 是 kprobes 的一部分,它允許攔截返回函數,而不必在返回點設定一個探針(或者可能有多個探針)。對於支援的架構(包括 ARMv8),只要選擇 kprobes,就可以選擇此功能。

jprobes

jprobes 允許通過提供一個具有相同呼叫簽名call signature的中間函數來攔截對一個函數的呼叫,這裡中間函數將被首先呼叫。jprobes 只是一個程式設計介面,它不能通過 debugfs 事件追蹤子系統來使用。因此,我們將不會在這裡進一步討論 jprobes。如果你想使用 jprobes,請參考 kprobes 文件。

呼叫 kprobes

kprobes 提供一系列能從核心程式碼中呼叫的 API 來設定探測點和當探測點被命中時呼叫的註冊函數。在不往核心中新增程式碼的情況下,kprobes 也是可用的,這是通過寫入特定事件追蹤的 debugfs 檔案來實現的,需要在檔案中設定探針地址和資訊,以便在探針被命中時記錄到追蹤紀錄檔中。後者是本文將要討論的重點。最後 kprobes 可以通過 perl 命令來使用。

kprobes API

核心開發人員可以在核心中編寫函數(通常在專用的偵錯模組中完成)來設定探測點,並且在探測指令執行前和執行後立即執行任何所需操作。這在 kprobes.txt 中有很好的解釋。

事件追蹤

事件追蹤子系統有自己的自己的文件註2 ,對於了解一般追蹤事件的背景可能值得一讀。事件追蹤子系統是追蹤點tracepoints和 kprobes 事件追蹤的基礎。事件追蹤文件重點關注追蹤點,所以請在查閱文件時記住這一點。kprobes 與追蹤點不同的是沒有預定義的追蹤點列表,而是採用動態建立的用於觸發追蹤事件資訊收集的任意探測點。事件追蹤子系統通過一系列 debugfs 檔案來控制和監視。事件追蹤(CONFIG_EVENT_TRACING)將在被如 kprobe 事件追蹤子系統等需要時自動選擇。

kprobes 事件

使用 kprobes 事件追蹤子系統,使用者可以在核心任意斷點處指定要報告的資訊,只需要指定任意現有可探測指令的地址以及格式化資訊即可確定。在執行過程中遇到斷點時,kprobes 將所請求的資訊傳遞給事件追蹤子系統的公共部分,這些部分將資料格式化並追加到追蹤紀錄檔中,就像追蹤點的工作方式一樣。kprobes 使用一個類似的但是大部分是獨立的 debugfs 檔案來控制和顯示追蹤事件資訊。該功能可使用 CONFIG_KPROBE_EVENT 來選擇。Kprobetrace 文件^ 註3 提供了如何使用 kprobes 事件追蹤的基本資訊,並且應當被參考用以了解以下介紹範例的詳細資訊。

kprobes 和 perf

perf 工具為 kprobes 提供了另一個命令列介面。特別地,perf probe 允許探測點除了由函數名加偏移量和地址指定外,還可由原始檔和行號指定。perf 介面實際上是使用 kprobes 的 debugfs 介面的封裝器。

Arm64 kprobes

上述所有 kprobes 的方面現在都在 arm64 上得到實現,然而實際上與其它架構上的有一些不同:

  • 註冊名稱引數當然是依架構而特定的,並且可以在 ARM ARM 中找到。
  • 目前不是所有的指令型別都可被探測。當前不可探測的指令包括 mrs/msr(除了 DAIF 讀取)、異常生成指令、eret 和 hint(除了 nop 變體)。在這些情況下,只探測一個附近的指令來代替是最簡單的。這些指令在探測的黑名單裡是因為在 kprobes 單步執行或者指令模擬時它們對處理器狀態造成的改變是不安全的,這是由於 kprobes 構造的單步執行上下文和指令所需要的不一致,或者是由於指令不能容忍在 kprobes 中額外的處理時間和例外處理(ldx/stx)。
  • 試圖識別在 ldx/stx 序列中的指令並且防止探測,但是理論上這種檢查可能會失敗,導致允許探測到的原子序列永遠不會成功。當探測原子程式碼序列附近時應該小心。
  • 注意由於 linux ARM64 呼叫約定的具體資訊,為探測函數可靠地複製棧幀是不可能的,基於此不要試圖用 jprobes 這樣做,這一點與支援 jprobes 的大多數其它架構不同。這樣的原因是被呼叫者沒有足夠的資訊來確定需要的棧數量。
  • 注意當探針被命中時,一個探針記錄的棧指標資訊將反映出使用中的特定棧指標,它是核心棧指標或者中斷棧指標。
  • 有一組核心函數是不能被探測的,通常因為它們作為 kprobes 處理的一部分被呼叫。這組函數的一部分是依架構特定的,並且也包含如異常入口程式碼等。

使用 kprobes 事件追蹤

kprobes 的一個常用例子是檢測函數入口和/或出口。因為只需要使用函數名來作為探針地址,它安裝探針特別簡單。kprobes 事件追蹤將檢視符號名稱並且確定地址。ARMv8 呼叫標準定義了函數引數和返回值的位置,並且這些可以作為 kprobes 事件處理的一部分被列印出來。

例子: 函數入口探測

檢測 USB 乙太網驅動程式復位功能:

  1. $ pwd
  2. /sys/kernel/debug/tracing
  3. $ cat> kprobe_events <<EOF
  4. p ax88772_reset %x0
  5. EOF
  6. $ echo1> events/kprobes/enable

此時每次該驅動的 ax8872_reset() 函數被呼叫,追蹤事件都將會被記錄。這個事件將顯示指向通過作為此函數的唯一引數的 X0(按照 ARMv8 呼叫標準)傳入的 usbnet 結構的指標。插入需要乙太網驅動程式的 USB 加密狗後,我們看見以下追蹤資訊:

  1. $ cat trace
  2. # tracer: nop
  3. #
  4. # entries-in-buffer/entries-written:1/1#P:8
  5. #
  6. # _—–=> irqs-off
  7. #/ _—-=> need-resched
  8. #|/ _—=> hardirq/softirq
  9. #||/ _–=> preempt-depth
  10. #|||/ delay
  11. # TASK-PID CPU#|||| TIMESTAMP FUNCTION
  12. #|||||||||
  13. kworker/0:0-4[000] d10972.102939: p_ax88772_reset_0:
  14. (ax88772_reset+0x0/0x230) arg1=0xffff800064824c80

這裡我們可以看見傳入到我們的探測函數的指標引數的值。由於我們沒有使用 kprobes 事件追蹤的可選標籤功能,我們需要的資訊自動被標註為 arg1。注意這指向我們需要 kprobes 記錄這個探針的一組值的第??個,而不是函數引數的實際位置。在這個例子中它也只是碰巧是我們探測函數的第一個引數。

例子: 函數入口和返回探測

kretprobe 功能專門用於探測函數返回。在函數入口 kprobes 子系統將會被呼叫並且建立勾點以便在函數返回時呼叫,勾點將記錄需求事件資訊。對最常見情況,返回資訊通常在 X0 暫存器中,這是非常有用的。在 %x0 中返回值也可以被稱為 $retval。以下例子也演示了如何提供一個可讀的標籤來展示有趣的資訊。

使用 kprobes 和 kretprobe 檢測核心 do_fork() 函數來記錄引數和結果的例子:

  1. $ cd/sys/kernel/debug/tracing
  2. $ cat> kprobe_events <<EOF
  3. p _do_fork %x0 %x1 %x2 %x3 %x4 %x5
  4. r _do_fork pid=%x0
  5. EOF
  6. $ echo1> events/kprobes/enable

此時每次對 _do_fork() 的呼叫都會產生兩個記錄到 trace 檔案的 kprobe 事件,一個報告呼叫引數值,另一個報告返回值。返回值在 trace 檔案中將被標記為 pid。這裡是三次 fork 系統呼叫執行後的 trace 檔案的內容:

  1. _$ cat trace
  2. # tracer: nop
  3. #
  4. # entries-in-buffer/entries-written:6/6#P:8
  5. #
  6. # _—–=> irqs-off
  7. #/ _—-=> need-resched
  8. #|/ _—=> hardirq/softirq
  9. #||/ _–=> preempt-depth
  10. #|||/ delay
  11. # TASK-PID CPU#|||| TIMESTAMP FUNCTION
  12. #|||||||||
  13. bash-1671[001] d204.946007: p__do_fork_0:(_do_fork+0x0/0x3e4) arg1=0x1200011 arg2=0x0 arg3=0x0 arg4=0x0 arg5=0xffff78b690d0 arg6=0x0
  14. bash-1671[001] d..1204.946391: r__do_fork_0:(SyS_clone+0x18/0x20<- _do_fork) pid=0x724
  15. bash-1671[001] d208.845749: p__do_fork_0:(_do_fork+0x0/0x3e4) arg1=0x1200011 arg2=0x0 arg3=0x0 arg4=0x0 arg5=0xffff78b690d0 arg6=0x0
  16. bash-1671[001] d..1208.846127: r__do_fork_0:(SyS_clone+0x18/0x20<- _do_fork) pid=0x725
  17. bash-1671[001] d214.401604: p__do_fork_0:(_do_fork+0x0/0x3e4) arg1=0x1200011 arg2=0x0 arg3=0x0 arg4=0x0 arg5=0xffff78b690d0 arg6=0x0
  18. bash-1671[001] d..1214.401975: r__do_fork_0:(SyS_clone+0x18/0x20<- _do_fork) pid=0x726_

例子: 解除參照指標引數

對於指標值,kprobes 事件處理子系統也允許解除參照和列印所需的記憶體內容,適用於各種基本資料型別。為了展示所需欄位,手動計算結構的偏移量是必要的。

檢測 _do_wait() 函數:

  1. $ cat> kprobe_events <<EOF
  2. p:wait_p do_wait wo_type=+0(%x0):u32 wo_flags=+4(%x0):u32
  3. r:wait_r do_wait $retval
  4. EOF
  5. $ echo1> events/kprobes/enable

注意在第一個探針中使用的引數標籤是可選的,並且可用於更清晰地識別記錄在追蹤紀錄檔中的資訊。帶符號的偏移量和括號表明了暫存器引數是指向記錄在追蹤紀錄檔中的記憶體內容的指標。:u32 表明了記憶體位置包含一個無符號的 4 位元組寬的資料(在這個例子中指區域性定義的結構中的一個 emum 和一個 int)。

探針標籤(冒號後)是可選的,並且將用來識別紀錄檔中的探針。對每個探針來說標籤必須是獨一無二的。如果沒有指定,將從附近的符號名稱自動生成一個有用的標籤,如前面的例子所示。

也要注意 $retval 引數可以只是指定為 %x0

這裡是兩次 fork 系統呼叫執行後的 trace 檔案的內容:

  1. $ cat trace
  2. # tracer: nop
  3. #
  4. # entries-in-buffer/entries-written:4/4#P:8
  5. #
  6. # _—–=> irqs-off
  7. #/ _—-=> need-resched
  8. #|/ _—=> hardirq/softirq
  9. #||/ _–=> preempt-depth
  10. #|||/ delay
  11. # TASK-PID CPU#|||| TIMESTAMP FUNCTION
  12. #|||||||||
  13. bash-1702[001] d175.342074: wait_p:(do_wait+0x0/0x260) wo_type=0x3 wo_flags=0xe
  14. bash-1702[002] d..1175.347236: wait_r:(SyS_wait4+0x74/0xe4<- do_wait) arg1=0x757
  15. bash-1702[002] d175.347337: wait_p:(do_wait+0x0/0x260) wo_type=0x3 wo_flags=0xf
  16. bash-1702[002] d..1175.347349: wait_r:(SyS_wait4+0x74/0xe4<- do_wait) arg1=0xfffffffffffffff6

例子: 探測任意指令地址

在前面的例子中,我們已經為函數的入口和出口插入探針,然而探測一個任意指令(除少數例外)是可能的。如果我們正在 C 函數中放置一個探針,第一步是檢視程式碼的組合版本以確定我們要放置探針的位置。一種方法是在 vmlinux 檔案上使用 gdb,並在要放置探針的函數中展示指令。下面是一個在 arch/arm64/kernel/modules.cmodule_alloc 函數執行此操作的範例。在這種情況下,因為 gdb 似乎更喜歡使用弱符號定義,並且它是與這個函數關聯的存根程式碼,所以我們從 System.map 中來獲取符號值:

  1. $ grep module_alloc System.map
  2. ffff2000080951c4 T module_alloc
  3. ffff200008297770 T kasan_module_alloc

在這個例子中我們使用了交叉開發工具,並且在我們的主機系統上呼叫 gdb 來檢查指令包含我們感興趣函數。

  1. $ ${CROSS_COMPILE}gdb vmlinux
  2. (gdb) x/30i0xffff2000080951c4
  3. 0xffff2000080951c4<module_alloc>:sub sp, sp,#0x30
  4. 0xffff2000080951c8<module_alloc+4>: adrp x3,0xffff200008d70000
  5. 0xffff2000080951cc<module_alloc+8>: add x3, x3,#0x0
  6. 0xffff2000080951d0<module_alloc+12>: mov x5,#0x713// #1811
  7. 0xffff2000080951d4<module_alloc+16>: mov w4,#0xc0// #192
  8. 0xffff2000080951d8<module_alloc+20>:
  9. mov x2,#0xfffffffff8000000// #-134217728
  10. 0xffff2000080951dc<module_alloc+24>: stp x29, x30,[sp,#16]0xffff2000080951e0<module_alloc+28>: add x29, sp,#0x10
  11. 0xffff2000080951e4<module_alloc+32>: movk x5,#0xc8, lsl #48
  12. 0xffff2000080951e8<module_alloc+36>: movk w4,#0x240, lsl #16
  13. 0xffff2000080951ec<module_alloc+40>: str x30,[sp]0xffff2000080951f0<module_alloc+44>: mov w7,#0xffffffff// #-1
  14. 0xffff2000080951f4<module_alloc+48>: mov x6,#0x0// #0
  15. 0xffff2000080951f8<module_alloc+52>: add x2, x3, x2
  16. 0xffff2000080951fc<module_alloc+56>: mov x1,#0x8000// #32768
  17. 0xffff200008095200<module_alloc+60>: stp x19, x20,[sp,#32]0xffff200008095204<module_alloc+64>: mov x20, x0
  18. 0xffff200008095208<module_alloc+68>: bl 0xffff2000082737a8<__vmalloc_node_range>
  19. 0xffff20000809520c<module_alloc+72>: mov x19, x0
  20. 0xffff200008095210<module_alloc+76>: cbz x0,0xffff200008095234<module_alloc+112>
  21. 0xffff200008095214<module_alloc+80>: mov x1, x20
  22. 0xffff200008095218<module_alloc+84>: bl 0xffff200008297770<kasan_module_alloc>
  23. 0xffff20000809521c<module_alloc+88>: tbnz w0,#31,0xffff20000809524c<module_alloc+136>
  24. 0xffff200008095220<module_alloc+92>: mov sp, x29
  25. 0xffff200008095224<module_alloc+96>: mov x0, x19
  26. 0xffff200008095228<module_alloc+100>: ldp x19, x20,[sp,#16]0xffff20000809522c<module_alloc+104>: ldp x29, x30,[sp],#32
  27. 0xffff200008095230<module_alloc+108>: ret
  28. 0xffff200008095234<module_alloc+112>: mov sp, x29
  29. 0xffff200008095238<module_alloc+116>: mov x19,#0x0// #0

在這種情況下,我們將在此函數中顯示以下原始碼行的結果:

  1. p = __vmalloc_node_range(size, MODULE_ALIGN, VMALLOC_START,
  2. VMALLOC_END, GFP_KERNEL, PAGE_KERNEL_EXEC,0,
  3. NUMA_NO_NODE, __builtin_return_address(0));

……以及在此程式碼行的函數呼叫的返回值:

  1. if(p &&(kasan_module_alloc(p,size)<0)){

我們可以在從呼叫外部函數的組合程式碼中識別這些。為了展示這些值,我們將在目標系統上的 0xffff20000809520c0xffff20000809521c 處放置探針。

  1. $ cat> kprobe_events <<EOF
  2. p 0xffff20000809520c%x0
  3. p 0xffff20000809521c%x0
  4. EOF
  5. $ echo1> events/kprobes/enable

現在將一個乙太網介面卡加密狗插入到 USB 埠後,我們看到以下寫入追蹤紀錄檔的內容:

  1. $ cat trace
  2. # tracer: nop
  3. #
  4. # entries-in-buffer/entries-written:12/12#P:8
  5. #
  6. # _—–=> irqs-off
  7. #/ _—-=> need-resched
  8. #|/ _—=> hardirq/softirq
  9. #||/ _–=> preempt-depth
  10. #|||/ delay
  11. # TASK-PID CPU#|||| TIMESTAMP FUNCTION
  12. #|||||||||
  13. systemd-udevd-2082[000] d77.200991: p_0xffff20000809520c:(module_alloc+0x48/0x98) arg1=0xffff200001188000
  14. systemd-udevd-2082[000] d77.201059: p_0xffff20000809521c:(module_alloc+0x58/0x98) arg1=0x0
  15. systemd-udevd-2082[000] d77.201115: p_0xffff20000809520c:(module_alloc+0x48/0x98) arg1=0xffff200001198000
  16. systemd-udevd-2082[000] d77.201157: p_0xffff20000809521c:(module_alloc+0x58/0x98) arg1=0x0
  17. systemd-udevd-2082[000] d77.227456: p_0xffff20000809520c:(module_alloc+0x48/0x98) arg1=0xffff2000011a0000
  18. systemd-udevd-2082[000] d77.227522: p_0xffff20000809521c:(module_alloc+0x58/0x98) arg1=0x0
  19. systemd-udevd-2082[000] d77.227579: p_0xffff20000809520c:(module_alloc+0x48/0x98) arg1=0xffff2000011b0000
  20. systemd-udevd-2082[000] d77.227635: p_0xffff20000809521c:(module_alloc+0x58/0x98) arg1=0x0
  21. modprobe-2097[002] d78.030643: p_0xffff20000809520c:(module_alloc+0x48/0x98) arg1=0xffff2000011b8000
  22. modprobe-2097[002] d78.030761: p_0xffff20000809521c:(module_alloc+0x58/0x98) arg1=0x0
  23. modprobe-2097[002] d78.031132: p_0xffff20000809520c:(module_alloc+0x48/0x98) arg1=0xffff200001270000
  24. modprobe-2097[002] d78.031187: p_0xffff20000809521c:(module_alloc+0x58/0x98) arg1=0x0

kprobes 事件系統的另一個功能是記錄統計資訊,這可在 inkprobe_profile 中找到。在以上追蹤後,該檔案的內容為:

  1. $ cat kprobe_profile
  2. p_0xffff20000809520c 60
  3. p_0xffff20000809521c 60

這表明我們設定的兩處斷點每個共發生了 8 次命中,這當然與追蹤紀錄檔資料是一致的。在 kprobetrace 文件中有更多 kprobe_profile 的功能描述。

也可以進一步過濾 kprobes 事件。用來控制這點的 debugfs 檔案在 kprobetrace 文件中被列出,然而它們內容的詳細資訊大多在 trace events 文件中被描述。

總結

現在,Linux ARMv8 對支援 kprobes 功能也和其它架構相當。有人正在做新增 uprobes 和 systemtap 支援的工作。這些功能/工具和其他已經完成的功能(如: perf、 coresight)允許 Linux ARMv8 使用者像在其它更老的架構上一樣偵錯和測試效能。


參考文獻

  • 註1: Jim Keniston, Prasanna S. Panchamukhi, Masami Hiramatsu. “Kernel Probes (kprobes).” GitHub. GitHub, Inc., 15 Aug. 2016. Web. 13 Dec. 2016.
  • 註2: Ts’o, Theodore, Li Zefan, and Tom Zanussi. “Event Tracing.” GitHub. GitHub, Inc., 3 Mar. 2016. Web. 13 Dec. 2016.
  • 註3: Hiramatsu, Masami. “Kprobe-based Event Tracing.” GitHub. GitHub, Inc., 18 Aug. 2016. Web. 13 Dec. 2016.

作者簡介 : David Long 在 Linaro Kernel - Core Development 團隊中擔任工程師。 在加入 Linaro 之前,他在商業和國防行業工作了數年,既做嵌入式實時工作又為Unix提供軟體開發工具。之後,在 Digital(又名 Compaq)公司工作了十幾年,負責 Unix 標準,C 編譯器和執行時庫的工作。之後 David 又去了一系列初創公司做嵌入式 Linux 和安卓系統,嵌入式客製化作業系統和 Xen 虛擬化。他擁有 MIPS,Alpha 和 ARM 平台的經驗(等等)。他使用過從 1979 年貝爾實驗室 V6 開始的大部分Unix作業系統,並且長期以來一直是 Linux 使用者和倡導者。他偶爾也因使用烙鐵和數位示波器偵錯裝置驅動而知名。


via: http://www.linaro.org/blog/kprobes-event-tracing-armv8/

作者:David Long 譯者:kimii 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

本文永久更新連結地址http://www.linuxidc.com/Linux/2017-12/149066.htm


IT145.com E-mail:sddin#qq.com