內核調測工具Kprobe之實踐篇
本文轉載自微信公眾號「人人都是極客」,作者布道師Peter。轉載本文請聯系人人都是極客公眾號。
Kprobe介紹
debug內核函數變量的時候最常用的是添加log,用printk看下相關的信息,但是這種方式往往需要重新編譯內核,然后再啟動設備。
而Kprobe可以在運行的內核中動態插入探測點,執行你預定義的操作。可以跟蹤內核幾乎所有的代碼地址,并且當斷點被擊中后會響應處理函數。
使用kprobe最常用的就是查詢函數調用的參數和返回值。
目前,使用kprobe可以通過兩種方式:
- 第一種是開發人員自行編寫內核模塊,向內核注冊探測點,探測函數可根據需要自行定制,使用靈活方便;
- 第二種方式是使用kprobes on trace,這種方式是kprobe和Ftrace結合使用,即可以通過kprobe來優化Ftrace來跟蹤函數的調用。
編寫kprobe探測模塊
Kprobe結構體與API介紹
- struct hlist_node hlist:被用于kprobe全局hash,索引值為被探測點的地址;
- struct list_head list:用于鏈接同一被探測點的不同探測kprobe;
- kprobe_opcode_t *addr:被探測點的地址;
- const char *symbol_name:被探測函數的名字;
- unsigned int offset:被探測點在函數內部的偏移,用于探測函數內部的指令,如果該值為0表示函數的入口;
- kprobe_pre_handler_t pre_handler:在被探測點指令執行之前調用的回調函數;
- kprobe_post_handler_t post_handler:在被探測指令執行之后調用的回調函數;
- kprobe_fault_handler_t fault_handler:在執行pre_handler、post_handler或單步執行被探測指令時出現內存異常則會調用該回調函數;
- kprobe_break_handler_t break_handler:在執行某一kprobe過程中觸發了斷點指令后會調用該函數,用于實現jprobe;
- kprobe_opcode_t opcode:保存的被探測點原始指令;
- struct arch_specific_insn ainsn:被復制的被探測點的原始指令,用于單步執行,架構強相關(可能包含指令模擬函數);
- u32 flags:狀態標記。
- int register_kprobe(struct kprobe *kp) //向內核注冊kprobe探測點
- void unregister_kprobe(struct kprobe *kp) //卸載kprobe探測點
- int register_kprobes(struct kprobe **kps, int num) //注冊探測函數向量,包含多個探測點
- void unregister_kprobes(struct kprobe **kps, int num) //卸載探測函數向量,包含多個探測點
- int disable_kprobe(struct kprobe *kp) //臨時暫停指定探測點的探測
- int enable_kprobe(struct kprobe *kp) //恢復指定探測點的探測
用例kprobe_example.c分析與演示
linux內核源碼中提供了kprobe的用例 samples/kprobes/kprobe_example.c
- /* For each probe you need to allocate a kprobe structure */
- static struct kprobe kp = {
- .symbol_name = "do_fork",
- };
- static int __init kprobe_init(void)
- {
- int ret;
- kp.pre_handler = handler_pre;
- kp.post_handler = handler_post;
- kp.fault_handler = handler_fault;
- ret = register_kprobe(&kp);
- if (ret < 0) {
- printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
- return ret;
- }
- printk(KERN_INFO "Planted kprobe at %p\n", kp.addr);
- return 0;
- }
- static void __exit kprobe_exit(void)
- {
- unregister_kprobe(&kp);
- printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr);
- }
- module_init(kprobe_init)
- module_exit(kprobe_exit)
- MODULE_LICENSE("GPL");
程序中定義了一個struct kprobe結構實例kp并初始化其中的symbol_name字段為“do_fork”,表明它將要探測do_fork函數。在模塊的初始化函數中,注冊了 pre_handler、post_handler和fault_handler這3個回調函數分別為handler_pre、handler_post和handler_fault,最后調用register_kprobe注冊。在模塊的卸載函數中調用unregister_kprobe函數卸載kp探測點。
- static int handler_pre(struct kprobe *p, struct pt_regs *regs)
- {
- ......
- #ifdef CONFIG_ARM64
- pr_info("<%s> pre_handler: p->addr = 0x%p, pc = 0x%lx,"
- " pstate = 0x%lx\n",
- p->symbol_name, p->addr, (long)regs->pc, (long)regs->pstate);
- #endif
- /* A dump_stack() here will give a stack backtrace */
- return 0;
- }
handler_pre回調函數的第一個入參是注冊的struct kprobe探測實例,第二個參數是保存的觸發斷點前的寄存器狀態,它在do_fork函數被調用之前被調用,該函數僅僅是打印了被探測點的地址,保存的個別寄存器參數。
- static void handler_post(struct kprobe *p, struct pt_regs *regs,
- unsigned long flags)
- {
- ......
- #ifdef CONFIG_ARM64
- pr_info("<%s> post_handler: p->addr = 0x%p, pstate = 0x%lx\n",
- p->symbol_name, p->addr, (long)regs->pstate);
- #endif
- }
handler_post回調函數的前兩個入參同handler_pre,第三個參數目前尚未使用,全部為0;該函數在do_fork函數調用之后被調用,這里打印的內容同handler_pre類似。
- static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
- {
- pr_info("fault_handler: p->addr = 0x%p, trap #%dn", p->addr, trapnr);
- /* Return 0 because we don't handle the fault. */
- return 0;
- }
handler_fault回調函數會在執行handler_pre、handler_post或單步執行do_fork時出現錯誤時調用,這里第三個參數時具體發生錯誤的trap number,與架構相關。
加載到內核中后,隨便在終端上敲一個命令,可以看到dmesg中打印如下信息:
- <6>pre_handler: p->addr = 0xc0439cc0, ip = c0439cc1, flags = 0x246
- <6>post_handler: p->addr = 0xc0439cc0, flags = 0x246
- <6>pre_handler: p->addr = 0xc0439cc0, ip = c0439cc1, flags = 0x246
- <6>post_handler: p->addr = 0xc0439cc0, flags = 0x246
- <6>pre_handler: p->addr = 0xc0439cc0, ip = c0439cc1, flags = 0x246
- <6>post_handler: p->addr = 0xc0439cc0, flags = 0x246
可以看到被探測點的地址為0xc0439cc0,用以下命令確定這個地址就是do_fork的入口地址。
- echo 0 > /proc/sys/kernel/kptr_restrict
- cat /proc/kallsyms | grep do_fork
- c0439cc0 T do_fork
kprobes on trace
- /sys/kernel/debug/kprobes/list: 列出內核中已經設置kprobe斷點的函數
- /sys/kernel/debug/kprobes/enabled: kprobe開啟/關閉開關
- /sys/kernel/debug/kprobes/blacklist: kprobe黑名單(無法設置斷點函數)
- /proc/sys/debug/kprobes-optimization: Turn kprobes optimization ON/OFF
Documentation/trace/kprobetrace.txt
使用前確定內核CONFIG打開:CONFIG_KPROBE_EVENT=y
/sys/kernel/debug/tracing/kprobe_events:添加斷點接口
/sys/kernel/debug/tracing/events/kprobes/enabled:斷點使能開關
/sys/kernel/debug/tracing/trace:查看trace日志接口
規則:
- Synopsis of kprobe_events-------------------------
- p[:[GRP/]EVENT] [MOD:]SYM[+offs]|MEMADDR [FETCHARGS] : Set a probe
- r[:[GRP/]EVENT] [MOD:]SYM[+0] [FETCHARGS] : Set a return probe
- -:[GRP/]EVENT : Clear a probe
- GRP : Group name. If omitted, use "kprobes" for it.
- EVENT : Event name. If omitted, the event name is generated
- based on SYM+offs or MEMADDR.
- MOD : Module name which has given SYM.
- SYM[+offs] : Symbol+offset where the probe is inserted.
- MEMADDR : Address where the probe is inserted.
- FETCHARGS : Arguments. Each probe can have up to 128 args.
- %REG : Fetch register REG
- @ADDR : Fetch memory at ADDR (ADDR should be in kernel)
- @SYM[+|-offs] : Fetch memory at SYM +|- offs (SYM should be a data symbol)
- $stackN : Fetch Nth entry of stack (N >= 0)
- $stack : Fetch stack address.
- $retval : Fetch return value.(*)
- $comm : Fetch current task comm.
- +|-offs(FETCHARG) : Fetch memory at FETCHARG +|- offs address.(**)
- NAME=FETCHARG : Set NAME as the argument name of FETCHARG.
- FETCHARG:TYPE : Set TYPE as the type of FETCHARG. Currently, basic types
- (u8/u16/u32/u64/s8/s16/s32/s64), hexadecimal types
- (x8/x16/x32/x64), "string" and bitfield are supported.
- (*) only for return probe.
- (**) this is useful for fetching a field of data structures.
查看對應的模塊:
- 130|mek_8q:/sys/kernel/debug/tracing # cat /proc/devices
- Character devices:
- 1 mem
- 4 /dev/vc/0
- 4 tty
- 4 ttyS
- 5 /dev/tty
- 5 /dev/console
- 5 /dev/ptmx
- 7 vcs
- 10 misc
- 13 input
- 29 fb
- 81 video4linux
- 89 i2c
- 90 mtd
- 108 ppp
- 116 alsa
可以在System.map文件里找一下有沒有你要觀察的內核函數方法。這個文件其實相當于內核的符號表(symbol table)。如果拿不準內核方法名的時候可以在這里面grep一下看看。
- mek_8q:/ # cat /proc/kallsyms | grep do_sys_open
- 0000000000000000 T do_sys_open
以do_sys_open為例添加kprobe為例:
- 添加kprobe:
- echo 'p:myprobe do_sys_open' > /sys/kernel/debug/tracing/kprobe_events
- 添加kretprobe,返回值是數字:
- echo 'r:myretprobe do_sys_open $retval' > /sys/kernel/debug/tracing/kprobe_events
- 添加kretprobe,返回值是字符串:
- echo 'r:myprobe getname +0($retval):string' > /sys/kernel/debug/tracing/kprobe_events
- 刪除添加的kprobe:
- echo '-:myprobe' > /sys/kernel/debug/tracing/events/kprobe_events
執行:
- cd /sys/kernel/debug/tracing
- echo 'p:myprobe do_sys_open' > kprobe_events
- echo 'r:myretprobe do_sys_open $retval' > kprobe_events
- echo 1 > tracing_on
- echo 1 > events/kprobes/myprobe/enable
結果為:
刪除注冊的kprobe:
- echo 0 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable
- echo 0 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable
- echo '-:myprobe' > /sys/kernel/debug/tracing/events/kprobe_events
- echo '-:myretprobe' > /sys/kernel/debug/tracing/events/kprobe_events