<pre id="bbfd9"><del id="bbfd9"><dfn id="bbfd9"></dfn></del></pre>

          <ruby id="bbfd9"></ruby><p id="bbfd9"><mark id="bbfd9"></mark></p>

          <p id="bbfd9"></p>

          <p id="bbfd9"><cite id="bbfd9"></cite></p>

            <th id="bbfd9"><form id="bbfd9"><dl id="bbfd9"></dl></form></th>

            <p id="bbfd9"><cite id="bbfd9"></cite></p><p id="bbfd9"></p>
            <p id="bbfd9"><cite id="bbfd9"><progress id="bbfd9"></progress></cite></p>
            C語言

            C語言調試器是如何工作的

            時間:2025-01-23 06:21:19 C語言 我要投稿
            • 相關推薦

            C語言調試器是如何工作的

              當你用GDB 的時候,可以看到它完全控制了應用程序進程。當你在程序運行的時候用 Ctrl + C,程序的運行就能夠終止,而GDB能展示它的當前地址、堆棧跟蹤信息之類的內容。你知道C語言調試器是如何工作的嗎?下面是小編為大家帶來的關于C語言調試器是如何工作的的知識,歡迎閱讀。

              但是它們怎么不工作呢?

              開始,讓我們先研究它怎樣才會不工作。它不能通過閱讀和分析程序的二進制信息來模擬程序的運行。它其實能做,而那應該能起作用(Valgrind 內存調試器就是這樣工作的),但是這樣的話會很慢。Valgrind會讓程序慢1000倍,但是GDB不會。它的工作機制與Qemu虛擬機一樣。

              所以到底是怎么回事?黑魔法?……不,如果那樣的話就太簡單了。

              另一種猜想?……?破解!是的,這里正是這樣的。操作系統內核也提供了一些幫助。

              首先,關于Linux的進程機制需要了解一件事:父進程可以獲得子進程的附加信息,也能夠ptrace它們。并且你可以猜到的是,調試器是被調試的進程的父進程(或者它會變成父進程,在Linux中進程可以將一個進程變為自己子進程:-))

              Linux Ptrace API

              Linux Ptrace API 允許一個(調試器)進程來獲取低等級的其他(被調試的)進程的信息。特別的,這個調試器可以:

              讀寫被調試進程的內存 :PTRACE_PEEKTEXT、PTRACE_PEEKUSER、PTRACE_POKE……

              讀寫被調試進程的CPU寄存器 PTRACE_GETREGSET、PTRACE_SETREGS

              因系統活動而被提醒:PTRACE_O_TRACEEXEC, PTRACE_O_TRACECLONE, PTRACE_O_EXITKILL, PTRACE_SYSCALL(你可以通過這些標識區分exec syscall、clone、exit以及其他系統調用)

              控制它的執行:PTRACE_SINGLESTEP、PTRACE_KILL、PTRACE_INTERRUPT、PTRACE_CONT (注意,CPU在這里是單步執行)

              修改它的信號處理:PTRACE_GETSIGINFO、PTRACE_SETSIGINFO

              Ptrace是如何實現的?

              Ptrace的實現不在本文討論的范圍內,所以我不想進一步討論,只是簡單地解釋它是如何工作的(我不是內核專家,如果我說錯了請一定指出來,并原諒我過分簡化:-))

              Ptrace 是Linux內核的一部分,所以它能夠獲取進程所有內核級信息:

              讀寫數據?Linux有copy_to/from_user。

              獲取CPU寄存器?用copy_regset_to/from_user很輕松(這里沒有什么復雜的,因為CPU寄存器在進程未被調度時保存在Linux的struct task_struct *調度結構中)。

              修改信號處理?更新域last_siginfo

              單步執行?在處理器出發執行前,設置進程task結構的right flag(ARM、x86)

              Ptrace是在很多計劃的操作中被Hooked(搜索 ptrace_event函數),所以它可以在被詢問時(PTRACE_O_TRACEEXEC選項和與它相關的),向調試器發出一個SIGTRAP信號。

              沒有Ptrace的系統會怎么樣呢?

              這個解釋超出了特定的Linux本地調試,但是對于大部分其他環境是合理的。要了解GDB在不同目標平臺請求的內容,你可以看一下它在目標棧里面的操作。

              在這個目標接口里,你可以看到所有C調試需要的高級操作:

              struct target_ops

              {

              struct target_ops *beneath;

              /* To the target under this one. */

              const char *to_shortname;

              /* Name this target type */

              const char *to_longname;

              /* Name for printing */

              const char *to_doc;

              /* Documentation. Does not include trailing

              newline, and starts with a one-line descrip-

              tion (probably similar to to_longname). */

              void (*to_attach) (struct target_ops *ops, const char *, int);

              void (*to_fetch_registers) (struct target_ops *, struct regcache *, int);

              void (*to_store_registers) (struct target_ops *, struct regcache *, int);

              int (*to__breakpoint) (struct target_ops *, struct gdbarch *,

              struct bp_target_info *);

              int (*to__watchpoint) (struct target_ops *,

              CORE_ADDR, int, int, struct expression *);

              }

              普通的GDB調用這些函數,然后目標相關的組件再實現它們。(概念上)這是一個棧,或者一個金字塔:棧頂的是非常通用的,比如:

              系統特定的Linux

              本地或遠程調試

              調試方式特定的(ptrace、ttrace)

              指令集特定的(Linux ARM、Linux x86)

              那個遠程目標很有趣,因為它通過一個連接協議(TCP/IP、串行端口)把兩臺“電腦”間的執行棧分離開來。

              那個遠程的部分可以是運行在另一臺Linux機器上的gdbserver。但是它也可以是一個硬件調試端口的界面(JTAG) 或者一個虛擬的機器管理程序(比如 Qemu),并能夠代替內核和ptrace的功能。那個遠程根調試器會查詢管理程序的結構,或者直接地查詢處理器硬件寄存器來代替對OS內核結構的查詢。

              想要深層次學習這個遠程協議,Embecosm 寫了一篇一個關于不同信息的詳細指南。Gdbserver的事件處理循環在這,而也可以在這里找到Qemu gdb-server stub 。

              總結一下

              我們能看到ptrace的API提供了這里所有底層機制被要求實現的調試器:

              獲取exec系統調用并從調用的地方阻止它執行

              查詢CPU的寄存器來獲得處理器當前指令以及棧的地址

              獲取clone或fork事件來檢測新線程

              查看并改變數據地址讀取并改變內存的變量

              但是這就是一個調試器的全部工作嗎?不,這只是那些非常低級的部分……它還會處理符號。這是,鏈接源程序和二進制文件。被忽視可能也是最重要的的一件事:斷點!我會首先解釋一下斷點是如何工作的,因為這部分內容非常有趣且需要技巧,然后回到符號處理。

              斷點不是Ptrace API的一部分

              就像我們之前看到的那樣,斷點不是ptrace API的一部分。但是我們可以改動內存并獲取被調試的程序信號。你看不到其中的相關之處?這是因為斷點的實現比較需要技巧并且還要一點hack!讓我們來檢驗一下如何在一個指定的地址設置一個斷點。

              1、這個調試器讀取(ptrace追蹤)存在地址里的二進制指令,并保存在它自己的數據結構中。

              2、它在這個位置寫入一個不合法的指令。不管這個指令是啥,只要它是不合法的。

              3、當被調試的程序運行到這個不合法的指令時(或者更準確地說,處理器將內存中的內容設置好時)它不會繼續運行(因為它是不合法的)。

              4、在現代多任務系統中,一個不合法的指令不會使整個系統崩潰掉,但是會通過引發一個中斷(或錯誤)把控制權交回給系統內核。

              5、這個中斷被Linux翻譯成一個SIGTRAP信號,然后被發送到處理器……或者發給它的父進程,就像調試器希望的那樣。

              6、調試器獲得信號并查看被調試的程序指令指針的值(換言之,是陷入 trap發生的地方)。如果這個IP地址是在斷點列表中,那么就是一個調試器的斷點(否則就是一個進程中的錯誤,只需要傳過信號并讓它崩潰)。

              7、現在,那個被調試的程序已經停在了斷點,調試器可以讓用戶來做任何他/她想要做的事,等待時機合適繼續執行。

              8、為了要繼續執行,這個調試器需要 1、寫入正確的指令來回到被調試的程序的內存; 2、單步執行(繼續執行單個CPU指令,伴隨著ptrace 單步執行); 3、把非法指令寫回去(使得這個執行過程下一次可以再次停止) ;4、讓這個執行正常運行

              很整潔,是不是?作為一個旁觀的評論,你可以注意到,如果不是所有線程同時停止的話這個算法是不會工作的(因為運行的線程可能會在合法的指令出現時傳出斷點)。我不會詳細討論GDB是如何解決這個問題的,但在這篇論文里已經說得很詳細了:使用GDB不間斷調試多線程程序。簡要地說,他們把指令寫到內存中的其他地方,然后把那個指令的指針指向那個地址并單步執行處理器。但是問題在于一些指令是和地址相關的,比如跳轉和條件跳轉……

              處理符號和調試信息

              現在,讓我們回到信號和調試信息處理。我沒有詳細地學習這部分,所以只是大體地說一說。

              首先,我們是否可以不使用調試信息和信號地址來調試呢?答案是可以。因為正如我們看到過的那樣,所有的低級指令是對CPU寄存器和內存地址來操作的,不是源程序層面的信息。因此,這個到源程序的鏈接只是為了方便用戶。沒有調試信息的時候,你看程序的方式就像是處理器(和內核)看到的一樣:二進制(匯編)指令和內存字節。GDB不需要進一步的信息來把二進制信息翻譯成CPU指令:

              (gdb) x/10x $pc # heXadecimal representation

              0x402c60: 0x56415741 0x54415541 0x55f48949 0x4853fd89

              0x402c70: 0x03a8ec81 0x8b480000 0x8b48643e 0x00282504

              0x402c80: 0x89480000 0x03982484

              (gdb) x/10i $pc # Instruction representation

              => 0x402c60: push %r15

              0x402c62: push %r14

              0x402c64: push %r13

              0x402c66: push %r12

              0x402c68: mov %rsi,%r12

              0x402c6b: push %rbp

              0x402c6c: mov %edi,%ebp

              0x402c6e: push %rbx

              0x402c6f: sub $0x3a8,%rsp

              0x402c76: mov (%rsi),%rdi

              現在,如果我們加上調試信息,GDB能夠把符號名稱和地址配對:

              (gdb) $pc

              $1 = (void (*)()) 0x402c60

              你可以通過 nm -a $file 來獲取ELF二進制的符號列表:

              nm -a /usr/lib/debug/usr/bin/ls.debug | grep " main"

              0000000000402c60 T main

              GDB還會能夠展示堆棧跟蹤信息(稍后會詳細說),但是只有感興趣的那部分:

              (gdb) where

              #0 write ()

              #1 0x0000003d492769e3 in _IO_new_file_write ()

              #2 0x0000003d49277e4c in new_do_write ()

              #3 _IO_new_do_write ()

              #4 0x0000003d49278223 in _IO_new_file_overflow ()

              #5 0x00000000004085bb in print_current_files ()

              #6 0x000000000040431b in main ()

              我們現在有了PC地址和相應的函數,就是這樣。在一個函數中,你將需要對著匯編來調試!

              現在讓我們加入調試信息:就是DWARF規范下的gcc -g選項。我不是特別熟悉這個規范,但我知道它提供的:

              地址到代碼行和行到地址的配對

              數據類型的定義,包括typedef和structure

              本地變量和函數參數以及它們的類型

              $ dwarfdump /usr/lib/debug/usr/bin/ls.debug | grep 402ce4

              0x00402ce4 [1289, 0] NS

              $ addr2line -e /usr/lib/debug/usr/bin/ls.debug 0x00402ce4

              /usr/src/debug/coreutils-8.21/src/ls.c:1289

              試一試dwarfdump來查看二進制文件里嵌入的信息。addr2line也能用到這些信息:

              很多源代碼層的調試命令會依賴于這些信息,比如next命令,這會在下一行的地址設置一個斷點,那個print命令會依賴于變量的類型來輸出(char、int、float,而不是二進制或十六進制)。

              最后總結

              我們已經見過調試器內部的好多方面了,所以我只會最后說幾點:

              這個堆棧跟蹤信息也是通過當前的幀是向上“解開(unwinded)”的($sp和$bp/#fp),每個堆棧幀處理一次。函數的名稱和參數以及本地變量名可以在調試信息中找到。

              監視點(&amp;lt;code&amp;gt;watchpoints)是通過處理器的幫助(如果有)實現的:在寄存器里標記哪些地址應該被監控,然后它會在那內存被讀寫的時候引發一個異常。如果不支持這項功能,或者你請求的斷點超過了處理器所支持的……那么調試器就會回到“手動”監視:一個指令一個指令地執行這個程序,并檢查是否當前的操作到達了一個監視點的地址。是的,這很慢!

              反向調試也可以這樣進行,記錄每個操作的效果,并反向執行。

              條件斷點是正常的斷點,除非在內部,調試器在將控制權交給用戶前檢查當前的情況。如果當前的情況不滿足,程序將會默默地繼續運行。

              還可以玩gdb gdb,或者更好的(好多了)gdb --pid $(pid of gdb),因為把兩個調試器放到同一個終端里是瘋狂的:-)。還可以調試系統:

              qemu-system-i386 -gdb tcp::1234

              gdb --pid $(pidof qemu-system-i386)

              gdb /boot/vmlinuz --exec "target remote localhost:1234"


            【C語言調試器是如何工作的】相關文章:

            如何學習c語言10-21

            C語言是如何調用硬件的10-01

            C語言如何輸入語句10-28

            新手如何學習C語言09-29

            如何搭建C語言環境10-27

            如何學習C語言編程10-28

            C語言EOF如何使用08-29

            如何理解C語言指針05-19

            c語言如何控制硬件09-14

                    <pre id="bbfd9"><del id="bbfd9"><dfn id="bbfd9"></dfn></del></pre>

                    <ruby id="bbfd9"></ruby><p id="bbfd9"><mark id="bbfd9"></mark></p>

                    <p id="bbfd9"></p>

                    <p id="bbfd9"><cite id="bbfd9"></cite></p>

                      <th id="bbfd9"><form id="bbfd9"><dl id="bbfd9"></dl></form></th>

                      <p id="bbfd9"><cite id="bbfd9"></cite></p><p id="bbfd9"></p>
                      <p id="bbfd9"><cite id="bbfd9"><progress id="bbfd9"></progress></cite></p>
                      飘沙影院