从零开始:用C++实现简易GDB通过代码了解背后的逻辑与实现

创作不易,方便的话点点关注,谢谢
本文是经过严格查阅相关权威文献和资料,形成的专业的可靠的内容。全文数据都有据可依,可回溯。特别申明:数据和资料已获得授权。本文内容,不涉及任何偏颇观点,用中立态度客观事实描述事情本身。

文章结尾有最新热度的文章,感兴趣的可以去看看。

文章有点长(3404字阅读时长:5分),期望您能坚持看完,并有所收获


大家好,今天我继续给大家分享干货。熟悉我的人,都知道我真正的干货一般在中间和末尾部分。请耐心看完!谢谢。

 

最近出一个新系列,使用简单的例子去理解日常使用的工具,帮助大家更深入学习C/C++使用技巧。

在软件开发的世界里,调试是一个不可或缺的环节。无论是初学者还是经验丰富的开发者,调试工具的使用往往决定了问题解决的效率和质量。在众多调试工具中,GDB(GNU Debugger)以其强大的功能和灵活性脱颖而出。本文将深入探讨GDB的特点、学习方法以及优化建议,帮助开发者更好地利用这一工具。

主要功能

实现一个简易版本的GDB调试工具是一个复杂的任务,因为它涉及到进程控制、信号处理、寄存器操作等底层系统调用。为了简化学习过程,我们将创建一个基本的调试工具,支持以下功能:

  • • 启动和附加到目标程序
  • • 设置断点
  • • 单步执行(step)
  • • 继续执行(continue)
  • • 显示寄存器信息
  • • 查看内存内容

代码实现

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

class SimpleDebugger {
private:
    pid_t child_pid;

public:
    SimpleDebugger(pid_t pid) : child_pid(pid) {}

    void run() {
        int status;
        waitpid(child_pid, &status, 0);

        if (WIFSTOPPED(status)) {
            std::cout << "Child stopped initially" << std::endl;
            handle_commands();
        } else {
            std::cerr << "Unexpected error" << std::endl;
            exit(1);
        }
    }

    void set_breakpoint_at_address(void* addr) {
        unsigned long data = ptrace(PTRACE_PEEKTEXT, child_pid, addr, nullptr);
        original_data = data;
        unsigned long trap = (data & 0xFFFFFFFFFFFFFF00) | 0xCC;
        ptrace(PTRACE_POKETEXT, child_pid, addr, trap);
        breakpoint_addr = addr;
    }

    void continue_execution() {
        if (breakpoint_hit) {
            // Restore the original instruction at the breakpoint address
            ptrace(PTRACE_POKETEXT, child_pid, breakpoint_addr, original_data);
            // Move the instruction pointer back by one byte to re-execute the original instruction
            struct user_regs_struct regs;
            ptrace(PTRACE_GETREGS, child_pid, nullptr, ®s);
            regs.rip -= 1;
            ptrace(PTRACE_SETREGS, child_pid, nullptr, ®s);
            breakpoint_hit = false;
        }
        ptrace(PTRACE_CONT, child_pid, nullptrnullptr);
    }

    void single_step() {
        ptrace(PTRACE_SINGLESTEP, child_pid, nullptrnullptr);
    }

    void get_registers() {
        struct user_regs_struct regs;
        ptrace(PTRACE_GETREGS, child_pid, nullptr, ®s);

        std::cout << "RIP: " << std::hex << regs.rip << std::dec << std::endl;
        std::cout << "RSP: " << std::hex << regs.rsp << std::dec << std::endl;
        std::cout << "RBP: " << std::hex << regs.rbp << std::dec << std::endl;
        std::cout << "RAX: " << std::hex << regs.rax << std::dec << std::endl;
        std::cout << "RBX: " << std::hex << regs.rbx << std::dec << std::endl;
        std::cout << "RCX: " << std::hex << regs.rcx << std::dec << std::endl;
        std::cout << "RDX: " << std::hex << regs.rdx << std::dec << std::endl;
        std::cout << "RSI: " << std::hex << regs.rsi << std::dec << std::endl;
        std::cout << "RDI: " << std::hex << regs.rdi << std::dec << std::endl;
    }

