探索Linux調試器中的變數處理技巧!
導讀 | 變數是偷偷摸摸的。有時,它們會很高興地留在暫存器中,但是一轉頭就會跑到堆疊中。為了優化,編譯器可能會完全將它們從視窗中拋出。無論變數在記憶體中的如何移動,我們都需要一些方法在調試器中追蹤和操作它們。這篇文章將會教你如何處理偵錯器中的變量,並使用 libelfin 來示範一個簡單的實作。 |
- 準備環境
- 斷點
- 暫存器和記憶體
- ELF 和 DWARF
- 原始碼和訊號
- 源碼級逐步執行
- 原始碼級斷點
- 堆疊展開
- 處理變數
- 高階話題
在開始之前,請確保你使用的 libelfin 版本是我分支上的 fbreg。這包含了一些 hack 來支援獲取當前堆疊幀的基址並評估位置列表,這些都不是由原生的 libelfin 提供的。你可能需要給 GCC 傳遞 -gdwarf-2 參數使其產生相容的 DWARF 資訊。但是在實作之前,我將詳細說明 DWARF 5 最新規範中的位置編碼方式。如果你想要了解更多信息,那麼你可以從這裡獲取該標準。
DWARF 位置某一給定時刻的記憶體中變數的位置使用 DW_AT_location 屬性編碼在 DWARF 資訊中。位置描述可以是單一位置描述、複合位置描述或位置清單。
- 簡單位置描述:描述了物件的一個連續的部分(通常是所有部分)的位置。簡單位置描述可以描述可尋址記憶體或暫存器中的位置,或缺少位置(具有或不具有已知值)。例如,DW_OP_fbreg -32: 一個整個儲存的變數 - 從堆疊幀基址開始的32個位元組。
- 複合位置描述:根據片段描述對象,每個對象可以包含在暫存器的一部分或儲存在與其他片段無關的記憶體位置。例如, DW_OP_reg3 DW_OP_piece 4 DW_OP_reg10 DW_OP_piece 2:前四個位元組位於暫存器 3 中,後兩個位元組位於暫存器 10 中的一個變數。
- 位置清單:描述了具有有限生存期或在生存期內更改位置的物件。比如:
- [ 0]
DW_OP_reg0 - [ 1]
DW_OP_reg3 - [ 2]
DW_OP_reg2
- [ 0]
- 根據程式計數器的目前值,位置在暫存器之間移動的變數。
根據位置描述的種類,DW_AT_location 以三種不同的方式進行編碼。 exprloc 編碼簡單和複合的位置描述。它們由一個位元組長度組成,後面跟著一個 DWARF 表達式或位置描述。 loclist 和 loclistptr 的編碼位置列表,它們在 .debug_loclists 部分中提供索引或偏移量,該部分描述了實際的位置列表。
DWARF 表達式使用 DWARF 表達式計算變數的實際位置。這包括操作堆疊值的一系列操作。有很多 DWARF 操作可用,所以我不會詳細解釋它們。相反,我會從每一個表達式中給出一些例子,給你一個可用的東西。另外,不要害怕這些;libelfin將為我們處理所有這些複雜性。
- 字面編碼
- DW_OP_lit0、DW_OP_lit1……DW_OP_lit31
- 將字面量壓入堆疊
- DW_OP_addr
- 將位址運算元壓入堆疊
- DW_OP_constu
- 將無符號值壓入堆疊
- DW_OP_lit0、DW_OP_lit1……DW_OP_lit31
- 暫存器值
- DW_OP_fbreg
- 壓入在堆疊幀基址找到的值,偏移給定值
- DW_OP_breg0、DW_OP_breg1…… DW_OP_breg31
- 將給定暫存器的內容加上給定的偏移量壓入堆疊
- DW_OP_fbreg
- 堆疊操作
- DW_OP_dup
- 複製堆疊頂端的值
- DW_OP_deref
- 將堆疊頂部視為記憶體位址,並將其替換為該位址的內容
- DW_OP_dup
- 算術和邏輯運算
- DW_OP_and
- 彈出堆疊頂端的兩個值,並壓回它們的邏輯 AND
- DW_OP_plus
- 與 DW_OP_and 相同,但會加值
- DW_OP_and
- 控制流程操作
- DW_OP_le、DW_OP_eq、DW_OP_gt 等
- 彈出前兩個值,比較它們,如果條件為真,則壓入 1,否則為 0
- DW_OP_bra
- 條件分支:如果堆疊的頂部不是 0,則透過 offset 在表達式中向後或向後跳過
- DW_OP_le、DW_OP_eq、DW_OP_gt 等
- 輸入轉換
- DW_OP_convert
- 將堆疊頂部的值轉換為不同的類型,它由給定偏移量的 DWARF 資訊條目描述
- DW_OP_convert
- 特殊操作
- DW_OP_nop
- 什麼都不做!
- DW_OP_nop
DWARF 類型的表示需要足夠強大來為偵錯器使用者提供有用的變數表示。使用者經常希望能夠在應用程式層級進行調試,而不是在機器層級進行調試,並且他們需要了解他們的變數正在做什麼。
DWARF 類型與大多數其他調試資訊一起編碼在 DIE 中。它們可以具有指示其名稱、編碼、大小、位元組等的屬性。無數的類型標籤可用於表示指標、陣列、結構體、typedef 以及 C 或 C 程式中可以看到的任何其他內容。
以這個簡單的結構體為例:
struct test{ int i; float j; int k[42]; test* next; };
這個結構體的父 DIE 是這樣的:
< 1><0x0000002a> DW_TAG_structure_type DW_AT_name "test" DW_AT_byte_size 0x000000b8 DW_AT_decl_file 0x00000001 test.cpp DW_AT_decl_line 0x00000001
上面說的是我們有一個叫做 test 的結構體,大小為 0xb8,在 test.cpp 的第 1 行宣告。接下來有許多描述成員的子 DIE。
< 2><0x00000032> DW_TAG_member DW_AT_name "i" DW_AT_type <0x00000063> DW_AT_decl_file 0x00000001 test.cpp DW_AT_decl_line 0x00000002 DW_AT_data_member_location 0 < 2><0x0000003e> DW_TAG_member DW_AT_name "j" DW_AT_type <0x0000006a> DW_AT_decl_file 0x00000001 test.cpp DW_AT_decl_line 0x00000003 DW_AT_data_member_location 4 < 2><0x0000004a> DW_TAG_member DW_AT_name "k" DW_AT_type <0x00000071> DW_AT_decl_file 0x00000001 test.cpp DW_AT_decl_line 0x00000004 DW_AT_data_member_location 8 < 2><0x00000056> DW_TAG_member DW_AT_name "next" DW_AT_type <0x00000084> DW_AT_decl_file 0x00000001 test.cpp DW_AT_decl_line 0x00000005 DW_AT_data_member_location 176(as signed = -80)
每個成員都有一個名稱、一個類型(它是一個 DIE 偏移量)、一個宣告檔案和行,以及一個指向其成員所在的結構體的位元組偏移。其類型指向如下。
< 1><0x00000063> DW_TAG_base_type DW_AT_name "int" DW_AT_encoding DW_ATE_signed DW_AT_byte_size 0x00000004 < 1><0x0000006a> DW_TAG_base_type DW_AT_name "float" DW_AT_encoding DW_ATE_float DW_AT_byte_size 0x00000004 < 1><0x00000071> DW_TAG_array_type DW_AT_type <0x00000063> < 2><0x00000076> DW_TAG_subrange_type DW_AT_type <0x0000007d> DW_AT_count 0x0000002a < 1><0x0000007d> DW_TAG_base_type DW_AT_name "sizetype" DW_AT_byte_size 0x00000008 DW_AT_encoding DW_ATE_unsigned < 1><0x00000084> DW_TAG_pointer_type DW_AT_type <0x0000002a>
如你所見,我筆記型電腦上的 int 是一個 4 位元組的有符號整數類型,float是一個 4 位元組的浮點數。整數數組類型透過指向 int 類型作為其元素類型,sizetype(可以認為是 size_t)作為索引類型,它具有 2a 個元素。 test * 類型是 DW_TAG_pointer_type,它引用 test DIE。
實作簡單的變數讀取器如上所述,libelfin 將為我們處理大部分複雜性。但是,它並沒有實現用於表示可變位置的所有方法,並且在我們的程式碼中處理這些將變得非常複雜。因此,我現在選擇只支援 exprloc。請根據需要新增對更多類型表達式的支援。如果你真的有勇氣,請提交補丁到 libelfin 中來幫助完成必要的支援!
處理變數主要是將不同部分定位在記憶體或暫存器中,讀取或寫入與之前相同。為了簡單起見,我只會告訴你如何實作讀取。
首先我們需要告訴 libelfin 如何從我們的進程中讀取暫存器。我們建立一個繼承自 expr_context 的類別並使用 ptrace 來處理所有內容:
class ptrace_expr_context : public dwarf::expr_context { public: ptrace_expr_context (pid_t pid) : m_pid{pid} {} dwarf::taddr reg (unsigned regnum) override { return get_register_value_from_dwarf_register(m_pid, regnum); } dwarf::taddr pc() override { struct user_regs_struct regs; ptrace(PTRACE_GETREGS, m_pid, nullptr, ®s); return regs.rip; } dwarf::taddr deref_size (dwarf::taddr address, unsigned size) override { //TODO take into account size return ptrace(PTRACE_PEEKDATA, m_pid, address, nullptr); } private: pid_t m_pid; };
讀取將由我們 debugger 類別中的 read_variables 函數處理:
void debugger::read_variables() { using namespace dwarf; auto func = get_function_from_pc(get_pc()); //... }
我們上面做的第一件事是找到我們目前進入的函數,然後我們需要循環訪問該函數中的條目來尋找變數:
for (const auto& die : func) { if (die.tag == DW_TAG::variable) { //... } }
我們透過尋找 DIE 中的 DW_AT_location 條目來取得位置資訊:
auto loc_val = die[DW_AT::location];
接著我們確保它是一個 exprloc,並請求 libelfin 來評估我們的表達式:
if (loc_val.get_type() == value::type::exprloc) { ptrace_expr_context context {m_pid}; auto result = loc_val.as_exprloc().evaluate(&context);
現在我們已經評估了表達式,我們需要讀取變數的內容。它可以在記憶體或暫存器中,因此我們將處理這兩種情況:
switch (result.location_type) { case expr_result::type::address: { auto value = read_memory(result.value); std::cout << at_name(die) << " (0x" << std::hex << result.value << ") = " << value << std::endl; break; } case expr_result::type::reg: { auto value = get_register_value_from_dwarf_register(m_pid, result.value); std::cout << at_name(die) << " (reg " << result.value << ") = " << value << std::endl; break; } default: throw std::runtime_error{"Unhandled variable location"}; }
你可以看到,我根據變數的類型,列印輸出了值而沒有解釋。希望透過這個程式碼,你可以看到如何支援編寫變量,或用給定的名字搜尋變數。
最後我們可以將它加入我們的命令解析器中:
else if(is_prefix(command, "variables")) { read_variables(); }
寫一些具有一些變數的小功能,不用最佳化並帶有偵錯資訊編譯它,然後查看是否可以讀取變數的值。嘗試寫入儲存變數的記憶體位址,並查看程式改變的行為。
已經有九篇文章了,剩下最後一篇!下一次我會討論一些你可能會感興趣的更高級的概念。現在你可以在這裡找到這個帖子的程式碼。
以上是探索Linux調試器中的變數處理技巧!的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

VS Code 系統要求:操作系統:Windows 10 及以上、macOS 10.12 及以上、Linux 發行版處理器:最低 1.6 GHz,推薦 2.0 GHz 及以上內存:最低 512 MB,推薦 4 GB 及以上存儲空間:最低 250 MB,推薦 1 GB 及以上其他要求:穩定網絡連接,Xorg/Wayland(Linux)

Linux系統的五個基本組件是:1.內核,2.系統庫,3.系統實用程序,4.圖形用戶界面,5.應用程序。內核管理硬件資源,系統庫提供預編譯函數,系統實用程序用於系統管理,GUI提供可視化交互,應用程序利用這些組件實現功能。

雖然 Notepad 無法直接運行 Java 代碼,但可以通過借助其他工具實現:使用命令行編譯器 (javac) 編譯代碼,生成字節碼文件 (filename.class)。使用 Java 解釋器 (java) 解釋字節碼,執行代碼並輸出結果。

VS Code擴展安裝失敗的原因可能包括:網絡不穩定、權限不足、系統兼容性問題、VS Code版本過舊、殺毒軟件或防火牆干擾。通過檢查網絡連接、權限、日誌文件、更新VS Code、禁用安全軟件以及重啟VS Code或計算機,可以逐步排查和解決問題。

要查看 Git 倉庫地址,請執行以下步驟:1. 打開命令行並導航到倉庫目錄;2. 運行 "git remote -v" 命令;3. 查看輸出中的倉庫名稱及其相應的地址。

VS Code 可以在 Mac 上使用。它具有強大的擴展功能、Git 集成、終端和調試器,同時還提供了豐富的設置選項。但是,對於特別大型項目或專業性較強的開發,VS Code 可能會有性能或功能限制。

Visual Studio Code (VSCode) 是一款跨平台、開源且免費的代碼編輯器,由微軟開發。它以輕量、可擴展性和對眾多編程語言的支持而著稱。要安裝 VSCode,請訪問官方網站下載並運行安裝程序。使用 VSCode 時,可以創建新項目、編輯代碼、調試代碼、導航項目、擴展 VSCode 和管理設置。 VSCode 適用於 Windows、macOS 和 Linux,支持多種編程語言,並通過 Marketplace 提供各種擴展。它的優勢包括輕量、可擴展性、廣泛的語言支持、豐富的功能和版

vscode 內置終端是一個開發工具,允許在編輯器內運行命令和腳本,以簡化開發流程。如何使用 vscode 終端:通過快捷鍵 (Ctrl/Cmd ) 打開終端。輸入命令或運行腳本。使用熱鍵 (如 Ctrl L 清除終端)。更改工作目錄 (如 cd 命令)。高級功能包括調試模式、代碼片段自動補全和交互式命令歷史。
