欢迎光临
我们一直在努力

GDB调试器集成 | 桌面开发实战06

目录

    • 前言
    • 1. GDB/MI 协议速览
      • 1.1 什么是 GDB/MI
      • 1.2 三种输出类型
      • 1.3 Token 机制
      • 1.4 核心变量与类型定义
    • 2. 启动调试:从点击到 GDB 运行
      • 2.1 菜单与快捷键
      • 2.2 启动调试的完整流程
      • 2.3 断点收集:从编辑器行号到 GDB 位置
      • 2.4 编辑器右键:动态添加断点
    • 3. GDB 输出解析:状态机设计
      • 3.1 为什么需要状态机
      • 3.2 核心解析函数
      • 3.3 暂停事件处理(核心)
    • 4. 调试控制:F9/F10/F11/Shift+F5
      • 4.1 命令发送封装
      • 4.2 各按钮对应命令
      • 4.3 继续执行(F9)
      • 4.4 单步执行(F10/F11)
      • 4.5 停止调试(Shift+F5)
    • 5. 悬停查值:鼠标停在变量上看它的值
      • 5.1 核心思路
      • 5.2 Timer 防抖设计
      • 5.3 鼠标悬停事件
      • 5.4 触发查询
      • 5.5 值获取与显示
      • 5.6 中断处理
    • 6. 变量监视窗口:WatchWidget
      • 6.1 功能需求
      • 6.2 UI 布局
      • 6.3 添加变量
      • 6.4 变量查询命令
      • 6.5 GDB 响应解析
      • 6.6 赋值与删除
    • 7. 内存透视窗口:MemoryWidget
      • 7.1 功能需求
      • 7.2 UI 布局
      • 7.3 内存查询命令
      • 7.4 响应解析
      • 7.5 自动刷新
      • 7.6 格式化输出示例
    • 8. 黄色执行箭头
      • 8.1 视觉设计
      • 8.2 实现方式
      • 8.3 左边栏箭头绘制
    • 9. 本篇小结
    • 系列回顾
    • 脚注

本系列适合 Qt 入门同学阅读,将以一个仿 VS Code 的 C/C++ 极简编辑器项目(MyNotepad_pro)为例,手把手带你从零实现图形化调试器。 配套代码仓库:MyNotepad_pro/mainwindow.cpp,核心逻辑集中在 GDB 调试器区域(约 600 行)。


前言

在前五篇实战中,我们搭建了界面框架、多标签页、语法高亮、一键编译和集成终端。但如果程序跑起来行为不对——变量值不符合预期、循环提前退出、内存越界——光靠 qDebug() 打印日志就像大海捞针。

GDB(GNU Debugger) 是 Linux/Cygwin 下最强大的 C/C++ 调试器,支持断点、单步执行、变量查看、内存检查等全套能力。但 GDB 的原生命令行界面对新手极不友好:

(gdb) break main
(gdb) run
(gdb) print x
(gdb) next

本篇将展示如何用 GDB/MI 协议(Machine Interface)把 GDB 包装成一个可视化的调试面板,让用户像使用 VS Code 一样——点断点、按 F10 单步、看变量值——完成调试全流程。

#mermaid-svg-2Tst2Hl4fsPrRlk0{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2Tst2Hl4fsPrRlk0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .error-icon{fill:#552222;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .marker.cross{stroke:#333333;}#mermaid-svg-2Tst2Hl4fsPrRlk0 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2Tst2Hl4fsPrRlk0 p{margin:0;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .cluster-label text{fill:#333;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .cluster-label span{color:#333;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .cluster-label span p{background-color:transparent;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .label text,#mermaid-svg-2Tst2Hl4fsPrRlk0 span{fill:#333;color:#333;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .node rect,#mermaid-svg-2Tst2Hl4fsPrRlk0 .node circle,#mermaid-svg-2Tst2Hl4fsPrRlk0 .node ellipse,#mermaid-svg-2Tst2Hl4fsPrRlk0 .node polygon,#mermaid-svg-2Tst2Hl4fsPrRlk0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .rough-node .label text,#mermaid-svg-2Tst2Hl4fsPrRlk0 .node .label text,#mermaid-svg-2Tst2Hl4fsPrRlk0 .image-shape .label,#mermaid-svg-2Tst2Hl4fsPrRlk0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .rough-node .label,#mermaid-svg-2Tst2Hl4fsPrRlk0 .node .label,#mermaid-svg-2Tst2Hl4fsPrRlk0 .image-shape .label,#mermaid-svg-2Tst2Hl4fsPrRlk0 .icon-shape .label{text-align:center;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .node.clickable{cursor:pointer;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .arrowheadPath{fill:#333333;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2Tst2Hl4fsPrRlk0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2Tst2Hl4fsPrRlk0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2Tst2Hl4fsPrRlk0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .cluster text{fill:#333;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .cluster span{color:#333;}#mermaid-svg-2Tst2Hl4fsPrRlk0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2Tst2Hl4fsPrRlk0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .icon-shape,#mermaid-svg-2Tst2Hl4fsPrRlk0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .icon-shape p,#mermaid-svg-2Tst2Hl4fsPrRlk0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .icon-shape .label rect,#mermaid-svg-2Tst2Hl4fsPrRlk0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2Tst2Hl4fsPrRlk0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2Tst2Hl4fsPrRlk0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2Tst2Hl4fsPrRlk0 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

信号槽

用户操作点击断点/按F10

MainWindow发送GDB/MI命令

QProcess写入stdin

gdb –interpreter=mi3

解析stdoutJSON/字段提取

UI更新变量表/内存/箭头

💡 为什么选 GDB/MI? 因为原生 GDB 输出是给人看的自然语言,格式随版本变化;而 MI 输出是机器可解析的结构化文本,字段位置稳定,非常适合 GUI 程序解析。


1. GDB/MI 协议速览

1.1 什么是 GDB/MI

GDB/MI 是 GDB 提供的一种 文本协议,通过 stdin 发送命令、stdout 接收响应。启动方式:

gdb –interpreter=mi3 ./your_program

其中 mi3 表示 MI 协议第 3 版1,是最新的稳定版本。

1.2 三种输出类型

GDB 的 MI 输出有三种类型,我们用 state 变量区分:

输出类型前缀含义示例
Result ^ 命令执行结果 ^done,value="42"
Exec * 程序状态变化 *stopped,reason="breakpoint-hit"
Notify = 异步通知 =thread-group-started

⚠️ 关键区别: ^ 开头的结果是"这条命令执行完了",* 开头的执行输出是"程序跑到了某处"。两者必须分开处理。

1.3 Token 机制

MI 协议用 token(数字编号)匹配请求和响应:

123-exec-run ← 发送(token=123)
123^running ← 收到(token=123,匹配)

代码中的 tokenCounter 从 0 递增,每次发送命令前递增。当收到 *stopped 事件时,如果 token 匹配上一次 exec-continue,才认为是"我们等的那次暂停"。

1.4 核心变量与类型定义

// mainwindow.h – 调试相关成员
QProcess* gdbProcess = nullptr; // GDB 进程
QString gdbState = "idle"; // "idle" / "running" / "stopped"
int tokenCounter = 0; // 请求编号
int watchTokenCounter = –1; // 上一次 var-update 的 token
QMap<int, QPair<QString,int>> breakpointMap; // id → {文件, 行号}

struct VariableInfo { // 变量信息
QString name; // 显示名("x"、"arr[0]")
QString expr; // GDB 表达式("x"、"*(&arr[0])")
QString value; // 当前值
QString type; // 类型名
};
QList<VariableInfo> watchedVariables; // 监视列表

GDB

QProcess

MainWindow

用户

GDB

QProcess

MainWindow

用户

#mermaid-svg-JOJ0g0fHAbhIr1qe{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JOJ0g0fHAbhIr1qe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JOJ0g0fHAbhIr1qe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JOJ0g0fHAbhIr1qe .error-icon{fill:#552222;}#mermaid-svg-JOJ0g0fHAbhIr1qe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JOJ0g0fHAbhIr1qe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JOJ0g0fHAbhIr1qe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JOJ0g0fHAbhIr1qe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JOJ0g0fHAbhIr1qe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JOJ0g0fHAbhIr1qe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JOJ0g0fHAbhIr1qe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JOJ0g0fHAbhIr1qe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JOJ0g0fHAbhIr1qe .marker.cross{stroke:#333333;}#mermaid-svg-JOJ0g0fHAbhIr1qe svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JOJ0g0fHAbhIr1qe p{margin:0;}#mermaid-svg-JOJ0g0fHAbhIr1qe .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JOJ0g0fHAbhIr1qe text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-JOJ0g0fHAbhIr1qe .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JOJ0g0fHAbhIr1qe .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-JOJ0g0fHAbhIr1qe .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-JOJ0g0fHAbhIr1qe .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-JOJ0g0fHAbhIr1qe #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-JOJ0g0fHAbhIr1qe .sequenceNumber{fill:white;}#mermaid-svg-JOJ0g0fHAbhIr1qe #sequencenumber{fill:#333;}#mermaid-svg-JOJ0g0fHAbhIr1qe #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-JOJ0g0fHAbhIr1qe .messageText{fill:#333;stroke:none;}#mermaid-svg-JOJ0g0fHAbhIr1qe .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JOJ0g0fHAbhIr1qe .labelText,#mermaid-svg-JOJ0g0fHAbhIr1qe .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-JOJ0g0fHAbhIr1qe .loopText,#mermaid-svg-JOJ0g0fHAbhIr1qe .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-JOJ0g0fHAbhIr1qe .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JOJ0g0fHAbhIr1qe .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-JOJ0g0fHAbhIr1qe .noteText,#mermaid-svg-JOJ0g0fHAbhIr1qe .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-JOJ0g0fHAbhIr1qe .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JOJ0g0fHAbhIr1qe .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JOJ0g0fHAbhIr1qe .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JOJ0g0fHAbhIr1qe .actorPopupMenu{position:absolute;}#mermaid-svg-JOJ0g0fHAbhIr1qe .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-JOJ0g0fHAbhIr1qe .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JOJ0g0fHAbhIr1qe .actor-man circle,#mermaid-svg-JOJ0g0fHAbhIr1qe line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-JOJ0g0fHAbhIr1qe :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

点击"开始调试"

write("42-exec-run\\n")

stdin

stdout: "42^running"

readyRead → parseGDBOutput()

state = "running"

stdout: "*stopped,reason='breakpoint-hit'"

readyRead → parseGDBOutput()

state = "stopped", 显示黄色箭头

更新UI


2. 启动调试:从点击到 GDB 运行

2.1 菜单与快捷键

调试相关 Action 在 createActions() 中注册:

startDebugAction = new QAction("开始调试", this);
startDebugAction->setShortcut(QKeySequence("F8")); // ← 注意不是F5
startDebugAction->setIcon(QIcon::fromTheme("debug-run"));
connect(startDebugAction, &QAction::triggered, this, &MainWindow::startDebugging);

stopDebugAction = new QAction("停止调试", this);
stopDebugAction->setShortcut(QKeySequence("Shift+F5"));

continueDebugAction = new QAction("继续", this);
continueDebugAction->setShortcut(QKeySequence("F9"));

stepOverAction = new QAction("单步跳过", this);
stepOverAction->setShortcut(QKeySequence("F10"));

stepIntoAction = new QAction("单步进入", this);
stepIntoAction->setShortcut(QKeySequence("F11"));

stepOutAction = new QAction("单步跳出", this);
stepOutAction->setShortcut(QKeySequence("Shift+F11"));

💡 F8 还是 F5? VS Code 默认 F5 启动调试,但我们选择 F8,原因是 F5 在本项目中已分配给"编译并运行"(Article 05)。调试和运行是两个不同操作——调试会启动 GDB,运行直接执行编译后的程序。

2.2 启动调试的完整流程

void MainWindow::startDebugging()
{
QString filePath = currentTabFilePath();
if (filePath.isEmpty()) return;

// 1. 保存当前文件
saveCurrentFile();

// 2. 编译(gcc -g 生成调试符号)
QProcess compile;
compile.setProgram("gcc");
compile.setArguments({"-g", "-o", "debug_output", filePath});
compile.start();
compile.waitForFinished();

if (compile.exitCode() != 0) {
QMessageBox::critical(this, "编译错误", QString::fromLocal8Bit(compile.readAllStandardError()));
return;
}

// 3. 启动 GDB
gdbProcess = new QProcess(this);
connect(gdbProcess, &QProcess::readyReadStandardOutput, this, &MainWindow::parseGDBOutput);
connect(gdbProcess, &QProcess::readyReadStandardError, this, [this]() {
ui->terminalOutput->append(QString::fromLocal8Bit(gdbProcess->readAllStandardError()));
});
connect(gdbProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, [this]() {
gdbState = "idle";
ui->terminalOutput->append("GDB 已退出");
});

gdbProcess->start("gdb", {"–interpreter=mi3", "./debug_output"});
if (!gdbProcess->waitForStarted()) {
QMessageBox::critical(this, "错误", "无法启动 GDB,请确认已安装");
return;
}

gdbState = "idle";

// 4. 启用调试面板
ui->debugWidget->show();
ui->debugOutput->show();
startDebugAction->setEnabled(false);
stopDebugAction->setEnabled(true);
continueDebugAction->setEnabled(true);

// 5. 收集编辑器中的断点,发送给 GDB
sendBreakpointsToGDB();
gdbProcess->write("42-exec-run\\n");

ui->terminalOutput->append("[GDB] 调试会话已启动");
}

2.3 断点收集:从编辑器行号到 GDB 位置

编辑器中的断点存储在 editor->breakpoints(一个 QSet<int> 存行号),需要转换成 GDB 能识别的位置字符串(如 main.cpp:15):

void MainWindow::sendBreakpointsToGDB()
{
if (!currentEditor()) return;
QString filePath = currentTabFilePath();

// 遍历编辑器中的断点
QList<int> bpList = currentEditor()->breakpoints.values();
for (int line : bpList) {
// 行号从1开始,GDB也从1开始,所以直接用
QString loc = QString("%1:%2").arg(filePath).arg(line);
sendGDBCommand("-break-insert " + loc);
}
}

每个 -break-insert 命令会返回一个断点 ID(如 ^done,bkptno="1"),我们可以用 breakpointMap 存储这个映射关系,方便后续删除断点。

💡 为什么要在启动前收集断点? 因为 GDB 启动后才会加载程序符号。如果在 -exec-run 之后才设断点,GDB 可能找不到源文件位置(特别是编译优化后行号偏移)。所以要在启动时一次性把所有断点告诉 GDB。

2.4 编辑器右键:动态添加断点

在 CodeEditor 的右键菜单中,用户可以点击行号旁边的区域来切换断点:

// CodeEditor.cpp – 鼠标点击断点区域
void CodeEditor::mousePressEvent(QMouseEvent *event)
{
if (event->position().x() < gutterWidth) {
// 点击行号区域 → 切换断点
QTextCursor cursor = cursorForPosition(event->pos());
int line = cursor.blockNumber() + 1;

if (breakpoints.contains(line)) {
breakpoints.remove(line); // 移除断点
} else {
breakpoints.insert(line); // 添加断点
}
viewport()->update(); // 重绘断点标记
return;
}
QPlainTextEdit::mousePressEvent(event);
}

断点标记用红色圆点绘制(在 lineNumberAreaPaintEvent 中):

if (breakpoints.contains(block.blockNumber() + 1)) {
painter.setPen(Qt::red);
painter.setBrush(Qt::red);
painter.drawEllipse(QPoint(centerX, block.center().y()), 5, 5);
}

💡 断点视觉设计: 红色实心圆是 VS Code 的断点标记风格。如果断点位置有编译错误(无有效代码),可以改为半透明红色圆点,提示用户该断点不会被命中。


3. GDB 输出解析:状态机设计

3.1 为什么需要状态机

GDB 的 MI 输出是 异步 的——我们发一条命令,可能收到多行响应,中间还夹杂着程序状态变化的通知。比如:

123^done ← 命令结果
*stopped,reason="breakpoint-hit" ← 程序暂停了
&output:"Reading symbols…" ← 调试信息

所以必须用状态机区分每行属于哪种类型。

3.2 核心解析函数

void MainWindow::parseGDBOutput()
{
QByteArray data = gdbProcess->readAllStandardOutput();
QStringList lines = QString::fromLocal8Bit(data).split("\\n");

for (const QString &line : lines) {
if (line.isEmpty()) continue;

QString trimmed = line.trimmed();

// ========== Result 输出(^) ==========
if (trimmed.startsWith("^")) {
if (trimmed == "^running") {
gdbState = "running";
}
else if (trimmed.startsWith("^done")) {
// 处理变量查询结果
if (trimmed.contains("value=")) {
int valIdx = trimmed.indexOf("value=");
QString val = trimmed.mid(valIdx + 7);
val.chop(1); // 去掉末尾引号
ui->terminalOutput->append(QString("[GDB] 变量值: %1").arg(val));
}
// 处理断点创建结果
else if (trimmed.contains("bkptno=")) {
int bpIdx = trimmed.indexOf("bkptno=");
QString bpNum = trimmed.mid(bpIdx + 8);
bpNum.chop(1);
ui->terminalOutput->append(QString("[GDB] 断点已设置: #%1").arg(bpNum));
}
gdbState = "stopped";
highlightExecutionLine(); // 高亮当前执行行
}
else if (trimmed.startsWith("^error")) {
int msgIdx = trimmed.indexOf("msg=");
if (msgIdx != –1) {
QString msg = trimmed.mid(msgIdx + 5);
msg.chop(1);
ui->terminalOutput->append(QString("[GDB 错误] %1").arg(msg));
}
}
continue;
}

// ========== Exec 输出(*) ==========
if (trimmed.startsWith("*")) {
QString event = trimmed.mid(1);

if (event.startsWith("stopped")) {
gdbState = "stopped";
handleStoppedEvent(event); // ← 核心:处理暂停事件
}
else if (event.startsWith("running")) {
gdbState = "running";
ui->terminalOutput->append("[GDB] 程序运行中…");
}
continue;
}

// ========== Notify 输出(=) ==========
if (trimmed.startsWith("=")) {
// 调试器退出通知
if (trimmed.contains("thread-group-exited")) {
ui->terminalOutput->append("[GDB] 调试会话结束");
gdbState = "idle";
}
continue;
}

// ========== Console 输出(&) ==========
if (trimmed.startsWith("&")) {
QString msg = trimmed.mid(1);
// 去掉引号包裹
if (msg.startsWith("\\"") && msg.endsWith("\\"")) {
msg = msg.mid(1, msg.length() – 2);
}
ui->terminalOutput->append(msg);
continue;
}
}
}

3.3 暂停事件处理(核心)

当程序因为断点、单步执行或异常暂停时,GDB 会发送 *stopped 事件。我们需要从中提取:

  • 文件名和行号:高亮执行位置
  • 断点编号:更新断点状态
  • 信号信息:判断是否异常终止

void MainWindow::handleStoppedEvent(const QString &event)
{
// 提取 reason
QString reason;
int reasonIdx = event.indexOf("reason=");
if (reasonIdx != –1) {
reason = event.mid(reasonIdx + 8, event.indexOf(",", reasonIdx) – reasonIdx – 8);
}

// 提取文件名和行号
QString file, line;
int fileIdx = event.indexOf("file=");
if (fileIdx != –1) {
file = event.mid(fileIdx + 6, event.indexOf(",", fileIdx) – fileIdx – 6);
}
int lineIdx = event.indexOf("line=");
if (lineIdx != –1) {
line = event.mid(lineIdx + 5, event.indexOf(",", lineIdx) – lineIdx – 5);
}

// 显示暂停信息
if (reason == "breakpoint-hit") {
int bkptIdx = event.indexOf("bkptno=");
QString bkptNum = event.mid(bkptIdx + 8, event.indexOf(",", bkptIdx) – bkptIdx – 8);
ui->terminalOutput->append(QString("[GDB] 命中断点 #%1 – %2:%3").arg(bkptNum, file, line));
} else if (reason == "end-stepping-range") {
ui->terminalOutput->append(QString("[GDB] 单步完成 – %1:%2").arg(file, line));
} else if (reason == "signal-received") {
QString signal;
int sigIdx = event.indexOf("signal-name=");
if (sigIdx != –1) {
signal = event.mid(sigIdx + 13, event.indexOf(",", sigIdx) – sigIdx – 13);
}
ui->terminalOutput->append(QString("[GDB] 收到信号 %1 – %2:%3").arg(signal, file, line));
} else {
ui->terminalOutput->append(QString("[GDB] 暂停 – %1:%2").arg(file, line));
}

// 跳转到对应文件和行
if (!file.isEmpty() && !line.isEmpty()) {
// 如果文件不在当前标签页,打开它
// 跳转到对应行
highlightExecutionLine(line.toInt());
}

// 更新所有标签页的调试状态
for (int i = 0; i < ui->tabWidget->count(); ++i) {
CodeEditor* editor = qobject_cast<CodeEditor*>(
qobject_cast<QScrollArea*>(ui->tabWidget->widget(i))->widget());
if (editor) {
editor->isDebugging = true;
editor->debugLine = line.toInt();
editor->viewport()->update();
}
}
}

