開發一個 Linux 調試器(九):處理變數
變數是偷偷摸摸的。有時,它們會很高興地呆在寄存器中,但是一轉頭就會跑到堆棧中。為了優化,編譯器可能會完全將它們從窗口中拋出。無論變數在內存中的如何移動,我們都需要一些方法在調試器中跟蹤和操作它們。這篇文章將會教你如何處理調試器中的變數,並使用 libelfin
演示一個簡單的實現。
系列文章索引
在開始之前,請確保你使用的 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 中的一個變數。 - 位置列表:描述了具有有限生存期或在生存期內更改位置的對象。比如:
<loclist with 3 entries follows>
[ 0]<lowpc=0x2e00><highpc=0x2e19>DW_OP_reg0
[ 1]<lowpc=0x2e19><highpc=0x2e3f>DW_OP_reg3
[ 2]<lowpc=0x2ec4><highpc=0x2ec7>DW_OP_reg2
- 根據程序計數器的當前值,位置在寄存器之間移動的變數。
根據位置描述的種類,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 <addr>
- 將地址操作數壓入堆棧
DW_OP_constu <unsigned>
- 將無符號值壓入堆棧
- 寄存器值
DW_OP_fbreg <offset>
- 壓入在堆棧幀基址找到的值,偏移給定值
DW_OP_breg0
、DW_OP_breg1
……DW_OP_breg31 <offset>
- 將給定寄存器的內容加上給定的偏移量壓入堆棧
- 堆棧操作
DW_OP_dup
- 複製堆棧頂部的值
DW_OP_deref
- 將堆棧頂部視為內存地址,並將其替換為該地址的內容
- 算術和邏輯運算
DW_OP_and
- 彈出堆棧頂部的兩個值,並壓回它們的邏輯
AND
- 彈出堆棧頂部的兩個值,並壓回它們的邏輯
DW_OP_plus
- 與
DW_OP_and
相同,但是會添加值
- 與
- 控制流操作
DW_OP_le
、DW_OP_eq
、DW_OP_gt
等- 彈出前兩個值,比較它們,並且如果條件為真,則壓入
1
,否則為0
- 彈出前兩個值,比較它們,並且如果條件為真,則壓入
DW_OP_bra <offset>
- 條件分支:如果堆棧的頂部不是
0
,則通過offset
在表達式中向後或向後跳過
- 條件分支:如果堆棧的頂部不是
- 輸入轉化
DW_OP_convert <DIE offset>
- 將堆棧頂部的值轉換為不同的類型,它由給定偏移量的 DWARF 信息條目描述
- 特殊操作
DW_OP_nop
- 什麼都不做!
DWARF 類型
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();
}
測試一下
編寫一些具有一些變數的小功能,不用優化並帶有調試信息編譯它,然後查看是否可以讀取變數的值。嘗試寫入存儲變數的內存地址,並查看程序改變的行為。
已經有九篇文章了,還剩最後一篇!下一次我會討論一些你可能會感興趣的更高級的概念。現在你可以在這裡找到這個帖子的代碼。
via: https://blog.tartanllama.xyz/writing-a-linux-debugger-variables/
作者:Simon Brand 譯者:geekpi 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive