strace是Linux系統下的一個用來跟蹤系統調用的工具,它的實作基礎是ptrace系統調用。使用strace工具可以跟蹤一個程式執行過程中發生的系統調用。
我這裡講到的内容有一點點和mips體系相關,不過不熟悉mips也不影響閱讀。
ptrace系統調用
ptrace系統調用提供了一種方法來跟蹤和控制程序的執行,它可以讀取和修改程序位址空間中的内容,包括寄存器的值。ptrace主要用于實作斷點調試和跟蹤系統調用。該系統調用的原型如下:
long ptrace(enum __ptrace_request request, pid_t pid, void *addr,void *data);
ptrace的四個參數的含義為:
1. request:用于選擇一個操作,見下文。
2. pid:目标程序即被跟蹤程序的pid。
3. addr和data用于修改和拷貝被跟蹤程序的程序位址空間的資料。
下面的内容中将用父程序指代跟蹤者,用子程序指代被跟蹤者。實際上,在一個程序被跟蹤之後,跟蹤者程序會在某種意義上充當被跟蹤程序的父程序(如使用ps指令就可以看到他們的父子關系),而子程序真正的父程序被儲存在其task_struct結構的real_parent成員中。
使用ptrace跟蹤程序
父程序跟蹤一個程序的方式有兩種:1.調用fork(),然後子程序打上PTRACE_TRACEME标記,并執行exec。2.父程序可以給自己打上PTRACE_ATTACH标記來跟蹤一個已有程序。
一個程序被跟蹤後,他隻要接收到一個信号(即使這個信号被設定為忽略)就會停止運作(SIGKILL除外),然後父程序會在每次調用wait()時得到子程序停止運作的通知,這時父程序就可以檢測和修改子程序了,随後父程序可以讓子程序繼續運作。
當父程序不想跟蹤了,可以通過設定PTRACE_KILL标記來終止子程序的運作。也可以通過設定PTRACE_DETACH标記讓子程序解除被跟蹤,繼續正常運作。
常用的request
PTRACE_TRACEME
程序設定這個request目的是讓自己被父程序跟蹤。任何發送到該子程序的信号(除了SIGKILL)都會導緻他停下來,并在父程序wait()的時候通知到父程序。另外,子程序後續調用exec會導緻子程序自己收到一個SIGTRAP信号,這是為了讓父程序有機會在exec的新程式開始執行前獲得控制權。
除非一個程序知道父程序要跟蹤他,一般不會去設定這個request。設定這個請求時,pid,addr和data三個參數都會被忽略。
這個request隻供子程序設定,其他的request都是隻供父程序使用的。相應的,下面的request中,參數pid為被跟蹤的子程序的pid。
PTRACE_ATTACH
将pid指定的程序作為自己要跟蹤的程序,并開始跟蹤。這和子程序自己調用PTRACE_TRACEME的效果相同。
設定這個request時,子程序會首先收到一個SIGSTOP信号,但并不會停止,隻是導緻跟蹤者程序第一次被中斷,進而開始跟蹤,否則隻能等到子程序接收到第一個信号時才開始跟蹤。之後當子程序有待決信号時,程序總是會暫停,這時父程序通過SIGCHLD信号得到通知。在子程序暫停前,父程序可使用wait函數等待。
參數addr和data會被忽略。
PTRACE_CONT
讓被停掉的子程序繼續運作,而當子程序再次接收到信号時會暫停。
參數data如果被設定為一個非零值并且不是SIGSTOP,那data就是父程序發送給子程序的信号,否則,不給子程序發送信号。這樣一來,父程序可以控制是否向子程序發送一個信号。
參數addr會被忽略。
PTRACE_SYSCALL
讓停止的子程序繼續運作(同PTRACE_CONT),而當子程序再次接收到信号時會暫停,另外,在子程序中發生系統調用時,在系統調用的入口和結束時子程序也會停止,這時父程序認為子程序是因為收到SIGTRAP信号而停止的。
由于子程序在系統調用的入口和結束時都會停止,父程序就可以在系統調用入口處停止後,獲得系統調用的參數資訊,而在系統調用結束時停止後,獲得系統調用的傳回值。
參數addr會被忽略。
PTRACE_DETACH
讓停止的子程序繼續運作(同PTRACE_CONT),但是在這之前會先與跟蹤它的父程序解除PTRACE_ATTACH或PTRACE_TRACEME時的關系。
參數addr會被忽略。
PTRACE_KILL
給子程序發送一個SIGKILL信号來終止子程序。addr和data參數會被忽略。
PTRACE_PEEKTEXT,PTRACE_PEEKDATA
讀取程序位址空間中addr位址處的内容,讀出的長度為一個word(4位元組),作為ptrace()的傳回值(long型)傳回。Linux中的代碼和資料的位址空間并不是分離的,是以這兩個request實際上意義相同。
參數data會被忽略。
PTRACE_PEEKUSR
在程序的USER區域讀取一個word的長度。參數addr是指相對USER開頭的offset,結果作為傳回值。參數data會被忽略。
在mips中的程序自身資訊和程序位址空間中并沒有所謂的USER區域,在核心中通過參數addr的值,判斷應該傳回什麼結果,如下:
/* Read the word at location addr in theUSER area. */
case PTRACE_PEEKUSR: {
struct pt_regs *regs;
unsigned long tmp = 0;
/* 獲得程序位址空間的pt_regs區域的内容。 */
regs =task_pt_regs(child);
ret = 0; /* Default return value. */
switch (addr) {
case 0 ... 31: /* 通用寄存器 */
tmp =regs->regs[addr];
break;
case FPR_BASE ...FPR_BASE + 31:
......
break;
case PC:
tmp =regs->cp0_epc;
break;
case CAUSE:
tmp =regs->cp0_cause;
break;
case BADVADDR:
tmp =regs->cp0_badvaddr;
break;
case MMHI:
tmp = regs->hi;
break;
case MMLO:
tmp = regs->lo;
break;
case FPC_CSR:
tmp =child->thread.fpu.fcr31;
break;
case FPC_EIR: { /* implementation / version register */
......
break;
}
case DSP_BASE ...DSP_BASE + 5: {
......
break;
}
case DSP_CONTROL:
......
break;
default:
tmp = 0;
ret = -EIO;
goto out;
}
ret = put_user(tmp,(unsigned long __user *) data);
break;
}
PTRACE_POKETEXT,PTRACE_POKEDATA
将data的内容拷貝到程序位址空間中addr指向的位址。
PTRACE_POKEUSR
将data的内容拷貝到程序USER區域中偏移為addr的地方,一般addr要求是word-aligned的。由于要修改USER區域,核心為保證完整健全,會禁止某些域被修改。
strace工具的實作原理
strace工具是一個使用者态的應用程式,用來追蹤程序的系統調用。它的基礎就是ptrace系統調用。安裝strace之後,就可以使用strace指令了。
最簡單的strace指令的用法就是:strace PROG,PROG是要執行的程式。strace指令執行的結果就是按照調用順序列印出所有的系統調用,包括函數名、參數清單以及傳回值。
使用strace跟蹤一個程序的系統調用的基本流程如圖1所示。

圖1 strace實作流程
從圖中可以看出strace做了以下幾件事情:
1. 設定SIGCHLD 信号的處理函數,這個處理函數隻要不是SIG_IGN即可。由于子程序停止後是通過SIGCHLD信号通知父程序的,是以這裡要防止SIGCHLD信号被忽略。
2. 建立子程序,在子程序中調用ptrace(PTRACE_TRACEME,0L, 0L, 0L)使其被父程序跟蹤,并通過execv函數執行被跟蹤的程式。
3. 通過wait()等待子程序停止,并獲得子程序停止時的狀态status。
4. 通過子程序的狀态檢視子程序是否已正常退出,如果是,則不再跟蹤,随後調用ptrace發送PTRACE_DETACH請求解除跟蹤關系。
5. 子程序停止後,列印系統調用的函數名、參數和傳回值。具體流程見圖2。
6. 通過PTRACE_SYSCALL讓子程序繼續運作,由于這個請求會讓子程序在系統調用的入口處和系統調用完成時都會停止并通知父程序,這樣,父程序就可以在系統調用開始之前獲得參數,結束之後獲得傳回值。
在系統調用的入口和結束時子程序停止運作時,這時父程序認為子程序是因為收到SIGTRAP信号而停止的。是以父程序在wait()後可以通過SIGTRAP來與其他信号區分開。
Strace中為每個要跟蹤的程序維護了一個TCB(Trace Control Block)結構,定義如下。它儲存了目前發生的系統調用的資訊。
/* Trace Control Block */
struct tcb {
int flags; /* See below for TCB_ values */
int pid; /* Process Id of this entry */
int qual_flg; /* qual_flags[scno] or DEFAULT_QUAL_FLAGS + RAW*/
int u_error; /* Error code */
long scno; /* System call number */
long u_arg[MAX_ARGS]; /* System call arguments */
long u_rval; /* Return value */
int curcol; /* Output column for this process */
FILE *outf; /* Output file for this process */
const char *auxstr;/*Auxiliary info from syscall (see RVAL_STR) */
const struct_sysent *s_ent;/* sysent[scno] or dummy struct for bad scno */
struct timeval stime;/*System time usage as of last process wait */
struct timeval dtime; /* Delta for system time usage */
struct timeval etime; /* Syscall entry time */
/* Support fortracing forked processes: */
long inst[2]; /* Saved clone args (badly named) */
};
上面已經提到,子程序會在系統調用前後各停止一次,是以列印系統調用資訊時分為兩個階段:在系統調用開始時可以擷取系統調用号和參數,在系統調用結束時可以擷取系統調用的傳回結果。通過給tcb結構的flags字段清除和添加TCB_INSYSCALL标志位來區分系統調用的開始和結束。
圖2 strace中列印系統調用的實作流程
例如編寫一個使用printf列印“Hello world”的程式hello.c,使用strace跟蹤該程式的系統調用可以看到如下結果:
# ./strace ./hello
execve("./hello ", ["./hello "], [/* 7 vars */])= 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS,-1, 0) = 0x2aaad000
stat("/etc/ld.so.cache", 0x7faf4ca8) = -1 ENOENT (No such file or directory)
open("/tmp/libgcc_s.so.1", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/lib/libgcc_s.so.1", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=1565445, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS,-1, 0) = 0x2aaae000
read(3,"\177ELF\1\2\1\0\0\0\0\0\0\0\0\0\0\3\0\10\0\0\0\1\0\0\263\200\0\0\0004"...,4096) = 4096
mmap(NULL, 241664, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =0x2aabe000
mmap(0x2aabe000, 169308, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED,3, 0) = 0x2aabe000
mmap(0x2aaf8000, 2400, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED,3, 0x2a000) = 0x2aaf8000
close(3) = 0
munmap(0x2aaae000, 4096) = 0
open("/tmp/libc.so.0", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/lib/libc.so.0", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=431732, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS,-1, 0) = 0x2aaae000
read(3, "\177ELF\1\2\1\0\0\0\0\0\0\0\0\0\0\3\0\10\0\0\0\1\0\0\252\200\0\0\0004"...,4096) = 4096
mmap(NULL, 471040, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =0x2aaf9000
mmap(0x2aaf9000, 380336, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED,3, 0) = 0x2aaf9000
mmap(0x2ab65000, 8088, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED,3, 0x5c000) = 0x2ab65000
mmap(0x2ab67000, 19376, PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x2ab67000
close(3) = 0
munmap(0x2aaae000, 4096) = 0
open("/tmp/libc.so.0", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/lib/libc.so.0", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=431732, ...}) = 0
close(3) = 0
stat("/lib/ld-uClibc.so.0", {st_mode=S_IFREG|0755,st_size=22604, ...}) = 0
mprotect(0x2ab65000, 4096, PROT_READ) = 0
mprotect(0x2aabc000, 4096, PROT_READ) = 0
ioctl(0, TIOCNXCL, {B115200 opost isig icanon echo ...}) = 0
ioctl(1, TIOCNXCL, {B115200 opost isig icanon echo ...}) = 0
write(1, "Hello world\n", 12Hello world
) = 12
exit(0) = ?
+++ exited with 0 +++
#
從結果可以看出,執行該程式調用了很多系統調用,并最終通過write系統調用列印出“Hello world”。
跟蹤一個正在運作的程序,使用-p選項加上程序的pid。
跟蹤某個特定的系統調用,使用-e選項加上系統調用名。
例如,跟蹤程序727的epoll_wait系統調用:strace -e epoll_wait -p 727