系統調用ptrace對gdb這種調試器來說是非常重要的,杯具的是,相關的文檔卻殘缺不詳–除非你覺得最好的文檔就是內核源碼!!下面,我會試着向大家展示ptrace在gdb這類工具中的作用.
1. 介紹
ptrace()是一個系統調用,它允許一個進程控制另外一個進程的執行.不僅如此,我們還可以借助於ptrace修改某個進程的空間(內存或寄存器),任何傳遞給一個進程(即被跟蹤進程)的信號(除了會直接殺死進程的SIGKILL信號)都會使得這個進程進入暫停狀態,這時系統通過wait()通知跟蹤進程,這樣,跟蹤進程就可以修改被跟蹤進程的行為了.
如果跟蹤進程在被跟蹤進程的內存中設置了相關的事件標志位,那么運行中被跟蹤進程也可能因為特殊的事件而暫停.跟蹤結束后,跟蹤進程甚至可以通過設置被跟蹤進程的退出碼(exit code)來殺死它,當然也可以讓它繼續運行.
注意: ptrace()是高度依賴於底層硬件的.使用ptrace的程序通常不容易在個鍾體系結構間移植.
2. 細節
ptrace的原型如下:
1.
#include <sys/ptrace.h>
2.
long
int
ptrace(
enum
__ptrace_request request, pid_t pid,
void
* addr,
void
* data)
我們可以看到,ptrace有4個參數,其中,request決定ptrace做什么,pid是被跟蹤進程的ID,data存儲從進程空間偏移量為addr的地方開始將被讀取/寫入的數據.
父進程可以通過將request設置為PTRACE_TRACEME,以此跟蹤被fork出來的子進程,它同樣可以通過使用PTRACE_ATTACH來跟蹤一個已經在運行的進程.
2.1 ptrace如何工作
不管ptrace是什么時候被調用的,它首先做的就是鎖住內核.在ptrace返回前,內核會被解鎖.在這個過程中,ptrace是如何工作的呢,我們看看request值(譯注: request的可選值值是定義在/usr/include/sys/ptrace.h中的宏)代表的含義吧.
PTRACE_TRACEME:
PTRACE_TRACEME是被父進程用來跟蹤子進程的.正如前面所說的,任何信號(除了SIGKILL),不管是從外來的還是由exec系統調用產生的,都將使得子進程被暫停,由父進程決定子進程的行為.在request為PTRACE_TRACEME情況下,ptrace()只干一件事,它檢查當前進程的ptrace標志是否已經被設置,沒有的話就設置ptrace標志,除了request的任何參數(pid,addr,data)都將被忽略.
PTRACE_ATTACH:
request為PTRACE_ATTACH也就意味着,一個進程想要控制另外一個進程.需要注意的是,任何進程都不能跟蹤控制起始進程init,一個進程也不能跟蹤自己.某種意義上,調用ptrace的進程就成為了ID為pid的進程的’父’進程.但是,被跟蹤進程的真正父進程是ID為getpid()的進程.
PTRACE_DETACH:
用來停止跟蹤一個進程.跟蹤進程決定被跟蹤進程的生死.PTRACE_DETACH會恢復PTRACE_ATTACH和PTRACE_TRACEME的所有改變.父進程通過data參數設置子進程的退出狀態(exit code).子進程的ptrace標志就被復位,然后子進程被移到它原來所在的任務隊列中.這時候,子進程的父進程的ID被重新寫回子進程的父進程標志位.可能被修改了的single-step標志位也會被復位.最后,子進程被喚醒,貌似神馬都沒有發生過;參數addr會被忽略.
PTRACE_PEEKTEXT, PTRACE_PEEKDATA, PTRACE_PEEKUSER:
這些宏用來讀取子進程的內存和用戶態空間(user space).PTRACE_PEEKTEXT和PTRACE_PEEKDATA從子進程內存讀取數據,兩者功能是相同的.PTRACE_PEEKUSER從子進程的user space讀取數據.它們讀一個字節的數據,保存在臨時的數據結構中,然后使用put_user()(它從內核態空間讀一個字符串到用戶態空間)將需要的數據寫入參數data,返回0表示成功.
對PTRACE_PEEKTEXT和PTRACE_PEEKDATA而言,參數addr是子進程內存中將被讀取的數據的地址.對PTRACE_PEEKUSER來說,參數addr是子進程用戶態空間的偏移量,此時data被無視.
PTRACE_POKETEXT, PTRACE_POKEDATA, PTRACE_POKEUSER:
這些宏行為與上面的幾個是類似的.唯一的不同是它們用來寫入data.(譯注: 這段文字與上面的描述差不多,為免繁復,不譯.)
PTRACE_SYSCALL, PTRACE_CONT:
這些宏用來喚醒暫停的子進程.在每次系統調用之后,PTRACE_SYSCALL使子進程暫停,而PTRACE_CONT讓子進程繼續運行.子進程的返回狀態都是由ptrace()參數data設置的.但是,這只限於返回狀態是有效的情況.ptrace()重置子進程的single-step位,設置/復位syscall-trace位,然后喚醒子進程;參數addr被無視.
PTRACE_SINGLESTEP:
PTRACE_SINGLESTEP的行為與PTRACE_SYSCALL無異,除了子進程在每次機器指令后都被暫停(PTRACE_SYSCALL是使子進程每次在系統調用后被暫停).single-step會被設置,跟PTRACE_SYSCALL一樣,參數data包含返回狀態,參數addr被無視.
PTRACE_KILL:
PTRACE_KILL被用來終止子進程.”謀殺”是這樣進行的: 首先ptrace() 查看子進程是不是已經死了.如果不是, 子進程的返回碼被設置為sigkill. single-step位被復位.然后子進程被喚醒,運行到返回碼時子進程就死掉了.
2.2 更加依賴於硬件的調用.
上面討論的request可選值是依賴於操作系統所在的體系結構和實現的.下面討論的request可選值可以用來get/set子進程的寄存器,這更加依賴於系統架構.對寄存器的設置包括通用寄存器,浮點寄存器和擴展的浮點寄存器.
PTRACE_GETREGS, PTRACE_GETFPREGS, PTRACE_GETFPXREGS:
這些宏用來讀子進程的寄存器.寄存器的值通過getreg()和__put_user()被讀入data中;參數addr被無視.
PTRACE_SETREGS, PTRACE_SETFPREGS, PTRACE_SETFPXREGS:
跟上面的描述相反,這些宏被用來設置寄存器.
2.3 ptrace()的返回值
成功的ptrace()調用返回0.如果出錯,將返回-1,errno也將被設置.PEEKDATA/PEEKTEXT,即使ptrace()調用成功,返回值也可能是-1,所以我們最好檢查一下errno,它的可能值如下:
EPERM: 權限錯誤,進程無法被跟蹤.
ESRCH: 目標進程不存在或者已經被跟蹤.
EIO: 參數request的值無效,或者從非法的內存讀/寫數據.
EFAULT: 需要讀/寫數據的內存未被映射.
EIO和EFAULT真的很難區分,它們代表很嚴重的錯誤.
3. 小例子
如果你覺得上面的說明太枯燥了,好吧,我保證再也不這么干了.下面舉個小例子,演示一下吧.
這是第一個,父進程對子進程中發生的每一次機器指令計數.
01.
#include <stdio.h>
02.
#include <stdlib.h>
03.
#include <signal.h>
04.
#include <syscall.h>
05.
#include <sys/ptrace.h>
06.
#include <sys/types.h>
07.
#include <sys/wait.h>
08.
#include <unistd.h>
09.
#include <errno.h>
10.
11.
int
main(
void
)
12.
{
13.
long
long
counter = 0;
/* machine instruction counter */
14.
int
wait_val;
/* child's return value */
15.
int
pid;
/* child's process id */
16.
17.
puts
(
"Please wait"
);
18.
19.
switch
(pid = fork()) {
20.
case
-1:
21.
perror
(
"fork"
);
22.
break
;
23.
case
0:
/* child process starts */
24.
ptrace(PTRACE_TRACEME, 0, 0, 0);
25.
/*
26.
* must be called in order to allow the
27.
* control over the child process
28.
*/
29.
execl(
"/bin/ls"
,
"ls"
, NULL);
30.
/*
31.
* executes the program and causes
32.
* the child to stop and send a signal
33.
* to the parent, the parent can now
34.
* switch to PTRACE_SINGLESTEP
35.
*/
36.
break
;
37.
/* child process ends */
38.
default
:
/* parent process starts */
39.
wait(&wait_val);
40.
/*
41.
* parent waits for child to stop at next
42.
* instruction (execl())
43.
*/
44.
while
(wait_val == 1407 ) {
45.
counter++;
46.
if
(ptrace(PTRACE_SINGLESTEP, pid, 0, 0) != 0)
47.
perror
(
"ptrace"
);
48.
/*
49.
* switch to singlestep tracing and
50.
* release child
51.
* if unable call error.
52.
*/
53.
wait(&wait_val);
54.
/* wait for next instruction to complete */
55.
}
56.
/*
57.
* continue to stop, wait and release until
58.
* the child is finished; wait_val != 1407
59.
* Low=0177L and High=05 (SIGTRAP)
60.
*/
61.
}
62.
printf
(
"Number of machine instructions : %lld\n"
, counter);
63.
return
0;
64.
}
運行一下代碼吧(可能程序會很慢,哈哈.).
譯注: 小小的解釋下吧,子進程開始運行,調用exec后移花栽木,這時子進程的原進程(未調用exec之前的進程)因為要死了,會向父進程發送SIGTRAP信號.父進程一直阻塞等待(第一條wait(&wait_val);語句).父進程捕獲到SIGTRAP信號,由此知道子進程已經結束了.接下來發生的就是最有趣的事情了,父進程通過request值為PTRACE_SINGLESTEP的ptrace調用,告訴操作系統,重新喚醒子進程,但是在每條機器指令運行之后暫停.再一次的,父進程阻塞等待子進程暫停(wait_val == 1407等價於WIFSTOPPED(wait_val) (個人看法,我還沒有查到子進程狀態的編碼資料,求~))並計數,子進程結束(非暫停,對應的是WIFEXITED)后,父進程跳出loop循環.
4. 結論
ptrace()在調試器中是被用得很多的,它也可以被用來跟蹤系統調用.調試器fork一個子進程並跟蹤它,然后子進程exec調用要被調試的目標程序,在目標程序的每一次機器指令之后父進程都可以查看它的寄存器的值.在下一篇文章中,我會盡情展示ptrace的牛逼之處,春節快樂.
作者: Sandeep S
I am a final year student of Government Engineering College in Thrissur, Kerala, India. My areas of interests include FreeBSD, Networking and also Theoretical Computer Science.