自己动手写一个 strace
這次主要分享一下一個(gè)動(dòng)手的東西,就是自己動(dòng)手寫(xiě)一個(gè)?strace?工具。
用過(guò)?strace?的同學(xué)都知道,strace?是用來(lái)跟蹤進(jìn)程調(diào)用的?系統(tǒng)調(diào)用,還可以統(tǒng)計(jì)進(jìn)程對(duì)?系統(tǒng)調(diào)用?的統(tǒng)計(jì)等。strace?的使用方式有兩種,如下:
strace?執(zhí)行的程序
strace -p?進(jìn)程pid
第一種用于跟蹤將要執(zhí)行的程序,而第二種用于跟蹤一個(gè)運(yùn)行中的進(jìn)程。
下圖就是使用?strace?對(duì)?ls?命令跟蹤的結(jié)果:
ptrace系統(tǒng)調(diào)用
要自己動(dòng)手寫(xiě)?strace?的第一步就是了解?ptrace()?系統(tǒng)調(diào)用的使用,我們來(lái)看看?ptrace()?系統(tǒng)調(diào)用的定義:
int ptrace(long request, long pid, long addr, long data);ptrace()?系統(tǒng)調(diào)用用于跟蹤進(jìn)程的運(yùn)行情況,下面介紹一下其各個(gè)參數(shù)的含義:
request:指定跟蹤的動(dòng)作。也就是說(shuō),通過(guò)傳入不同的?request?參數(shù)可以對(duì)進(jìn)程進(jìn)行不同的跟蹤操作。其可選值有:
PTRACE_TRACEME
PTRACE_PEEKTEXT
PTRACE_POKETEXT
PTRACE_CONT
PTRACE_SINGLESTEP
...
pid:指定要跟蹤的進(jìn)程PID。
addr:指定要讀取或者修改的內(nèi)存地址。
data:對(duì)于不同的?request?操作,data?有不同的作用,下面會(huì)介紹。
前面介紹過(guò),使用?strace?跟蹤進(jìn)程有兩種方式,一種是通過(guò)?strace?命令啟動(dòng)進(jìn)程,另外一種是通過(guò)?-p?指定要跟蹤的進(jìn)程。
ptrace()?系統(tǒng)調(diào)用也提供了兩種?request?來(lái)實(shí)現(xiàn)上面兩種方式:
第一種通過(guò)?PTRACE_TRACEME?來(lái)實(shí)現(xiàn)
第二種通過(guò)?PTRACE_ATTACH?來(lái)實(shí)現(xiàn)
本文我們主要介紹使用第一種方式。由于第一種方式使用跟蹤程序來(lái)啟動(dòng)被跟蹤的程序,所以需要啟動(dòng)兩個(gè)進(jìn)程。通常要?jiǎng)?chuàng)建新進(jìn)程可以使用?fork()?系統(tǒng)調(diào)用,所以自然而然地我們也使用?fork()?系統(tǒng)調(diào)用。
我們新建一個(gè)文件?strace.c,輸入代碼如下:
int main(int argc, char *argv[]) {pid_t child;child = fork();if (child == 0) {// 子進(jìn)程...} else {// 父進(jìn)程...}return 0; }上面的代碼通過(guò)調(diào)用?fork()?來(lái)創(chuàng)建一個(gè)子進(jìn)程,但是沒(méi)有做任何事情。之后,我們就會(huì)在?子進(jìn)程?中運(yùn)行被跟蹤的程序,而在?父進(jìn)程?中運(yùn)行跟蹤進(jìn)程的代碼。
運(yùn)行被跟蹤程序
前面說(shuō)過(guò),被跟蹤的程序需要在子進(jìn)程中運(yùn)行,而要運(yùn)行一個(gè)程序,可以通過(guò)調(diào)用?execl()?系統(tǒng)調(diào)用。所以可以通過(guò)下面的代碼,在子進(jìn)程中運(yùn)行?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 {// 父進(jìn)程...}return 0; }execl()?用于執(zhí)行指定的程序,如果執(zhí)行成功就不會(huì)返回,所以?execl(...)?的下一行代碼?exit(0)?不會(huì)被執(zhí)行到。
由于我們需要跟蹤?ls?命令,所以在執(zhí)行?ls?命令前,必須調(diào)用?ptrace(PTRACE_TRACEME, 0, NULL, NULL)?來(lái)告訴系統(tǒng)需要跟蹤這個(gè)進(jìn)程,代碼如下:
#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 {// 父進(jìn)程...}return 0; }這樣,被跟蹤進(jìn)程部分的代碼就完成了,接下來(lái)開(kāi)始編寫(xiě)跟蹤進(jìn)程部分代碼。
編寫(xiě)跟蹤進(jìn)程代碼
如果編譯運(yùn)行上面的代碼,會(huì)發(fā)現(xiàn)什么效果也沒(méi)有。這是因?yàn)楫?dāng)在子進(jìn)程調(diào)用?ptrace(PTRACE_TRACEME, 0, NULL, NULL)?后,并且調(diào)用?execl()?系統(tǒng)調(diào)用,那么子進(jìn)程會(huì)發(fā)送一個(gè)?SIGCHLD?信號(hào)給父進(jìn)程(跟蹤進(jìn)程)并且自己停止運(yùn)行,直到父進(jìn)程發(fā)送調(diào)試命令,才會(huì)繼續(xù)運(yùn)行。
由于上面的代碼中,父進(jìn)程(跟蹤進(jìn)程)并沒(méi)有發(fā)送任何調(diào)試命令就退出運(yùn)行,所以子進(jìn)程(被跟蹤進(jìn)程)在沒(méi)有運(yùn)行的情況下就跟著父進(jìn)程一起退出了,那么就不會(huì)看到任何效果。
現(xiàn)在我們開(kāi)始編寫(xiě)跟蹤進(jìn)程的代碼。
由于被跟蹤進(jìn)程會(huì)發(fā)送一個(gè)?SIGCHLD?信息給跟蹤進(jìn)程,所以我們先要在跟蹤進(jìn)程的代碼中接收?SIGCHLD?信號(hào),接收信號(hào)通過(guò)使用?wait()?系統(tǒng)調(diào)用完成,代碼如下:
#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); // 接收被子進(jìn)程發(fā)送過(guò)來(lái)的 SIGCHLD 信號(hào)}return 0; }上面的代碼通過(guò)調(diào)用?wait()?系統(tǒng)調(diào)用來(lái)接收被跟蹤進(jìn)程發(fā)送過(guò)來(lái)的?SIGCHLD?信號(hào),接下來(lái)需要開(kāi)始向被跟蹤進(jìn)程發(fā)送調(diào)試命令,來(lái)對(duì)被跟蹤進(jìn)程進(jìn)行調(diào)試。
由于本文介紹怎么跟蹤進(jìn)程調(diào)用了哪些?系統(tǒng)調(diào)用,所以我們需要使用?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); // 接收被子進(jìn)程發(fā)送過(guò)來(lái)的 SIGCHLD 信號(hào)// 1. 發(fā)送 PTRACE_SYSCALL 命令給被跟蹤進(jìn)程 (調(diào)用系統(tǒng)調(diào)用前,可以獲取系統(tǒng)調(diào)用的參數(shù))ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子進(jìn)程發(fā)送過(guò)來(lái)的 SIGCHLD 信號(hào)// 2. 發(fā)送 PTRACE_SYSCALL 命令給被跟蹤進(jìn)程 (調(diào)用系統(tǒng)調(diào)用后,可以獲取系統(tǒng)調(diào)用的返回值)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子進(jìn)程發(fā)送過(guò)來(lái)的 SIGCHLD 信號(hào)}return 0; }從上面的代碼可以發(fā)現(xiàn),我們調(diào)用了兩次?ptrace(PTRACE_SYSCALL, child, NULL, NULL),這是因?yàn)楦櫹到y(tǒng)調(diào)用時(shí),需要跟蹤系統(tǒng)調(diào)用前的環(huán)境(比如獲取系統(tǒng)調(diào)用的參數(shù))和系統(tǒng)調(diào)用后的環(huán)境(比如獲取系統(tǒng)調(diào)用的返回值),所以就需要調(diào)用兩次?ptrace(PTRACE_SYSCALL, child, NULL, NULL)。
獲取進(jìn)程寄存器的值
Linux系統(tǒng)調(diào)用是通過(guò)?CPU寄存器?來(lái)傳遞參數(shù)的,所以要想獲取調(diào)用了哪個(gè)系統(tǒng)調(diào)用,必須獲取進(jìn)程寄存器的值。獲取進(jìn)程寄存器的值,可以通過(guò)?ptrace()?系統(tǒng)調(diào)用的?PTRACE_GETREGS?命令來(lái)實(shí)現(xiàn),代碼如下:
#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); // 接收被子進(jìn)程發(fā)送過(guò)來(lái)的 SIGCHLD 信號(hào)// 1. 發(fā)送 PTRACE_SYSCALL 命令給被跟蹤進(jìn)程 (調(diào)用系統(tǒng)調(diào)用前,可以獲取系統(tǒng)調(diào)用的參數(shù))ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子進(jìn)程發(fā)送過(guò)來(lái)的 SIGCHLD 信號(hào)ptrace(PTRACE_GETREGS, child, 0, ®s); // 獲取被跟蹤進(jìn)程寄存器的值orig_rax = regs.orig_rax; // 獲取rax寄存器的值printf("orig_rax: %d\n", orig_rax); // 打印rax寄存器的值// 2. 發(fā)送 PTRACE_SYSCALL 命令給被跟蹤進(jìn)程 (調(diào)用系統(tǒng)調(diào)用后,可以獲取系統(tǒng)調(diào)用的返回值)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子進(jìn)程發(fā)送過(guò)來(lái)的 SIGCHLD 信號(hào)}return 0; }上面的代碼通過(guò)調(diào)用?ptrace(PTRACE_GETREGS, child, 0, ®s)?來(lái)獲取進(jìn)程寄存器的值,PTRACE_GETREGS?命令需要在?data?參數(shù)傳入類(lèi)型為?user_regs_struct?結(jié)構(gòu)的指針,user_regs_struct?結(jié)構(gòu)定義如下(在文件?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?結(jié)構(gòu)的?orig_rax?保存了系統(tǒng)調(diào)用號(hào),所以我們可以通過(guò)?orig_rax?的值來(lái)知道調(diào)用了哪個(gè)系統(tǒng)調(diào)用。
編譯運(yùn)行上面的代碼,會(huì)輸出結(jié)果:orig_rax: 12,就是說(shuō)當(dāng)前調(diào)用的是編號(hào)為 12 的系統(tǒng)調(diào)用。那么編號(hào)為 12 的系統(tǒng)調(diào)用是哪個(gè)系統(tǒng)調(diào)用呢?可以通過(guò)下面鏈接來(lái)查看:
https://www.cnblogs.com/gavanwanggw/p/6920826.html
通過(guò)查閱系統(tǒng)調(diào)用表,可以知道編號(hào) 12 的系統(tǒng)調(diào)用為?brk(),如下:
系統(tǒng)調(diào)用號(hào) 函數(shù)名 入口點(diǎn) 源碼 ... 12 brk sys_brk mm/mmap.c ...上面的程序只跟蹤了一個(gè)系統(tǒng)調(diào)用,那么怎么跟蹤所有的系統(tǒng)調(diào)用呢?很簡(jiǎn)單,只需要把跟蹤的代碼放到一個(gè)無(wú)限循環(huán)中即可。代碼如下:
#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); // 接收被子進(jìn)程發(fā)送過(guò)來(lái)的 SIGCHLD 信號(hào)while (1) {// 1. 發(fā)送 PTRACE_SYSCALL 命令給被跟蹤進(jìn)程 (調(diào)用系統(tǒng)調(diào)用前,可以獲取系統(tǒng)調(diào)用的參數(shù))ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子進(jìn)程發(fā)送過(guò)來(lái)的 SIGCHLD 信號(hào)if (WIFEXITED(status)) { // 如果子進(jìn)程退出了, 那么終止跟蹤break;}ptrace(PTRACE_GETREGS, child, 0, ®s); // 獲取被跟蹤進(jìn)程寄存器的值orig_rax = regs.orig_rax; // 獲取rax寄存器的值printf("orig_rax: %d\n", orig_rax); // 打印rax寄存器的值// 2. 發(fā)送 PTRACE_SYSCALL 命令給被跟蹤進(jìn)程 (調(diào)用系統(tǒng)調(diào)用后,可以獲取系統(tǒng)調(diào)用的返回值)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子進(jìn)程發(fā)送過(guò)來(lái)的 SIGCHLD 信號(hào)if (WIFEXITED(status)) { // 如果子進(jìn)程退出了, 那么終止跟蹤break;}}}return 0; }if (WIFEXITED(status)) ...?這行代碼用于判斷子進(jìn)程(被跟蹤進(jìn)程)是否已經(jīng)退出,如果退出了就停止跟蹤。現(xiàn)在可以編譯并運(yùn)行這個(gè)程序,輸出結(jié)果如下:
[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 ...從執(zhí)行結(jié)果來(lái)看,只是打印系統(tǒng)調(diào)用號(hào)不太直觀,那么我們?cè)趺磧?yōu)化呢?
我們可以定義一個(gè)系統(tǒng)調(diào)用號(hào)與系統(tǒng)調(diào)用名的對(duì)應(yīng)表來(lái)實(shí)現(xiàn)更清晰的輸出結(jié)果,如下:
#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); // 接收被子進(jìn)程發(fā)送過(guò)來(lái)的 SIGCHLD 信號(hào)while (1) {// 1. 發(fā)送 PTRACE_SYSCALL 命令給被跟蹤進(jìn)程 (調(diào)用系統(tǒng)調(diào)用前,可以獲取系統(tǒng)調(diào)用的參數(shù))ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子進(jìn)程發(fā)送過(guò)來(lái)的 SIGCHLD 信號(hào)if(WIFEXITED(status)) { // 如果子進(jìn)程退出了, 那么終止跟蹤break;}ptrace(PTRACE_GETREGS, child, 0, ®s); // 獲取被跟蹤進(jìn)程寄存器的值orig_rax = regs.orig_rax; // 獲取rax寄存器的值printf("syscall: %s()\n", find_syscall_symbol(orig_rax)); // 打印系統(tǒng)調(diào)用// 2. 發(fā)送 PTRACE_SYSCALL 命令給被跟蹤進(jìn)程 (調(diào)用系統(tǒng)調(diào)用后,可以獲取系統(tǒng)調(diào)用的返回值)ptrace(PTRACE_SYSCALL, child, NULL, NULL);wait(&status); // 接收被子進(jìn)程發(fā)送過(guò)來(lái)的 SIGCHLD 信號(hào)if(WIFEXITED(status)) { // 如果子進(jìn)程退出了, 那么終止跟蹤break;}}}return 0; }上面例子添加了一個(gè)函數(shù)?find_syscall_symbol()?來(lái)獲取系統(tǒng)調(diào)用號(hào)對(duì)應(yīng)的系統(tǒng)調(diào)用名,實(shí)現(xiàn)也比較簡(jiǎn)單。編譯運(yùn)行后輸出結(jié)果如下:
[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() ...從執(zhí)行結(jié)果來(lái)看,現(xiàn)在可以打印系統(tǒng)調(diào)用的名字了,但我們知道?strace?命令還會(huì)打印系統(tǒng)調(diào)用參數(shù)的值,我們可以通過(guò)?ptrace()?系統(tǒng)調(diào)用的?PTRACE_PEEKTEXT?和?PTRACE_PEEKDATA?來(lái)獲取參數(shù)的值,所以有興趣的就自己實(shí)現(xiàn)這個(gè)效果了。
本文完整代碼在:
https://github.com/liexusong/build-strace-by-myself/blob/main/strace.c
推薦閱讀:
專(zhuān)輯|Linux文章匯總
專(zhuān)輯|程序人生
專(zhuān)輯|C語(yǔ)言
我的知識(shí)小密圈
關(guān)注公眾號(hào),后臺(tái)回復(fù)「1024」獲取學(xué)習(xí)資料網(wǎng)盤(pán)鏈接。
歡迎點(diǎn)贊,關(guān)注,轉(zhuǎn)發(fā),在看,您的每一次鼓勵(lì),我都將銘記于心~
嵌入式Linux
微信掃描二維碼,關(guān)注我的公眾號(hào)
總結(jié)
以上是生活随笔為你收集整理的自己动手写一个 strace的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: mysql sqlite 语法_浅谈sq
- 下一篇: 此操作要求使用 IIS 集成管线模式