您的位置 首页 制造

开发一个Linux调试器就需要了解ELF和DWARF

开发一个Linux调试器就需要了解ELF和DWARF-ELF和DWARF可能是在程序员日常生活中经常使用但是可能却没有听说过的两个部件。ELF(Executable and Linkable Format)是Linux世界最广泛中使用的一种Object File Format;它指定了一种将各部分数据存储在二进制文件的方式,比如说代码,静态数据,调试信息,以及一些字符串等这些数据。

到目前为止,或许你现已听到了关于调试信息或许关于除了解析代码以外的了解源代码的办法的DWARF的只言片语。今日,咱们将介绍源代码级的调试信息的细节,以备在该系列的余下部分运用它。

ELF和DWARF简介

ELF和DWARF或许是在程序员日常日子中常常运用可是或许却没有听说过的两个部件。ELF(Executable and Linkable Format)是Linux国际最广泛中运用的一种Object File Format;它指定了一种将各部分数据存储在二进制文件的办法,比方说代码,静态数据,调试信息,以及一些字符串等这些数据。一同,也告知加载器以何种办法对待二进制文件以及准备好履行,这触及到将二进制文件的不同部分加载到内存中,以及依据其他一些组件的方位来批改(重定位)相关的数据位等等。我不会在文章中包括太多的ELF相关的常识,可是假如感兴趣的话你能够看一下这个精彩的图表或许这个ELF规范文档。

DWARF是ELF文件一般运用的调试信息格局。一般来讲DWARF对ELF来说并不是有必要的,可是这两者是被串联开发在一同的,而且一同运用十分好。这个格局答应编译器告知调试器源代码是怎么与被履行的二进制文件相关的。调试信息被分割在ELF不同的区段中,每一部分都传达了本区块的相关信息。一下是一些预界说的一些区段,假如信息过期的话,能够从这儿获取最新信息,DWARF调试信息简介:

.debug_abbrev在.debug_info中运用的缩写

.debug_aranges内存地址和汇编间的映射

.debug_frame调用栈帧信息

.debug_info包括DWARF信息进口(DIEs)的中心数据

.debug_line行号信息

.debug_loc 方位描绘

.debug_macinfo宏界说描绘

.debug_pubnames大局方针和函数查找表

.debug_pubtypes大局类型查找表

.debug_rangesDIEs引证地址规模

.debug_str在.debug_info中运用的字符串表

.debug_types类型描绘信息

咱们最感兴趣的是.debug_line和.debug_info区段,所以让咱们用一个简略的程序来看一下一些DWARF信息吧:

int main() {    long a = 3;    long b = 2;    long c = a + b;    a = 4;}

DWARF行号表

假如在编译程序的时分指定了-g选项,然后经过dwarfdump运转成果,应该相似以下信息的行号区段:

.debug_line: line number info for a single cuSource lines (from CU-DIE at .debug_info offset 0x0000000b):            NS new statement, BB new basic block, ET end of text sequence            PE prologue end, EB epilogue begin            IS=val ISA number, DI=val discriminator value        [lno,col] NS BB ET PE EB IS= DI= uri: “filepath”0x00400670  [   1, 0] NS uri: “/home/simon/play/MiniDbg/examples/variable.cpp”0x00400676  [   2,10] NS PE0x0040067e  [   3,10] NS0x00400686  [   4,14] NS0x0040068a  [   4,16]0x0040068e  [   4,10]0x00400692  [   5, 7] NS0x0040069a  [   6, 1] NS0x0040069c  [   6, 1] NS ET

开端的一大串信息是关于怎么了解dump的一些阐明,主行号信息从0x00400770这行开端。本质上,它映射了代码内存地址和在文件中的行和列信息。NS表明该地址标志着新句子的开端,这一般用于设置断点或单步。PE标志着函数头部的完毕,这有助于设置函数进口断点。ET标明该映射块的完毕。信息实践上并不是像这样编码,实践的编码是一种十分节约空间的程序,由它来树立这些行号信息。

那么,假如咱们想在variable.cpp中的第4行下一个断点,应该怎么做呢? 查找与该文件相对应的条目,然后找到相关的行号,找到相关的地址,然后设置一个断点就能够了。在咱们的小程序中,便是这一条:

0x00400686  [   4,14] NS

所以咱们需求在0x00400686地址处设置一个断点。假如你想测验一下,你能够用你现已写过的调试器手艺完结。

相反的作业也是如此,假如咱们有一个内存方位 – 比方一个RIP,而且想要找出它在源代码中的哪个方位,只需内行号信息表中找到最接近的映射地址,并从中获取行号即可。

DWARF调试信息

.debug_info是DWARF的中心地点。它给了咱们程序中存在的关于类型,功用,变量,期望和愿望的信息。该区段的基本单位是DWARF信息进口,也便是被亲热地称为DIE的东西。DIE包括一个标签,告知你代表什么样的源代码级的条目,后边是一系列适用于该条意图特点。以下是之前的那个简略程序的.debug_info:

.debug_infoCOMPILE_UNIT

:< 0><0x0000000b>  DW_TAG_compile_unit                    DW_AT_producer              clang version 3.9.1 (tags/RELEASE_391/final)                    DW_AT_language              DW_LANG_C_plus_plus                    DW_AT_name                  /super/secret/path/MiniDbg/examples/variable.cpp                    DW_AT_stmt_list             0x00000000                    DW_AT_comp_dir              /super/secret/path/MiniDbg/build                    DW_AT_low_pc                0x00400670                    DW_AT_high_pc               0x0040069cLOCAL_SYMBOLS:< 1><0x0000002e>    DW_TAG_subprogram                      DW_AT_low_pc                0x00400670                      DW_AT_high_pc               0x0040069c                      DW_AT_frame_base            DW_OP_reg6                      DW_AT_name                  main                      DW_AT_decl_file             0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp                      DW_AT_decl_line             0x00000001                      DW_AT_type                  <0x00000077>                      DW_AT_external              yes(1)< 2><0x0000004c>      DW_TAG_variable                        DW_AT_location              DW_OP_fbreg -8                        DW_AT_name                  a                        DW_AT_decl_file             0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp                        DW_AT_decl_line             0x00000002                        DW_AT_type                  <0x0000007e>< 2><0x0000005a>      DW_TAG_variable                        DW_AT_locaTIon              DW_OP_fbreg -16                        DW_AT_name                  b                        DW_AT_decl_file             0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp                        DW_AT_decl_line             0x00000003                        DW_AT_type                  <0x0000007e>< 2><0x00000068>      DW_TAG_variable                        DW_AT_locaTIon              DW_OP_fbreg -24                        DW_AT_name                  c                        DW_AT_decl_file             0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp                        DW_AT_decl_line             0x00000004                        DW_AT_type                  <0x0000007e>< 1><0x00000077>    DW_TAG_base_type                      DW_AT_name                  int                      DW_AT_encoding              DW_ATE_signed                      DW_AT_byte_size             0x00000004< 1><0x0000007e>    DW_TAG_base_type                      DW_AT_name                  long int                      DW_AT_encoding              DW_ATE_signed                      DW_AT_byte_size             0x00000008

榜首个DIE表明一个编译单元(CU),它本质上是一个源文件,其间包括一切#include而且被解析的包括文件。以下是它们的包括注释的特点:

DW_AT_producer   clang version 3.9.1 (tags/RELEASE_391/final)    <-- The compiler which produced                                                                     this binaryDW_AT_language   DW_LANG_C_plus_plus                             <-- The source languageDW_AT_name       /super/secret/path/MiniDbg/examples/variable.cpp  <-- The name of the file which                                                                     this CU representsDW_AT_stmt_list  0x00000000                                      <-- An offset into the line table                                                                     which tracks this CUDW_AT_comp_dir   /super/secret/path/MiniDbg/build                  <-- The compilaTIon directoryDW_AT_low_pc     0x00400670                                      <-- The start of the code for                                                                     this CUDW_AT_high_pc    0x0040069c                                      <-- The end of the code for                                                                     this CU

其他DIE遵从相似的计划,你能够直观地看出不同特点的意义。

现在咱们能够测验运用咱们新发现的DWARF常识来处理一些实践问题。

此时处于哪个函数中?

比方说咱们有一个RIP,并想弄清楚咱们处在那个函数中。一个简略的算法是:

for each compile unit:    if the pc is between DW_AT_low_pc and DW_AT_high_pc:        for each funcTIon in the compile unit:            if the pc is between DW_AT_low_pc and DW_AT_high_pc:                return function information

这能够用于大多数方针,可是在成员函数和内联存在的情况下,作业会变得愈加困难。例如,存在内联的情况下,一旦咱们发现某个函数规模包括了RIP,需求对该DIE的子条目进行递归,以检查是否有任何更匹配的内联函数。我不会在这个调试器的代码中处理内联,可是假如你喜爱,你能够增加对它的支撑。

怎么在函数上下断点?

相同的,这取决于是否要支撑成员函数,命名空间等。关于独自的函数,你能够在不同的编译单元中的函数中迭代查找,直到找到具有正确称号的函数。假如你的编译器满足友爱的填写了.debug_pubnames部分,则能够更有效地做到这一点。

一旦找到该函数,就能够在给定的内存地址DW_AT_low_pc上设置断点。可是,这将会在在函数头部开端时中止,最好在用户代码开端时中止。因为行表信息能够指定指定函数头部完毕的内存地址,因而能够直接内行表中查找DW_AT_low_pc的值,然后持续读取,直到找到符号为函数头部完毕的条目。有些编译器不会输出这个信息,所以别的一个挑选是在该函数的第二行条目给出的地址上设置一个断点。

假定咱们要在示例程序中的main设置一个断点。咱们查找main函数,并得到这个DIE:

< 1><0x0000002e>    DW_TAG_subprogram                      DW_AT_low_pc                0x00400670                      DW_AT_high_pc               0x0040069c                      DW_AT_frame_base            DW_OP_reg6                      DW_AT_name                  main                      DW_AT_decl_file             0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp                      DW_AT_decl_line             0x00000001                      DW_AT_type                  <0x00000077>                      DW_AT_external              yes(1)

这告知咱们,函数从0x00400670开端。假如咱们内行号表中检查,咱们得到这个条目:

0x00400670  [   1, 0] NS uri: “/super/secret/path/MiniDbg/examples/variable.cpp”

咱们想越过函数头部,所以咱们读取下一个条目:

0x00400676  [   2,10] NS PE

Clang在这个条目中包括了头部完毕标志,所以咱们知道在这儿停下来,并在地址0x00400676上设置一个断点。

怎么读取变量内容?

读取变量或许十分杂乱。它们是能够在整个函数中改动的难以捉摸的东西,存储在寄存器中,放在内存中,被优化,被隐藏在角落里,等等等等杂乱无章。还好,咱们简略的比如的确很简略。假如咱们想要读取变量a的内容,则需求检查一下它的DW_AT_location 特点。

DW_AT_location              DW_OP_fbreg -8

reg6 在x86架构上是RBP,由System V x86_64 ABI指定。现在咱们读取RBP的内容,从中减去8,就找到了咱们的变量。假如咱们想实践上的了解这个变量,还需求检查它的类型:

< 2><0x0000004c>      DW_TAG_variable                        DW_AT_name                  a                        DW_AT_type                  <0x0000007e>

假如在调试信息中查找这种类型,咱们得到这个DIE:

< 1><0x0000007e>    DW_TAG_base_type                      DW_AT_name                  long int                      DW_AT_encoding              DW_ATE_signed                      DW_AT_byte_size             0x00000008

这告知咱们,该类型是一个8字节(64位)有符号整数类型,因而咱们能够直接将这些字节解释为int64_t并将其显现给用户。

当然,这些类型或许会比这更杂乱,因为它们有必要能够表达相似于C ++类型的东西,可是这给出了它们怎么作业的基本思想。

暂时回到RBP,Clang能够很好地依据RBP来追寻帧基址。最近版别的GCC更倾向于DW_OP_call_frame_cfa,它触及解析.eh_frame ELF部分,这是一个彻底不同的文章,我并不计划写。假如你告知GCC运用DWARF 2而不是更新的版别,它会倾向于输出方位列表,这更简略阅览:

DW_AT_frame_base             low-off : 0x00000000 addr  0x00400696 high-off  0x00000001 addr 0x00400697>DW_OP_breg7+8 low-off : 0x00000001 addr  0x00400697 high-off  0x00000004 addr 0x0040069a>DW_OP_breg7+16 low-off : 0x00000004 addr  0x0040069a high-off  0x00000031 addr 0x004006c7>DW_OP_breg6+16 low-off : 0x00000031 addr  0x004006c7 high-off  0x00000032 addr 0x004006c8>DW_OP_breg7+8

方位列表依据RIP给出不同的方位。这个比如展现了假如RIP坐落距DW_AT_low_pc的0x0偏移的方位,那么帧基址间隔寄存器7中存储的值的偏移量为8,假如它坐落0x1和0x4之间,那么它间隔寄存器7中存储的值偏移为16,等等。

歇息歇息

这么多信息会让你的脑筋晕晕乎乎,但好消息是,在接下来的几篇文章中,咱们将有一个库来为咱们完结这些困难的作业。了解实践操作中的内容,特别是在呈现问题时,或许你期望支撑一些DWARF内容(在运用的任何DWARF库中未完结)时依然有用。

假如你想了解有关DWARF的更多信息,那么能够从这儿获取相关规范。在编撰本文时,DWARF 5刚刚被发布,可是DWARF 4更受欢迎。

Linux渠道下调试器的编写(五):源码和信号

在之前的几部分中咱们学习了关于DWARF信息以及这些信息是怎么在被履行的机器码和高档言语之间树立起联络的。在这部分中,咱们将完结一些能够被调试器运用的DWARF相关原语。咱们还将借此机会让调试器在射中止点之时输出当时源代码的上下文信息。

树立DWAR解析器

正如在再还系列的开端时所说到的,咱们将会运用libelfin来处理DWARF信息。期望你在我的榜首篇文章时就现已得到了该东西,假如没有的话,你可运用我从库房fork出的fbreg分支。

一旦弄好了libelfin,便是时分把它加入到咱们的调试器中了。榜首步,解析ELF可履行文件而且从中获取DWARF信息。运用libelfin来完结这一步是十分简略的,只是需求对调试器做如下的改动:

class debugger {public:    debugger (std::string prog_name, pid_t pid)         : m_prog_name{std::move(prog_name)}, m_pid{pid} {        auto fd = open(m_prog_name.c_str(), O_RDONLY);        m_elf = elf::elf{elf::create_mmap_loader(fd)};        m_dwarf = dwarf::dwarf{dwarf::elf::create_loader(m_elf)};    }    //…private:    //…    dwarf::dwarf m_dwarf;    elf::elf m_elf;};

## 调试信息原语接下来咱们能够完结依据RIP的值来检索行条目和函数DIE。先从“`get_function_from_pc“`开端吧:“`c++dwarf::die debugger::get_function_from_pc(uint64_t pc) {    for (auto &cu : m_dwarf.compilation_units()) {        if (die_pc_range(cu.root()).contains(pc)) {            for (const auto& die : cu.root()) {                if (die.tag == dwarf::DW_TAG::subprogram) {                    if (die_pc_range(die).contains(pc)) {                        return die;                    }                }            }        }    }    throw std::out_of_range{“Cannot find function”};}

这儿我采取了一个比较蠢笨的办法,只需遍历编译单元,直到知道到包括RIP的代码,然后一向迭代,直到在子节点中找到相关函数(DW_TAG_subprogram)。正如在上篇说到的,你能够想成员函数相同来处理这些,假如你想的话你还能够运用内联。 接下来是get_line_entry_from_pc:

dwarf::line_table::iterator debugger::get_line_entry_from_pc(uint64_t pc) {    for (auto &cu : m_dwarf.compilation_units()) {        if (die_pc_range(cu.root()).contains(pc)) {            auto < = cu.get_line_table();            auto it = lt.find_address(pc);            if (it == lt.end()) {                throw std::out_of_range{"Cannot find line entry"};            }            else {                return it;            }        }    }    throw std::out_of_range{"Cannot find line entry"};}

相同的,咱们只需找到正确的廉价单元,然后恳求行列表来获取相关条目。

输出源码

当射中止点的时分或许在源码上单步的时分,咱们需求知道源代码被履行到哪里了。

void debugger::print_source(const std::string& file_name, unsigned line, unsigned n_lines_context) {    std::ifstream file {file_name};    //Work out a window around the desired line    auto start_line = line <= n_lines_context ? 1 : line - n_lines_context;    auto end_line = line + n_lines_context + (line < n_lines_context ? n_lines_context - line : 0) + 1;    char c{};    auto current_line = 1u;    //Skip lines up until start_line    while (current_line != start_line && file.get(c)) {        if (c == '\n') {            ++current_line;        }    }    //Output cursor if we're at the current line    std::cout << (current_line==line ? "> ” : ”  “);    //Write lines up until end_line    while (current_line <= end_line && file.get(c)) {        std::cout << c;        if (c == '\n') {            ++current_line;            //Output cursor if we're at the current line            std::cout << (current_line==line ? "> ” : ”  “);        }    }    //Write newline and make sure that the stream is flushed properly    std::cout << std::endl;}

现在,能够输出源码了,只需求将其挂载到咱们的调试器中。当调试器从断点或许(实践上)但不中获取信号的时分是显现源码的上好机遇了。这样做的话,调试器就需求一个更好的信号处理了。

更好的信号处理

咱们期望能够输出什么样的信号被发送给了进程,一同亦期望知道该信号是怎么被发生的。例如,咱们想知道收到的SIGTRAP信号是因为射中止点仍是一个单步履行完发生的,亦或许是因为新线程树立而发生的,等等。 走运的是,ptrace再一次援助了咱们。ptrace有一个参数PTRACE_GETSIGINFO,该参数将会给出进程之前宣布的信号的相关信息。如下:

siginfo_t debugger::get_signal_info() {    siginfo_t info;    ptrace(PTRACE_GETSIGINFO, m_pid, nullptr, &info);    return info;}

这儿呈现了一个siginfo_t的方针,它供给了如下的信息:

siginfo_t {    int      si_signo;     /* Signal number */    int      si_errno;     /* An errno value */    int      si_code;      /* Signal code */    int      si_trapno;    /* Trap number that caused                              hardware-generated signal                              (unused on most architectures) */    pid_t    si_pid;       /* Sending process ID */    uid_t    si_uid;       /* Real user ID of sending process */    int      si_status;    /* Exit value or signal */    clock_t  si_utime;     /* User time consumed */    clock_t  si_stime;     /* System time consumed */    sigval_t si_value;     /* Signal value */    int      si_int;       /* POSIX.1b signal */    void    *si_ptr;       /* POSIX.1b signal */    int      si_overrun;   /* Timer overrun count;                              POSIX.1b timers */    int      si_timerid;   /* Timer ID; POSIX.1b timers */    void    *si_addr;      /* Memory location which caused fault */    long     si_band;      /* Band event (was int in                              glibc 2.3.2 and earlier) */    int      si_fd;        /* File descriptor */    short    si_addr_lsb;  /* Least significant bit of address                              (since Linux 2.6.32) */    void    *si_lower;     /* Lower bound when address violation                              occurred (since Linux 3.19) */    void    *si_upper;     /* Upper bound when address violation                              occurred (since Linux 3.19) */    int      si_pkey;      /* Protection key on PTE that caused                              fault (since Linux 4.6) */    void    *si_call_addr; /* Address of system call instruction                              (since Linux 3.5) */    int      si_syscall;   /* Number of attempted system call                              (since Linux 3.5) */    unsigned int si_arch;  /* Architecture of attempted system call                              (since Linux 3.5) */}

我将运用si——signo来找出是哪一个信号被发送,然后运用si_code来获取有关该信号的更多信息。放置该段代码的最佳当地是在咱们的wait_for_signal函数中:

void debugger::wait_for_signal() {    int wait_status;    auto options = 0;    waitpid(m_pid, &wait_status, options);    auto siginfo = get_signal_info();    switch (siginfo.si_signo) {    case SIGTRAP:        handle_sigtrap(siginfo);        break;    case SIGSEGV:        std::cout << "Yay, segfault. Reason: " << siginfo.si_code << std::endl;        break;    default:        std::cout << "Got signal " << strsignal(siginfo.si_signo) << std::endl;    }}

现在处理SIGTRAP只需知道SI_KERNEL或许TRAP_BPKPT将会在断点射中时被发送,TRAP_TRACE将会在单步完结的时分被发送:

void debugger::handle_sigtrap(siginfo_t info) {    switch (info.si_code) {    //one of these will be set if a breakpoint was hit    case SI_KERNEL:    case TRAP_BRKPT:    {        set_pc(get_pc()-1); //put the pc back where it should be        std::cout << "Hit breakpoint at address 0x" << std::hex << get_pc() << std::endl;        auto line_entry = get_line_entry_from_pc(get_pc());        print_source(line_entry->file->path, line_entry->line);        return;    }    //this will be set if the signal was sent by single stepping    case TRAP_TRACE:        return;    default:        std::cout << "Unknown SIGTRAP code " << info.si_code << std::endl;        return;    }}

你能够处理一堆不同风格的信号。概况请参阅man sigaction。 因为咱们现在在得到SIGTRAP时批改RIP,所以能够去掉step_over_breakpoint中的部分代码:

void debugger::step_over_breakpoint() {    if (m_breakpoints.count(get_pc())) {        auto& bp = m_breakpoints[get_pc()];        if (bp.is_enabled()) {            bp.disable();            ptrace(PTRACE_SINGLESTEP, m_pid, nullptr, nullptr);            wait_for_signal();            bp.enable();        }    }}

测验

现在,你应该能够在某些地址设置断点,运转程序,检查鼠标符号的正在被履行的代码的源代码了。

下一次咱们将增加源码级的断点。能够在此处获取源码

 

声明:本文内容来自网络转载或用户投稿,文章版权归原作者和原出处所有。文中观点,不代表本站立场。若有侵权请联系本站删除(kf@86ic.com)https://www.86ic.net/bandaoti/zhizao/90075.html

为您推荐

联系我们

联系我们

在线咨询: QQ交谈

邮箱: kf@86ic.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

返回顶部