自己动手编写一个Linux调试器系列之2 断点的设置 by lantie@15PB

自己动手编写一个Linux调试器系列之2 断点的设置 lantie@15PB

Paste_Image.png

在本系列的第一篇文章中,我们编写了一个小型进程启动器作为调试器的基础。在这篇文章中,我们将学习如何在x86 Linux上断点的工作原理,并继续编写我们的工具增加设置断点的功能。


系列索引

  1. 准备工作
  2. 断点的设置
  3. 寄存器和内存
  4. ELF文件和调试信息
  5. 源码和信号
  6. 源码级单步
  7. 源码级断点
  8. 堆栈解除
  9. 处理变量
  10. 高级主题

断点是如何形成的

有两种类型的断点:硬件断点和软件断点。硬件断点通常需要设置处理器中寄存器的值以产生断点,而软件断点则需要修改正在执行的代码。本文将以软件断点为主,因为它更简单,而且想要多少都可以,在X86上,同时只能设置4个硬件断点,不过硬件断点既可以在读取或写入给定地址触发,也可以在执行时触发。

我上面说过,软件断点是通过修改执行代码来实现的,所以问题来了:

  • 我们如何修改代码?
  • 我们做什么修改才能设置断点?
  • 调试器如何收到通知?

第一个问题的答案当然是ptrace。我们之前使用它来跟踪和继续执行程序,现在也可以使用它来读写内存。

我们修改代码之后必须使处理器在执行断点地址时停止并发送信号通知程序。在x86上,是通过使用 int 3 指令覆盖这个地址上的指令来实现的。x86具有中断向量表,操作系统可以使用它来注册各种事件的处理程序,例如页面错误,保护故障和无效操作码。这有点像注册错误处理回调,但是是硬件级别的。当处理器执行 int 3指令时,系统会执行断点中断处理程序,在Linux系统下,进程会产生一个SIGTRAP的信号。你可以在下图中看到此过程,其中我们用0xcc覆盖mov指令的第一个字节,这是 int 3的指令编码。

最后一个需要解决的问题是调试器如何通知用户断点已经触发。如果你还记得上一篇文章,我们可以使用waitpid来收听发送给调试程序的信号。我们在这里可以做同样的事情:设置断点、继续程序、调用waitpid并等到SIGTRAP信号发生。然后通过打印已经到达的源代码位置或者改变有界面的调试器中的选中行来将该断点已触发传送给用户。


实现软件断点

我们将实现一个breakpoint类来表示某个位置上的断点,我们可以根据需要启用或禁用断点。

class breakpoint {
public:
    breakpoint(pid_t pid, std::intptr_t addr)
        : m_pid{pid}, m_addr{addr}, m_enabled{false}, m_saved_data{}
    {}

    void enable();
    void disable();

    auto is_enabled() const -> bool { return m_enabled; }
    auto get_address() const -> std::intptr_t { return m_addr; }

private:
    pid_t m_pid;
    std::intptr_t m_addr;
    bool m_enabled;
    uint8_t m_saved_data; //data which used to be at the breakpoint address
};

这大多数只是跟踪状态,真正的难点在enabledisable函数中。

如上所述,我们需要使用编码为0xccint 3指令来替换当前在给定地址处的指令。我们还想保存以前在该地址中的内容,以便以后可以恢复代码。我们不想忘记执行用户的代码。

void breakpoint::enable() {
    auto data = ptrace(PTRACE_PEEKDATA, m_pid, m_addr, nullptr);
    m_saved_data = static_cast<uint8_t>(data & 0xff); //save bottom byte
    uint64_t int3 = 0xcc;
    uint64_t data_with_int3 = ((data & ~0xff) | int3); //set bottom byte to 0xcc
    ptrace(PTRACE_POKEDATA, m_pid, m_addr, data_with_int3);

    m_enabled = true;
}

ptrace的PTRACE_PEEKDATA参数可以实现读取跟踪进程的内存。我们给它一个进程ID和地址,它给我们返回目前在该地址的64位。 (m_saved_data&〜0xff)将该数据的底部字节置零,然后按位或与我们的int 3指令设置断点。 最后,我们通过使用PTRACE_POKEDATA参数将新数据覆盖那部分内存来设置断点。

disable函数更容易,但仍然有些麻烦。由于ptrace内存请求是对整个字而不是字节操作,因此,我们需要首先读取要恢复的字,然后用原始数据覆盖低字节并将其写回内存中。

void breakpoint::disable() {
    auto data = ptrace(PTRACE_PEEKDATA, m_pid, m_addr, nullptr);
    auto restored_data = ((data & ~0xff) | m_saved_data);
    ptrace(PTRACE_POKEDATA, m_pid, m_addr, restored_data);

    m_enabled = false;
}

向调试器中添加断点的设置

我们将对debugger类进行三个更改,以支持通过用户界面设置断点:

  1. debugger添加存储断点的数据结构
  2. 编写一个 set_breakpoint_at_address函数
  3. 在我们的handle_command函数中添加一个break命令

我们会将断点存储在 std::unordered_map<std::intptr_t, breakpoint>结构中,以便检查一个给定地址是否有断点,如果有的话获取断点对象信息是容易和快速的。

class debugger {
    //...
    void set_breakpoint_at_address(std::intptr_t addr);
    //...
private:
    //...
    std::unordered_map<std::intptr_t,breakpoint> m_breakpoints;
}

set_breakpoint_at_address函数中,我们将创建一个新的断点,启用它,将其添加到数据结构中,并为用户打印一条消息。如果你喜欢,可以考虑将所有信息打印提出封装成一个库和命令行工具,以便于调试器使用,现在为了简单起见,我们将它们先放在一起。

void debugger::set_breakpoint_at_address(std::intptr_t addr) {
    std::cout << "Set breakpoint at address 0x" << std::hex << addr << std::endl;
    breakpoint bp {m_pid, addr};
    bp.enable();
    m_breakpoints[addr] = bp;
}

现在我们将扩充我们的命令处理程序来调用我们的新函数。

void debugger::handle_command(const std::string& line) {
    auto args = split(line,' ');
    auto command = args[0];

    if (is_prefix(command, "cont")) {
        continue_execution();
    }
    else if(is_prefix(command, "break")) {
        std::string addr {args[1], 2}; //naively assume that the user has written 0xADDRESS
        set_breakpoint_at_address(std::stol(addr, 0, 16));
    }
    else {
        std::cerr << "Unknown command\n";
    }
}

以上代码中,我简单地删除了字符串的前两个字符,并在结果上调用了std :: stol,感觉这样使解析更加健壮了。 std :: stol有选择的将一个基数转换为十六进制读取会比较方便。


从断点恢复程序运行

如果你尝试这样做,你可能会注意到,如果你从断点继续运行程序,没有任何反应。这是因为断点仍然设置在内存中,所以它会不停重复触发断点。比较简单恢复程序运行的解决方案是禁用断点,单步,重新启用它,然后继续。 不幸的是,我们还需要修改程序计数器(x86下是EIP)以指出断点之前的位置,所以我们将留下这个直到下一个关于操作寄存器的文章。


测试一下

当然,如果你不知道要设置断点的地址,那么设置断点并不是很有用。在将来,我们将添加在函数名或源代码行上设置断点的功能,但是现在,我们可以手工完成它。

测试调试器的一种简单方法是编写一个hello world程序,它将写入std::err(避免缓冲),并在调用输出操作时设置断点。如果你继续进行调试,那么希望执行将停止而不打印任何东西。然后您可以重新启动调试器,并在调用后设置一个断点,并且您应该看到正在成功打印的消息。

找到地址的一种方法是使用objdump。如果您打开一个shell并执行objdump -d <您的程序>,那么您应该看到程序的反汇编代码。然后,您应该能够找到main并找到您想要设置断点的调用指令的地方。例如,我构建了一个hello world示例,将其反汇编,并将其main函数反汇编:

0000000000400936 <main>:
  400936:    55                       push   rbp
  400937:    48 89 e5                 mov    rbp,rsp
  40093a:    be 35 0a 40 00           mov    esi,0x400a35
  40093f:    bf 60 10 60 00           mov    edi,0x601060
  400944:    e8 d7 fe ff ff           call   400820 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
  400949:    b8 00 00 00 00           mov    eax,0x0
  40094e:    5d                       pop    rbp
  40094f:    c3                       ret

如您所见,我们希望在0x400944上设置一个断点,以查看有无输出,0x400949可以看到输出。`


完成

到此,您现在应该有一个调试器,它可以启动一个程序,并允许用户在内存地址上设置断点。下次我们将增加读写能力,读写内存和寄存器。如果你有任何问题,请在评论中告诉我。

你可以在这里找到这篇文章的代码

说明

原文来自:https://blog.tartanllama.xyz/writing-a-linux-debugger-breakpoints/
翻译来自:lantie@15PB 专注于信息安全教育 http://www.15pb.com.cn

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容