    void read_memory(unsigned long addr, size_t len) {
        char buffer[len];
        for (size_t i = 0; i < len; i += sizeof(long)) {
            long word = ptrace(PTRACE_PEEKDATA, child_pid, addr + i, nullptr);
            memcpy(buffer + i, &word, sizeof(word));
        }
        std::cout << "Memory content at " << std::hex << addr << ": ";
        for (size_t i = 0; i < len; i++) {
            std::cout << std::hex << static_cast<unsigned int>(buffer[i]) << " ";
        }
        std::cout << std::dec << std::endl;
    }

private:
    void* breakpoint_addr = nullptr;
    unsigned long original_data = 0;
    bool breakpoint_hit = false;

    void handle_commands() {
        std::string command;
        while (true) {
            std::cout << "(simple_gdb) ";
            std::getline(std::cin, command);

            std::istringstream iss(command);
            std::string cmd;
            iss >> cmd;

            if (cmd == "continue") {
                continue_execution();
                int status;
                waitpid(child_pid, &status, 0);
                if (WIFEXITED(status)) {
                    std::cout << "Program exited normally" << std::endl;
                    break;
                } else if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) {
                    breakpoint_hit = true;
                    std::cout << "Breakpoint hit at address: " << std::hex << breakpoint_addr << std::dec << std::endl;
                }
            } else if (cmd == "step") {
                single_step();
                int status;
                waitpid(child_pid, &status, 0);
                if (WIFEXITED(status)) {
                    std::cout << "Program exited normally" << std::endl;
                    break;
                }
            } else if (cmd == "registers") {
                get_registers();
            } else if (cmd == "memory") {
                unsigned long addr;
                size_t len;
                iss >> std::hex >> addr >> len;
                read_memory(addr, len);
            } else if (cmd == "breakpoint") {
                unsigned long addr;
                iss >> std::hex >> addr;
                set_breakpoint_at_address(reinterpret_cast<void*>(addr));
                std::cout << "Breakpoint set at address: " << std::hex << addr << std::dec << std::endl;
            } else if (cmd == "quit") {
                kill(child_pid, SIGKILL);
                break;
            } else {
                std::cerr << "Unknown command: " << cmd << std::endl;
            }
        }
    }
};

int main(int argc, char** argv) {
    if (argc < 2) {
        std::cerr << "Usage: " << argv[0] << " [args...]" << std::endl;
        return 1;
    }

    pid_t child_pid = fork();
    if (child_pid == 0) {
        // Child process
        ptrace(PTRACE_TRACEME, 0nullptrnullptr);
        execvp(argv[1], &argv[1]);
        perror("execvp");
        exit(1);
    } else if (child_pid > 0) {
        // Parent process
        SimpleDebugger debugger(child_pid);
        debugger.run();
    } else {
        // Fork failed
        perror("fork");
        return 1;
    }

    return 0;
}