💡 暂停事件 vs 命令结果: 当我们发送 -exec-continue 时,GDB 先回复 ^running(命令被接受),然后程序继续运行。当程序再次暂停时,GDB 发送 *stopped(事件)。这两个是独立的消息,必须分别处理。


4. 调试控制:F9/F10/F11/Shift+F5

4.1 命令发送封装

所有调试命令都通过同一个函数发送,自动附加 token:

void MainWindow::sendGDBCommand(const QString &cmd)
{
if (!gdbProcess || gdbProcess->state() != QProcess::Running) return;
tokenCounter++;
QString fullCmd = QString("%1%2\\n").arg(tokenCounter).arg(cmd);
gdbProcess->write(fullCmd.toLocal8Bit());
}

4.2 各按钮对应命令

用户操作快捷键GDB/MI 命令作用
继续 F9 -exec-continue 从当前位置继续运行到下一个断点
单步跳过 F10 -exec-next 执行当前行,遇到函数调用不进入
单步进入 F11 -exec-step 执行当前行,遇到函数调用进入函数
单步跳出 Shift+F11 -exec-finish 执行完当前函数并返回
停止调试 Shift+F5 -exec-interrupt + kill 中断程序并结束调试会话

4.3 继续执行(F9)

void MainWindow::continueExecution()
{
if (gdbState != "stopped") return;

// 刷新所有标签页的断点到 GDB
for (int i = 0; i < ui->tabWidget->count(); ++i) {
CodeEditor* editor = qobject_cast<CodeEditor*>(
qobject_cast<QScrollArea*>(ui->tabWidget->widget(i))->widget());
if (editor) {
QString filePath = tabFilePaths.value(ui->tabWidget->widget(i));
// 先删除旧断点,再重新添加
sendGDBCommand("-break-delete 1-100");
QList<int> bpList = editor->breakpoints.values();
for (int line : bpList) {
sendGDBCommand(QString("-break-insert %1:%2").arg(filePath).arg(line));
}
}
}

sendGDBCommand("-exec-continue");
gdbState = "running";

// 清除所有标签页的调试高亮
for (int i = 0; i < ui->tabWidget->count(); ++i) {
CodeEditor* editor = qobject_cast<CodeEditor*>(
qobject_cast<QScrollArea*>(ui->tabWidget->widget(i))->widget());
if (editor) {
editor->isDebugging = false;
editor->debugLine = –1;
editor->extraSelections.clear();
editor->setExtraSelections({});
editor->viewport()->update();
}
}
}

💡 为什么要删除旧断点? 因为用户可能在调试过程中添加/删除断点。最简单的做法是每次继续前把所有断点重新同步一遍。虽然低效,但逻辑清晰,适合入门项目。

4.4 单步执行(F10/F11)

void MainWindow::stepOver()
{
if (gdbState == "stopped") {
sendGDBCommand("-exec-next");
gdbState = "running";
}
}

void MainWindow::stepInto()
{
if (gdbState == "stopped") {
sendGDBCommand("-exec-step");
gdbState = "running";
}
}

void MainWindow::stepOut()
{
if (gdbState == "stopped") {
sendGDBCommand("-exec-finish");
gdbState = "running";
}
}

4.5 停止调试(Shift+F5)

void MainWindow::stopDebugging()
{
if (gdbProcess && gdbProcess->state() == QProcess::Running) {
// 1. 中断程序(如果还在运行)
sendGDBCommand("-exec-interrupt");
gdbProcess->kill(); // 发送 SIGKILL
gdbProcess->waitForFinished();
}

// 2. 清理 UI
ui->debugWidget->hide();
ui->debugOutput->hide();
ui->terminalOutput->append("[GDB] 调试已停止");

// 3. 重置所有标签页调试状态
for (int i = 0; i < ui->tabWidget->count(); ++i) {
CodeEditor* editor = qobject_cast<CodeEditor*>(
qobject_cast<QScrollArea*>(ui->tabWidget->widget(i))->widget());
if (editor) {
editor->isDebugging = false;
editor->debugLine = –1;
editor->extraSelections.clear();
editor->setExtraSelections({});
editor->viewport()->update();
}
}

// 4. 重置按钮状态
startDebugAction->setEnabled(true);
stopDebugAction->setEnabled(false);
continueDebugAction->setEnabled(false);

// 5. 删除临时编译产物
QFile::remove("debug_output");

delete gdbProcess;
gdbProcess = nullptr;
gdbState = "idle";
}

