
本文是经过严格查阅相关权威文献和资料,形成的专业的可靠的内容。全文数据都有据可依,可回溯。特别申明:数据和资料已获得授权。本文内容,不涉及任何偏颇观点,用中立态度客观事实描述事情本身。
文章结尾有最新热度的文章,感兴趣的可以去看看。
文章有点长(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, nullptr, nullptr);
}
void single_step() {
ptrace(PTRACE_SINGLESTEP, child_pid, nullptr, nullptr);
}
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, 0, nullptr, nullptr);
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. 主函数:
• 使用 fork创建子进程。• 子进程调用 ptrace(PTRACE_TRACEME)并执行目标程序。• 父进程创建 SimpleDebugger对象并开始处理调试命令。
• run方法:等待子进程停止,并进入命令循环。• set_breakpoint_at_address方法:在指定地址插入断点,并保存原始数据以便恢复。• continue_execution方法:继续执行目标程序。如果命中断点,则恢复原始指令并调整指令指针。• single_step方法:单步执行目标程序。• get_registers方法:获取并显示目标程序的寄存器信息。• read_memory方法:读取并显示目标程序的内存内容。• handle_commands方法:处理用户输入的调试命令。
• 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++统一环境
国外Rust程序员分享:Crossbeam使多线程通信如此简单高效
国外Rust程序员分享:从头开始编写一个实时操作系统(RTOS)
C++开发中被低估的std::vector,竟是性能与安全的双重保障!
C++ 高手进阶之路:精通 std::mutex,拿捏多线程同步
码农必看:C++23 新特性揭秘:std::expected 告别繁琐错误码与异常处理
用C++构建音乐播放器MusikCube,两三兆却功能强大令人惊叹
为什么大家都在用Rust提升Python性能?真相让人意想不到!
必看!Discord从Go切换到Rust语言背后带来提升性能并降低延迟