代码解释

  1. 1. 主函数
  • • 使用 fork 创建子进程。
  • • 子进程调用 ptrace(PTRACE_TRACEME) 并执行目标程序。
  • • 父进程创建 SimpleDebugger 对象并开始处理调试命令。
  • 2. SimpleDebugger 类
    • • run 方法:等待子进程停止,并进入命令循环。
    • • set_breakpoint_at_address 方法:在指定地址插入断点,并保存原始数据以便恢复。
    • • continue_execution 方法:继续执行目标程序。如果命中断点,则恢复原始指令并调整指令指针。
    • • single_step 方法:单步执行目标程序。
    • • get_registers 方法:获取并显示目标程序的寄存器信息。
    • • read_memory 方法:读取并显示目标程序的内存内容。
    • • handle_commands 方法:处理用户输入的调试命令。
  • 3. 调试命令
    • • continue:继续执行目标程序直到下一个断点或结束。
    • • step:单步执行目标程序。
    • • registers:显示目标程序的寄存器信息。
    • • memory
      :查看指定地址处的内存内容。
    • • breakpoint
      :在指定地址设置断点。
    • • quit:终止目标程序并退出调试器。

    编译和运行

    你可以使用以下命令来编译和运行这个程序:

    g++ -std=c++17 -o simple_gdb simple_gdb.cpp
    ./simple_gdb ./target_program arg1 arg2 ...

    其中 target_program 是你要调试的目标程序,arg1 arg2 ... 是传递给目标程序的参数。

    示例目标程序

    为了测试调试器,你可以编写一个简单的目标程序,例如:

    // target_program.cpp
    #include 

    void foo() {
        std::cout << "Inside foo()" << std::endl;
    }

    int main() {
        std::cout << "Starting program" << std::endl;
        foo();
        std::cout << "Ending program" << std::endl;
        return 0;
    }

    编译目标程序:

    g++ -std=c++17 -o target_program target_program.cpp

    然后使用你的简易调试器来调试它:

    ./simple_gdb ./target_program

    演示

    假设我们有一个简单的目标程序 target_program,我们可以在这个程序中设置断点并进行调试。

    启动调试器

    ./simple_gdb ./target_program

    设置断点

    假设我们在 main 函数的入口地址设置断点:

    (simple_gdb) breakpoint 0x401189
    Breakpoint set at address: 0x401189

    继续执行

    (simple_gdb) continue
    Breakpoint hit at address: 0x401189

    查看寄存器

    (simple_gdb) registers
    RIP: 0x401189
    RSP: 0x7fffffffe3f0
    RBP: 0x0
    RAX: 0x0
    RBX: 0x0
    RCX: 0x0
    RDX: 0x7ffff7dd68b0
    RSI: 0x7fffffffe4e8
    RDI: 0x40127c

    单步执行

    (simple_gdb) step

    查看内存

    假设我们要查看某个地址的内存内容:

    (simple_gdb) memory 0x40127c 20
    Memory content at 0x40127c: 53 48 83 ec 10 be 7c 12 ...

    退出调试器

    (simple_gdb) quit

    总结

    这个优化后的简易版GDB调试工具实现了基本的调试功能,包括启动和附加到目标程序、设置断点、单步执行、继续执行、显示寄存器信息和查看内存内容。通过这些功能,你可以初步了解如何使用 ptrace 进行进程控制和调试。希望这个示例能帮助你更好地理解调试工具的工作原理!以上就是我的分享。这些分析皆源自我的个人经验,希望上面分享的这些东西对大家有帮助,感谢大家!

    参考文献

    • • GDB官方文档
    • • GNU Project Debugger: Key Features and Tutorial
    • • Debugging a Linux Application with GDB - Intel
    • • Optimize Options (Using the GNU Compiler Collection)
    • • Hard to use GDB with large projects - Reddit Discussions
      点个“在看”不失联

     

    最新热门文章推荐:

    告别选择困难:用VSCode和Docker构建跨平台C++统一环境

    国外C++程序员分享:C++23多线程竟让图像处理速度飙升

    国外Rust程序员分享:Crossbeam使多线程通信如此简单高效

    国外Rust程序员分享:用与不用线程池的差距竟如此之大!

    国外Rust程序员分享:从头开始编写一个实时操作系统(RTOS)

    C++编程中最容易忽视的关键点,你注意到了吗?

    高手必备:如何像专业人士一样优化C++代码?

    C++开发中被低估的std::vector,竟是性能与安全的双重保障!

    Go并发模式:我后悔没有早点掌握的5种并发模式

    告别繁琐!Go1.23中的iter包如何简化你的代码逻辑

    一文读懂C++20std::span:语法、用例全攻略

    C++ 高手进阶之路:精通 std::mutex,拿捏多线程同步

    码农必看:C++23 新特性揭秘:std::expected 告别繁琐错误码与异常处理

    码农必看:C++20编译时计算如何优化你的代码?

    C++虚函数:看似神秘,实则背后都是精心设计

    用C++构建音乐播放器MusikCube,两三兆却功能强大令人惊叹

    别再盲目找库!Rust 编程最关键的10个库都在这儿了

    为什么大家都在用Rust提升Python性能?真相让人意想不到!

    必看!Discord从Go切换到Rust语言背后带来提升性能并降低延迟

    请使用浏览器的分享功能分享到微信等