日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

一篇掌握GDB調試程序的核心技術-ptrace系統調用與使用示例

 

 

前言:在程序出現bug的時候,最好的解決辦法就是通過 GDB 調試程序,然后找到程序出現問題的地方。比如程序出現 段錯誤(內存地址不合法)時,就可以通過 GDB 找到程序哪里訪問了不合法的內存地址而導致的。 本文不是介紹GDB不是使用方式,而是大概介紹 GDB 的實現原理,當然是 GDB 是一個龐大而復雜的項目,不可能只通過一篇文章就能解釋清楚,所以本文主要是介紹 GDB 使用的核心的技術 - ptrace。

 

一,ptrace系統調用

  • ptrace() 系統調用是 linux 提供的一個調試進程的工具,ptrace() 系統調用非常強大,它提供非常多的調試方式讓我們去調試某一個進程,下面是 ptrace() 系統調用的定義:
long ptrace(enum __ptrace_request request,  pid_t pid, void *addr,  void *data);

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

  1. request:指定調試的指令,指令的類型很多,如:PTRACE_TRACEME、PTRACE_PEEKUSER、PTRACE_CONT、PTRACE_GETREGS等等,下面會介紹不同指令的作用。
  2. pid:進程的ID(這個不用解釋了)。
  3. addr:進程的某個地址空間,可以通過這個參數對進程的某個地址進行讀或寫操作。
  4. data:根據不同的指令,有不同的用途,下面會介紹。

二,ptrace使用示例

  • 下面通過一個簡單例子來說明 ptrace() 系統調用的使用,這個例子主要介紹怎么使用 ptrace() 系統調用獲取當前被調試(追蹤)進程的各個寄存器的值,代碼如下(ptrace.c):
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <stdio.h>
int main()
{   pid_t child;
    struct user_regs_struct regs;

    child = fork();  // 創建一個子進程
    if(child == 0) { // 子進程
        ptrace(PTRACE_TRACEME, 0, NULL, NULL); // 表示當前進程進入被追蹤狀態
        execl("/bin/ls", "ls", NULL);          // 執行 `/bin/ls` 程序
    } 
    else { // 父進程
        wait(NULL); // 等待子進程發送一個 SIGCHLD 信號
        ptrace(PTRACE_GETREGS, child, NULL, ®s); // 獲取子進程的各個寄存器的值
        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); // 繼續運行子進程
        sleep(1);
    }
    return 0;
}
  • 通過命令 gcc ptrace.c -o ptrace 編譯并運行上面的程序會輸出如下結果:
Register: rdi[0], rsi[0], rdx[0], rax[0], orig_rax[59]
ptrace  ptrace.c
  • 上面結果的第一行是由父進程輸出的,主要是打印了子進程執行 /bin/ls 程序后各個寄存器的值。而第二行是由子進程輸出的,主要是打印了執行 /bin/ls 程序后面輸出的結果。

 

更多linux內核視頻教程文檔資料免費領取后臺私信【內核】自行獲取.

一篇掌握GDB調試程序的核心技術-ptrace系統調用與使用示例

 

Linux內核源碼/內存調優/文件系統/進程管理/設備驅動/網絡協議棧-學習視頻教程-騰訊課堂

 

下面解釋一下上面程序的執行流程:

  1. 主進程調用 fork() 系統調用創建一個子進程。
  2. 的進程調用 ptrace(PTRACE_TRACEME,...) 把自己設置為被追蹤狀態,并且調用 execl() 執行 /bin/ls 程序。
  3. 被設置為追蹤(TRACE)狀態的子進程執行 execl() 的程序后,會向父進程發送 SIGCHLD 信號,并且暫停自身的執行。
  4. 父進程通過調用 wait() 接收子進程發送過來的信號,并且開始追蹤子進程。
  5. 父進程通過調用 ptrace(PTRACE_GETREGS, child, ...) 來獲取到子進程各個寄存器的值,并且打印寄存器的值。
  6. 父進程通過調用 ptrace(PTRACE_CONT, child, ...) 讓子進程繼續執行下去。
  • 從上面的例子可以知道,通過向 ptrace() 函數的 request 參數傳入不同的值時,就會有不同的效果。比如傳入 PTRACE_TRACEME 就可以讓進程進入被追蹤狀態,而轉入 PTRACE_GETREGS 時,就可以獲取被追蹤的子進程各個寄存器的值等。

 

三,ptrace實現原理

本文使用的 Linux 2.4.16 版本的內核

  • 看懂本文需要的基礎:進程調度,內存管理和信號處理等相關知識。
  • 調用 ptrace() 系統函數時會觸發調用內核的 sys_ptrace() 函數,由于不同的原因 CPU 架構有著不同的調試方式,所以 Linux 為每種不同的 CPU 架構實現了不同的 sys_ptrace() 函數,而本文主要介紹的是 X86 CPU 的調試方式,所以 sys_ptrace() 函數所在文件是 linux-2.4.16/arch/i386/kernel/ptrace.c。

sys_ptrace() 函數的主體是一個 switch 語句,會傳入的 request 參數不同進行不同的操作,如下:

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 對應的進程 task_struct 對象
    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() 函數首先根據進程的 pid 獲取到進程的 task_struct 對象。然后根據傳入不同的 request 參數在 switch 語句中進行不同的操作。

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() 提供的操作比較多,所以本文只會挑選一些比較有代表性的操作進行解說,比如 PTRACE_TRACEME、PTRACE_SINGLESTEP、PTRACE_PEEKTEXT、PTRACE_PEEKDATA 和 PTRACE_CONT 等,而其他的操作,有興趣的朋友可以自己去分析其實現原理。

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

  • 當要調試一個進程時,需要使進程進入被追蹤模式,怎么使進程進入被追蹤模式呢?有兩個方法:
  1. 被調試的進程調用 ptrace(PTRACE_TRACEME, ...) 來使自己進入被追蹤模式。
  2. 調試進程(如GDB)調用 ptrace(PTRACE_ATTACH, pid, ...) 來使指定的進程進入追蹤模式。
  • 第一種方式是進程自己主動進入被追蹤模式,而第二種是進程被動進入被追蹤模式。
  • 被調試的進程必須進入追蹤模式才能進行調試,因為 Linux 會對被追蹤的進程進行一些特殊的處理。下面我們主要介紹第一種進入追蹤模式的實現,就是 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; // 標志 PTRACE 狀態
        ret = 0;
        goto out;
    }
    ...
}
  • 從上面的代碼可以發現,ptrace() 對 PTRACE_TRACEME 的處理就是把當前進程標志為 PTRACE 狀態。
  • 當然事情不會這么簡單,因為當一個進程被標記為 PTRACE 狀態后,當調用 exec() 函數去執行一個外部程序時,將會暫停當前進程的運行,并且發送一個 SIGCHLD 給父進程。父進程接收到 SIGCHLD 信號后就可以對被調試的進程進行調試。
  • 我們來看看 exec() 函數是怎樣實現上述功能的,exec() 函數的執行過程為 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);
    ...
}
  • 從上面代碼可以看出,當進程被標記為 PTRACE 狀態時,執行 exec() 函數后便會發送一個 SIGTRAP 的信號給當前進程。
  • 我們再來看看,進程是怎么處理的 SIGTRAP 信號的。信號是通過 do_signal() 函數進行處理的,而對 SIGTRAP 信號的處理邏輯如下:

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);

        // 如果進程被標記為 PTRACE 狀態
        if ((current->ptrace & PT_PTRACED) && signr != SIGKILL) {
            /* 讓調試器運行  */
            current->exit_code = signr;
            current->state = TASK_STOPPED;   // 讓自己進入停止運行狀態
            notify_parent(current, SIGCHLD); // 發送 SIGCHLD 信號給父進程
            schedule();                      // 讓出CPU的執行權限
            ...
        }
    }
}

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

  1. 如果當前進程被標記為 PTRACE 狀態,那么就使自己進入停止運行的狀態。
  2. 發送 SIGCHLD 信號給父進程。
  3. 讓出 CPU 的執行權限,使 CPU 執行其他進程。
  • 執行以上過程后,追蹤進程便進入了調試模式,過程如下圖:
一篇掌握GDB調試程序的核心技術-ptrace系統調用與使用示例

 

