一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

【轉(zhuǎn)】調(diào)試器是怎樣工作的: Part 1 – 基礎(chǔ)

 果果許 2012-04-17

一點(diǎn)聲明
原文作者: http://eli./
原文鏈接: http://eli./2011/01/23/how-debuggers-work-part-1/

translated by Arthur1989, 個(gè)人主頁: http:///

我想寫一個(gè)系列,解釋調(diào)試器是如何實(shí)現(xiàn)的,盡管我不確定總共會(huì)有多少篇文章,也不清楚主題到底應(yīng)該包括些什么,我還是先寫了這第一篇.讓我們開始吧.

In this part

在這篇文章中,我將解釋實(shí)現(xiàn)Linux下的調(diào)試器的基石–ptrace系統(tǒng)調(diào)用.本文的所有例子都是在32位Ubuntu上編寫的,需要注意的是,這些代碼是高度依賴于計(jì)算機(jī)體系結(jié)構(gòu)的,好在移植他們應(yīng)該不會(huì)太難.

Motivation

試著想想調(diào)試器需要干些什么,它可以啟動(dòng)一個(gè)進(jìn)程并調(diào)試之,或者將自己綁定到一個(gè)正在運(yùn)行的進(jìn)程.調(diào)試器還可以在代碼中單步執(zhí)行,,設(shè)置斷點(diǎn),查看變量值和堆棧跟蹤信息(stack trace)(譯注: 堆棧跟蹤很大程度上歸功于寄存器ebp的設(shè)計(jì),由于ebp指向的地址保存的總是”上一層函數(shù)調(diào)用的ebp值”,一直遞歸尋找,就能找出目的函數(shù)的ebp,而在每層函數(shù)調(diào)用中,都能通過當(dāng)層的ebp往棧底找到返回地址和參數(shù)值,往棧頂找出局部變量值.–這個(gè)設(shè)計(jì)真的是太精巧了—再注: 嚴(yán)重感謝DDD(libfetion項(xiàng)目發(fā)起者)的指點(diǎn): “調(diào)試器可以通過保持”ebp”來處理每一個(gè)棧針,但有時(shí)候?yàn)榱藘?yōu)化程序,使得程序能更快的運(yùn)行,有時(shí)候不會(huì)保存ebp的。因?yàn)槌绦蛎窟M(jìn)行一次函數(shù)調(diào)用,系統(tǒng)就得將ebp壓一次棧,這個(gè)相對來說有一定的性能損耗的。另外一種解決方式是使用 unwind 來組織和解析棧信息。它的原理是在編譯的時(shí)候,將每個(gè)函數(shù)要壓棧的信息(這些信息是可以提前計(jì)算出來的)都保存到一個(gè)表里,調(diào)試器在解析棧的時(shí)候,通過查詢那個(gè)表的函數(shù)信息,從而可以完全的解析出整個(gè)棧。有關(guān)unwind的信息,你可以去看下“調(diào)試器是怎樣工作的: Part 3 – 調(diào)試信息”中講到的DWARF,它里面有這方面的信息?!?SPAN style="PADDING-BOTTOM: 0px; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; COLOR: rgb(136,136,136); PADDING-TOP: 0px">–作為參考,不妨讀一讀這篇文章<Getting the call stack without a frame pointer>). 很多調(diào)試器還有一些高級特性,比如說執(zhí)行表達(dá)式,調(diào)用在被調(diào)試進(jìn)程內(nèi)存空間中函數(shù),甚至對正在運(yùn)行的進(jìn)程的內(nèi)存做出修改,并監(jiān)測它帶來的影響.

現(xiàn)代調(diào)試器都是集合了各種特技的怪獸[1],但是它們的實(shí)現(xiàn)基礎(chǔ)卻出奇的簡單,調(diào)試器不過是使用了操作系統(tǒng)和編譯器/鏈接器提供的一些基礎(chǔ)服務(wù)罷了,剩下的就是編程的問題了.

Linux debugging – ptrace

Linux下調(diào)試器的大殺器正是ptrace系統(tǒng)調(diào)用[2].它功能強(qiáng)大,但用起來卻很復(fù)雜,ptrace允許一個(gè)進(jìn)程控制另外一個(gè)進(jìn)程,甚至能夠peek and poke被跟蹤進(jìn)程的內(nèi)部[3].對ptrace的詳細(xì)解釋至少需要半本書的篇幅,所以下面我重點(diǎn)著墨于ptrace的使用.

現(xiàn)在,讓我們開始吧.

Stepping through the code of a process

這里有一份代碼,它是一個(gè)處于”被跟蹤”(“traced”)模式的進(jìn)程,CPU將單步執(zhí)行的它機(jī)器代碼(匯編指令).完整的代碼請見后文.

