用 70 行代码给你自己写一个 strace
基本上任何使用了一段時(shí)間 Linux 的人,最后都會(huì)知道并愛上 strace 命令。strace 是系統(tǒng)調(diào)用跟蹤器,它跟蹤程序執(zhí)行的進(jìn)入內(nèi)核以與外面的世界交互的調(diào)用。如果你還不熟悉這個(gè)令人驚奇的多才多藝的工具,我建議你看一下我的朋友和合作伙伴 Greg Price 的出色的博客 blog post 中關(guān)于這一主題的內(nèi)容,然后再回到這里。
我們都愛 strace,但你是否曾經(jīng)好奇它是如何工作的呢?它是如何把它自己注入到內(nèi)核和用戶空間程序之間的呢?這篇博客將用大約 70 行 C 代碼走查一個(gè)小小的 strace 實(shí)現(xiàn)。它的功能不會(huì)像真的那樣好,但在這個(gè)過程中,你將了解關(guān)于它使用的核心接口所需了解的大部分內(nèi)容。
在 Linux(還可能在其它一些 UNIX)上 strace 使用了被稱為 [ptrace](http://linux.die.net/man/2/ptrace) 的有點(diǎn)神秘的接口,進(jìn)程追蹤接口。ptrace 允許一個(gè)進(jìn)程監(jiān)視另一個(gè)進(jìn)程的狀態(tài),并深入調(diào)查(或甚至是控制)它的內(nèi)部狀態(tài)。
ptrace 是一個(gè)復(fù)雜的系統(tǒng)調(diào)用,它接收一個(gè)神奇的 “request” 首參數(shù),然后依賴于它的值執(zhí)行完全不同的事情。它通常的原型看起來像這樣:
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);然而,由于不同的 request 值使用剩余的從 0 個(gè)到 3 個(gè)參數(shù),glibc 中它的原型為可變參數(shù)函數(shù),允許一個(gè)開發(fā)者只列出給定調(diào)用所需要的參數(shù)個(gè)數(shù)。
為了使一個(gè)進(jìn)程跟蹤另一個(gè),它附到那個(gè)進(jìn)程上,并臨時(shí)變?yōu)槟莻€(gè)進(jìn)程的父進(jìn)程。當(dāng)一個(gè)進(jìn)程被 ptraced,跟蹤器可以請(qǐng)求它的子進(jìn)程隨時(shí)在各種事件發(fā)生時(shí)停下來,比如子進(jìn)程執(zhí)行了一個(gè)系統(tǒng)調(diào)用。當(dāng)這發(fā)生時(shí),內(nèi)核將以 SIGTRAP 停止子進(jìn)程。由于此時(shí)跟蹤器是子進(jìn)程的父進(jìn)程,這樣它就可以使用標(biāo)準(zhǔn)的 UNIX waitpid 系統(tǒng)調(diào)用觀察到這一點(diǎn)。
我們的小型 strace 將只支持 strace 的 strace COMMAND 形式(對(duì)照 strace -p),并且我們將只打印系統(tǒng)調(diào)用號(hào)和返回值 - 不解碼名字或參數(shù)或任何其它事情。因此一次簡單的運(yùn)行可能看起來像下面這樣:
$ ./ministrace ls … syscall(6) = 0 syscall(54) = 0 syscall(54) = 0 syscall(5) = 3 syscall(221) = 1 syscall(220) = 272 syscall(220) = 0 syscall(6) = 0 syscall(197) = 0 syscall(192) = -1219706880盡管不是世界上最有用的東西,但它展示了核心的跟蹤工具。因此,讓我們來看下代碼:
#include <sys/ptrace.h> #include <sys/reg.h> #include <sys/wait.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h>我們從必要的頭文件開始。sys/ptrace.h 定義了 ptrace 和 __ptrace_request 常量,我們還將需要 sys/reg.h 幫忙解碼系統(tǒng)調(diào)用。更多相關(guān)的內(nèi)容在后面。其它的你應(yīng)該都認(rèn)得出來。
int do_child(int argc, char **argv); int do_trace(pid_t child);int main(int argc, char **argv) {if (argc < 2) {fprintf(stderr, "Usage: %s prog args\n", argv[0]);exit(1);}pid_t child = fork();if (child == 0) {return do_child(argc-1, argv+1);} else {return do_trace(child);} }我們將從入口點(diǎn)開始。我們檢查我們被傳入了一個(gè)命令,然后我們通過 fork() 創(chuàng)建兩個(gè)進(jìn)程 - 一個(gè)用于執(zhí)行被跟蹤的程序,而另一個(gè)跟蹤它。
int do_child(int argc, char **argv) {char *args [argc+1];memcpy(args, argv, argc * sizeof(char*));args[argc] = NULL;子進(jìn)程從一些瑣碎的參數(shù)整理開始,這是由于 execvp 想要一個(gè)由 NULL 終止的參數(shù)數(shù)組。
ptrace(PTRACE_TRACEME);kill(getpid(), SIGSTOP);return execvp(args[0], args); }接下來,我們僅執(zhí)行提供的參數(shù)列表,但首先,我們需要啟動(dòng)跟蹤進(jìn)程,以使父進(jìn)程可以開始在非常早期就開始跟蹤新執(zhí)行的程序。
如果子進(jìn)程知道它想要被跟蹤,它可以執(zhí)行 PTRACE_TRACEME ptrace 請(qǐng)求,這將啟動(dòng)追蹤。此外,這意味著下一個(gè)發(fā)送給這個(gè)進(jìn)程的信號(hào)將停止它并通知它的父進(jìn)程(通過 wait),這樣父進(jìn)程就知道要開始跟蹤了。因此,在執(zhí)行了一個(gè) TRACEME 之后,我們 SIGSTOP 我們自己,以使父進(jìn)程可以通過 exec 調(diào)用繼續(xù)我們的執(zhí)行。
(你可能已經(jīng)注意到了,strace COMMAND 輸出總是以一個(gè) execve 調(diào)用開始。現(xiàn)在你應(yīng)該已經(jīng)理解為什么了 —— 實(shí)際上,我們打算在 kill 返回后立即開始跟蹤,因此我們看到了啟動(dòng)新程序的 execve 調(diào)用。)
int wait_for_syscall(pid_t child);int do_trace(pid_t child) {int status, syscall, retval;waitpid(child, &status, 0);與此同時(shí),在父進(jìn)程中,我們聲明了稍后需要的函數(shù)的原型,并開始跟蹤。我們立即開始 waitpid 在子進(jìn)程上,一旦子進(jìn)程給自己發(fā)送了上面的SIGSTOP,它將返回,并準(zhǔn)備好被跟蹤。
ptrace(PTRACE_SETOPTIONS, child, 0, PTRACE_O_TRACESYSGOOD);我前面提到 ptrace 基本上把子進(jìn)程上的所有事件都轉(zhuǎn)為 SIGTRAP。這很不方便,因?yàn)樗馕吨?dāng)你看到子進(jìn)程由于 SIGTRAP 而停止時(shí),沒有很好的辦法來知道它是由于它可能停止的多種原因中的哪種而停止的。
PTRACE SETOPTIONS 允許我們?cè)O(shè)置許多選項(xiàng)來控制我們要如何跟蹤子進(jìn)程。這里我們使用它來設(shè)置 PTRACE_O_TRACESYSGOOD,這意味著當(dāng)子進(jìn)程由于系統(tǒng)調(diào)用相關(guān)的原因停止時(shí),我們實(shí)際上會(huì)看到它以信號(hào)號(hào)SIGTRAP | 0x80 停止,這樣我們可以簡單地從其它停止中區(qū)分出系統(tǒng)調(diào)用導(dǎo)致地停止。由于(出于這個(gè) demo 的目的),我們只關(guān)注系統(tǒng)調(diào)用,這還是非常方便的。
while(1) {if (wait_for_syscall(child) != 0) break;現(xiàn)在我們進(jìn)入跟蹤循環(huán)。wait_for_syscall,在下面定義,將運(yùn)行子進(jìn)程直到進(jìn)入或退出一個(gè)系統(tǒng)調(diào)用。如果它返回非 0,則子進(jìn)程已經(jīng)退出,我們終止循環(huán)。
syscall = ptrace(PTRACE_PEEKUSER, child, sizeof(long)*ORIG_EAX);fprintf(stderr, "syscall(%d) = ", syscall);否則,盡管,我們知道子進(jìn)程進(jìn)入了一個(gè)系統(tǒng)調(diào)用,這樣我們需要解碼系統(tǒng)調(diào)用號(hào)(以及潛在的參數(shù),如果這是一個(gè)不那么簡單的例子)。PTRACE_PEEKUSER ptrace 請(qǐng)求從子進(jìn)程的 “user area” 讀取一個(gè)字的數(shù)據(jù),這是一個(gè)邏輯區(qū)域,它持有它所有的寄存器和其它的內(nèi)部非內(nèi)存狀態(tài)。在 i386 上,系統(tǒng)調(diào)用號(hào)位于 %eax。出于各種各樣的技術(shù)原因,然而,內(nèi)核在此時(shí)已經(jīng)破壞了子進(jìn)程的 %eax,但它在一個(gè)不同的偏移量處保存了原始值,ORIG_EAX,這來自于 sys/regs.h。
if (wait_for_syscall(child) != 0) break;一旦我們有了系統(tǒng)調(diào)用號(hào),我們?cè)俅?wait_for_syscall,這應(yīng)該會(huì)讓我們停止在系統(tǒng)調(diào)用返回處。
retval = ptrace(PTRACE_PEEKUSER, child, sizeof(long)*EAX);fprintf(stderr, "%d\n", retval);i386 上的返回值也是在 %eax 中傳遞的,因此這次我們可以直接讀取它,并打印返回值,然后返回到循環(huán)的頂部并等待下一次系統(tǒng)調(diào)用。
}return 0; }一旦子進(jìn)程退出,我們也返回。
int wait_for_syscall(pid_t child) {int status;while (1) {ptrace(PTRACE_SYSCALL, child, 0, 0);wait_for_syscall 是一個(gè)簡單的輔助函數(shù)。我們使用 PTRACE_SYSCALL 來繼續(xù)子進(jìn)程,這允許一個(gè)停止的子進(jìn)程繼續(xù)執(zhí)行直到下一次進(jìn)入或退出一個(gè)系統(tǒng)調(diào)用。
waitpid(child, &status, 0);然后我們 waitpid 等待有趣的事情發(fā)生在子進(jìn)程身上。
if (WIFSTOPPED(status) && WSTOPSIG(status) & 0x80)return 0;由于我們上面設(shè)置的 PTRACE_O_SYSGOOD ,我們可以通過檢查被停止的子進(jìn)程是否由一個(gè)最高位設(shè)置了的信號(hào)停止的來探測(cè)一個(gè)系統(tǒng)調(diào)用停止。如果是這樣,我們就返回。
if (WIFEXITED(status))return 1;} }如果子進(jìn)程退出,我們就完成了;否則,它是因?yàn)槲覀儾魂P(guān)心的原因而停止的(例如,execve),因此我們循環(huán)再次啟動(dòng)它,直到它遇到系統(tǒng)調(diào)用。
這就是它的全部。如果你想下載并試用,你可以在 github 上找到我剛剛發(fā)布的版本。
讓它更有用
雖然它可以工作,但我認(rèn)為以前的版本并不是特別有用。你不得不手動(dòng)解碼系統(tǒng)調(diào)用號(hào),且你無法獲得任何系統(tǒng)調(diào)用參數(shù)。
把代碼都包含在這篇博客中可能有點(diǎn)長,但我已經(jīng)把一個(gè)稍微更實(shí)用的版本發(fā)布到了相同的 github 倉庫的?master 。它包含一個(gè) Python 腳本來掃描 Linux 源碼以提取系統(tǒng)調(diào)用號(hào)及參數(shù)個(gè)數(shù)和類型,且它知道如何解碼字符串參數(shù),以使你可以看到文件名及 read 和 write 的數(shù)據(jù)。
讀取參數(shù)很容易 —— 在 i386 上,它們?cè)诩拇嫫髦袀鬟f,因此,對(duì)于每一個(gè)參數(shù),只是另一次 PTRACE_GETUSER。也許最有趣的片段就是 read_string 函數(shù)了,它用于從子進(jìn)程中讀取一個(gè) NULL 結(jié)尾的字符串。(當(dāng)然,以 NULL 結(jié)尾是不正確的 —— 真正的 strace 知道 read() 和 write() 的 count 參數(shù),比如。但這已經(jīng)足夠做一個(gè) demo 了。)
char *read_string(pid_t child, unsigned long addr) {read_string 接收一個(gè)要讀取的子進(jìn)程的進(jìn)程 ID,及它打算讀取的字符串的地址作為參數(shù):
char *val = malloc(4096);int allocated = 4096, read;unsigned long tmp;我們需要一些變量。一個(gè)拷入字符串的緩沖區(qū),我們已經(jīng)拷貝的數(shù)據(jù)及分配的數(shù)據(jù)的計(jì)數(shù)器,及一個(gè)臨時(shí)變量用于讀取內(nèi)存。
while (1) {if (read + sizeof tmp > allocated) {allocated *= 2;val = realloc(val, allocated);}我們?cè)诒匾獣r(shí)增加緩沖。我們一次一個(gè)字地讀取數(shù)據(jù)。
tmp = ptrace(PTRACE_PEEKDATA, child, addr + read);if(errno != 0) {val[read] = 0;break;}PTRACE_PEEKDATA 返回子進(jìn)程在指定偏移量處的數(shù)據(jù)工作。因?yàn)樗褂梅祷刂?#xff0c;所以我們需要檢查 errno 來判斷它是否失敗。如果它失敗了(可能由于子進(jìn)程傳遞了一個(gè)無效的指針),我們僅返回我們截止目前已經(jīng)獲得的字符串,確保在最后添加我們自己的 NULL。
memcpy(val + read, &tmp, sizeof tmp);if (memchr(&tmp, 0, sizeof tmp) != NULL)break;read += sizeof tmp;然后,將我們讀到的數(shù)據(jù)附加起來就很簡單了,如果我們發(fā)現(xiàn)一個(gè)終止 NULL 就跳出循環(huán),否則循環(huán)讀取另一個(gè)字。
}return val; }【原文】Write yourself an strace in 70 lines of code
總結(jié)
以上是生活随笔為你收集整理的用 70 行代码给你自己写一个 strace的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: strace 哇,好多系统调用
- 下一篇: 基于 FFmpeg 的播放器 demo