?

 

  • 當父進程(調試進程)接收到 SIGCHLD 信號后,表示被調試進程已經標記為被追蹤狀態并且停止運行,那么調試進程就可以開始進行調試了。
  • 獲取被調試進程的內存數據(PTRACE_PEEKTEXT / PTRACE_PEEKDATA)
  • 調試進程(如GDB)可以通過調用 ptrace(PTRACE_PEEKDATA, pid, addr, data) 立即獲取被調試進程 addr 處虛擬內存地址的數據,但每次只能讀取一個大小為 4字節的數據。
  • 我們來看看 ptrace() 對 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;
    }
    ...
}
  • 從上面代碼可以看出,對 PTRACE_PEEKTEXT 和 PTRACE_PEEKDATA 的處理是相同的,主要是通過調用 access_process_vm() 函數來讀取被調試進程 addr 處的虛擬內存地址的數據。
  • access_process_vm() 函數的實現主要涉及到 內存管理 相關的知識,可以參考我以前對內存管理分析的文章,這里主要大概說明一下 access_process_vm() 的原理。
  • 我們知道每個進程都有個 mm_struct 的內存管理對象,而 mm_struct 對象有個表示虛擬內存與物理內存映射關系的頁目錄的指針 pgd。如下:
struct mm_struct {
    ...
    pgd_t *pgd; /* 頁目錄指針 */
    ...
}
  • 而 access_process_vm() 函數就是通過進程的頁目錄來找到 addr 虛擬內存地址映射的物理內存地址,然后把此物理內存地址處的數據復制到 data 變量中。如下圖所示:
一篇掌握GDB調試程序的核心技術-ptrace系統調用與使用示例

 

  • access_process_vm() 函數的實現這里就不分析了,有興趣的讀者可以參考我之前對內存管理分析的文章自行進行分析。

單步調試模式(PTRACE_SINGLESTEP)

  • 單步調試是一個比較有趣的功能,當把被調試進程設置為單步調試模式后,被調試進程沒執行一條CPU指令都會停止執行,并且向父進程(調試進程)發送一個 SIGCHLD 信號。
  • 我們來看看 ptrace() 函數對 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;
    }
    ...
}
  • 要把被調試的進程設置為單步調試模式,英特爾的 X86 CPU 提供了一個硬件的機制,就是通過把 eflags 寄存器的 Trap Flag 設置為1即可。
  • 當把 eflags 寄存器的 Trap Flag 設置為1后,CPU 每執行一條指令便會產生一個異常,然后會觸發 Linux 的異常處理,Linux 便會發送一個 SIGTRAP 信號給被調試的進程。
  • eflags 寄存器的各個標志如下圖:
一篇掌握GDB調試程序的核心技術-ptrace系統調用與使用示例

 

?

  • 從上圖可知,eflags 寄存器的第8位就是單步調試模式的標志。
  • 所以 ptrace() 函數的以下2行代碼就是設置 eflags 進程的單步調試標志:
tmp = get_stack_long(child, EFL_OFFSET) | TRAP_FLAG;
put_stack_long(child, EFL_OFFSET, tmp);
  • 而 get_stack_long(proccess, offset) 函數用于獲取進程棧 offset 處的值,而 EFL_OFFSET 偏移量就是 eflags 寄存器的值。
  • 所以上面兩行代碼的意思就是:
  1. 獲取進程的 eflags 寄存器的值,并且設置 Trap Flag 標志。
  2. 把新的值設置到進程的 eflags 寄存器中。
  • 設置完 eflags 寄存器的值后,就調用 wake_up_process() 函數把被調試的進程喚醒,讓其進入運行狀態。 單步調試過程如下圖:
一篇掌握GDB調試程序的核心技術-ptrace系統調用與使用示例

 

?

 

  • 處于單步調試模式時,被調試進程每執行一條指令都會觸發一次 SIGTRAP 信號,而被調試進程處理 SIGTRAP 信號時會發送一個 SIGCHLD 信號給父進程(調試進程),并且讓自己停止執行。
  • 而父進程(調試進程)接收到 SIGCHLD 后面,就可以對被調試的進程進行各種操作,比如讀取被調試進程內存的數據和寄存器的數據,或者通過調用 ptrace(PTRACE_CONT, child,...) 來讓被調試進程進行運行等。

四,小結

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

分享到:
標簽:調試 程序
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定