自己动手写一个 strace
這次主要分享一下一個動手的東西,就是自己動手寫一個?strace?工具。
用過?strace?的同學都知道,strace?是用來跟蹤進程調用的?系統調用,還可以統計進程對?系統調用?的統計等。strace?的使用方式有兩種,如下:
strace?執行的程序
strace -p?進程pid
第一種用于跟蹤將要執行的程序,而第二種用于跟蹤一個運行中的進程。
下圖就是使用?strace?對?ls?命令跟蹤的結果:
ptrace系統調用
要自己動手寫?strace?的第一步就是了解?ptrace()?系統調用的使用,我們來看看?ptrace()?系統調用的定義:
int ptrace(long request, long pid, long addr, long data);ptrace()?系統調用用于跟蹤進程的運行情況,下面介紹一下其各個參數的含義:
request:指定跟蹤的動作。也就是說,通過傳入不同的?request?參數可以對進程進行不同的跟蹤操作。其可選值有:
PTRACE_TRACEME
PTRACE_PEEKTEXT
PTRACE_POKETEXT
PTRACE_CONT
PTRACE_SINGLESTEP
...
pid:指定要跟蹤的進程PID。
addr:指定要讀取或者修改的內存地址。
data:對于不同的?request?操作,data?有不同的作用,下面會介紹。
前面介紹過,使用?strace?跟蹤進程有兩種方式,一種是通過?strace?命令啟動進程,另外一種是通過?-p?指定要跟蹤的進程。
ptrace()?系統調用也提供了兩種?request?來實現上面兩種方式:
第一種通過?PTRACE_TRACEME?來實現
第二種通過?PTRACE_ATTACH?來實現
本文我們主要介紹使用第一種方式。由于第一種方式使用跟蹤程序來啟動被跟蹤的程序,所以需要啟動兩個進程。通常要創建新進程可以使用?fork()?系統調用,所以自然而然地我們也使用?fork()?系統調用。
我們新建一個文件?strace.c,輸入代碼如下:
int main(int argc, char *argv[]) {pid_t child;child = fork();if (child == 0) {// 子進程...} else {// 父進程...}return 0; }上面的代碼通過調用?fork()?來創建一個子進程,但是沒有做任何事情。之后,我們就會在?子進程?中運行被跟蹤的程序,而在?父進程?中運行跟蹤進程的代碼。
運行被跟蹤程序
前面說過,被跟蹤的程序需要在子進程中運行,而要運行一個程序,可以通過調用?execl()?系統調用。所以可以通過下面的代碼,在子進程中運行?ls?命令:
#include <unistd.h> #include <stdlib.h>int main(int argc, char *argv[]) {pid_t child;child = fork();if (child == 0) {execl("/bin/ls", "/bin/ls", NULL);exit(0);} else {// 父進程...}return 0; }execl()?用于執行指定的程序,如果執行成功就不會返回,所以?execl(...)?的下一行代碼?exit(0)?不會被執行到。
由于我們需要跟蹤?ls?命令,所以在執行?ls?命令前,必須調用?ptrace(PTRACE_TRACEME, 0, NULL, NULL)?來告訴系統需要跟蹤這個進程,代碼如下:
#include <sys/ptrace.h> #include <unistd.h> #include <stdlib.h>int main(int argc, char *argv[]) {pid_t child;child = fork();if (child == 0) {ptrace(PTRACE_TRACEME, 0, NULL, NULL);execl("/bin/ls", "/bin/ls", NULL);exit(0);} else {// 父進程...}return 0; }這樣,被跟蹤進程部分的代碼就完成了,接下來開始編寫跟蹤進程部分代碼。
編寫跟蹤進程代碼
如果編譯運行上面的代碼,會發現什么效果也沒有。這是因為當在子進程調用?ptrace(PTRACE_TRACEME, 0, NULL, NULL)?后,并且調用?execl()?系統調用,那么子進程會發送一個?SIGCHLD?信號給父進程(跟蹤進程)并且自己停止運行,直到父進程發送調試命令,才會繼續運行。
由于上面的代碼中,父進程(跟蹤進程)并沒有發送任何調試命令就退出運行,所以子進程(被跟蹤進程)在沒有運行的情況下就跟著父進程一起退出了,那么就不會看到任何效果。
現在我們開始編寫跟蹤進程的代碼。
由于被跟蹤進程會發送一個?SIGCHLD?信息給跟蹤進程,所以我們先要在跟蹤進程的代碼中接收?SIGCHLD?信號,接收信號通過使用?wait()?系統調用完成,代碼如下:
#include <sys/ptrace.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h>int main(int argc, char *argv[]) {pid_t child;int status;child = fork();if (child == 0) {ptrace(PTRACE_TRACEME, 0, NULL, NULL);execl("/bin/ls", "/bin/ls", NULL);exit(0);} else {wait(&status); // 接收被子進程發送過來的 SIGCHLD 信號}return 0; }上面的代碼通過調用?wait()?系統調用來接收被跟蹤進程發送過來的?SIGCHLD?信號,接下來需要開始向被跟蹤進程發送調試命令,來對被跟蹤進程進行調試。
由于本文介紹怎么跟蹤進程調用了哪些?系統調用,所以我們需要使用?ptrace()?的?PTRACE_SYSCALL?命令,代碼如下:
#include <sys/ptrace.h> #include <sys/user.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h>int main(int argc, char *argv[]) {pid_t child;int status;struct user_regs_struct regs;int orig_rax;child = fork();if (child == 0) {ptrace(PTRACE_TRACEME, 0, NULL, NULL);execl("/bin/ls", "/bin/ls", NULL);exit(0);} else {wait(&status); // 接收被子進程發送過來的 SIGCHLD 信號// 1. 發送 PTRACE_SYSCALL 命令給被跟蹤進程 (調用系統調用前,可以獲取系統調用的參數)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子進程發送過來的 SIGCHLD 信號// 2. 發送 PTRACE_SYSCALL 命令給被跟蹤進程 (調用系統調用后,可以獲取系統調用的返回值)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子進程發送過來的 SIGCHLD 信號}return 0; }從上面的代碼可以發現,我們調用了兩次?ptrace(PTRACE_SYSCALL, child, NULL, NULL),這是因為跟蹤系統調用時,需要跟蹤系統調用前的環境(比如獲取系統調用的參數)和系統調用后的環境(比如獲取系統調用的返回值),所以就需要調用兩次?ptrace(PTRACE_SYSCALL, child, NULL, NULL)。
獲取進程寄存器的值
Linux系統調用是通過?CPU寄存器?來傳遞參數的,所以要想獲取調用了哪個系統調用,必須獲取進程寄存器的值。獲取進程寄存器的值,可以通過?ptrace()?系統調用的?PTRACE_GETREGS?命令來實現,代碼如下:
#include <sys/ptrace.h> #include <sys/user.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h>int main(int argc, char *argv[]) {pid_t child;int status;struct user_regs_struct regs;int orig_rax;child = fork();if (child == 0) {ptrace(PTRACE_TRACEME, 0, NULL, NULL);execl("/bin/ls", "/bin/ls", NULL);exit(0);} else {wait(&status); // 接收被子進程發送過來的 SIGCHLD 信號// 1. 發送 PTRACE_SYSCALL 命令給被跟蹤進程 (調用系統調用前,可以獲取系統調用的參數)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子進程發送過來的 SIGCHLD 信號ptrace(PTRACE_GETREGS, child, 0, ®s); // 獲取被跟蹤進程寄存器的值orig_rax = regs.orig_rax; // 獲取rax寄存器的值printf("orig_rax: %d\n", orig_rax); // 打印rax寄存器的值// 2. 發送 PTRACE_SYSCALL 命令給被跟蹤進程 (調用系統調用后,可以獲取系統調用的返回值)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子進程發送過來的 SIGCHLD 信號}return 0; }上面的代碼通過調用?ptrace(PTRACE_GETREGS, child, 0, ®s)?來獲取進程寄存器的值,PTRACE_GETREGS?命令需要在?data?參數傳入類型為?user_regs_struct?結構的指針,user_regs_struct?結構定義如下(在文件?sys/user.h?中):
struct user_regs_struct {unsigned long r15,r14,r13,r12,rbp,rbx,r11,r10;unsigned long r9,r8,rax,rcx,rdx,rsi,rdi,orig_rax;unsigned long rip,cs,eflags;unsigned long rsp,ss;unsigned long fs_base, gs_base;unsigned long ds,es,fs,gs; };其中?user_regs_struct?結構的?orig_rax?保存了系統調用號,所以我們可以通過?orig_rax?的值來知道調用了哪個系統調用。
編譯運行上面的代碼,會輸出結果:orig_rax: 12,就是說當前調用的是編號為 12 的系統調用。那么編號為 12 的系統調用是哪個系統調用呢?可以通過下面鏈接來查看:
https://www.cnblogs.com/gavanwanggw/p/6920826.html
通過查閱系統調用表,可以知道編號 12 的系統調用為?brk(),如下:
系統調用號 函數名 入口點 源碼 ... 12 brk sys_brk mm/mmap.c ...上面的程序只跟蹤了一個系統調用,那么怎么跟蹤所有的系統調用呢?很簡單,只需要把跟蹤的代碼放到一個無限循環中即可。代碼如下:
#include <sys/ptrace.h> #include <sys/user.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h>int main(int argc, char *argv[]) {pid_t child;int status;struct user_regs_struct regs;int orig_rax;child = fork();if (child == 0) {ptrace(PTRACE_TRACEME, 0, NULL, NULL);execl("/bin/ls", "/bin/ls", NULL);exit(0);} else {wait(&status); // 接收被子進程發送過來的 SIGCHLD 信號while (1) {// 1. 發送 PTRACE_SYSCALL 命令給被跟蹤進程 (調用系統調用前,可以獲取系統調用的參數)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子進程發送過來的 SIGCHLD 信號if (WIFEXITED(status)) { // 如果子進程退出了, 那么終止跟蹤break;}ptrace(PTRACE_GETREGS, child, 0, ®s); // 獲取被跟蹤進程寄存器的值orig_rax = regs.orig_rax; // 獲取rax寄存器的值printf("orig_rax: %d\n", orig_rax); // 打印rax寄存器的值// 2. 發送 PTRACE_SYSCALL 命令給被跟蹤進程 (調用系統調用后,可以獲取系統調用的返回值)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子進程發送過來的 SIGCHLD 信號if (WIFEXITED(status)) { // 如果子進程退出了, 那么終止跟蹤break;}}}return 0; }if (WIFEXITED(status)) ...?這行代碼用于判斷子進程(被跟蹤進程)是否已經退出,如果退出了就停止跟蹤。現在可以編譯并運行這個程序,輸出結果如下:
[root@localhost liexusong]$ ./strace orig_rax: 12 orig_rax: 9 orig_rax: 21 orig_rax: 2 orig_rax: 5 orig_rax: 9 orig_rax: 3 orig_rax: 2 orig_rax: 0 orig_rax: 5 orig_rax: 9 orig_rax: 10 orig_rax: 9 orig_rax: 9 orig_rax: 3 orig_rax: 2 orig_rax: 0 orig_rax: 5 orig_rax: 9 orig_rax: 10 ...從執行結果來看,只是打印系統調用號不太直觀,那么我們怎么優化呢?
我們可以定義一個系統調用號與系統調用名的對應表來實現更清晰的輸出結果,如下:
#include <sys/ptrace.h> #include <sys/user.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h>struct syscall {int code;char *name; } syscall_table[] = {{0, "read"},{1, "write"},{2, "open"},{3, "close"},{4, "stat"},{5, "fstat"},{6, "lstat"},{7, "poll"},{8, "lseek"},...{-1, NULL}, }char *find_syscall_symbol(int code) {struct syscall *sc;for (sc = syscall_table; sc->code >= 0; sc++) {if (sc->code == code) {return sc->name;}}return NULL; }int main(int argc, char *argv[]) {pid_t child;int status;struct user_regs_struct regs;int orig_rax;child = fork();if (child == 0) {ptrace(PTRACE_TRACEME, 0, NULL, NULL);execl("/bin/ls", "/bin/ls", NULL);exit(0);} else {wait(&status); // 接收被子進程發送過來的 SIGCHLD 信號while (1) {// 1. 發送 PTRACE_SYSCALL 命令給被跟蹤進程 (調用系統調用前,可以獲取系統調用的參數)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子進程發送過來的 SIGCHLD 信號if(WIFEXITED(status)) { // 如果子進程退出了, 那么終止跟蹤break;}ptrace(PTRACE_GETREGS, child, 0, ®s); // 獲取被跟蹤進程寄存器的值orig_rax = regs.orig_rax; // 獲取rax寄存器的值printf("syscall: %s()\n", find_syscall_symbol(orig_rax)); // 打印系統調用// 2. 發送 PTRACE_SYSCALL 命令給被跟蹤進程 (調用系統調用后,可以獲取系統調用的返回值)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子進程發送過來的 SIGCHLD 信號if(WIFEXITED(status)) { // 如果子進程退出了, 那么終止跟蹤break;}}}return 0; }上面例子添加了一個函數?find_syscall_symbol()?來獲取系統調用號對應的系統調用名,實現也比較簡單。編譯運行后輸出結果如下:
[root@localhost liexusong]$ ./strace syscall: brk() syscall: mmap() syscall: access() syscall: open() syscall: fstat() syscall: mmap() syscall: close() syscall: open() syscall: read() syscall: fstat() syscall: mmap() syscall: mprotect() syscall: mmap() syscall: mmap() syscall: close() ...從執行結果來看,現在可以打印系統調用的名字了,但我們知道?strace?命令還會打印系統調用參數的值,我們可以通過?ptrace()?系統調用的?PTRACE_PEEKTEXT?和?PTRACE_PEEKDATA?來獲取參數的值,所以有興趣的就自己實現這個效果了。
本文完整代碼在:
https://github.com/liexusong/build-strace-by-myself/blob/main/strace.c
推薦閱讀:
專輯|Linux文章匯總
專輯|程序人生
專輯|C語言
我的知識小密圈
關注公眾號,后臺回復「1024」獲取學習資料網盤鏈接。
歡迎點贊,關注,轉發,在看,您的每一次鼓勵,我都將銘記于心~
嵌入式Linux
微信掃描二維碼,關注我的公眾號
總結
以上是生活随笔為你收集整理的自己动手写一个 strace的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql sqlite 语法_浅谈sq
- 下一篇: 此操作要求使用 IIS 集成管线模式