ARMv8 上的 kprobes 事件跟蹤
介紹
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 允許通過提供一個具有相同 調用簽名 的中間函數來攔截對一個函數的調用,這裡中間函數將被首先調用。jprobes 只是一個編程介面,它不能通過 debugfs 事件追蹤子系統來使用。因此,我們將不會在這裡進一步討論 jprobes。如果你想使用 jprobes,請參考 kprobes 文檔。
調用 kprobes
kprobes 提供一系列能從內核代碼中調用的 API 來設置探測點和當探測點被命中時調用的註冊函數。在不往內核中添加代碼的情況下,kprobes 也是可用的,這是通過寫入特定事件追蹤的 debugfs 文件來實現的,需要在文件中設置探針地址和信息,以便在探針被命中時記錄到追蹤日誌中。後者是本文將要討論的重點。最後 kprobes 可以通過 perl 命令來使用。
kprobes API
內核開發人員可以在內核中編寫函數(通常在專用的調試模塊中完成)來設置探測點,並且在探測指令執行前和執行後立即執行任何所需操作。這在 kprobes.txt 中有很好的解釋。
事件追蹤
事件追蹤子系統有自己的自己的文檔 注2 ,對於了解一般追蹤事件的背景可能值得一讀。事件追蹤子系統是 追蹤點 和 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 乙太網驅動程序複位功能:
$ pwd
/sys/kernel/debug/tracing
$ cat > kprobe_events <<EOF
p ax88772_reset %x0
EOF
$ echo 1 > events/kprobes/enable
此時每次該驅動的 ax8872_reset()
函數被調用,追蹤事件都將會被記錄。這個事件將顯示指向通過作為此函數的唯一參數的 X0
(按照 ARMv8 調用標準)傳入的 usbnet
結構的指針。插入需要乙太網驅動程序的 USB 加密狗後,我們看見以下追蹤信息:
$ cat trace
# tracer: nop
#
# entries-in-buffer/entries-written: 1/1 #P:8
#
# _—–=> irqs-off
# / _—-=> need-resched
# | / _—=> hardirq/softirq
# || / _–=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
kworker/0:0-4 [000] d… 10972.102939: p_ax88772_reset_0:
(ax88772_reset+0x0/0x230) arg1=0xffff800064824c80
這裡我們可以看見傳入到我們的探測函數的指針參數的值。由於我們沒有使用 kprobes 事件追蹤的可選標籤功能,我們需要的信息自動被標註為 arg1
。注意這指向我們需要 kprobes 記錄這個探針的一組值的第一個,而不是函數參數的實際位置。在這個例子中它也只是碰巧是我們探測函數的第一個參數。
例子: 函數入口和返回探測
kretprobe 功能專門用於探測函數返回。在函數入口 kprobes 子系統將會被調用並且建立鉤子以便在函數返回時調用,鉤子將記錄需求事件信息。對最常見情況,返回信息通常在 X0
寄存器中,這是非常有用的。在 %x0
中返回值也可以被稱為 $retval
。以下例子也演示了如何提供一個可讀的標籤來展示有趣的信息。
使用 kprobes 和 kretprobe 檢測內核 do_fork()
函數來記錄參數和結果的例子:
$ cd /sys/kernel/debug/tracing
$ cat > kprobe_events <<EOF
p _do_fork %x0 %x1 %x2 %x3 %x4 %x5
r _do_fork pid=%x0
EOF
$ echo 1 > events/kprobes/enable
此時每次對 _do_fork()
的調用都會產生兩個記錄到 trace 文件的 kprobe 事件,一個報告調用參數值,另一個報告返回值。返回值在 trace 文件中將被標記為 pid
。這裡是三次 fork 系統調用執行後的 trace 文件的內容:
_$ cat trace
# tracer: nop
#
# entries-in-buffer/entries-written: 6/6 #P:8
#
# _—–=> irqs-off
# / _—-=> need-resched
# | / _—=> hardirq/softirq
# || / _–=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
bash-1671 [001] d… 204.946007: p__do_fork_0: (_do_fork+0x0/0x3e4) arg1=0x1200011 arg2=0x0 arg3=0x0 arg4=0x0 arg5=0xffff78b690d0 arg6=0x0
bash-1671 [001] d..1 204.946391: r__do_fork_0: (SyS_clone+0x18/0x20 <- _do_fork) pid=0x724
bash-1671 [001] d… 208.845749: p__do_fork_0: (_do_fork+0x0/0x3e4) arg1=0x1200011 arg2=0x0 arg3=0x0 arg4=0x0 arg5=0xffff78b690d0 arg6=0x0
bash-1671 [001] d..1 208.846127: r__do_fork_0: (SyS_clone+0x18/0x20 <- _do_fork) pid=0x725
bash-1671 [001] d… 214.401604: p__do_fork_0: (_do_fork+0x0/0x3e4) arg1=0x1200011 arg2=0x0 arg3=0x0 arg4=0x0 arg5=0xffff78b690d0 arg6=0x0
bash-1671 [001] d..1 214.401975: r__do_fork_0: (SyS_clone+0x18/0x20 <- _do_fork) pid=0x726_
例子: 解引用指針參數
對於指針值,kprobes 事件處理子系統也允許解引用和列印所需的內存內容,適用於各種基本數據類型。為了展示所需欄位,手動計算結構的偏移量是必要的。
檢測 _do_wait()
函數:
$ cat > kprobe_events <<EOF
p:wait_p do_wait wo_type=+0(%x0):u32 wo_flags=+4(%x0):u32
r:wait_r do_wait $retval
EOF
$ echo 1 > events/kprobes/enable
注意在第一個探針中使用的參數標籤是可選的,並且可用於更清晰地識別記錄在追蹤日誌中的信息。帶符號的偏移量和括弧表明了寄存器參數是指向記錄在追蹤日誌中的內存內容的指針。:u32
表明了內存位置包含一個無符號的 4 位元組寬的數據(在這個例子中指局部定義的結構中的一個 emum 和一個 int)。
探針標籤(冒號後)是可選的,並且將用來識別日誌中的探針。對每個探針來說標籤必須是獨一無二的。如果沒有指定,將從附近的符號名稱自動生成一個有用的標籤,如前面的例子所示。
也要注意 $retval
參數可以只是指定為 %x0
。
這裡是兩次 fork 系統調用執行後的 trace 文件的內容:
$ cat trace
# tracer: nop
#
# entries-in-buffer/entries-written: 4/4 #P:8
#
# _—–=> irqs-off
# / _—-=> need-resched
# | / _—=> hardirq/softirq
# || / _–=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
bash-1702 [001] d… 175.342074: wait_p: (do_wait+0x0/0x260) wo_type=0x3 wo_flags=0xe
bash-1702 [002] d..1 175.347236: wait_r: (SyS_wait4+0x74/0xe4 <- do_wait) arg1=0x757
bash-1702 [002] d… 175.347337: wait_p: (do_wait+0x0/0x260) wo_type=0x3 wo_flags=0xf
bash-1702 [002] d..1 175.347349: wait_r: (SyS_wait4+0x74/0xe4 <- do_wait) arg1=0xfffffffffffffff6
例子: 探測任意指令地址
在前面的例子中,我們已經為函數的入口和出口插入探針,然而探測一個任意指令(除少數例外)是可能的。如果我們正在 C 函數中放置一個探針,第一步是查看代碼的彙編版本以確定我們要放置探針的位置。一種方法是在 vmlinux 文件上使用 gdb,並在要放置探針的函數中展示指令。下面是一個在 arch/arm64/kernel/modules.c
中 module_alloc
函數執行此操作的示例。在這種情況下,因為 gdb 似乎更喜歡使用弱符號定義,並且它是與這個函數關聯的存根代碼,所以我們從 System.map 中來獲取符號值:
$ grep module_alloc System.map
ffff2000080951c4 T module_alloc
ffff200008297770 T kasan_module_alloc
在這個例子中我們使用了交叉開發工具,並且在我們的主機系統上調用 gdb 來檢查指令包含我們感興趣函數。
$ ${CROSS_COMPILE}gdb vmlinux
(gdb) x/30i 0xffff2000080951c4
0xffff2000080951c4 <module_alloc>: sub sp, sp, #0x30
0xffff2000080951c8 <module_alloc+4>: adrp x3, 0xffff200008d70000
0xffff2000080951cc <module_alloc+8>: add x3, x3, #0x0
0xffff2000080951d0 <module_alloc+12>: mov x5, #0x713 // #1811
0xffff2000080951d4 <module_alloc+16>: mov w4, #0xc0 // #192
0xffff2000080951d8 <module_alloc+20>:
mov x2, #0xfffffffff8000000 // #-134217728
0xffff2000080951dc <module_alloc+24>: stp x29, x30, [sp,#16] 0xffff2000080951e0 <module_alloc+28>: add x29, sp, #0x10
0xffff2000080951e4 <module_alloc+32>: movk x5, #0xc8, lsl #48
0xffff2000080951e8 <module_alloc+36>: movk w4, #0x240, lsl #16
0xffff2000080951ec <module_alloc+40>: str x30, [sp] 0xffff2000080951f0 <module_alloc+44>: mov w7, #0xffffffff // #-1
0xffff2000080951f4 <module_alloc+48>: mov x6, #0x0 // #0
0xffff2000080951f8 <module_alloc+52>: add x2, x3, x2
0xffff2000080951fc <module_alloc+56>: mov x1, #0x8000 // #32768
0xffff200008095200 <module_alloc+60>: stp x19, x20, [sp,#32] 0xffff200008095204 <module_alloc+64>: mov x20, x0
0xffff200008095208 <module_alloc+68>: bl 0xffff2000082737a8 <__vmalloc_node_range>
0xffff20000809520c <module_alloc+72>: mov x19, x0
0xffff200008095210 <module_alloc+76>: cbz x0, 0xffff200008095234 <module_alloc+112>
0xffff200008095214 <module_alloc+80>: mov x1, x20
0xffff200008095218 <module_alloc+84>: bl 0xffff200008297770 <kasan_module_alloc>
0xffff20000809521c <module_alloc+88>: tbnz w0, #31, 0xffff20000809524c <module_alloc+136>
0xffff200008095220 <module_alloc+92>: mov sp, x29
0xffff200008095224 <module_alloc+96>: mov x0, x19
0xffff200008095228 <module_alloc+100>: ldp x19, x20, [sp,#16] 0xffff20000809522c <module_alloc+104>: ldp x29, x30, [sp],#32
0xffff200008095230 <module_alloc+108>: ret
0xffff200008095234 <module_alloc+112>: mov sp, x29
0xffff200008095238 <module_alloc+116>: mov x19, #0x0 // #0
在這種情況下,我們將在此函數中顯示以下源代碼行的結果:
p = __vmalloc_node_range(size, MODULE_ALIGN, VMALLOC_START,
VMALLOC_END, GFP_KERNEL, PAGE_KERNEL_EXEC, 0,
NUMA_NO_NODE, __builtin_return_address(0));
……以及在此代碼行的函數調用的返回值:
if (p && (kasan_module_alloc(p, size) < 0)) {
我們可以在從調用外部函數的彙編代碼中識別這些。為了展示這些值,我們將在目標系統上的 0xffff20000809520c
和 0xffff20000809521c
處放置探針。
$ cat > kprobe_events <<EOF
p 0xffff20000809520c %x0
p 0xffff20000809521c %x0
EOF
$ echo 1 > events/kprobes/enable
現在將一個乙太網適配器加密狗插入到 USB 埠後,我們看到以下寫入追蹤日誌的內容:
$ cat trace
# tracer: nop
#
# entries-in-buffer/entries-written: 12/12 #P:8
#
# _—–=> irqs-off
# / _—-=> need-resched
# | / _—=> hardirq/softirq
# || / _–=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
systemd-udevd-2082 [000] d… 77.200991: p_0xffff20000809520c: (module_alloc+0x48/0x98) arg1=0xffff200001188000
systemd-udevd-2082 [000] d… 77.201059: p_0xffff20000809521c: (module_alloc+0x58/0x98) arg1=0x0
systemd-udevd-2082 [000] d… 77.201115: p_0xffff20000809520c: (module_alloc+0x48/0x98) arg1=0xffff200001198000
systemd-udevd-2082 [000] d… 77.201157: p_0xffff20000809521c: (module_alloc+0x58/0x98) arg1=0x0
systemd-udevd-2082 [000] d… 77.227456: p_0xffff20000809520c: (module_alloc+0x48/0x98) arg1=0xffff2000011a0000
systemd-udevd-2082 [000] d… 77.227522: p_0xffff20000809521c: (module_alloc+0x58/0x98) arg1=0x0
systemd-udevd-2082 [000] d… 77.227579: p_0xffff20000809520c: (module_alloc+0x48/0x98) arg1=0xffff2000011b0000
systemd-udevd-2082 [000] d… 77.227635: p_0xffff20000809521c: (module_alloc+0x58/0x98) arg1=0x0
modprobe-2097 [002] d… 78.030643: p_0xffff20000809520c: (module_alloc+0x48/0x98) arg1=0xffff2000011b8000
modprobe-2097 [002] d… 78.030761: p_0xffff20000809521c: (module_alloc+0x58/0x98) arg1=0x0
modprobe-2097 [002] d… 78.031132: p_0xffff20000809520c: (module_alloc+0x48/0x98) arg1=0xffff200001270000
modprobe-2097 [002] d… 78.031187: p_0xffff20000809521c: (module_alloc+0x58/0x98) arg1=0x0
kprobes 事件系統的另一個功能是記錄統計信息,這可在 inkprobe_profile
中找到。在以上追蹤後,該文件的內容為:
$ cat kprobe_profile
p_0xffff20000809520c 6 0
p_0xffff20000809521c 6 0
這表明我們設置的兩處斷點每個共發生了 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
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive