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

歡迎訪問 生活随笔!

生活随笔

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

生活经验

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

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

文章目錄

      • Ptrace 的使用
      • GDB 的基本實現(xiàn)原理
        • Example1 通過ptrace 修改 被追蹤進程的內(nèi)存數(shù)據(jù)
        • Example2 通過ptrace 對被追蹤進程進行單步調(diào)試
      • Ptrace的實現(xiàn)
        • PTRACE_TRACEME
        • PTRACE_ATTACH
        • PTRACE_CONT
        • PTRACE_SINGLESTEP
        • PTRACE_PEEKDATA
        • PTRACE_POKEDATA
        • PTRACE_GETREGS

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

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

GDB基本的調(diào)試功能都是通過一個系統(tǒng)調(diào)用ptrace來實現(xiàn)的。

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

Ptrace 的使用

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

接口聲明如下:

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

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

  • 追蹤進程會變?yōu)楸蛔粉欉M程 的父進程

    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
    
  • 進程狀態(tài)會進入 TASK_TRACED ,表示當前進程正在被追蹤,此時進程會暫停下來,等待追蹤進程的操作。這個狀態(tài)有點像TASK_STOPPED,都是讓進程暫停下來等待被喚醒或者操作。只是TASK_TRACED狀體的進程 不接受SIGCONT信號,只接受ptrace指定的PTRACE_DETACHPTRACE_CONT 請求從而喚醒進程執(zhí)行操作。

  • 發(fā)送給被追蹤進程的信號會被轉(zhuǎn)發(fā)給父進程,除了SIGKILL,子進程則會被阻塞。

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

接下來描述一下ptrace接口的參數(shù)含義:

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

    • PTRACE_TRACEMEPTRACE_ATTACH 都是和進程建立追蹤關(guān)系

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

    • PTRACE_POKETEXT,PTRACE_POKEDATA,PTRACE_POKEUSR等表示修改子進程的內(nèi)存,寄存器的內(nèi)容

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

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

      如果父進程在在子進程前結(jié)束,則會自動解除追蹤關(guān)系。

  • pid表示 要跟蹤的進程pid

  • addr表示進程的內(nèi)存地址

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

GDB 的基本實現(xiàn)原理

gdb調(diào)試的基本架構(gòu)如下

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

兩者共同點是 底層都通過ptrace系統(tǒng)調(diào)用進行調(diào)試。

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

接下來通過ptrace來簡單看一下gdb的實現(xiàn)原理:

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

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

Example1 通過ptrace 修改 被追蹤進程的內(nèi)存數(shù)據(jù)

通過ptrace 修改 被追蹤進程的內(nèi)存數(shù)據(jù)

#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: // 父進程先修改子進程內(nèi)存中的值,但是父進程內(nèi)存中的數(shù)據(jù)不變check(ptrace(PTRACE_ATTACH, pid ,0 ,0),"PT_ATTACH"); // 鏈接到子進程check(ptrace(PTRACE_POKEDATA, pid ,str ,u.data),"PT_WRITE_D"); // 修改子進程內(nèi)存中str的內(nèi)容check(ptrace(PTRACE_CONT, pid ,0 ,0),"PT_CONTINUE"); // 子進程繼續(xù)運行printf("Parent's data is %s\n", str);wait(NULL);break;}return 0;
}

執(zhí)行結(jié)果如下,可以看到父進程已經(jīng)將子進程內(nèi)存中的str數(shù)據(jù)前8個字節(jié)做了更改,但是父進程內(nèi)存中的數(shù)據(jù)還是沒有變化。

$ ./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 對被追蹤進程進行單步調(diào)試

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

總體的邏輯如下:

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

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

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

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

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


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

#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位系統(tǒng)下內(nèi)核態(tài)部分的結(jié)束地址//(32位系統(tǒng)虛擬進程空間內(nèi)核地址占用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 函數(shù)棧頂指針的偏移地址
*/ipoffs = M_OFFSETOF(struct user, regs.eip);spoffs = M_OFFSETOF(struct user, regs.esp);
/*
** 通過Ptrace 將輸入PID所代表的進程作為當前進程的子進程,并建立追蹤關(guān)系。
** 此時會目標子進程發(fā)送一個SIGSTOP的信號,調(diào)用waitpid來感知子進程的狀態(tài)變化。
*/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 進程)的狀態(tài)切換,并且與當前追蹤進程建立了父子關(guān)系
*/while (1) {
/*
** 通過ptrace的PTRACE_SINGLESTEP進行單步調(diào)試,調(diào)試過程會向子進程發(fā)送SIGTRAP信號
** 通過wait系統(tǒng)調(diào)用進行捕獲
*/ if ((res = ptrace(PTRACE_SINGLESTEP, Tpid, 0, signo)) < 0) {perror("Ptrace singlestep error");exit(1);}res = wait(&stat);
/*
** 捕獲到SIGTRAP信號之后,將信號置0,準備開啟下一個單步調(diào)試。
** 如果發(fā)現(xiàn)子進程接受到的信號是SIGHUP和SIGINT(子進程接受到了暫停信號
** 那么就停止單步調(diào)試,父進程退出。
*/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;}
/*
** 單步調(diào)試之后,兩個寄存器的地址會發(fā)生變化,所以需要重新獲取以下
*/ip = ptrace(PTRACE_PEEKUSER, Tpid, ipoffs, 0);sp = ptrace(PTRACE_PEEKUSER, Tpid, spoffs, 0);
/*
** 通過 ldd 查看輸入的PID進程的內(nèi)存分布如下
**     libc.so.6 => /lib/i686/libc.so.6 (0x40030000)
**     /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
** 這里跳過內(nèi)核態(tài)的地址
*/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指針的結(jié)尾,完成單步追蹤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指針不會每次追蹤都發(fā)生變化,而指令寄存器地址IP每次都發(fā)生變化,因為每次執(zhí)行的指令都不一樣,這和我們描述ptrace單步調(diào)試代碼邏輯時的ESP和EIP寄存器關(guān)系圖邏輯一樣的。

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

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的實現(xiàn)

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

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

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

# tracer: function_graph
#
# CPU  TASK/PID         DURATION                  FUNCTION CALLS
# |     |    |           |   |                     |   |   |   |3)  <...>-46083   |               |  SyS_ptrace() {                           # 系統(tǒng)調(diào)用入口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 調(diào)試一個新的進程會進入PTRACE_TRACEME選項,gdb ./new_process

ptrace系統(tǒng)調(diào)用入口如下:

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

PTRACE_ATTACH

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

在后續(xù)會通過signal_wake_up_state函數(shù)喚醒處于stopped狀態(tài)的進程

PTRACE_CONT

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

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

PTRACE_SINGLESTEP

將進程的標志寄存器設(shè)置為單步模式,讓被調(diào)試進程繼續(xù)運行。當執(zhí)行完一條指令之后,會觸發(fā)INT中斷,并發(fā)信號給控制進程,等待下一次的執(zhí)行。

PTRACE_PEEKDATA

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

PTRACE_POKEDATA

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

PTRACE_GETREGS

獲取被追蹤進程 指定寄存器中的數(shù)據(jù)

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

總結(jié)

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

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。