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