日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

一文带你看透 GDB 的 实现原理 -- ptrace真香

發布時間:2023/11/27 生活经验 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 一文带你看透 GDB 的 实现原理 -- ptrace真香 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

      • Ptrace 的使用
      • GDB 的基本實現原理
        • Example1 通過ptrace 修改 被追蹤進程的內存數據
        • Example2 通過ptrace 對被追蹤進程進行單步調試
      • Ptrace的實現
        • PTRACE_TRACEME
        • PTRACE_ATTACH
        • PTRACE_CONT
        • PTRACE_SINGLESTEP
        • PTRACE_PEEKDATA
        • PTRACE_POKEDATA
        • PTRACE_GETREGS

GDB本身能夠attach到一個運行的進程,實時獲取運行中進程的內存數據,增加斷點,查看當前運行狀態下函數變量值,甚至直接修改函數的變量。

這個機制本身就很有趣,也很實用,接下來探索一下GDB核心功能的詳細實現。

GDB基本的調試功能都是通過一個系統調用ptrace來實現的。

ps: 限于本人能力有限,對底層CPU 執行的正確邏輯沒法做到萬無一失,歡迎大家批評指正,相互學習討論。

Ptrace 的使用

ptrace 主要被用做進程追蹤,追蹤進程的什么內容呢?這里有很多可選的配置,比如進程內存的值、進程寄存器的值,進程接收到的信號,指定進程以何種方式運行等等;

接口聲明如下:

#include <sys/ptrace.h>long ptrace(enum __ptrace_request request, pid_t pid,void *addr, void *data);

調用ptrace 追蹤進程時(gdb attach -p $pid),被追蹤進程會發生如下事情:

  • 追蹤進程會變為被追蹤進程 的父進程

    Baron+ 215677 154756  0 11:57 pts/1    S      0:00  |   \_ gdb attach -p 215063           
    Baron+ 218064 215677  7 11:59 pts/1    S+     0:00  |       \_ /home/Baron/write_test
    
  • 進程狀態會進入 TASK_TRACED ,表示當前進程正在被追蹤,此時進程會暫停下來,等待追蹤進程的操作。這個狀態有點像TASK_STOPPED,都是讓進程暫停下來等待被喚醒或者操作。只是TASK_TRACED狀體的進程 不接受SIGCONT信號,只接受ptrace指定的PTRACE_DETACHPTRACE_CONT 請求從而喚醒進程執行操作。

  • 發送給被追蹤進程的信號會被轉發給父進程,除了SIGKILL,子進程則會被阻塞。

  • 父進程收到信號之后可以對子進程進行修改,來讓子進程繼續運行。

接下來描述一下ptrace接口的參數含義:

  • request 作為ptrace的核心配置,提供非常多的進程追蹤能力

    • PTRACE_TRACEMEPTRACE_ATTACH 都是和進程建立追蹤關系

      • PTRACE_TRACEME表示被追蹤進程調用,讓父進程來追蹤自己。通常是gdb調試新進程時使用。
      • PTRACE_ATTACH父進程attach到正在運行的子進程上,這種追蹤方式會檢查權限,普通用戶無法追蹤root用戶下的進程
    • PTRACE_PEEKTEXTPTRACE_PEEKDATAPTRACE_PEEKUSERPTRACE_GETREGS等表示讀取子進程內存,寄存器等內容

    • PTRACE_POKETEXT,PTRACE_POKEDATA,PTRACE_POKEUSR等表示修改子進程的內存,寄存器的內容

    • PTRACE_CONTPTRACE_SYSCALL, PTRACE_SINGLESTEP表示被控制進程以何種方式追蹤

      • PTRACE_CONT表示重新啟動被追蹤進程
      • PTRACE_SYSCALL每次進入或者退出系統調用時都會觸發一次SIGTRAP(Trace/breakpoint trap),strace的追蹤系統調用就是通過該配置進行追蹤的,進入時獲取參數,退出時獲取系統調用返回值。
      • PTRACE_SINGLESTEP 每執行完一次指令之后會觸發一次sigtrap,支持獲取當前進程的內存/寄存器狀態。gdb的next指令通過該選項實現。
    • PTRACE_DETACH, PTRACE_KILL解除父子進程之間的追蹤關系

      如果父進程在在子進程前結束,則會自動解除追蹤關系。

  • pid表示 要跟蹤的進程pid

  • addr表示進程的內存地址

  • data 根據前面設置的requet選項而變化,比如要開始追蹤時則設置request= PTRACE_CONT,同時將data設置為對應signal數字(SIGTRAP – 5)。

GDB 的基本實現原理

gdb調試的基本架構如下

  • 本地調試 通過本地gdb 命令行或者mi圖形接口進行調試
  • 遠端調試 就是在當前設備通過遠端的gdb server對遠端設備的目標程序進行調試

兩者共同點是 底層都通過ptrace系統調用進行調試。

ptrace的基本使用我們已經看了一遍,如果想要了解更加詳細的信息,可以通過man 2 ptrace進一步了解。

接下來通過ptrace來簡單看一下gdb的實現原理:

  • 當我們使用gdb設置斷點的時候,gdb會將斷點處的指令修改為INT 3(x86開始支持的專門用作調試的CPU指令,使得cpu終端到調試器),同時將斷點信息以及修改前的指令保存起來。
  • 當被調試的子進程執行到斷點處時 觸發INT 3中斷,從而產生SIGTRAP信號。
  • 因為此時父進程已經和調試進程建立追蹤關系,ptrace會將子進程的SIGTRAP信號發送給父進程,此時父進程先和已有的斷點信息進行對比,比如確認INT 3指令的位置,來確認當前信號是否因為斷點產生。
  • 如果是,則會等待用戶輸入指令,進行下一步處理,如果不是,則不予理會,繼續執行后續代碼。

通過以上原理可以看出,gdb會修改子進程的代碼(將設置斷點處的子進程指令修改為INT 3),那就涉及到修改子進程內存的情況了。這里是通過ptracePTRACE_POKEDATA選項進行修改。

Example1 通過ptrace 修改 被追蹤進程的內存數據

通過ptrace 修改 被追蹤進程的內存數據

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>#include <sys/ptrace.h>void check(long ret, char *str) {if (ret == -1) {printf("execute %s failed with %ld !!!\n", str, ret);}printf("Execute %s success! \n", str);
}char str[] = "Ptrace is testing";int main() {pid_t pid = fork();union{char cdata[8];u_int64_t data; }u = {"CHANGE T"};switch (pid){case 0: // 子進程先休眠2秒sleep(2); printf("Child's data is %s\n", str);break;case -1:printf("Fork failed ");exit(1) ;break;default: // 父進程先修改子進程內存中的值,但是父進程內存中的數據不變check(ptrace(PTRACE_ATTACH, pid ,0 ,0),"PT_ATTACH"); // 鏈接到子進程check(ptrace(PTRACE_POKEDATA, pid ,str ,u.data),"PT_WRITE_D"); // 修改子進程內存中str的內容check(ptrace(PTRACE_CONT, pid ,0 ,0),"PT_CONTINUE"); // 子進程繼續運行printf("Parent's data is %s\n", str);wait(NULL);break;}return 0;
}

執行結果如下,可以看到父進程已經將子進程內存中的str數據前8個字節做了更改,但是父進程內存中的數據還是沒有變化。

$ ./ptrace_change 
Execute PT_ATTACH success! 
Execute PT_WRITE_D success! 
Execute PT_CONTINUE success! 
Parent's data is Ptrace is testing
Child's data is CHANGE Ts testing

Example2 通過ptrace 對被追蹤進程進行單步調試

通過ptrace 對被追蹤進程進行單步調試,以下代碼是在32位系統上調試的,所以寄存器的表示還是eip,而x86_64的系統下寄存器都已經變更為rip了。

總體的邏輯如下:

  • 追蹤給定的進程pid, 通過PTRACE_ATTACH作為父進程與 給定進程建立追蹤關系
  • 獲取被追蹤進程的 CPU存放的下一個指令的存放地址 — EIP,CPU 存放當前主線程的棧頂指針偏移地址 — ESP
  • 通過ptrace的PTRACE_SINGLESTEP選項不斷得將EIP和ESP指針向下移動,每執行一條指令,寄存器指針移動一次,直到兩個寄存器指針到達棧尾,結束調試

當然打印并不只打印寄存器的地址,像GDB每一次單步追蹤會等待用戶的輸入,這個時候可以查看或者修改esp和eip當前狀態下的進程內存中的數據。

看ptrace測試 代碼之前先簡單描述一下ESP和EIP寄存器的關系:

進程開始運行的時候,左側CPU的ESP寄存器指向主線程的函數棧頂(函數的執行是不斷得壓棧和彈棧的)
右側的EIP寄存器則保存CPU執行的下一條匯編指令(后文有一個簡單的測試程序的全指令截圖,可以看看)

當開始運行的時候,一個函數語句可能需要多條匯編指令來完成,所以EIP改變多次,ESP才會發生一次改變。


通過n次的指令執行程序主體代碼, 運行完成的標記就是ESP指向函數棧底,EIP指令指針也指向函數棧底。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <wait.h>
#include <sys/ptrace.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <sys/signal.h>#define M_OFFSETOF(STRUCT, ELEMENT) \(unsigned int) &((STRUCT *)NULL)->ELEMENT;#define D_LINUXNONUSRCONTEXT 0x40000000 // 32位系統下內核態部分的結束地址//(32位系統虛擬進程空間內核地址占用1個G)int main (int argc, char *argv[]) {int Tpid, stat, res;
int signo;
int ip, sp;
int ipoffs, spoffs;
int initialSP = -1;
int initialIP = -1;
struct user u_area;
struct user_regs_struct regs;/*
** 傳入指定進程的PID 
*/if (argv[1] == NULL) {printf("Need pid of traced process\n");printf("Usage: pt  pid  \n");exit(1);}Tpid = strtoul(argv[1], NULL, 10);printf("Tracing pid %d \n",Tpid );
/*
** 獲取EIP 偏移地址 -- 保存CPU 下一個指令的寄存器地址
** 獲取ESP 偏移地址 -- 保存CPU 函數棧頂指針的偏移地址
*/ipoffs = M_OFFSETOF(struct user, regs.eip);spoffs = M_OFFSETOF(struct user, regs.esp);
/*
** 通過Ptrace 將輸入PID所代表的進程作為當前進程的子進程,并建立追蹤關系。
** 此時會目標子進程發送一個SIGSTOP的信號,調用waitpid來感知子進程的狀態變化。
*/printf("Attaching to process %d\n",Tpid);if ((ptrace(PTRACE_ATTACH, Tpid, 0, 0)) != 0) {;printf("Attach result %d\n",res);}res = waitpid(Tpid, &stat, WUNTRACED);if ((res != Tpid) || !(WIFSTOPPED(stat)) ) {printf("Unexpected wait result res %d stat %x\n",res,stat);exit(1);}printf("Wait result stat %x pid %d\n",stat, res);stat = 0;signo = 0;
/*
** 完成子進程(輸入的PID 進程)的狀態切換,并且與當前追蹤進程建立了父子關系
*/while (1) {
/*
** 通過ptrace的PTRACE_SINGLESTEP進行單步調試,調試過程會向子進程發送SIGTRAP信號
** 通過wait系統調用進行捕獲
*/ if ((res = ptrace(PTRACE_SINGLESTEP, Tpid, 0, signo)) < 0) {perror("Ptrace singlestep error");exit(1);}res = wait(&stat);
/*
** 捕獲到SIGTRAP信號之后,將信號置0,準備開啟下一個單步調試。
** 如果發現子進程接受到的信號是SIGHUP和SIGINT(子進程接受到了暫停信號
** 那么就停止單步調試,父進程退出。
*/if ((signo = WSTOPSIG(stat)) == SIGTRAP) {signo = 0;}if ((signo == SIGHUP) || (signo == SIGINT)) {ptrace(PTRACE_CONT, Tpid, 0, signo);printf("Child took a SIGHUP or SIGINT. We are done\n");break;}
/*
** 單步調試之后,兩個寄存器的地址會發生變化,所以需要重新獲取以下
*/ip = ptrace(PTRACE_PEEKUSER, Tpid, ipoffs, 0);sp = ptrace(PTRACE_PEEKUSER, Tpid, spoffs, 0);
/*
** 通過 ldd 查看輸入的PID進程的內存分布如下
**     libc.so.6 => /lib/i686/libc.so.6 (0x40030000)
**     /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
** 這里跳過內核態的地址
*/if (ip & D_LINUXNONUSRCONTEXT) {continue;} if (initialIP == -1) {initialIP = ip;initialSP = sp;printf("---- Starting LOOP IP %x SP %x ---- \n",initialIP, initialSP);} else { // 直到運行到ESP指針和EIP指針的結尾,完成單步追蹤if ((ip == initialIP) && (sp == initialSP)) {ptrace(PTRACE_CONT, Tpid, 0, signo);printf("----- LOOP COMPLETE -----\n");break;}}printf("Stat %x IP %x SP %x  Last signal %d\n",stat, ip, sp,signo);}printf("Debugging complete\n");sleep(5);return(0);
}

測試代碼如下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int main() {int *a[10] = {0};int i = 0;int j = 0;while(i < 1000) {a[i] = (int *)malloc(sizeof(int)*10);if(a[i] == NULL){printf("malloc failed\n");exit(1);}else {printf("malloc address is %x\n",(unsigned int)a[i]);}for(;j < 10; ++j){a[i][j] = j;}i++;sleep(1);}for(i =0;i < 1000 ;++i) {free (a[i]);}return 0;}

測試代碼對應的CPU指令如下
perf top -p pid

先運行測試代碼,再編譯運行ptrace追蹤代碼./test_ptrace $pid,可以看到ptrace追蹤代碼如下輸出:
其中IP和SP指向的地址可看到 SP指針不會每次追蹤都發生變化,而指令寄存器地址IP每次都發生變化,因為每次執行的指令都不一樣,這和我們描述ptrace單步調試代碼邏輯時的ESP和EIP寄存器關系圖邏輯一樣的。

因為還不是linux手藝人,還沒法深入淺出linux系統,所以這里只能通過自己的猜測和工具來 彌補體系結構這塊知識的缺失了。

Tracing pid 314201 
Attaching to process 314201
Wait result stat 137f pid 314201
---- Starting LOOP IP a88e0840 SP b60b6418 ----
Stat 57f IP a88e0840 SP b60b6418  Last signal 0
Stat 57f IP a88e0846 SP b60b6418  Last signal 0
Stat 57f IP a88e0848 SP b60b6418  Last signal 0
Stat 57f IP a88e06f4 SP b60b6420  Last signal 0
Stat 57f IP a88e06f6 SP b60b6420  Last signal 0
Stat 57f IP a88e06f8 SP b60b6420  Last signal 0
Stat 57f IP a88e0720 SP b60b6420  Last signal 0
Stat 57f IP a88e0727 SP b60b65d8  Last signal 0
Stat 57f IP a88e0729 SP b60b65d8  Last signal 0
Stat 57f IP a88e072a SP b60b65e0  Last signal 0
......
Stat 57f IP a88e06ef SP b60b6420  Last signal 0
Stat 57f IP a88e0830 SP b60b6418  Last signal 0
Stat 57f IP a88e0837 SP b60b6418  Last signal 0
Stat 57f IP a88e0839 SP b60b6418  Last signal 0
Stat 57f IP a88e083e SP b60b6418  Last signal 0
----- LOOP COMPLETE -----
Debugging complete

Ptrace的實現

這里不可能將每一個ptrace的選項的實現都講明白,只能在主線的調試流程上看看當 attach獲取被追蹤進程內存數據單步調試 這一些功能的背后內核做了什么。

使用frtrace 抓取SyS_ptrace函數的執行邏輯,關于ftrace的使用可以參考關于 Rocksdb 性能分析 需要知道的一些“小技巧“ – perf_context的“內功” ,systemtap、perf、 ftrace的顏值

這個抓取主要是通過執行gdb的一些調試命令來讓ptrace的不同選項得到運行,抓取attach,breadpoint,r,n等基本gdb指令的結果如下(主體的處理邏輯還是比較長的,這里僅僅貼一部分邏輯):

# tracer: function_graph
#
# CPU  TASK/PID         DURATION                  FUNCTION CALLS
# |     |    |           |   |                     |   |   |   |3)  <...>-46083   |               |  SyS_ptrace() {                           # 系統調用入口3)  <...>-46083   |               |    ptrace_get_task_struct() {             # 獲取進程的task_struc3)  <...>-46083   |               |      find_task_by_vpid() {3)  <...>-46083   |               |        find_task_by_pid_ns() {3)  <...>-46083   |   0.523 us    |          find_pid_ns();3)  <...>-46083   |   1.178 us    |        }3)  <...>-46083   |   1.858 us    |      }3)  <...>-46083   |   2.387 us    |    }3)  <...>-46083   |               |    ptrace_attach() {                     # attach 入口3)  <...>-46083   |               |      mutex_lock_interruptible() {3)  <...>-46083   |   0.037 us    |        _cond_resched();3)  <...>-46083   |   0.707 us    |      }3)  <...>-46083   |   0.087 us    |      _raw_spin_lock();3)  <...>-46083   |               |      __ptrace_may_access() {3)  <...>-46083   |   0.105 us    |        get_dumpable();3)  <...>-46083   |               |        security_ptrace_access_check() {3)  <...>-46083   |               |          yama_ptrace_access_check() {3)  <...>-46083   |   0.068 us    |            cap_ptrace_access_check();3)  <...>-46083   |   0.584 us    |          }3)  <...>-46083   |   0.043 us    |          cap_ptrace_access_check();3)  <...>-46083   |   1.404 us    |        }3)  <...>-46083   |   2.947 us    |      }
......

ps: 后文涉及到的ptrace源碼是 linux-3.10.1.0.1版本

PTRACE_TRACEME

通過gdb 調試一個新的進程會進入PTRACE_TRACEME選項,gdb ./new_process

ptrace系統調用入口如下:

確認能夠建立連接之后通過_ptrace_link將當前進程new_process和gdb追蹤進程建立父子關系

PTRACE_ATTACH

通過gdb attach到一個正在運行的進程上時會進入這個邏輯,gdb attach -p pid

在后續會通過signal_wake_up_state函數喚醒處于stopped狀態的進程

PTRACE_CONT

使得因正在被調試而暫停,或者斷掉的進程恢復運行,gdb的n,r,c等命令讓進程重新運行都是通過該選項實現的

進入到arch_ptrace之后,通過ptrace_reuqest --> ptrace_resume對該選項進行處理

PTRACE_SINGLESTEP

將進程的標志寄存器設置為單步模式,讓被調試進程繼續運行。當執行完一條指令之后,會觸發INT中斷,并發信號給控制進程,等待下一次的執行。

PTRACE_PEEKDATA

讀取虛擬進程內存中的數據,像gdb的p 打印變量 就是該選項的功能,與選項PTRACE_PEEKTEXT一樣,只不過讀取的是不同的地址空間的數據。TEXT是代碼段的數據,程序執行代碼中的一段數據,DATA段存儲已經初始化的靜態數據和全局變量數據。

PTRACE_POKEDATA

修改被追蹤進程指定內存地址中的數據,通過設置access_process_vm函數最后一個參數來表示是寫入內存中的數據還是從內存中讀數據。

PTRACE_GETREGS

獲取被追蹤進程 指定寄存器中的數據

而對應的PTRACE_SETREG即修改用戶進程寄存器內容,通過__get_user函數將data中的數據寫入到regs數組之中。

總結

以上是生活随笔為你收集整理的一文带你看透 GDB 的 实现原理 -- ptrace真香的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。