#mermaid-svg-6R1jHZMCJSevxXiW{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6R1jHZMCJSevxXiW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6R1jHZMCJSevxXiW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6R1jHZMCJSevxXiW .error-icon{fill:#552222;}#mermaid-svg-6R1jHZMCJSevxXiW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6R1jHZMCJSevxXiW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6R1jHZMCJSevxXiW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6R1jHZMCJSevxXiW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6R1jHZMCJSevxXiW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6R1jHZMCJSevxXiW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6R1jHZMCJSevxXiW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6R1jHZMCJSevxXiW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6R1jHZMCJSevxXiW .marker.cross{stroke:#333333;}#mermaid-svg-6R1jHZMCJSevxXiW svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6R1jHZMCJSevxXiW p{margin:0;}#mermaid-svg-6R1jHZMCJSevxXiW .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-6R1jHZMCJSevxXiW .cluster-label text{fill:#333;}#mermaid-svg-6R1jHZMCJSevxXiW .cluster-label span{color:#333;}#mermaid-svg-6R1jHZMCJSevxXiW .cluster-label span p{background-color:transparent;}#mermaid-svg-6R1jHZMCJSevxXiW .label text,#mermaid-svg-6R1jHZMCJSevxXiW span{fill:#333;color:#333;}#mermaid-svg-6R1jHZMCJSevxXiW .node rect,#mermaid-svg-6R1jHZMCJSevxXiW .node circle,#mermaid-svg-6R1jHZMCJSevxXiW .node ellipse,#mermaid-svg-6R1jHZMCJSevxXiW .node polygon,#mermaid-svg-6R1jHZMCJSevxXiW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6R1jHZMCJSevxXiW .rough-node .label text,#mermaid-svg-6R1jHZMCJSevxXiW .node .label text,#mermaid-svg-6R1jHZMCJSevxXiW .image-shape .label,#mermaid-svg-6R1jHZMCJSevxXiW .icon-shape .label{text-anchor:middle;}#mermaid-svg-6R1jHZMCJSevxXiW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6R1jHZMCJSevxXiW .rough-node .label,#mermaid-svg-6R1jHZMCJSevxXiW .node .label,#mermaid-svg-6R1jHZMCJSevxXiW .image-shape .label,#mermaid-svg-6R1jHZMCJSevxXiW .icon-shape .label{text-align:center;}#mermaid-svg-6R1jHZMCJSevxXiW .node.clickable{cursor:pointer;}#mermaid-svg-6R1jHZMCJSevxXiW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6R1jHZMCJSevxXiW .arrowheadPath{fill:#333333;}#mermaid-svg-6R1jHZMCJSevxXiW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6R1jHZMCJSevxXiW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6R1jHZMCJSevxXiW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6R1jHZMCJSevxXiW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6R1jHZMCJSevxXiW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6R1jHZMCJSevxXiW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6R1jHZMCJSevxXiW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6R1jHZMCJSevxXiW .cluster text{fill:#333;}#mermaid-svg-6R1jHZMCJSevxXiW .cluster span{color:#333;}#mermaid-svg-6R1jHZMCJSevxXiW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6R1jHZMCJSevxXiW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6R1jHZMCJSevxXiW rect.text{fill:none;stroke-width:0;}#mermaid-svg-6R1jHZMCJSevxXiW .icon-shape,#mermaid-svg-6R1jHZMCJSevxXiW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6R1jHZMCJSevxXiW .icon-shape p,#mermaid-svg-6R1jHZMCJSevxXiW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6R1jHZMCJSevxXiW .icon-shape .label rect,#mermaid-svg-6R1jHZMCJSevxXiW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6R1jHZMCJSevxXiW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6R1jHZMCJSevxXiW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6R1jHZMCJSevxXiW :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

No

Yes

用户按 F10

gdbState == stopped?

忽略

sendGDBCommand('-exec-next')

gdbState = running

GDB 回复 ^running

GDB 回复 *stopped

parseGDBOutput() 处理

提取 file/line

高亮执行行 + 更新标签页

gdbState = stopped


5. 悬停查值:鼠标停在变量上看它的值

5.1 核心思路

当鼠标悬停在编辑器中的变量名上 400ms 后,发送 GDB 查询命令,获取该变量的当前值并显示为工具提示(tooltip)。

5.2 Timer 防抖设计

// CodeEditor 构造函数
evaluationTimer = new QTimer(this);
evaluationTimer->setSingleShot(true); // 单次触发
evaluationTimer->setInterval(400); // 400ms 延迟
connect(evaluationTimer, &QTimer::timeout, this, &CodeEditor::triggerEvaluation);

5.3 鼠标悬停事件

void CodeEditor::mouseMoveEvent(QMouseEvent *event)
{
QTextCursor cursor = cursorForPosition(event->pos());
int charPos = cursor.positionInBlock();

// 提取光标所在的单词(变量名)
QTextBlock block = cursor.block();
QString lineText = block.text();
int start = charPos, end = charPos;

while (start > 0 && (lineText[start–1].isLetterOrNumber() || lineText[start–1] == '_'))
start—;
while (end < lineText.length() && (lineText[end].isLetterOrNumber() || lineText[end] == '_'))
end++;

QString word = lineText.mid(start, end – start);

// 检查是否在调试状态,且单词是有效变量名
if (isDebugging && !word.isEmpty() && word[0].isLetter()) {
QString prevChar = (start > 0) ? QString(lineText[start–1]) : "";
if (prevChar.isEmpty() || (!prevChar.isLetterOrNumber() && prevChar != '_' && prevChar != '.')) {
currentEvalExpression = word;
evaluationTimer->start(); // 重新计时(防抖)
}
}

QPlainTextEdit::mouseMoveEvent(event);
}

5.4 触发查询

void CodeEditor::triggerEvaluation()
{
if (currentEvalExpression.isEmpty()) return;

// 通过信号通知 MainWindow 发送 GDB 命令
emit evaluateVariableRequested(currentEvalExpression);
}

// mainwindow.cpp – 连接信号
connect(editor, &CodeEditor::evaluateVariableRequested, this, &MainWindow::requestEvaluation);

void MainWindow::requestEvaluation(const QString &expr)
{
if (gdbState != "stopped") return;

watchTokenCounter = tokenCounter + 1; // 记录这次请求的 token
sendGDBCommand(QString("-data-evaluate-expression %1").arg(expr));
gdbState = "running";
}

5.5 值获取与显示

在 parseGDBOutput 中处理响应:

if (trimmed.startsWith("^done")) {
if (trimmed.contains("value=")) {
int valIdx = trimmed.indexOf("value=");

// 提取引号内的值
int startQuote = trimmed.indexOf('"', valIdx);
int endQuote = trimmed.lastIndexOf('"');
if (startQuote != –1 && endQuote != –1 && endQuote > startQuote) {
QString val = trimmed.mid(startQuote + 1, endQuote – startQuote – 1);

// 判断是否是我们等待的变量查询
if (watchTokenCounter == tokenCounter) {
// 在当前编辑器中设置工具提示
QTextCursor cursor = textCursor();
int line = cursor.blockNumber() + 1;

// 显示在工具提示栏(或状态栏)
QString tooltip = QString("📝 %1 = %2").arg(currentEvalExpression).arg(val);
// … 显示逻辑
}
}
}
}

💡 400ms 延迟的意义: 如果用户快速移动鼠标,每经过一个变量都触发查询,GDB 会被淹没。400ms 的防抖确保只在用户"停"在某个变量上时才查询。这个值是经验值——太短会频繁查询,太长会感觉卡顿。

5.6 中断处理

如果用户在 GDB 查询过程中按了 Ctrl+C(或点击其他变量),需要取消上一次查询:

void MainWindow::interruptExecution()
{
if (gdbProcess && gdbProcess->state() == QProcess::Running) {
gdbProcess->write("\\x03"); // 发送 SIGINT(Ctrl+C)
ui->terminalOutput->append("[GDB] 已中断程序执行");
gdbState = "stopped";
}
}


6. 变量监视窗口:WatchWidget

6.1 功能需求

用户可以在 WatchWidget 中输入变量名(如 x、arr[0]、*ptr),实时查看其值。程序暂停时自动更新所有监视变量。

6.2 UI 布局

// WatchWidget 构造函数
QVBoxLayout* layout = new QVBoxLayout(this);

QHBoxLayout* addLayout = new QHBoxLayout();
varNameEdit = new QLineEdit(this);
varNameEdit->setPlaceholderText("输入变量名…");
QPushButton* addButton = new QPushButton("添加", this);
QPushButton* removeButton = new QPushButton("删除", this);
QPushButton* clearButton = new QPushButton("清空", this);

