首页
社区
课程
招聘
[翻译]自己动手编写一个Linux调试器系列之5 源码和信号 by lantie@15PB
发表于: 2017-10-20 23:30 5771

[翻译]自己动手编写一个Linux调试器系列之5 源码和信号 by lantie@15PB

2017-10-20 23:30
5771

自己动手编写一个Linux调试器系列之5 源码和信号 by lantie@15PB

目录

 

在上一部分中,我们了解了DWARF格式的信息以及如何使用它来读取变量,并将高级语言的源代码与正在执行的机器码关联起来。在这一部分中,我们将通过在调试器中使用DWARF原型。我们还将利用这个机会让我们的调试器在遇到断点时打印当前源上下文。

安装 DWARF 解析器

还记得在本系列的开始我提到了的libelfin吗,我们将在本节使用libelfin来处理我们的DWARF信息。 你可以去看下第一篇,但如果不想去或是记不得了,请现在这样做,下载libelfin库并确保你使用我修改过的fbreg分支(github上的代码)

 

一旦你编译好了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;
};

使用open来代替std::ifstream,因为ELF加载器需要一个Unix文件描述符来传递给mmap,以便它可以将文件映射到内存中,而不是一次读取它。

调试信息原型

接下来,我们可以实现一些函数,从PC值获取行条目和函数DIE。 我们从get_function_from_pc开始:

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"};
}

在这里,我采取一种简单的方式,只需迭代编译单元,直到找到包含程序计数器的代码,然后遍历子函数,直到找到相关函数(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 &lt = 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"};
}

再次,我们只是找到正确的编译单元,然后从debugline表来获取相关条目。

打印源码

当我们点击一个断点或者绕过我们的代码时,我们会想知道我们最终的来源,我们可以打印源码。

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_KERNELTRAP_BRKPTTRAP_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时更正程序计数器,因此我们可以从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();
        }
    }
}

测试一下

现在你应该能够在某个地址设置一个断点,运行该程序并查看使用当前执行的标有光标的行打印的源代码。

 

下次我们将添加设置源码级断点的功能。 在此期间,您可以在此处获取此帖的代码

说明

原文: https://blog.tartanllama.xyz/writing-a-linux-debugger-source-signal/
译文作者: lantie@15PB 15PB信息安全教育 http://www.15pb.com.cn

自己动手实践-Libelfin的下载与编译

  1. 使用git下载作者修改的分支
    git clone https://github.com/TartanLlama/libelfin.git
    
  2. 使用make命令编译生成动态库
    libelfin# make
    

自己动手实践-Libelfin的使用

添加下载的libelfin目录到Clion项目中配置CMakeLists.txt

add_custom_target(
        libelfin
        COMMAND make
        WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/libelfin
)

target_link_libraries(MyDebugger
        ${PROJECT_SOURCE_DIR}/libelfin/dwarf/libdwarf++.so
        ${PROJECT_SOURCE_DIR}/libelfin/elf/libelf++.so)

add_dependencies(MyDebugger libelfin)

自己动手实践-使用自己的调试器

  1. 使用gcc编译生成自己的hello15pb程序
    gcc -g hello15pb.c -o hello15pb
    
  2. 使用objdump -d <文件> | grep main命令查看例子程序 hello15pbmain函数的地址

图片描述

  1. 使用调试器调试程序hello15pb,打印源码

    图片描述

下载

使用Clion编译的完整工程


[课程]Android-CTF解题方法汇总!

上传的附件:
收藏
免费 1
支持
分享
最新回复 (5)
雪    币: 35
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
2017-10-24 09:15
0
雪    币: 5
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
薛老师,期待你的原创
2017-11-4 09:41
0
雪    币: 371
活跃值: (94)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
4
大佬还更新吗???????????
2017-11-17 14:26
0
雪    币: 371
活跃值: (94)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
5
这节的薛老师给的例子我这里编译出错:
CMakeFiles/MyDebugger.dir/main.cpp.o:  In  function  `dwarf::elf::elf_loader<elf::elf>::load(dwarf::section_type,  unsigned  long*)':
/home/darmao/Desktop/My5/libelfin/dwarf/dwarf++.hh:1509:  undefined  reference  to  `elf::elf::get_section(std::__cxx11::basic_string<char,  std::char_traits<char>,  std::allocator<char>  >  const&)  const'
collect2:  error:  ld  returned  1  exit  status
CMakeFiles/MyDebugger.dir/build.make:200:  recipe  for  target  'MyDebugger'  failed
求薛老师指点
2017-11-17 17:16
0
雪    币: 371
活跃值: (94)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
6
老师这个函数有点问题,设置一个断点以后,再继续运行还是会回到这个断点,在打印源码的时候改了pc指针,打印完了以后没有改回来
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);  //这里将pc指针设置成了当前指令,在打印完了以后,应该修改回来为当前指令的下一条指令
                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);
                //set_pc(get_pc()+1);这里应该加上这么一句
                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;
        }
}
2017-11-17 21:22
0
游客
登录 | 注册 方可回帖
返回
//