開發一個 Linux 偵錯程式(九):處理變數

Simon Brand發表於2017-10-06

變數是偷偷摸摸的。有時,它們會很高興地呆在暫存器中,但是一轉頭就會跑到堆疊中。為了優化,編譯器可能會完全將它們從視窗中丟擲。無論變數在記憶體中的如何移動,我們都需要一些方法在偵錯程式中跟蹤和操作它們。這篇文章將會教你如何處理偵錯程式中的變數,並使用 libelfin 演示一個簡單的實現。

系列文章索引

  1. 準備環境
  2. 斷點
  3. 暫存器和記憶體
  4. ELF 和 DWARF
  5. 原始碼和訊號
  6. 原始碼級逐步執行
  7. 原始碼級斷點
  8. 堆疊展開
  9. 處理變數
  10. 高階話題

在開始之前,請確保你使用的 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 表示式或位置描述。loclistloclistptr 的編碼位置列表,它們在 .debug_loclists 部分中提供索引或偏移量,該部分描述了實際的位置列表。

DWARF 表示式

使用 DWARF 表示式計算變數的實際位置。這包括操作堆疊值的一系列操作。有很多 DWARF 操作可用,所以我不會詳細解釋它們。相反,我會從每一個表示式中給出一些例子,給你一個可用的東西。另外,不要害怕這些;libelfin 將為我們處理所有這些複雜性。

  • 字面編碼
    • DW_OP_lit0DW_OP_lit1……DW_OP_lit31
      • 將字面量壓入堆疊
    • DW_OP_addr <addr>
      • 將地址運算元壓入堆疊
    • DW_OP_constu <unsigned>
      • 將無符號值壓入堆疊
  • 暫存器值
    • DW_OP_fbreg <offset>
      • 壓入在堆疊幀基址找到的值,偏移給定值
    • DW_OP_breg0DW_OP_breg1…… DW_OP_breg31 <offset>
      • 將給定暫存器的內容加上給定的偏移量壓入堆疊
  • 堆疊操作
    • DW_OP_dup
      • 複製堆疊頂部的值
    • DW_OP_deref
      • 將堆疊頂部視為記憶體地址,並將其替換為該地址的內容
  • 算術和邏輯運算
    • DW_OP_and
      • 彈出堆疊頂部的兩個值,並壓回它們的邏輯 AND
    • DW_OP_plus
      • DW_OP_and 相同,但是會新增值
  • 控制流操作
    • DW_OP_leDW_OP_eqDW_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, &regs);
        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

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

相關文章