addLayout->addWidget(varNameEdit);
addLayout->addWidget(addButton);
addLayout->addWidget(removeButton);
addLayout->addWidget(clearButton);

varTable = new QTableWidget(this);
varTable->setColumnCount(3); // 名称、值、类型
varTable->setHorizontalHeaderLabels({"名称", "值", "类型"});
varTable->horizontalHeader()->setStretchLastSection(true);

layout->addLayout(addLayout);
layout->addWidget(varTable);

6.3 添加变量

void WatchWidget::addVariable()
{
QString varName = varNameEdit->text().trimmed();
if (varName.isEmpty()) return;

// 检查是否已存在
for (const VariableInfo &v : qAsConst(watchedVariables)) {
if (v.name == varName) return;
}

VariableInfo var;
var.name = varName;
var.expr = varName; // 初始表达式就是变量名
var.value = "—";
var.type = "—";
watchedVariables.append(var);

// 更新表格
int row = varTable->rowCount();
varTable->insertRow(row);
varTable->setItem(row, 0, new QTableWidgetItem(var.name));
varTable->setItem(row, 1, new QTableWidgetItem(var.value));
varTable->setItem(row, 2, new QTableWidgetItem(var.type));

varNameEdit->clear();

// 如果正在调试,立即查询值
if (isDebugging) {
refreshAllVariables();
}
}

6.4 变量查询命令

每个变量用 -var-create 创建一个 GDB 对象,后续用 -var-update 获取最新值:

void WatchWidget::refreshAllVariables()
{
if (!isDebugging) return;

for (int i = 0; i < watchedVariables.size(); ++i) {
VariableInfo &var = watchedVariables[i];

// 首次创建变量对象
if (var.name.isEmpty() || var.value == "—") {
// 发送: -var-create myvar * x
QString cmd = QString("-var-create %1 * %2").arg(var.name).arg(var.expr);
emit sendGDBCommandRequested(cmd);
} else {
// 后续更新值
QString cmd = QString("-var-update %1").arg(var.name);
emit sendGDBCommandRequested(cmd);
}
}
}

6.5 GDB 响应解析

// 在 parseGDBOutput 中处理变量响应
if (trimmed.contains("changelist=")) {
// -var-update 响应,包含变量值变化列表
// 格式: ^done,changelist=[{name="x",value="42",type="int"}]

int changeIdx = trimmed.indexOf("changelist=[");
if (changeIdx != –1) {
QString changes = trimmed.mid(changeIdx + 12);
changes.chop(2); // 去掉 "]\\n"

// 按逗号分隔每个变化
QStringList changeItems = changes.split("},{");
for (const QString &item : changeItems) {
QString name, value, type;

int nameIdx = item.indexOf("name=");
if (nameIdx != –1) {
name = item.mid(nameIdx + 6, item.indexOf(",", nameIdx) – nameIdx – 6);
name = name.trimmed().remove('"');
}

int valIdx = item.indexOf("value=");
if (valIdx != –1) {
value = item.mid(valIdx + 7, item.indexOf(",", valIdx) – valIdx – 7);
value = value.trimmed().remove('"');
}

int typeIdx = item.indexOf("type=");
if (typeIdx != –1) {
type = item.mid(typeIdx + 6);
type = type.trimmed().remove('"');
}

// 更新 UI
for (int row = 0; row < varTable->rowCount(); ++row) {
if (varTable->item(row, 0) && varTable->item(row, 0)->text() == name) {
varTable->item(row, 1)->setText(value);
varTable->item(row, 2)->setText(type);
}
}
}
}
}

6.6 赋值与删除

// 赋值:在 GDB 中修改变量值
void WatchWidget::assignVariable(const QString &varName, const QString &newValue)
{
if (!isDebugging) return;

// 发送: -var-assign x 100
QString cmd = QString("-var-assign %1 %2").arg(varName).arg(newValue);
emit sendGDBCommandRequested(cmd);

// 刷新显示
refreshAllVariables();
}

// 删除:从监视列表中移除
void WatchWidget::removeVariable()
{
int row = varTable->currentRow();
if (row < 0) return;

QString varName = varTable->item(row, 0)->text();

// 从 GDB 中删除变量对象
if (isDebugging) {
QString cmd = QString("-var-delete %1").arg(varName);
emit sendGDBCommandRequested(cmd);
}

// 从列表和表格中移除
watchedVariables.removeAt(row);
varTable->removeRow(row);
}

💡 -var-create2 vs -data-evaluate-expression: 前者创建一个"活的"变量对象,后续可以用 -var-update 批量刷新;后者是一次性查询,适合悬停查值。监视窗口用 -var-create,悬停用 -data-evaluate-expression。


7. 内存透视窗口:MemoryWidget

7.1 功能需求

内存窗口允许用户输入一个内存地址(如 0x7fff5fbff8c0),显示该地址起始的一段内存内容,同时显示十六进制和 ASCII 两种视图。

7.2 UI 布局

// MemoryWidget 构造函数
QVBoxLayout* layout = new QVBoxLayout(this);

QHBoxLayout* addrLayout = new QHBoxLayout();
addrEdit = new QLineEdit(this);
addrEdit->setPlaceholderText("输入内存地址 (如 0x7fff5fbff8c0)");
QPushButton* refreshBtn = new QPushButton("刷新", this);
QPushButton* autoRefreshBtn = new QPushButton("自动刷新", this);
autoRefreshBtn->setCheckable(true);

addrLayout->addWidget(new QLabel("地址:"));
addrLayout->addWidget(addrEdit);
addrLayout->addWidget(refreshBtn);
addrLayout->addWidget(autoRefreshBtn);

hexView = new QPlainTextEdit(this);
hexView->setReadOnly(true);
hexView->setFont(QFont("Consolas", 10)); // 等宽字体

asciiView = new QPlainTextEdit(this);
asciiView->setReadOnly(true);
asciiView->setFont(QFont("Consolas", 10));

layout->addLayout(addrLayout);
layout->addWidget(new QLabel("十六进制:"));
layout->addWidget(hexView);
layout->addWidget(new QLabel("ASCII:"));
layout->addWidget(asciiView);

7.3 内存查询命令

GDB/MI 提供 -data-read-memory-bytes3 命令,可以读取指定地址的内存:

void MemoryWidget::readMemory()
{
QString addr = addrEdit->text().trimmed();
if (addr.isEmpty() || !isDebugging) return;

// 发送命令:读取从 addr 开始的 128 字节
// -data-read-memory-bytes &addr 128
QString cmd = QString("-data-read-memory-bytes &%1 128").arg(addr);
emit sendGDBCommandRequested(cmd);
}

💡 & 前缀的意义: 在 GDB 中,&addr 表示"取 addr 的地址"。如果用户输入 0x7fff5fbff8c0,GDB 会读取该地址的内存;如果输入 x,GDB 会先求值 &x,获取变量 x 的地址,然后读取内存。

7.4 响应解析

// parseGDBOutput 中处理内存数据
if (trimmed.contains("memory=")) {
// 格式: ^done,memory=[{addr="0x…",bytes="48656c6c6f"}]

int memIdx = trimmed.indexOf("memory=[");
if (memIdx != –1) {
QString memData = trimmed.mid(memIdx + 9);
memData.chop(2); // 去掉 "]\\n"

int bytesIdx = memData.indexOf("bytes=\\"");
if (bytesIdx != –1) {
QString hexStr = memData.mid(bytesIdx + 7);
hexStr.chop(1); // 去掉引号

// 解析十六进制字符串
QByteArray bytes = QByteArray::fromHex(hexStr.toLatin1());

// 格式化为十六进制视图
QString hexDisplay;
QString asciiDisplay;
const int bytesPerLine = 16;

for (int i = 0; i < bytes.length(); i += bytesPerLine) {
// 地址列
hexDisplay += QString("0x%1: ").arg(startAddr + i, 8, 16, QChar('0'));

// 十六进制列
for (int j = 0; j < bytesPerLine && (i+j) < bytes.length(); ++j) {
hexDisplay += QString("%1 ").arg((quint8)bytes[i+j], 2, 16, QChar('0'));
if (j == 7) hexDisplay += " "; // 中间分隔
}

// ASCII 列
for (int j = 0; j < bytesPerLine && (i+j) < bytes.length(); ++j) {
char c = bytes[i+j];
asciiDisplay += (c >= 32 && c <= 126) ? c : '.';
}

hexDisplay += "\\n";
asciiDisplay += "\\n";
}

hexView->setPlainText(hexDisplay);
asciiView->setPlainText(asciiDisplay);
}
}
}

7.5 自动刷新

当程序暂停时(断点命中、单步完成),自动刷新内存内容:

void WatchWidget::autoRefreshMemory()
{
if (autoRefreshEnabled && isDebugging) {
readMemory();
}
}

// 在 handleStoppedEvent 中调用
void MainWindow::handleStoppedEvent(const QString &event)
{
// … 其他处理 …

// 自动刷新内存窗口
if (memoryWidget->autoRefreshEnabled) {
memoryWidget->readMemory();
}
}

7.6 格式化输出示例

假设用户输入地址 0x7fff5fbff8c0,程序正在运行:

0x7fff5fbff8c0: 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 00 00 00 00
0x7fff5fbff8d0: 01 00 00 00 2a 00 00 00 ff 7f 00 00 00 00 00 00

对应的 ASCII 视图:

Hello World!….
….*………..

💡 内存地址获取方式: 用户可以:

  • 直接在代码中用 qDebug() << (void*)&variable 打印地址
  • 在 GDB 终端中用 print &variable 获取
  • 在内存窗口中输入变量名(如 x),GDB 会自动取地址

  • 8. 黄色执行箭头

    8.1 视觉设计

    调试时,当前执行行用黄色背景高亮,左边显示一个黄色小箭头。这是 VS Code 的经典调试标记。

    8.2 实现方式

    在 CodeEditor 中用 QTextEdit::ExtraSelection 实现:

    // mainwindow.cpp – 高亮执行行
    void MainWindow::highlightExecutionLine(int line)
    {
    for (int i = 0; i < ui->tabWidget->count(); ++i) {
    CodeEditor* editor = qobject_cast<CodeEditor*>(
    qobject_cast<QScrollArea*>(ui->tabWidget->widget(i))->widget());
    if (editor) {
    editor->debugLine = line;
    editor->isDebugging = true;

    // 设置 ExtraSelection
    QList<QTextEdit::ExtraSelection> extraSelections;
    QTextEdit::ExtraSelection selection;

    selection.format.setBackground(QColor(255, 255, 0, 100)); // 半透明黄色
    selection.format.setProperty(QTextFormat::FullWidthSelection, true);
    selection.cursor = editor->textCursor();
    selection.cursor.setPosition(editor->document()->findBlockByLineNumber(line – 1).position());
    selection.cursor.select(QTextCursor::BlockUnderCursor);

    extraSelections.append(selection);
    editor->setExtraSelections(extraSelections);

    // 确保执行行可见
    editor->centerCursor();

    // 重绘左边栏(显示黄色箭头)
    editor->viewport()->update();
    }
    }
    }

    8.3 左边栏箭头绘制

    在 lineNumberAreaPaintEvent 中,如果当前行是调试执行行,绘制黄色箭头:

    void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event)
    {
    QPainter painter(lineNumberArea);
    // … 省略行号绘制 …

    QTextBlock block = firstVisibleBlock();
    while (block.isValid() && block.top() <= event->rect().bottom()) {
    int blockNumber = block.blockNumber();
    int top = qRound(blockBoundingGeometry(block).translated(contentOffset()).top());
    int bottom = top + qRound(blockBoundingRect(block).height());

    // 绘制调试箭头
    if (isDebugging && blockNumber + 1 == debugLine) {
    painter.setPen(Qt::NoPen);
    painter.setBrush(QColor(255, 200, 0)); // 黄色

    // 绘制向右的小三角形
    QPolygon triangle;
    triangle << QPoint(centerX – 6, top + 2)
    << QPoint(centerX + 6, top + 8)
    << QPoint(centerX – 6, top + 14);
    painter.drawPolygon(triangle);
    }

    block = block.next();
    }
    }

    💡 黄色箭头 vs 蓝色行号: 在 VS Code 中,当前执行行用黄色背景+黄色箭头;当前选中的行用蓝色行号。两者可以同时存在,但我们的项目简化为只显示黄色调试标记。


    9. 本篇小结

    #mermaid-svg-35WxQJAfFcygiO4S{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-35WxQJAfFcygiO4S .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-35WxQJAfFcygiO4S .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-35WxQJAfFcygiO4S .error-icon{fill:#552222;}#mermaid-svg-35WxQJAfFcygiO4S .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-35WxQJAfFcygiO4S .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-35WxQJAfFcygiO4S .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-35WxQJAfFcygiO4S .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-35WxQJAfFcygiO4S .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-35WxQJAfFcygiO4S .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-35WxQJAfFcygiO4S .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-35WxQJAfFcygiO4S .marker{fill:#333333;stroke:#333333;}#mermaid-svg-35WxQJAfFcygiO4S .marker.cross{stroke:#333333;}#mermaid-svg-35WxQJAfFcygiO4S svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-35WxQJAfFcygiO4S p{margin:0;}#mermaid-svg-35WxQJAfFcygiO4S .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-35WxQJAfFcygiO4S .cluster-label text{fill:#333;}#mermaid-svg-35WxQJAfFcygiO4S .cluster-label span{color:#333;}#mermaid-svg-35WxQJAfFcygiO4S .cluster-label span p{background-color:transparent;}#mermaid-svg-35WxQJAfFcygiO4S .label text,#mermaid-svg-35WxQJAfFcygiO4S span{fill:#333;color:#333;}#mermaid-svg-35WxQJAfFcygiO4S .node rect,#mermaid-svg-35WxQJAfFcygiO4S .node circle,#mermaid-svg-35WxQJAfFcygiO4S .node ellipse,#mermaid-svg-35WxQJAfFcygiO4S .node polygon,#mermaid-svg-35WxQJAfFcygiO4S .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-35WxQJAfFcygiO4S .rough-node .label text,#mermaid-svg-35WxQJAfFcygiO4S .node .label text,#mermaid-svg-35WxQJAfFcygiO4S .image-shape .label,#mermaid-svg-35WxQJAfFcygiO4S .icon-shape .label{text-anchor:middle;}#mermaid-svg-35WxQJAfFcygiO4S .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-35WxQJAfFcygiO4S .rough-node .label,#mermaid-svg-35WxQJAfFcygiO4S .node .label,#mermaid-svg-35WxQJAfFcygiO4S .image-shape .label,#mermaid-svg-35WxQJAfFcygiO4S .icon-shape .label{text-align:center;}#mermaid-svg-35WxQJAfFcygiO4S .node.clickable{cursor:pointer;}#mermaid-svg-35WxQJAfFcygiO4S .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-35WxQJAfFcygiO4S .arrowheadPath{fill:#333333;}#mermaid-svg-35WxQJAfFcygiO4S .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-35WxQJAfFcygiO4S .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-35WxQJAfFcygiO4S .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-35WxQJAfFcygiO4S .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-35WxQJAfFcygiO4S .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-35WxQJAfFcygiO4S .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-35WxQJAfFcygiO4S .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-35WxQJAfFcygiO4S .cluster text{fill:#333;}#mermaid-svg-35WxQJAfFcygiO4S .cluster span{color:#333;}#mermaid-svg-35WxQJAfFcygiO4S div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-35WxQJAfFcygiO4S .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-35WxQJAfFcygiO4S rect.text{fill:none;stroke-width:0;}#mermaid-svg-35WxQJAfFcygiO4S .icon-shape,#mermaid-svg-35WxQJAfFcygiO4S .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-35WxQJAfFcygiO4S .icon-shape p,#mermaid-svg-35WxQJAfFcygiO4S .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-35WxQJAfFcygiO4S .icon-shape .label rect,#mermaid-svg-35WxQJAfFcygiO4S .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-35WxQJAfFcygiO4S .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-35WxQJAfFcygiO4S .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-35WxQJAfFcygiO4S :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    GDB/MI 协议

    启动调试-exec-run

    断点管理-break-insert/delete

    调试控制-exec-continue/next/step

    变量查询-data-evaluate-expression

    内存读取-data-read-memory-bytes

    状态机解析

    UI 更新

    黄色执行箭头

    变量监视表

    内存十六进制视图

    控制台输出

    功能GDB/MI 命令快捷键实现难度
    启动调试 -exec-run F8 ⭐⭐
    断点 -break-insert/-break-delete 点击行号 ⭐
    继续 -exec-continue F9 ⭐
    单步跳过 -exec-next F10 ⭐
    单步进入 -exec-step F11 ⭐
    单步跳出 -exec-finish Shift+F11 ⭐
    停止 -exec-interrupt + kill Shift+F5 ⭐⭐
    悬停查值 -data-evaluate-expression 鼠标悬停 ⭐⭐⭐
    变量监视 -var-create/-var-update 手动添加 ⭐⭐⭐
    内存读取 -data-read-memory-bytes 手动输入 ⭐⭐⭐⭐

    GDB/MI 的核心难点:

  • 异步响应:命令结果和程序事件混合在一起,必须用状态机区分
  • Token 匹配:多个请求并发时,用 token 区分哪个响应对应哪个请求
  • 字段解析:MI 输出是逗号分隔的 key=value 格式,嵌套引号和转义字符需要小心处理
  • 状态同步:UI 状态(按钮启用/禁用)必须与 GDB 状态保持一致
  • 💡 本篇的代码量约 600 行,是整个项目中最复杂的部分。建议读者逐段理解,特别是 parseGDBOutput 函数中的状态机逻辑。如果觉得太难,可以先跳过 GDB/MI 协议细节,只看 UI 部分的实现。


    系列回顾

    至此,“桌面开发实战” 系列六篇文章全部完成。让我们回顾整个项目的演进:

    篇章主题核心内容
    01 环境搭建 Qt Creator + CMake 配置
    02 界面框架 无边框窗口、自定义标题栏、菜单系统
    03 多标签页 QTabWidget、文件操作、最近文件
    04 视觉增强 行号、语法高亮、括号匹配、智能补全
    05 终端编译 QProcess 集成、F5 编译运行
    06 GDB 调试 GDB/MI 协议、断点、单步、变量监视、内存查看

    从一个空白窗口到功能完整的 C/C++ 编辑器,我们用约 4500 行代码实现了 VS Code 的核心子集:

    • ✅ 无边框窗口 + 自定义标题栏
    • ✅ 多标签页 + 文件操作
    • ✅ 语法高亮 + 括号匹配
    • ✅ 智能补全(成对符号、Tab 片段)
    • ✅ 集成终端 + 一键编译
    • ✅ GDB 图形化调试(断点、单步、变量、内存)
    • ✅ 黄色执行箭头 + 当前行高亮

    还有哪些可以扩展?

    • 项目管理器(文件树、CMakeLists.txt 解析)
    • 代码跳转(Go to Definition/Declaration)
    • 代码格式化(clang-format 集成)
    • Git 集成(diff 视图、提交界面)
    • 多窗口支持
    • 插件系统

    💡 这个项目虽然是"极简编辑器",但涵盖了 Qt Widgets 开发的核心知识点:信号槽、事件系统、QProcess、QPainter、QSettings、多线程(QtConcurrent)、文档模型(QPlainTextEdit)。掌握了这些,你就具备了开发任何桌面应用的基础能力。


    下一篇预告: 本系列暂告一段落。如果需要扩展更多功能(如项目管理器、代码跳转、Git 集成),可以继续关注后续系列更新。感谢阅读!🎉


    脚注


  • GDB/MI 协议文档见 GNU GDB Manual – MI。mi3 是 MI 协议的第 3 版,GDB 7.0+ 支持。 ↩︎

  • -var-create 创建的变量对象会一直存在,直到用 -var-delete 删除或 GDB 退出。如果创建了太多变量对象,会影响 GDB 性能。建议在变量不再需要时及时删除。 ↩︎

  • -data-read-memory-bytes 的第三个参数是字节数,最大建议 1024 字节。读取过多内存会拖慢 GDB 响应速度。如果需要查看大块内存,可以分多次读取。 ↩︎

  • 赞(0)
    未经允许不得转载:171主机测评 » GDB调试器集成 | 桌面开发实战06
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址