kprobes 與 uprobes(核心探針與使用者探針)
/ KAY-probes, YOO-probes /
大多數追蹤只能在某人事先規劃好的點上觀察。但有時你需要看某個特定函式——某個核心常式、或運行中程式內部的某個函式——而沒有人替它加過追蹤點。kprobes 與 uprobes 就是讓你能在執行期、動態地、不重新編譯任何東西,就把探針放在「幾乎任何」函式上的機制:kprobe 掛在任意核心函式上,uprobe 掛在使用者空間程式裡的任意函式上。
用淺白步驟說明這個訣竅。要探測一個函式,核心會把目標位址處的第一條機器指令取出,換成一條中斷點指令(x86 上是單一位元組的 int3,0xCC),把原本那條指令存到一旁。當執行抵達那個位址時,中斷點觸發一個陷阱進入核心;核心執行你掛上的處理常式(它能讀函式的引數、暫存器或記憶體),接著單步執行那條存起來的原指令,再讓執行像沒事一樣繼續。一個回返探針變體(kretprobe / uretprobe)會額外在函式返回時設陷阱,於是你能讀回傳值、量出這次呼叫花了多久。uprobe 運作方式相同,只是中斷點被補進使用者空間二進位檔的程式碼裡,由核心攔截。這些探針通常由 ftrace 驅動,或者——今天遠更常見地——掛到 eBPF 程式上。
它之所以重要,是因為它正是讓追蹤真正臨時且強大的東西:你不被限制在某人預見過的事件裡——你一意識到需要,就能問任何具名函式。這是靜態、穩定 tracepoint 的動態對應物。誠實的提醒:因為探針把一個中斷點補進活程式碼,它在每次命中時都加入一個陷阱(幾百奈秒),所以探測一個熱門函式可能造成傷害;而且因為它是以名稱或位址鎖定某個內部函式、而非一個穩定介面,當核心或程式更新時,探針可能壞掉或位移——kprobe 精確但跨版本本質上脆弱,這正是 tracepoint 為那些要長期觀察的東西而存在的原因。
# kprobe:追蹤每一次對核心 do_sys_openat2 的呼叫,印出檔名引數 $ bpftrace -e 'kprobe:do_sys_openat2 { printf("%s 開啟 %s\n", comm, str(arg1)); }' # uprobe:追蹤 /bin/bash 內部的 readline(),擷取使用者輸入了什麼 $ bpftrace -e 'uretprobe:/bin/bash:readline { printf("輸入:%s\n", str(retval)); }'
kprobe 以名稱對任意核心函式插樁;uretprobe 讀一個使用者函式的回傳值——兩者皆即時放置、不需重新編譯。
kprobes 以名稱鎖定內部實作細節,所以與 tracepoint 不同,它對跨核心版本「不」提供任何穩定承諾:一段今天能用的腳本,若那個函式被改名、內聯或移除,升級後可能悄悄地不再匹配。