tft每日頭條

 > 生活

 > gdb使用教程

gdb使用教程

生活 更新时间:2024-12-29 07:19:33

gdb使用教程(gdb如何調用函數)1

編譯自: https://jvns.ca/blog/2018/01/04/how-does-gdb-call-functions/

作者: Julia Evans

譯者: Lv Feng

(之前的 gdb 系列文章:gdb 如何工作(2016)[1] 和三步上手 gdb(2014)[2])

在這周,我發現我可以從 gdb 上調用 C 函數。這看起來很酷,因為在過去我認為 gdb 最多隻是一個隻讀調試工具。

我對 gdb 能夠調用函數感到很吃驚。正如往常所做的那樣,我在 Twitter[3] 上詢問這是如何工作的。我得到了大量的有用答案。我最喜歡的答案是 Evan Klitzke 的示例 C 代碼[4],它展示了 gdb 如何調用函數。代碼能夠運行,這很令人激動!

我(通過一些跟蹤和實驗)認為那個示例 C 代碼和 gdb 實際上如何調用函數不同。因此,在這篇文章中,我将會闡述 gdb 是如何調用函數的,以及我是如何知道的。

  • 暫停程序(因為它已經在運行中)

  • 找到你想調用的函數的地址(使用符号表)

  • 使程序(目标程序)跳轉到那個地址

  • 當函數返回時,恢複之前的指令指針和寄存器

  • 通過符号表來找到想要調用的函數的地址非常容易。下面是一段非常簡單但能夠工作的代碼,我在 Linux 上使用這段代碼作為例子來講解如何找到地址。這段代碼使用 elf crate[5]。如果我想找到 PID 為 2345 的進程中的 foo 函數的地址,那麼我可以運行 elf_symbol_value("/proc/2345/exe", "foo")。

    fn elf_symbol_value(file_name: &str, symbol_name: &str) -> Result<u64, Box<std::error::Error>> {

    // 打開 ELF 文件

    let file = elf::File::open_path(file_name).ok().ok_or("parse error")?;

    // 在所有的段 & 符号中循環,直到找到正确的那個

    let sections = &file.sections;

    for s in sections {

    for sym in file.get_symbols(&s).ok().ok_or("parse error")? {

    if sym.name == symbol_name {

    return Ok(sym.value);

    }

    }

    }

    None.ok_or("No symbol found")?

    }

    這并不能夠真的發揮作用,你還需要找到文件的内存映射,并将符号偏移量加到文件映射的起始位置。找到内存映射并不困難,它位于 /proc/PID/maps 中。

    總之,找到想要調用的函數地址對我來說很直接,但是其餘部分(改變指令指針,恢複寄存器等)看起來就不這麼明顯了。

    你不能僅僅進行跳轉

    我已經說過,你不能夠僅僅找到你想要運行的那個函數地址,然後跳轉到那兒。我在 gdb 中嘗試過那樣做(jump foo),然後程序出現了段錯誤。毫無意義。

    如何從 gdb 中調用 C 函數

    首先,這是可能的。我寫了一個非常簡潔的 C 程序,它所做的事隻有 sleep 1000 秒,把這個文件命名為 test.c :

    #include <unistd.h>

    int foo() {

    return 3;

    }

    int main() {

    sleep(1000);

    }

    接下來,編譯并運行它:

    $ gcc -o test test.c

    $ ./test

    最後,我們使用 gdb 來跟蹤 test 這一程序:

    $ sudo gdb -p $(pgrep -f test)

    (gdb) p foo()

    $1 = 3

    (gdb) quit

    我運行 p foo() 然後它運行了這個函數!這非常有趣。

    這有什麼用?

    下面是一些可能的用途:

    • 它使得你可以把 gdb 當成一個 C 應答式程序(REPL),這很有趣,我想對開發也會有用

    • 在 gdb 中進行調試的時候展示/浏覽複雜數據結構的功能函數(感謝 @invalidop[6])

    • 在進程運行時設置一個任意的名字空間[7](我的同事 nelhage 對此非常驚訝)

    • 可能還有許多我所不知道的用途

    它是如何工作的

    當我在 Twitter 上詢問從 gdb 中調用函數是如何工作的時,我得到了大量有用的回答。許多答案是“你從符号表中得到了函數的地址”,但這并不是完整的答案。

    有個人告訴了我兩篇關于 gdb 如何工作的系列文章:原生調試:第一部分[8],原生調試:第二部分[9]。第一部分講述了 gdb 是如何調用函數的(指出了 gdb 實際上完成這件事并不簡單,但是我将會盡力)。

    步驟列舉如下:

    1. 停止進程

    2. 創建一個新的棧框(遠離真實棧)

    3. 保存所有寄存器

    4. 設置你想要調用的函數的寄存器參數

    5. 設置棧指針指向新的 棧框(stack frame)

    6. 在内存中某個位置放置一條陷阱指令

    7. 為陷阱指令設置返回地址

    8. 設置指令寄存器的值為你想要調用的函數地址

    9. 再次運行進程!

    (LCTT 譯注:如果将這個調用的函數看成一個單獨的線程,gdb 實際上所做的事情就是一個簡單的線程上下文切換)

    我不知道 gdb 是如何完成這些所有事情的,但是今天晚上,我學到了這些所有事情中的其中幾件。

    創建一個棧框

    如果你想要運行一個 C 函數,那麼你需要一個棧來存儲變量。你肯定不想繼續使用當前的棧。準确來說,在 gdb 調用函數之前(通過設置函數指針并跳轉),它需要設置棧指針到某個地方。

    這兒是 Twitter 上一些關于它如何工作的猜測:

    我認為它在當前棧的棧頂上構造了一個新的棧框來進行調用!

    以及

    你确定是這樣嗎?它應該是分配一個僞棧,然後臨時将 sp (棧指針寄存器)的值改為那個棧的地址。你可以試一試,你可以在那兒設置一個斷點,然後看一看棧指針寄存器的值,它是否和當前程序寄存器的值相近?

    我通過 gdb 做了一個試驗:

    (gdb) p $rsp

    $7 = (void *) 0x7ffea3d0bca8

    (gdb) break foo

    Breakpoint 1 at 0x40052a

    (gdb) p foo()

    Breakpoint 1, 0x000000000040052a in foo ()

    (gdb) p $rsp

    $8 = (void *) 0x7ffea3d0bc00

    這看起來符合“gdb 在當前棧的棧頂構造了一個新的棧框”這一理論。因為棧指針($rsp)從 0x7ffea3d0bca8 變成了 0x7ffea3d0bc00 —— 棧指針從高地址往低地址長。所以 0x7ffea3d0bca8 在 0x7ffea3d0bc00 的後面。真是有趣!

    所以,看起來 gdb 隻是在當前棧所在位置創建了一個新的棧框。這令我很驚訝!

    改變指令指針

    讓我們來看一看 gdb 是如何改變指令指針的!

    (gdb) p $rip

    $1 = (void (*)()) 0x7fae7d29a2f0 <__nanosleep_nocancel 7>

    (gdb) b foo

    Breakpoint 1 at 0x40052a

    (gdb) p foo()

    Breakpoint 1, 0x000000000040052a in foo ()

    (gdb) p $rip

    $3 = (void (*)()) 0x40052a <foo 4>

    的确是!指令指針從 0x7fae7d29a2f0 變為了 0x40052a(foo 函數的地址)。

    我盯着輸出看了很久,但仍然不理解它是如何改變指令指針的,但這并不影響什麼。

    如何設置斷點

    上面我寫到 break foo 。我跟蹤 gdb 運行程序的過程,但是沒有任何發現。

    下面是 gdb 用來設置斷點的一些系統調用。它們非常簡單。它把一條指令用 cc 代替了(這告訴我們 int3 意味着 send SIGTRAP https://defuse.ca/online-x86-assembler.html),并且一旦程序被打斷了,它就把指令恢複為原先的樣子。

    我在函數 foo 那兒設置了一個斷點,地址為 0x400528 。

    PTRACE_POKEDATA 展示了 gdb 如何改變正在運行的程序。

    // 改變 0x400528 處的指令

    25622 ptrace(PTRACE_PEEKTEXT, 25618, 0x400528, [0x5d00000003b8e589]) = 0

    25622 ptrace(PTRACE_POKEDATA, 25618, 0x400528, 0x5d00000003cce589) = 0

    // 開始運行程序

    25622 ptrace(PTRACE_CONT, 25618, 0x1, SIG_0) = 0

    // 當到達斷點時獲取一個信号

    25622 ptrace(PTRACE_GETSIGINFO, 25618, NULL, {si_signo=SIGTRAP, si_code=SI_KERNEL, si_value={int=-1447215360, ptr=0x7ffda9bd3f00}}) = 0

    // 将 0x400528 處的指令更改為之前的樣子

    25622 ptrace(PTRACE_PEEKTEXT, 25618, 0x400528, [0x5d00000003cce589]) = 0

    25622 ptrace(PTRACE_POKEDATA, 25618, 0x400528, 0x5d00000003b8e589) = 0

    在某處放置一條陷阱指令

    當 gdb 運行一個函數的時候,它也會在某個地方放置一條陷阱指令。這是其中一條。它基本上是用 cc 來替換一條指令(int3)。

    5908 ptrace(PTRACE_PEEKTEXT, 5810, 0x7f6fa7c0b260, [0x48f389fd89485355]) = 0

    5908 ptrace(PTRACE_PEEKTEXT, 5810, 0x7f6fa7c0b260, [0x48f389fd89485355]) = 0

    5908 ptrace(PTRACE_POKEDATA, 5810, 0x7f6fa7c0b260, 0x48f389fd894853cc) = 0

    0x7f6fa7c0b260 是什麼?我查看了進程的内存映射,發現它位于 /lib/x86_64-linux-gnu/libc-2.23.so 中的某個位置。這很奇怪,為什麼 gdb 将陷阱指令放在 libc 中?

    讓我們看一看裡面的函數是什麼,它是 __libc_siglongjmp 。其他 gdb 放置陷阱指令的地方的函數是 __longjmp 、___longjmp_chk 、dl_main 和 _dl_close_worker 。

    為什麼?我不知道!也許出于某種原因,當函數 foo() 返回時,它調用 longjmp ,從而 gdb 能夠進行返回控制。我不确定。

    gdb 如何調用函數是很複雜的!

    我将要在這兒停止了(現在已經淩晨 1 點),但是我知道的多一些了!

    看起來“gdb 如何調用函數”這一問題的答案并不簡單。我發現這很有趣并且努力找出其中一些答案,希望你也能夠找到。

    我依舊有很多未回答的問題,關于 gdb 是如何完成這些所有事的,但是可以了。我不需要真的知道關于 gdb 是如何工作的所有細節,但是我很開心,我有了一些進一步的理解。


    via: https://jvns.ca/blog/2018/01/04/how-does-gdb-call-functions/

    作者:Julia Evans[10] 譯者:ucasFL 校對:wxy

    本文由 LCTT 原創編譯,Linux中國 榮譽推出

    點擊“了解更多”可訪問文内鍊接

    ,

    更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

    查看全部

    相关生活资讯推荐

    热门生活资讯推荐

    网友关注

    Copyright 2023-2024 - www.tftnews.com All Rights Reserved