日本黄色一级经典视频|伊人久久精品视频|亚洲黄色色周成人视频九九九|av免费网址黄色小短片|黄色Av无码亚洲成年人|亚洲1区2区3区无码|真人黄片免费观看|无码一级小说欧美日免费三级|日韩中文字幕91在线看|精品久久久无码中文字幕边打电话

當(dāng)前位置:首頁 > 單片機(jī) > 程序喵大人

在程序出現(xiàn)bug的時(shí)候,最好的解決辦法就是通過GDB調(diào)試程序,然后找到程序出現(xiàn)問題的地方。比如程序出現(xiàn)段錯(cuò)誤(內(nèi)存地址不合法)時(shí),就可以通過GDB找到程序哪里訪問了不合法的內(nèi)存地址而導(dǎo)致的。

本文不是介紹 GDB 的使用方式,而是大概介紹 GDB 的實(shí)現(xiàn)原理,當(dāng)然 GDB 是一個(gè)龐大而復(fù)雜的項(xiàng)目,不可能只通過一篇文章就能解釋清楚,所以本文主要是介紹 GDB 使用的核心的技術(shù) -ptrace。

ptrace系統(tǒng)調(diào)用

ptrace()系統(tǒng)調(diào)用是 Linux 提供的一個(gè)調(diào)試進(jìn)程的工具,ptrace()系統(tǒng)調(diào)用非常強(qiáng)大,它提供非常多的調(diào)試方式讓我們?nèi)フ{(diào)試某一個(gè)進(jìn)程,下面是ptrace()系統(tǒng)調(diào)用的定義:

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

下面解釋一下ptrace()各個(gè)參數(shù)的作用:

  • request:指定調(diào)試的指令,指令的類型很多,如:PTRACE_TRACEME、PTRACE_PEEKUSER、PTRACE_CONT、PTRACE_GETREGS等等,下面會(huì)介紹不同指令的作用。
  • pid:進(jìn)程的ID(這個(gè)不用解釋了)。
  • addr:進(jìn)程的某個(gè)地址空間,可以通過這個(gè)參數(shù)對(duì)進(jìn)程的某個(gè)地址進(jìn)行讀或?qū)懖僮鳌?
  • data:根據(jù)不同的指令,有不同的用途,下面會(huì)介紹。

ptrace()系統(tǒng)調(diào)用詳細(xì)的介紹可以參考以下鏈接:https://man7.org/linux/man-pages/man2/ptrace.2.html

ptrace使用示例

下面通過一個(gè)簡單例子來說明ptrace()系統(tǒng)調(diào)用的使用,這個(gè)例子主要介紹怎么使用ptrace()系統(tǒng)調(diào)用獲取當(dāng)前被調(diào)試(追蹤)進(jìn)程的各個(gè)寄存器的值,代碼如下(ptrace.c):

#include  #include  #include  #include  #include  #include  int main() { pid_t child; struct user_regs_struct regs; child = fork(); // 創(chuàng)建一個(gè)子進(jìn)程 if(child == 0) { // 子進(jìn)程 ptrace(PTRACE_TRACEME, 0, NULL, NULL); // 表示當(dāng)前進(jìn)程進(jìn)入被追蹤狀態(tài) execl("/bin/ls", "ls", NULL); // 執(zhí)行 `/bin/ls` 程序 } else { // 父進(jìn)程 wait(NULL); // 等待子進(jìn)程發(fā)送一個(gè) SIGCHLD 信號(hào) ptrace(PTRACE_GETREGS, child, NULL, ®s); // 獲取子進(jìn)程的各個(gè)寄存器的值 printf("Register: rdi[%ld], rsi[%ld], rdx[%ld], rax[%ld], orig_rax[%ld]\n",
                regs.rdi, regs.rsi, regs.rdx,regs.rax, regs.orig_rax); // 打印寄存器的值 ptrace(PTRACE_CONT, child, NULL, NULL); // 繼續(xù)運(yùn)行子進(jìn)程 sleep(1);
    } return 0;
}

通過命令gcc ptrace.c -o ptrace編譯并運(yùn)行上面的程序會(huì)輸出如下結(jié)果:

Register: rdi[0], rsi[0], rdx[0], rax[0], orig_rax[59]
ptrace  ptrace.c

上面結(jié)果的第一行是由父進(jìn)程輸出的,主要是打印了子進(jìn)程執(zhí)行/bin/ls程序后各個(gè)寄存器的值。而第二行是由子進(jìn)程輸出的,主要是打印了執(zhí)行/bin/ls程序后輸出的結(jié)果。

下面解釋一下上面程序的執(zhí)行流程:

  1. 主進(jìn)程調(diào)用fork()系統(tǒng)調(diào)用創(chuàng)建一個(gè)子進(jìn)程。
  2. 子進(jìn)程調(diào)用ptrace(PTRACE_TRACEME,...)把自己設(shè)置為被追蹤狀態(tài),并且調(diào)用execl()執(zhí)行/bin/ls程序。
  3. 被設(shè)置為追蹤(TRACE)狀態(tài)的子進(jìn)程執(zhí)行execl()的程序后,會(huì)向父進(jìn)程發(fā)送SIGCHLD信號(hào),并且暫停自身的執(zhí)行。
  4. 父進(jìn)程通過調(diào)用wait()接收子進(jìn)程發(fā)送過來的信號(hào),并且開始追蹤子進(jìn)程。
  5. 父進(jìn)程通過調(diào)用ptrace(PTRACE_GETREGS, child, ...)來獲取到子進(jìn)程各個(gè)寄存器的值,并且打印寄存器的值。
  6. 父進(jìn)程通過調(diào)用ptrace(PTRACE_CONT, child, ...)讓子進(jìn)程繼續(xù)執(zhí)行下去。

從上面的例子可以知道,通過向ptrace()函數(shù)的request參數(shù)傳入不同的值時(shí),就有不同的效果。比如傳入PTRACE_TRACEME就可以讓進(jìn)程進(jìn)入被追蹤狀態(tài),而傳入PTRACE_GETREGS時(shí),就可以獲取被追蹤的子進(jìn)程各個(gè)寄存器的值等。

本來我想使用ptrace實(shí)現(xiàn)一個(gè)簡單的調(diào)試工具,但在網(wǎng)上找到了一位 Google 的大神Eli Bendersky寫了類似的系列文章,所以我就不再重復(fù)工作了,在這里貼一下文章的鏈接:

  • https://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1/
  • https://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints
  • https://eli.thegreenplace.net/2011/02/07/how-debuggers-work-part-3-debugging-information

但由于Eli Bendersky大神的文章只是介紹使用ptrace實(shí)現(xiàn)一個(gè)簡單的進(jìn)程調(diào)試工具,而沒有介紹ptrace的原理和實(shí)現(xiàn),所以這里為了填補(bǔ)這個(gè)空缺,下面就詳細(xì)介紹一下ptrace的原理與實(shí)現(xiàn)。

ptrace實(shí)現(xiàn)原理

本文使用的 Linux 2.4.16 版本的內(nèi)核

看懂本文需要的基礎(chǔ):進(jìn)程調(diào)度,內(nèi)存管理和信號(hào)處理相關(guān)知識(shí)。

調(diào)用ptrace()系統(tǒng)函數(shù)時(shí)會(huì)觸發(fā)調(diào)用內(nèi)核的sys_ptrace()函數(shù),由于不同的 CPU 架構(gòu)有著不同的調(diào)試方式,所以 Linux 為每種不同的 CPU 架構(gòu)實(shí)現(xiàn)了不同的sys_ptrace()函數(shù),而本文主要介紹的是X86 CPU的調(diào)試方式,所以sys_ptrace()函數(shù)所在文件是linux-2.4.16/arch/i386/kernel/ptrace.c。

sys_ptrace()函數(shù)的主體是一個(gè)switch語句,會(huì)傳入的request參數(shù)不同進(jìn)行不同的操作,如下:

asmlinkage int sys_ptrace(long request, long pid, long addr, long data) { struct task_struct *child; struct user *dummy = NULL; int i, ret;

    ...

    read_lock(&tasklist_lock);
    child = find_task_by_pid(pid); // 獲取 pid 對(duì)應(yīng)的進(jìn)程 task_struct 對(duì)象 if (child)
        get_task_struct(child);
    read_unlock(&tasklist_lock); if (!child) goto out; if (request == PTRACE_ATTACH) {
        ret = ptrace_attach(child); goto out_tsk;
    }

    ... switch (request) { case PTRACE_PEEKTEXT: case PTRACE_PEEKDATA:
        ... case PTRACE_PEEKUSR:
        ... case PTRACE_POKETEXT: case PTRACE_POKEDATA:
        ... case PTRACE_POKEUSR:
        ... case PTRACE_SYSCALL: case PTRACE_CONT:
        ... case PTRACE_KILL: 
        ... case PTRACE_SINGLESTEP:
        ... case PTRACE_DETACH:
        ...
    }
out_tsk:
    free_task_struct(child);
out:
    unlock_kernel(); return ret;
}

從上面的代碼可以看出,sys_ptrace()函數(shù)首先根據(jù)進(jìn)程的pid獲取到進(jìn)程的task_struct對(duì)象。然后根據(jù)傳入不同的request參數(shù)在switch語句中進(jìn)行不同的操作。

ptrace()支持的所有request操作定義在linux-2.4.16/include/linux/ptrace.h文件中,如下:

#define PTRACE_TRACEME         0 #define PTRACE_PEEKTEXT        1 #define PTRACE_PEEKDATA        2 #define PTRACE_PEEKUSR         3 #define PTRACE_POKETEXT        4 #define PTRACE_POKEDATA        5 #define PTRACE_POKEUSR         6 #define PTRACE_CONT            7 #define PTRACE_KILL            8 #define PTRACE_SINGLESTEP      9 #define PTRACE_ATTACH       0x10 #define PTRACE_DETACH       0x11 #define PTRACE_SYSCALL        24 #define PTRACE_GETREGS        12 #define PTRACE_SETREGS        13 #define PTRACE_GETFPREGS      14 #define PTRACE_SETFPREGS      15 #define PTRACE_GETFPXREGS     18 #define PTRACE_SETFPXREGS     19 #define PTRACE_SETOPTIONS     21 

由于ptrace()提供的操作比較多,所以本文只會(huì)挑選一些比較有代表性的操作進(jìn)行解說,比如PTRACE_TRACEME、PTRACE_SINGLESTEP、PTRACE_PEEKTEXT、PTRACE_PEEKDATA和PTRACE_CONT等,而其他的操作,有興趣的朋友可以自己去分析其實(shí)現(xiàn)原理。

進(jìn)入被追蹤模式(PTRACE_TRACEME操作)

當(dāng)要調(diào)試一個(gè)進(jìn)程時(shí),需要使進(jìn)程進(jìn)入被追蹤模式,怎么使進(jìn)程進(jìn)入被追蹤模式呢?有兩個(gè)方法:

  • 被調(diào)試的進(jìn)程調(diào)用ptrace(PTRACE_TRACEME, ...)來使自己進(jìn)入被追蹤模式。
  • 調(diào)試進(jìn)程(如GDB)調(diào)用ptrace(PTRACE_ATTACH, pid, ...)來使指定的進(jìn)程進(jìn)入被追蹤模式。

第一種方式是進(jìn)程自己主動(dòng)進(jìn)入被追蹤模式,而第二種是進(jìn)程被動(dòng)進(jìn)入被追蹤模式。

被調(diào)試的進(jìn)程必須進(jìn)入被追蹤模式才能進(jìn)行調(diào)試,因?yàn)?Linux 會(huì)對(duì)被追蹤的進(jìn)程進(jìn)行一些特殊的處理。下面我們主要介紹第一種進(jìn)入被追蹤模式的實(shí)現(xiàn),就是PTRACE_TRACEME的操作過程,代碼如下:

asmlinkage int sys_ptrace(long request, long pid, long addr, long data) {
    ... if (request == PTRACE_TRACEME) { if (current->ptrace & PT_PTRACED) goto out;
        current->ptrace |= PT_PTRACED; // 標(biāo)志 PTRACE 狀態(tài) ret = 0; goto out;
    }
    ...
}

從上面的代碼可以發(fā)現(xiàn),ptrace()對(duì)PTRACE_TRACEME的處理就是把當(dāng)前進(jìn)程標(biāo)志為PTRACE狀態(tài)。

當(dāng)然事情不會(huì)這么簡單,因?yàn)楫?dāng)一個(gè)進(jìn)程被標(biāo)記為PTRACE狀態(tài)后,當(dāng)調(diào)用exec()函數(shù)去執(zhí)行一個(gè)外部程序時(shí),將會(huì)暫停當(dāng)前進(jìn)程的運(yùn)行,并且發(fā)送一個(gè)SIGCHLD給父進(jìn)程。父進(jìn)程接收到SIGCHLD信號(hào)后就可以對(duì)被調(diào)試的進(jìn)程進(jìn)行調(diào)試。

我們來看看exec()函數(shù)是怎樣實(shí)現(xiàn)上述功能的,exec()函數(shù)的執(zhí)行過程為sys_execve() -> do_execve() -> load_elf_binary():

static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs) {
    ... if (current->ptrace & PT_PTRACED)
        send_sig(SIGTRAP, current, 0);
    ...
}

從上面代碼可以看出,當(dāng)進(jìn)程被標(biāo)記為PTRACE狀態(tài)時(shí),執(zhí)行exec()函數(shù)后便會(huì)發(fā)送一個(gè)SIGTRAP的信號(hào)給當(dāng)前進(jìn)程。

我們再來看看,進(jìn)程是怎么處理SIGTRAP信號(hào)的。信號(hào)是通過do_signal()函數(shù)進(jìn)行處理的,而對(duì)SIGTRAP信號(hào)的處理邏輯如下:

int do_signal(struct pt_regs *regs, sigset_t *oldset) { for (;;) { unsigned long signr;

        spin_lock_irq(¤t->sigmask_lock);
        signr = dequeue_signal(¤t->blocked, &info);
        spin_unlock_irq(¤t->sigmask_lock); // 如果進(jìn)程被標(biāo)記為 PTRACE 狀態(tài) if ((current->ptrace & PT_PTRACED) && signr != SIGKILL) { /* 讓調(diào)試器運(yùn)行  */ current->exit_code = signr;
            current->state = TASK_STOPPED; // 讓自己進(jìn)入停止運(yùn)行狀態(tài) notify_parent(current, SIGCHLD); // 發(fā)送 SIGCHLD 信號(hào)給父進(jìn)程 schedule(); // 讓出CPU的執(zhí)行權(quán)限 ...
        }
    }
}

上面的代碼主要做了3件事:

  1. 如果當(dāng)前進(jìn)程被標(biāo)記為 PTRACE 狀態(tài),那么就使自己進(jìn)入停止運(yùn)行狀態(tài)。
  2. 發(fā)送 SIGCHLD 信號(hào)給父進(jìn)程。
  3. 讓出 CPU 的執(zhí)行權(quán)限,使 CPU 執(zhí)行其他進(jìn)程。

執(zhí)行以上過程后,被追蹤進(jìn)程便進(jìn)入了調(diào)試模式,過程如下圖:

traceme

當(dāng)父進(jìn)程(調(diào)試進(jìn)程)接收到SIGCHLD信號(hào)后,表示被調(diào)試進(jìn)程已經(jīng)標(biāo)記為被追蹤狀態(tài)并且停止運(yùn)行,那么調(diào)試進(jìn)程就可以開始進(jìn)行調(diào)試了。

獲取被調(diào)試進(jìn)程的內(nèi)存數(shù)據(jù)(PTRACE_PEEKTEXT / PTRACE_PEEKDATA)

調(diào)試進(jìn)程(如GDB)可以通過調(diào)用ptrace(PTRACE_PEEKDATA, pid, addr, data)來獲取被調(diào)試進(jìn)程addr處虛擬內(nèi)存地址的數(shù)據(jù),但每次只能讀取一個(gè)大小為 4字節(jié)的數(shù)據(jù)。

我們來看看ptrace()對(duì)PTRACE_PEEKDATA操作的處理過程,代碼如下:

asmlinkage int sys_ptrace(long request, long pid, long addr, long data) {
    ... switch (request) { case PTRACE_PEEKTEXT: case PTRACE_PEEKDATA: { unsigned long tmp; int copied;

        copied = access_process_vm(child, addr, &tmp, sizeof(tmp), 0);
        ret = -EIO; if (copied != sizeof(tmp)) break;
        ret = put_user(tmp, (unsigned long *)data); break;
    }
    ...
}

從上面代碼可以看出,對(duì)PTRACE_PEEKTEXT和PTRACE_PEEKDATA的處理是相同的,主要是通過調(diào)用access_process_vm()函數(shù)來讀取被調(diào)試進(jìn)程addr處的虛擬內(nèi)存地址的數(shù)據(jù)。

access_process_vm()函數(shù)的實(shí)現(xiàn)主要涉及到內(nèi)存管理相關(guān)的知識(shí),可以參考我以前對(duì)內(nèi)存管理分析的文章,這里主要大概說明一下access_process_vm()的原理。

我們知道每個(gè)進(jìn)程都有個(gè)mm_struct的內(nèi)存管理對(duì)象,而mm_struct對(duì)象有個(gè)表示虛擬內(nèi)存與物理內(nèi)存映射關(guān)系的頁目錄的指針pgd。如下:

struct mm_struct { ... pgd_t *pgd; /* 頁目錄指針 */ ...
}

而access_process_vm()函數(shù)就是通過進(jìn)程的頁目錄來找到addr虛擬內(nèi)存地址映射的物理內(nèi)存地址,然后把此物理內(nèi)存地址處的數(shù)據(jù)復(fù)制到data變量中。如下圖所示:

memory_map

access_process_vm()函數(shù)的實(shí)現(xiàn)這里就不分析了,有興趣的讀者可以參考我之前對(duì)內(nèi)存管理分析的文章自行進(jìn)行分析。

單步調(diào)試模式(PTRACE_SINGLESTEP)

單步調(diào)試是一個(gè)比較有趣的功能,當(dāng)把被調(diào)試進(jìn)程設(shè)置為單步調(diào)試模式后,被調(diào)試進(jìn)程沒執(zhí)行一條CPU指令都會(huì)停止執(zhí)行,并且向父進(jìn)程(調(diào)試進(jìn)程)發(fā)送一個(gè) SIGCHLD 信號(hào)。

我們來看看ptrace()函數(shù)對(duì)PTRACE_SINGLESTEP操作的處理過程,代碼如下:

asmlinkage int sys_ptrace(long request, long pid, long addr, long data) {
    ... switch (request) { case PTRACE_SINGLESTEP: { /* set the trap flag. */ long tmp;
        ...
        tmp = get_stack_long(child, EFL_OFFSET) | TRAP_FLAG;
        put_stack_long(child, EFL_OFFSET, tmp);
        child->exit_code = data; /* give it a chance to run. */ wake_up_process(child);
        ret = 0; break;
    }
    ...
}

要把被調(diào)試的進(jìn)程設(shè)置為單步調(diào)試模式,英特爾的 X86 CPU 提供了一個(gè)硬件的機(jī)制,就是通過把eflags寄存器的Trap Flag設(shè)置為1即可。

當(dāng)把eflags寄存器的Trap Flag設(shè)置為1后,CPU 每執(zhí)行一條指令便會(huì)產(chǎn)生一個(gè)異常,然后會(huì)觸發(fā) Linux 的異常處理,Linux 便會(huì)發(fā)送一個(gè)SIGTRAP信號(hào)給被調(diào)試的進(jìn)程。eflags寄存器的各個(gè)標(biāo)志如下圖:

eflags-register

從上圖可知,eflags寄存器的第8位就是單步調(diào)試模式的標(biāo)志。

所以ptrace()函數(shù)的以下2行代碼就是設(shè)置eflags進(jìn)程的單步調(diào)試標(biāo)志:

tmp = get_stack_long(child, EFL_OFFSET) | TRAP_FLAG;
put_stack_long(child, EFL_OFFSET, tmp);

而get_stack_long(proccess, offset)函數(shù)用于獲取進(jìn)程棧offset處的值,而EFL_OFFSET偏移量就是eflags寄存器的值。所以上面兩行代碼的意思就是:

  1. 獲取進(jìn)程的eflags寄存器的值,并且設(shè)置Trap Flag標(biāo)志。
  2. 把新的值設(shè)置到進(jìn)程的eflags寄存器中。

設(shè)置完eflags寄存器的值后,就調(diào)用wake_up_process()函數(shù)把被調(diào)試的進(jìn)程喚醒,讓其進(jìn)入運(yùn)行狀態(tài)。單步調(diào)試過程如下圖:

single-trace

處于單步調(diào)試模式時(shí),被調(diào)試進(jìn)程每執(zhí)行一條指令都會(huì)觸發(fā)一次SIGTRAP信號(hào),而被調(diào)試進(jìn)程處理SIGTRAP信號(hào)時(shí)會(huì)發(fā)送一個(gè)SIGCHLD信號(hào)給父進(jìn)程(調(diào)試進(jìn)程),并且讓自己停止執(zhí)行。

而父進(jìn)程(調(diào)試進(jìn)程)接收到SIGCHLD后,就可以對(duì)被調(diào)試的進(jìn)程進(jìn)行各種操作,比如讀取被調(diào)試進(jìn)程內(nèi)存的數(shù)據(jù)和寄存器的數(shù)據(jù),或者通過調(diào)用ptrace(PTRACE_CONT, child,...)來讓被調(diào)試進(jìn)程進(jìn)行運(yùn)行等。

小結(jié)

由于ptrace()的功能十分強(qiáng)大,所以本文只能拋磚引玉,沒能對(duì)其所有功能進(jìn)行分析。另外斷點(diǎn)功能并不是通過ptrace()函數(shù)實(shí)現(xiàn)的,而是通過int3指令來實(shí)現(xiàn)的,在Eli Bendersky大神的文章有介紹。而對(duì)于ptrace()的所有功能,只能讀者自己慢慢看代碼來體會(huì)了。


本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時(shí)聯(lián)系本站刪除。
關(guān)閉