首要的計(jì)劃是將代碼分為兩部分,一部分是執(zhí)行用戶所提供的指令(user-supplied command)的子進(jìn)程,另外一部分是一個(gè)父進(jìn)程,它跟蹤子進(jìn)程.下面是main函數(shù):

int main(int argc, char** argv)
{
    pid_t child_pid;

    if (argc < 2) {
        fprintf(stderr, "Expected a program name as argument\n");
        return -1;
    }

    child_pid = fork();
    if (child_pid == 0)
        run_target(argv[1]);
    else if (child_pid > 0)
        run_debugger(child_pid);
    else {
        perror("fork");
        return -1;
    }

    return 0;
}

上面的代碼很簡單: 用fork創(chuàng)建一個(gè)子進(jìn)程[4]. 第二條if語句運(yùn)行子進(jìn)程(這里叫做”target”),接下來的else if 分支執(zhí)行父進(jìn)程(這里叫做”debugger”).

這是target的代碼:

void run_target(const char* programname)
{
    procmsg("target started. will run '%s'\n", programname);

    /* Allow tracing of this process */
    if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0) {
        perror("ptrace");
        return;
    }

    /* Replace this process's image with the given program */
    execl(programname, programname, 0);
}

最引人的就是ptrace語句.ptrace原型如下(in sys/ptrace.h):

long ptrace(enum __ptrace_request request, pid_t pid,
                 void *addr, void *data);

參數(shù)request有很多預(yù)定義的可選常數(shù)PTRACE_*,系統(tǒng)調(diào)用ptrace將作用于ID為pid的進(jìn)程之上.參數(shù)addr和data分別是地址和指向數(shù)據(jù)的指針,它們可以用來操作內(nèi)存.上面代碼中的ptrace做出了PTRACE_TRACEME 請求,意味著子進(jìn)程通知OS,讓父進(jìn)程跟蹤自己.PTRACE_TRACEME在man-page中有很清楚的解釋:

Indicates that this process is to be traced by its parent. Any signal (except SIGKILL) delivered to this process will cause it to stop and its parent to be notified via wait(). Also, all subsequent calls to exec() by this process will cause a SIGTRAP to be sent to it, giving the parent a chance to gain control before the new program begins execution. A process probably shouldn’t make this request if its parent isn’t expecting to trace it. (pid, addr, and data are ignored.)

PTRACE_TRACEME被父進(jìn)程用來跟蹤子進(jìn)程,任何信號(除了SIGKILL)都會(huì)暫停子進(jìn)程,接著阻塞于wait()等待的父進(jìn)程被喚醒.子進(jìn)程內(nèi)部對exec()的調(diào)用將發(fā)出SIGTRAP信號,這可以讓父進(jìn)程在子進(jìn)程新程序開始運(yùn)行之前就完全控制它.如果父進(jìn)程不打算跟蹤子進(jìn)程,子進(jìn)程就不應(yīng)該發(fā)出PTRACE_TRACEME請求.(在PTRACE_TRACEME請求時(shí),參數(shù)pid,addr和data被忽略.)

注意run_target在做出ptrace調(diào)用之后的第一步,就是用execl執(zhí)行參數(shù)programname.正如引文解釋的那樣,子進(jìn)程在執(zhí)行execl的新程序之前會(huì)被暫停,而且父進(jìn)程將收到OS發(fā)送的信號.

是時(shí)候看看父進(jìn)程了:

void run_debugger(pid_t child_pid)
{
    int wait_status;
    unsigned icounter = 0;
    procmsg("debugger started\n");

    /* Wait for child to stop on its first instruction */
    wait(&wait_status);

    while (WIFSTOPPED(wait_status)) {
        icounter++;
        /* Make the child execute another instruction */
        if (ptrace(PTRACE_SINGLESTEP, child_pid, 0, 0) < 0) {
            perror("ptrace");
            return;
        }

        /* Wait for child to stop on its next instruction */
        wait(&wait_status);
    }

    procmsg("the child executed %u instructions\n", icounter);
}

回憶一下,子進(jìn)程內(nèi)部執(zhí)行exec調(diào)用將發(fā)出SIGTRAP信號,父進(jìn)程中的第一條wait()就是等待這個(gè)信號的,父進(jìn)程通過WIFSTOPPED查看子進(jìn)程是否由于信號被暫停.

接下來發(fā)生的就是最有趣的事了,父進(jìn)程通過request值為 PTRACE_SINGLESTEP的對pid為ID的進(jìn)程做ptrace調(diào)用,告訴操作系統(tǒng),重新喚醒子進(jìn)程,但是在每條機(jī)器指令運(yùn)行之后暫停.再一次的,父進(jìn)程阻塞等待子進(jìn)程暫停并計(jì)數(shù),子進(jìn)程結(jié)束(WIFEXITED返回真)后,父進(jìn)程跳出loop循環(huán).

icounter對子進(jìn)程執(zhí)行的每條指令計(jì)數(shù).這還挺有用的,哈哈.

A test run

編譯下面的代碼,運(yùn)行之,讓它被跟蹤.

#include <stdio.h>

int main()
{
    printf("Hello, world!\n");
    return 0;
}

出乎意料的,跟蹤進(jìn)程執(zhí)行了很長的時(shí)間,報(bào)告說上面的代碼執(zhí)行了超過100,000條機(jī)器指令.開什么玩笑,僅僅一行printf就..? 搞什么鬼啊? 答案很值得討論[5].Linux下gcc默認(rèn)將程序動(dòng)態(tài)鏈接到C運(yùn)行庫,這意味著,當(dāng)一個(gè)程序被運(yùn)行時(shí),發(fā)生的第一件事就是動(dòng)態(tài)庫裝載器(dynamic library loader)搜索共享庫(shared libraries),這就解釋了為什么上面的小例子會(huì)執(zhí)行了這么多機(jī)器指令.

如果用-static標(biāo)志編譯代碼,讓它靜態(tài)鏈接(這時(shí)候可執(zhí)行文件大小超過了500KB),這時(shí)跟蹤程序報(bào)告說樣例執(zhí)行的機(jī)器指令只有大概7,000條.還是有點(diǎn)多,但是考慮到在main之前l(fā)ibc的初始化,還有main退出后的清理工作,這也就說得過去了.再說了,printf也挺復(fù)雜的.

但是我還是不滿意,我想要的是可驗(yàn)證的程序,也就是說,程序執(zhí)行的整個(gè)過程我都一清二楚.所以我寫了一個(gè)匯編版本的”Hello, world!”:

section    .text
    ; The _start symbol must be declared for the linker (ld)
    global _start

_start:

    ; Prepare arguments for the sys_write system call:
    ;   - eax: system call number (sys_write)
    ;   - ebx: file descriptor (stdout)
    ;   - ecx: pointer to string
    ;   - edx: string length
    mov    edx, len
    mov    ecx, msg
    mov    ebx, 1
    mov    eax, 4

    ; Execute the sys_write system call
    int    0x80

    ; Execute sys_exit
    mov    eax, 1
    int    0x80

section   .data
msg db    'Hello, world!', 0xa
len equ    $ - msg

跟蹤進(jìn)程報(bào)告有7條指令被執(zhí)行,很好,我可以很容易地驗(yàn)證它.

Deep into the instruction stream

有了上面的匯編代碼,我們看看ptrace的另一個(gè)強(qiáng)悍功能吧–它可以用來跟蹤進(jìn)程的狀態(tài).這是run_debugger的下一個(gè)版本:

void run_debugger(pid_t child_pid)
{
    int wait_status;
    unsigned icounter = 0;
    procmsg("debugger started\n");

    /* Wait for child to stop on its first instruction */
    wait(&wait_status);

    while (WIFSTOPPED(wait_status)) {
        icounter++;
        struct user_regs_struct regs;
        ptrace(PTRACE_GETREGS, child_pid, 0, &regs);
        unsigned instr = ptrace(PTRACE_PEEKTEXT, child_pid, regs.eip, 0);

        procmsg("icounter = %u.  EIP = 0x%08x.  instr = 0x%08x\n",
                    icounter, regs.eip, instr);

        /* Make the child execute another instruction */
        if (ptrace(PTRACE_SINGLESTEP, child_pid, 0, 0) < 0) {
            perror("ptrace");
            return;
        }

        /* Wait for child to stop on its next instruction */
        wait(&wait_status);
    }

    procmsg("the child executed %u instructions\n", icounter);
}

唯一的不同是while循環(huán)的前面幾條代碼.有兩條ptrace語句,第一條讀取被跟蹤進(jìn)程的寄存器,保存到數(shù)據(jù)結(jié)構(gòu)user_regs_struct(參考sys/user.h)中.

在獲得了所有寄存器的值后,我們接著用PTRACE_PEEKTEXT讀取eip(x86的擴(kuò)展指令指針(extended instruction pointer))對應(yīng)的機(jī)器指令[6].看一下輸出結(jié)果:

$ simple_tracer traced_helloworld
[5700] debugger started
[5701] target started. will run 'traced_helloworld'
[5700] icounter = 1.  EIP = 0x08048080.  instr = 0x00000eba
[5700] icounter = 2.  EIP = 0x08048085.  instr = 0x0490a0b9
[5700] icounter = 3.  EIP = 0x0804808a.  instr = 0x000001bb
[5700] icounter = 4.  EIP = 0x0804808f.  instr = 0x000004b8
[5700] icounter = 5.  EIP = 0x08048094.  instr = 0x01b880cd
Hello, world!
[5700] icounter = 6.  EIP = 0x08048096.  instr = 0x000001b8
[5700] icounter = 7.  EIP = 0x0804809b.  instr = 0x000080cd
[5700] the child executed 7 instructions

OK,現(xiàn)在除了icounter,我們還能看到eip和它對應(yīng)的機(jī)器指令.怎樣驗(yàn)證輸出結(jié)果呢? 我們可以借助objdump -d:

$ objdump -d traced_helloworld

traced_helloworld:     file format elf32-i386

Disassembly of section .text:

08048080 <.text>:
 8048080:     ba 0e 00 00 00          mov    $0xe,%edx
 8048085:     b9 a0 90 04 08          mov    $0x80490a0,%ecx
 804808a:     bb 01 00 00 00          mov    $0x1,%ebx
 804808f:     b8 04 00 00 00          mov    $0x4,%eax
 8048094:     cd 80                   int    $0x80
 8048096:     b8 01 00 00 00          mov    $0x1,%eax
 804809b:     cd 80                   int    $0x80

objdump -d的輸出和跟蹤進(jìn)程的輸出是一致的.

Attaching to a running process

正如你知道的那樣調(diào)試器還可以調(diào)試正在運(yùn)行的進(jìn)程.很容易知道,這可以用PTRACE_ATTACH請求來實(shí)現(xiàn).我不再給出代碼了,因?yàn)檫@不難編寫.

The code

完整的代碼在這里.用gcc -Wall -pedantic –std=c99 code.c編譯它.

Conclusion and next steps

好吧,我承認(rèn),這篇文章覆蓋的內(nèi)容很少 -– 離真正可以工作的調(diào)試器還差得很遠(yuǎn).不管怎么樣,我希望至少現(xiàn)在看來,調(diào)試器沒有那么神秘了.ptrace很好很強(qiáng)大,我們剛上路呢.

在C代碼中單步執(zhí)行確實(shí)很有用,但它不是萬能的.以C版本的”Hello, world!”為例,在執(zhí)行main函數(shù)之前,程序已經(jīng)單步執(zhí)行了上萬條用于C庫初始化的機(jī)器指令了,可以看出,這很不方便.我們的需求其實(shí)是在main入口前面放置斷點(diǎn),然后step到main.這該如何實(shí)現(xiàn)呢,請看part22.

References

我覺得這些資源不錯(cuò):

Playing with ptrace, Part I
Process tracing using ptrace
How debugger works


注:

[1] 我沒查證過,但是我保證gdb的源代碼行數(shù)(LOC)至少有六位數(shù).
[2] man 2 ptrace.
[3] Peek and poke是用以讀寫內(nèi)存的黑話.
[4] 這篇文章要求讀者對Unix/Linux編程有一定的經(jīng)驗(yàn).我假設(shè)讀者熟悉(至少是概念上的) fork, exec族和Unix信號.
[5] 假設(shè)你和我一樣,對底層細(xì)節(jié)著迷的話.:-)
[6] 提醒: 這篇文章很多內(nèi)容是平臺相關(guān)的.我做了一些設(shè)定 –- 比如說,x86指令并不一定要求是4字節(jié).(在我的32位Ubuntu上unsigned的長度).

    本站是提供個(gè)人知識管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    99久久国产精品免费| 中文字幕一二区在线观看| 国产原创中文av在线播放| 韩国激情野战视频在线播放| 国产小青蛙全集免费看| 亚洲综合精品天堂夜夜| 色一欲一性一乱—区二区三区| 毛片在线观看免费日韩| 69老司机精品视频在线观看| 成人免费在线视频大香蕉| 免费久久一级欧美特大黄孕妇| 91国内视频一区二区三区| 91精品国产av一区二区| 俄罗斯胖女人性生活视频| 午夜精品成年人免费视频| 免费久久一级欧美特大黄孕妇| 日韩国产亚洲欧美激情| 男人把女人操得嗷嗷叫| 国产一区二区三区丝袜不卡 | 日本美国三级黄色aa| 欧美一区二区三区十区| 午夜亚洲少妇福利诱惑| 好吊妞视频只有这里有精品| 国产熟女一区二区不卡| 伊人天堂午夜精品草草网| 婷婷激情五月天丁香社区 | 成年人视频日本大香蕉久久| 人妻少妇av中文字幕乱码高清| 国产偷拍盗摄一区二区| 日韩精品免费一区三区| 欧美日韩精品久久亚洲区熟妇人| 护士又紧又深又湿又爽的视频| 久久综合九色综合欧美| 中文字幕亚洲人妻在线视频| 国产又爽又猛又粗又色对黄| 日韩av亚洲一区二区三区| 亚洲精品深夜福利视频| 99热九九在线中文字幕| 精品偷拍一区二区三区| 日本午夜免费观看视频| 国产香蕉国产精品偷在线观看|