Linux:斷點原理與實現

前言

從事編程工做的咱們,總有調試的時刻,不論是經過 IDE 調試開發中的代碼,仍是經過 GDB 排查正在運行的進程。ios

特別是常用 GDB 的童鞋,對它提供的強大功能更加如數家珍,其中就不乏 breakpoint(斷點)c++

恰好最近作到 Ptrace 相關的實驗,也順便擼了這篇小文來分享下 斷點 當中的道理。編程

簡單 GDB 示範

// test.cpp

#include<iostream>
#include<unistd.h>

void test1(){
    std::cout << "test" << std::endl;
}

int main() {
    while (true) {
        std::cout << "main: " << getpid() << std::endl;
        test1();
        sleep(1);
    }
    return 0;
}

編譯運行segmentfault

g++ -std=c++11 test.cpp && ./a.out

// 輸出
main: 22346
test
main: 22346
test
main: 22346
...

開啓 GDB,而且在 test1 函數斷點函數

sudo gdb a.out -p 22346

// 輸出
... (省略打印的信息, 直接輸入命令)

(gdb) break test1       // 在 test1 函數斷點
Breakpoint 1 at 0x40091a

(gdb) c                 // 繼續運行
Continuing.

Breakpoint 1, 0x000000000040091a in test1() ()

(gdb) i r rip           // 查看 cpu 下一條指令的內容
rip            0x40091a 0x40091a <test1()+4>

回頭看 a.out 的輸出,能夠看到已經停在 main: 5693 再也不打印了,而進程狀態也變成了 T:
請在這裏輸入圖片描述優化

T 狀態意味着:(TASK_STOPPED or TASK_TRACED),暫停狀態或跟蹤狀態,接下來就能夠經過 GDB 實現各類調試的操做了。ui

咱們此次也要實現相似的效果,不過只是一個超簡化版本,只考慮:在指定的位置暫停,得到進程的控制權spa

前置知識準備

在實現以前,咱們須要瞭解下必要的知識:.net

寄存器:RIP

若是以前沒有了解 寄存器 的童鞋能夠先看看:https://www.jianshu.com/p/029...調試

直接摘抄裏面的一段描述:

rip 指令地址寄存器,用來存儲 CPU 即將要執行的指令地址。

每次 CPU 執行完相應的彙編指令以後,rip 寄存器的值就會自行累加;

Ptrace

若是以前沒有了解 Ptrace 的童鞋能夠先看看:http://fancy-blogs.com/2018/0...

在ptrace中有兩個角色:

  • tracee:被追蹤者,它是被監控的進程,經過ptrace系統調用的操做做用在它之上 (譬如:上文的 22346 進程);
  • tracer:追蹤者,它負責監視並處理被追蹤者傳來的信息(譬如:GDB);

下文會直接引用這兩個名詞。

實現思路

實現的思路很是簡單

1. 先肯定咱們要斷點的地址

在 GDB 中,咱們是習慣對 行號 或者 函數名 直接設置斷點,行號相對來講比較複雜,咱們先展現 函數名 的。

在 Linux 環境下編譯出來的可執行文件都是遵循 ELF 格式,若是沒有特殊處理,它會保留比較完整的 符號表

就拿開頭的程序來當例子,能夠經過 readelf -s a.out 查看:

請在這裏輸入圖片描述

這個符號表記錄了進程須要用到的符號分別在什麼位置。

如圖,第一列就是符號的地址(十六進制),第二列是長度,最後一列是符號名字

咱們這裏須要在 test1 這個函數打斷點,也就是紅色圈出來的地方,這裏可能會有童鞋想問爲啥是:_Z5test1v

這裏主要是 cpp 的名字修飾問題:https://blog.csdn.net/u013220...,不礙事。

咱們如今能夠看到前面的地址就是 0x400916;

2. 經過 Ptrace 得到 tracee 的控制權
// 創建追蹤的關係, 不少童鞋可能會用 PTRACE_ATTACH,它和 PTRACE_SEIZE 的區別就是,它會立刻暫停 tracee,而 PTRACE_SEIZE 不會
ptrace(PTRACE_SEIZE, pid, addr, data) 

// 中斷 tracee 的行爲,將控制權交給 tracer
ptrace(PTRACE_INTERRUPT, pid, addr, data)       

// 感知 tracee 的狀態變動,便於下一步操做
waitpid(pid, &status, options)
3. 保留當前 rip 的指令內容,並用 中斷指令 替換
// 獲取 tracee addr 內存的內容
ptrace(PTRACE_PEEKDATA, pid, addr, data)  

// 修改 tracee 指定內存的內容
ptrace(PTRACE_POKEDATA, pid, addr, data) 

// 獲取 tracee 當前的寄存器內容
ptrace(PTRACE_GETREGS, pid, addr, data) 
  
// 設置 tracee 當前的寄存器內容
ptrace(PTRACE_SETREGS, pid, addr, data)
4. 恢復運行,等待 trap 觸發
// 讓 tracee 繼續運行  
ptrace(PTRACE_CONT, pid, addr, data)
5. 恢復 rip 指令,結束調試

完整 Tracer 代碼

#include <sys/ptrace.h>
#include <iostream>
#include <stdio.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <string>

void dowait(pid_t pid) {
    int status, signum;
    while (true) {
        waitpid(pid, &status, 0);
        if (WIFSTOPPED(status)) {
            signum = WSTOPSIG(status);
            if (signum == SIGTRAP) {
                break;
            } else {
                std::cout << "Other signum, skipping..." << std::endl;
                ptrace(PTRACE_CONT, pid, 0, 0);
            }
        }
    }
}


void break_onece(pid_t pid, long addr) {

    // 保存 addr 舊的指令和寄存器(主要是 rip)
    long old_code = ptrace(PTRACE_PEEKDATA, pid, addr, NULL);
    user_regs_struct old_regs;
    ptrace(PTRACE_GETREGS, pid, NULL, &old_regs);


    long trap_code = old_code;
    unsigned char *p = (unsigned char*) &trap_code;

    // Trap 中斷指令的十六進制數值
    p[0] = 0xcc;

    // 用 Trap 覆蓋 addr 數值,等 cpu 執行至此就會中斷
    if (ptrace(PTRACE_POKEDATA, pid, addr, trap_code)) {
        std::cout << "Break failed" << std::endl;
        return;
    }

    ptrace(PTRACE_CONT, pid, NULL, NULL);
    dowait(pid);

    // 敲入任意字符以繼續,能夠在此加入其它調試邏輯(海闊憑魚躍!!!)
    std::cout << "Next ? " << std::endl;
    std::string instruction;
    std::cin >> instruction;

    // 恢復 rip, 不然會因缺少有效 rip 致使 tracee coredump
    ptrace(PTRACE_SETREGS, pid, NULL, &old_regs);

    // 恢復 addr 原值
    ptrace(PTRACE_POKEDATA, pid, addr, old_code);
    ptrace(PTRACE_CONT, pid, 0, 0);
}

void quit(pid_t pid) {
    ptrace(PTRACE_DETACH, pid, NULL, NULL);
    std::cout << "quit!" << std::endl;
    exit(0);
}

int main(int argc, char* argv[]) {
    pid_t pid = std::stoi(argv[1]);

    if (ptrace(PTRACE_SEIZE, pid, NULL, NULL)) {
        perror("ptrace_seize failed");
        return -1;
    }

    if(ptrace(PTRACE_INTERRUPT, pid, 0, 0)) {
        perror("interrupt failed");
        quit(pid);
    }

    dowait(pid);

    // 想斷點的地址
    long break_addr = 0x400916;
    break_onece(pid, break_addr);

    quit(pid);
    return 1;
}

編譯 & 運行

g++ trace_test.cpp -std=c++11 -o trace_test

./trace_test 22346 # 本文開頭的進程

總結

關於斷點的原理網上有不少文章提到,但比較多也是走馬觀花一筆帶過,意猶未盡,乾脆直接用最淺顯的例子下降你們練手
成本!

其實在文中提到的例子也有很是多能夠優化的點:

  • 好比:函數地址獲取的方式,既然提到 ELF 的符號表,那麼應該經過解析這個表,將用戶傳入的用戶名,轉換成地址;
  • 再好比:應該維護一份全局的斷點表,儲存任意多的斷點,也讓每一個斷點處能夠重複利用;
  • 甚至還好比:涉及到 Ptrace 的錯誤返回都要優雅處理,由於在每一個返回值不爲 0 的狀況下,貿然進行下一步是很是危險的,很是大可能致使 tracee coredump;

每一個好比均可以展開研究,因此歡迎期待後續。

歡迎各位大神指點交流, QQ討論羣: 258498217
轉載請註明來源: http://www.javashuo.com/article/p-uhokhuyp-em.html

相關文章
相關標籤/搜索