二十分鐘Linux ftrace原理拋磚引玉
版權宣告:本文為博主原創,無版權,未經博主允許可以隨意轉載,無需註明出處,隨意修改或保持可作為原創!https://blog.csdn.net/dog250/article/details/84667690
週末要去忙別的事情,所以沒有時間總結些東西了,那就今晚寫簡單點吧。
我們可以通過objdump -D看到核心模組或者使用者態程式裡面的函式開頭的指令,以便知道如果想hook它的話,要預先備份多少指令。
但是如何看到核心函式的開頭幾個指令呢?
我試圖去objdump系統boot目錄下的vmlinux,但是什麼也看不到。這裡說一句,如果你的/boot目錄下只有vmlinuz,那麼首先你必須將其解壓成vmlinux,這個比較容易,核心原始碼或者核心標頭檔案開發包中都自帶了這個指令碼:
/usr/src/linux-headers-$(uname -r)/scripts/extract-vmlinux vmlinuz-$(uname -r) > vmlinux
然後去objdump這個生成的vmlinux的話,很遺憾,沒有函式的名字。此時我能想到的辦法就是自己寫一個模組,然後從/proc/kallsyms檔案中根據函式名字找到函式的起始地址,將此地址作為引數傳遞給核心模組,然後核心模組從該地址出開始列印即可,類似:
[root@localhost ~]# addr=`cat /proc/kallsyms |grep ip_rcv$|cut -d " " -f 1` [root@localhost ~]# echo $addr ffffffff8ae36bc0 [root@localhost ~]# insmod ./getinst.ko addr=$addr [root@localhost ~]# dmesg ... 66 66 66 66 90 55 48 89 e5 41 55 ...
嗯,這嚴格遵循了UNIX的哲學,用小玩意兒組合的方式完成一件場面相對大的事。
但這並不好。我希望vmlinux作為一個二進位制程式被objdump,因此我需要對應當前uname -r版本的debuginfo中的vmlinux,debuginfo中攜帶大量的字元符號資訊。
於是就從centos官網上下載了一個,安裝之,最後其vmlinux的位置在:
/usr/lib/debug/usr/lib/modules/`uname -r`/vmlinux
用上面的方法將其dump:
[root@localhost ~]# objdump -D /usr/lib/debug/usr/lib/modules/`uname -r`/vmlinux >./dump
很久的時間,最終dump的大小是:
[root@localhost ]# ll 總用量 4263968 ... -rw-r--r--. 1 root root 3513205250 11月 23 23:59 dump ...
使用vim開啟是比較費勁的,我是將其切割開啟的,來看看同樣的ip_rcv函式:
... 1815334 ffffffff81636bc0 <ip_rcv>: 1815335 ffffffff81636bc0:e8 2b 25 0f 00callqffffffff817290f0 <__fentry__> 1815336 ffffffff81636bc5:55push%rbp 1815337 ffffffff81636bc6:48 89 e5mov%rsp,%rbp 1815338 ffffffff81636bc9:41 55push%r13 1815339 ffffffff81636bcb:41 54push%r12 ...
有點問題, 同樣都是0xffffffff81636bc0這個地址,前5個位元組怎麼和我用模組dump下來的不一樣??
這個時候,我們看看objdump結果的 callqffffffff817290f0 <fentry > ,我們看看 fentry 到底是什麼:
... 2085623 ffffffff817290f0 <__fentry__>: 2085624 ffffffff817290f0:c3retq 2085625 ffffffff817290f1:0f 1f 44 00 00nopl0x0(%rax,%rax,1) 2085626 ffffffff817290f6:66 2e 0f 1f 84 00 00nopw%cs:0x0(%rax,%rax,1) 2085627 ffffffff817290fd:00 00 00 ...
非常簡單,你可以理解為就是一個ret(後面的各種尺寸的nop都是為了為了指令替換做支撐的 )。
然而我們知道,這麼一個在應用程式看來看似沒用的call-and-ret序列,在CPU看來場面確實及其巨集大的,所以在Linux核心啟動的過程中,這個call __fentry__被替換成了標準的 5位元組nop ,即 66 66 66 66 90 ,也就是我的模組裡列印的結果。
也就是說Linux核心啟動的過程中對每一個函式進行了一次 hot hook 。所以說,靜態的vmlinux中函式開頭的5位元組指令被動態替換成了執行時的nop!
那麼,何必多此一舉呢?
這就要引入ftrace了。仔細看dump檔案裡的下面這些資訊:
... 2085650 ffffffff8172915c <ftrace_call>: 2085651 ffffffff8172915c:e8 2f 00 00 00callqffffffff81729190 <ftrace_stub> 2085652 ffffffff81729161:4c 8b 4c 24 40mov0x40(%rsp),%r9 2085653 ffffffff81729166:4c 8b 44 24 48mov0x48(%rsp),%r8 2085654 ffffffff8172916b:48 8b 7c 24 70mov0x70(%rsp),%rdi 2085655 ffffffff81729170:48 8b 74 24 68mov0x68(%rsp),%rsi 2085656 ffffffff81729175:48 8b 54 24 60mov0x60(%rsp),%rdx 2085657 ffffffff8172917a:48 8b 4c 24 58mov0x58(%rsp),%rcx 2085658 ffffffff8172917f:48 8b 44 24 50mov0x50(%rsp),%rax 2085659 ffffffff81729184:48 81 c4 a8 00 00 00add$0xa8,%rsp 2085660 2085661 ffffffff8172918b <ftrace_graph_call>: 2085662 ffffffff8172918b:e9 00 00 00 00jmpqffffffff81729190 <ftrace_stub> 2085663 2085664 ffffffff81729190 <ftrace_stub>: 2085665 ffffffff81729190:c3retq 2085666 ffffffff81729191:0f 1f 44 00 00nopl0x0(%rax,%rax,1) 2085667 ffffffff81729196:66 2e 0f 1f 84 00 00nopw%cs:0x0(%rax,%rax,1) 2085668 ffffffff8172919d:00 00 00 ...
其實這些輔助函式連同__fentry__就是用來支撐ftrace機制的,使得你一旦開啟ftrace,便可以動態地對核心函式進行跟蹤除錯,非常方便,當然,如果你不開啟ftrace,那麼額外的支撐指令就像nop一樣虛無。
這裡只是花了20分鐘時間拋個磚,順勢看下去,就會理解ftrace的原理和應用了。
順便說一下,核心在使能ftrace的時候,其實也是需要將5位元組nop換回call指令的,這又是一次hot hook的過程,為了防止 CPU0在取指第3個位元組的時候,CPU1替換了第4個位元組,導致CPU0誤解了完整的5位元組指令,造成不可預知的後果。 核心採用的機制和我前段時間hot hook時使用的機制是一致的,即先原子替換第一個位元組為 0xcc , 即一個斷點指令,然後再統一替換後面的。只不過我是借用了kprobe的int3,而ftrace則是直接替換,具體參見下面的函式:
void ftrace_replace_code(int enable);
浙江溫州皮鞋:mans_shoe:溼,下雨:umbrella:️進水不會胖!