宁静淡雅什么意思:使用 CDT 调试器,第 2 部分: 使用 Eclipse CDT 和 MI 访问 gdb

来源:百度文库 编辑:中财网 时间:2024/04/28 08:08:27

简介: Eclipse C/C++ 开发工具(C/C++ Development Tooling,CDT)提供了非常优秀的图形调试环境,它提供了断点(breakpoint)、检查点(watchpoint)、变量、寄存器、反汇编、信号和内存内容。您仍可以为这个环境添加新的性能或者访问这些视图来显示定制调试器的输出。但首先应该了解 C/C++ 调试接口(C/C++ Debugger Interface,CDI)以及它如何与 Eclipse 通信。“使用 CDT 调试器” 系列的第 1 部分从较高的层次描述了 CDI。第 2 部分将学习 CDT 如何与 GNU Debugger(gdb)对话。具体指 CDT 如何使用 CDI 和 MachineInterface(MI)与 gdb 交互。


GNU Debugger(gdb)是目前最受欢迎的开源调试器。它最初是为 C 语言设计的,后来被移植到各种计算系统(从小型嵌入式设备到大型超级计算机)中调试多种语言的代码。gdb 通常被用作命令行可执行文件,但可以通过使用不太著名的 MI 协议的软件访问 gdb。本文解释了 MI 的工作原理以及 CDT 如何使用 MI 与 gdb 通信。CDT 调试器交互的具体示例应该对学习定制的 C/C++ 调试器很有帮助。

此处讨论的 Java? 类以 CDI 提供的类和接口为基础,这些在 “使用 CDT 调制器” 系列的 第 1 部分 中介绍过。为了避免混淆,再次解释一下 CDI 和 MI 之间的区别:

  • CDI 由 Eclipse/CDT 开发人员创建,因此 CDT 可以访问外部调试器。
  • MI 由 gdb 开发人员创建,因此外部应用程序可以访问 gdb。

这似乎是一个简单的区别,但我将展示的许多类在 CDI 和 MI 中均有涉及,有时很难界定一个接口的结束和下一个接口的开始。如果了解 CDI 和 MI 如何一起工作,您能更好地链接定制调试器工具和 CDT,不管它们是否基于 gdb。

了解 GNU Debugger Machine Interface(gdb/MI)

大多数人使用诸如 runprintinfo 这样的简单指令由命令行访问 gdb。这是 gdb 与人类一方的 接口。访问 gdb 的第二个方法旨在通过软件与调试器交互:MachineInterface(MI)。调试器执行的任务和以前相同,但命令和输出响应有很大的不同。

示例能清楚地说明这一点。比如说您想要调试一个基于以下代码的应用程序。


清单 1. 一个简单的 C 语言应用程序:simple.c
                int main() {    int x = 4;x += 6;// x = 10x *= 5;// x = 50    return (0);}

gcc -g -O0 simple.c -osimple 编译代码后,一个常规的调试会话如清单 2 所示。


清单 2. 调试会话
                                         $ gdb -q simple                         (gdb) break main                         (gdb) run1int main() {(gdb) step2int x = 4;(gdb) step3x += 6;     // x = 10(gdb) print x$1 = 4(gdb) step4x *= 5;     // x = 50(gdb) print x$2 = 10(gdb) quit            

清单 3 显示使用 MI 命令的同一个 gdb 会话(以粗体显示)。


清单 3. 使用 MI 的调试会话
                                $ gdb -q -i mi simple(gdb)-break-insert-main^done,bkpt={number="1",type="breakpoint",disp="keep",enabled="y",addr="0x00401075",func="main",file="simple.c",fullname="/home/mscarpino/simple.c",line="1",times="0"}(gdb)-exec-run^running(gdb)*stopped,reason="breakpoint-hit",bkptno="1",thread-id="1",frame={addr="0x00401075",func="main",args=[],file="simple.c",fullname="/home/mscarpino/simple.c",line="1"}(gdb)-exec-step^running(gdb)*stopped,reason="end-stepping-range",thread-id="1",frame={addr="0x0040107a",func="main",args=[],file="simple.c",fullname="/home/mscarpino/simple.c",line="2"}(gdb)-exec-step^running(gdb)*stopped,reason="end-stepping-range",thread-id="1",frame={addr="0x00401081",func="main",args=[],file="simple.c",fullname="/home/mscarpino/simple.c",line="3"}(gdb)-var-create x_name * x^done,name="x_name",numchild="0",type="int"(gdb)-var-evaluate-expression x_name^done,value="4"(gdb)-exec-step^running(gdb)*stopped,reason="end-stepping-range",thread-id="1",frame={addr="0x00401081",func="main",args=[],file="simple.c",fullname="/home/mscarpino/simple.c",line="4"}(gdb)-var-update x_name^done,changelist=[{name="x_name",in_scope="true",type_changed="false"}](gdb)-var-evaluate-expression x_name^done,value="10"(gdb)-var-delete x_name^done,ndeleted="1"(gdb)-gdb-exit            

-i mi 标志告诉 gdb 使用 MI 协议通信,您可以看到显著的区别。命令名称和输出性质都有了显著改变。输出记录的第一行是 ^running^done,接下来是结果信息。这个输出被称为结果记录 ,它包括 ^error 和错误消息。

在许多情况下,MI 结果记录之后是 (gdb) 和带外(out-of-band,OOB)记录。这些记录提供目标状态或调试环境的额外信息。-exec-step 后的 *stopped 消息是一个 OOB 记录,它提供关于断点、检查点和目标暂停或结束原因的信息。在先前的会话中,gdb 在每个 -exec-step 后返回 *stopped,reason="end-stepping-range" 和目标状态。

gdb/MI 很难理解,但非常适合软件进程间的通信。CDT 通过创建发送和接收数据的伪终端(pseudo-terminal,pty)来实现通信。然后,它启动 gdb 并创建两个会话对象来管理调试数据。

启动调试器

正如 第 1 部分 中所描述的,当用户单击 Debug 时,CDT 访问 ICDebugger2 实例并调用它来创建 ICDISession。该调试器类必须在扩展 org.eclipse.cdt.debug.core.CDebugger 扩展点的插件中标识出。清单 4 显示了 CDT 中这个扩展的样子。


清单 4. CDT 默认的调试器扩展
                                                    

这说明 GDBCDIDebugger2 执行开始调试进程的 createSession() 方法。当 CDT 调用该方法时,它提供给调制器包含配置参数的启动对象、即将调试的可执行文件的名称和进程监视器。GDBCDIDebugger2 使用这些信息形成启动 gdb 可执行文件的字符串:

gdb -q -nw -i mi-version -tty pty-slave executable-name

GDBCDIDebugger2 为正运行的 gdb 可执行文件创建 MIProcess,然后创建两个会话对象来管理剩余的调试进程:MISessionSessionMISession 对象管理与 gdb 的通信,Session 对象把 gdb 会话连接到 第 1 部分 中描述的 CDI。本文接下来具体讨论这些会话对象。

MISession

启动 gdb 后,GDBCDIDebugger2 首先要做的就是创建一个 MISession 对象。这个对象使用三对对象处理所有对 gdb 调试器的访问:

  • OutputStream(向 gdb 进程发送数据)和 InputStream(接收响应)
  • 输入和输出 CommandQueue(持有 MI 命令)
  • TxThread(把输出 CommandQueue 的命令发送到 OutputStream)和 RxThread(发送 InputStream 的接收命令并将其放到输入 CommandQueue

示例将验证这些对象如何一起工作。如果远程执行调试会话,CDT 通过向 gdb 发送 remotebaud 命令(后跟波特率)发起通信。为了完成该过程,它调用 MISessionpostCommand 方法,该方法把 remotebaud 命令添加到会话的输出 CommandQueue 中。这会唤醒 TxThread,它将命令写入与 gdb 进程连接的 OutputStream 中。它还将命令添加到会话的输入 CommandQueue 中。

同时,RxThread 不断读取来自 gdb 进程的 InputStream。当新输出可用时,RxThread 通过 MIParser 发送新输出来获得结果记录和 OOB 记录。然后搜寻输入 CommandQueue 以查找触发输出的 gdb 命令。如果 RxThread 包含 gdb 的输出和相应的命令,它将创建一个 MIEvent 来传播调试器状态的改变。

当数据在 gdb 中来回传输时,TxThreadRxThread 创建并触发 MIEvent。例如,如果 TxThread 发送一个将断点切换到 gdb 的命令,它将创建一个 MIBreakpointChangedEvent。如果 RxThread 接收到来自 gdb 的结果记录为 ^running 的响应,则将创建一个 MIRunningEvent。这些事件不是 第 1 部分 所描述的 ICDIEvent 接口的实现。要搞清 MIEventICDIEvent 之间如何联系,您应该理解 Session 对象。

SessionTargetEventManager

创建 MISession 后,GDBCDIDebugger2 创建一个 Session 对象来管理 CDI 的操作。当其构造器被调用时,Session 创建许多对象来协助进行管理。有两个对象尤其重要:Target(管理 CDI 模型并向调试器发送命令)和 EventManager(侦听由调试器创建的 MIEvent)。

正如 第 1 部分 中解释的一样,Target 接收来自 CDT 的调试命令并为调试器打包命令。例如,当您单击 Step Over 按钮时,CDT 查找当前的 Target 并调用 stepOver 方法。Target 的响应方式是创建 MIExecNext 命令和调用 MISession.postCommand() 执行步骤。MISession 将命令添加到其输出 CommandQueue 中,在这里使用上面描述的方式把命令传输给调试器。

会话的 EventManager 接收 gdb 输出(被打包到一个 MIEvent 中)。当创建这个对象时,将其作为运行中的 MISession 的一个 Observer 添加。当 MISession 触发 MIEvent 时,EventManager 进行解释并创建相应的 ICDIEvents。例如,当 MISession 触发 MIRegisterChangedEvent 时,EventManager 创建一个称为 ChangedEvent 的 CDI 事件。创建 CDI 事件后,EventManager 告诉所有相关的侦听器状态发生了改变。许多侦听器是 CDI 模型的元素,但一个重要的例外是一个称为 CDebugTarget 的对象。这是另一个模型层次结构(接下来将解释)的一部分。

CDI 和 Eclipse 调试模型

对于与 Eclipse 调试视图(比如 Register View 和 Variable View)交互的调试插件,您必须遵守 Eclipse 的规则:必须使用从 Eclipse 调试平台获得的事件和元素。Eclipse 调试模型的根元素是一个 IDebugTarget,其它元素包括 IVariableIExpressionIThread。这些名称很相似,是因为 CDI 模型层次结构是在 Eclipse 调试模型层次结构之后构造的。但 CDI 模型与 Eclipse 调试模型之间不能直接对话。

鉴于这个原因,CDT 包含一组类,将 CDI 类封装起来,从而将 CDI 模型和 Eclipse 调试模型联系起来。CDebugTarget 是这个包装器模型层次结构的根,它侦听由 CDI EventManager 触发的事件。当它接收到新事件时,CDebugTarget 处理大量的 ifswitch 语句来决定如何响应。例如,如果 CDI 事件是 ICDIResumedEventCDebugTarget 将执行清单 5 中的代码。


清单 5. 将 CDI 事件转换为 DebugEvents
                switch( event.getType() ) {case ICDIResumedEvent.CONTINUE:detail = DebugEvent.CLIENT_REQUEST;break;case ICDIResumedEvent.STEP_INTO:case ICDIResumedEvent.STEP_INTO_INSTRUCTION:detail = DebugEvent.STEP_INTO;break;case ICDIResumedEvent.STEP_OVER:case ICDIResumedEvent.STEP_OVER_INSTRUCTION: detail = DebugEvent.STEP_OVER;break;case ICDIResumedEvent.STEP_RETURN:detail = DebugEvent.STEP_RETURN;break;}

CDebugTarget 通过创建 DebugEvents 来响应 CDI 事件,这个过程通常涉及单步调试、中断、重新执行。创建这些事件后,它访问 Eclipse DebugPlugin 并调用 fireDebugEventSet 方法。这通知所有 Eclipse 调试侦听器状态发生了改变。即所有将其自身作为 DebugEventListener 添加的对象接收到 DebugEvent。这包括 Eclipse 调试视图,比如 Memory View 和 Variables View。

CDT 调试视图

只有使用适当的数据更新 Eclipse 的图形化显示时,MI-CDI-wrapper-Eclipse 通信才是有用的。图 1 显示 CDT 调试透视图,您可以看到许多呈现目标执行状态的视图。许多视图 — Breakpoints、Modules 和 Expressions — 都由 Eclipse 提供,但 CDT 在透视图中添加了三个视图:Executables View、Disassembly View 和 Signals。


图 1. CDT 调试透视图


这些视图以相似的方式创建和接收调试事件。本节将解释 Signals 视图。该视图(上面已突出显示)列出所有目标能接收到的信号并显示哪些信号可以传递给进程。视图第一次出现时,SignalsViewContentProvider 调用 CDebugTarget 来提供一系列信号,这个目标访问 CDI 目标并在其 CDI 模型层次结构中请求信号。当返回 ICDISignals 数组后,CDebugTarget 更新它自身的模型元素并将它们发送至 SignalsViewContentProviderSignalsViewContentProvider 使用这些模型元素来填充 Signals 视图。

当右键单击 Signals 视图中的条目时,Resume with Signal 上下文菜单选项允许您继续执行目标并将选定的信号发送到进程。这个选项调用 SignalsActionDelegate。当选中该选项时,delegate 调用 CDI 目标并使用与所选信号相对应的 ICDISignal 恢复执行。这个目标为信号创建一个 MI 命令并调用 MISession.postCommand() 向 gdb 发送命令。

当 gdb 响应后,更新 Signals 视图的过程需要五个步骤:

  1. MISession 分析来自 gdb 的 MI 输出并判断某个信号设置是否被改变。如果是的话,触发 MISignalChangedEvent
  2. CDI EventManager 侦听 MISignalChangedEvent 并通过创建一个 CDI 事件 ChangedEvent 进行响应。然后触发事件并警告所有 ICDIEventListeners
  3. CDebugTarget 接收来自 EventManager 的事件并判断 ChangedEvent 是否和信号改变有关。如果是的话,调用它的 CSignalManager 来处理 CDI 事件。
  4. CSignalManager 更新它的模型元素并触发 DebugEvent,后者的类型由 DebugEvent.CHANGE 给定。
  5. SignalViewEventHandler 接收 DebugEvent,检查并确保它处理信号和刷新 Signals 视图。

了解 Signals 视图的相关操作非常重要,原因有二:它可以作为一个具体的示例演示各种不同的模型元素如何协同工作,另外它展示了如何构建与 Eclipse、gdb、CDI 交互的相似视图。

结束语

两个会话对象(MISessionSession),两个目标(CDebugTargetTarget)和两个层次结构完全不同的模型元素 — CDT 调试器的操作非常复杂,您可能怀疑开发人员是否会使用这么复杂的工具。然而 CDT 调试器的代码是通过模块化的方式编写的,对其内部工作原理的理解越透彻,就越容易插入自己的模块。记住:学习的过程虽然艰难,但为 CDT 添加新特性要比从头构建定制调试应用程序简单得多。


参考资料

学习

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文。

  • 访问 Eclipse.org 中的 Eclipse CDT。

  • 阅读 CDT 项目主管的博客。

  • 查阅 “ Eclipse 推荐读物列表”。

  • 浏览 developerWorks 中所有 Eclipse 内容。

  • 您是 Eclipse 新用户吗?请阅读 developerWorks 文章 “ Eclipse 平台入门” 以了解它的起源和架构,以及如何用插件扩展 Eclipse。

  • 查看 IBM developerWorks 的 Eclipse 项目资源 以扩展 Eclipse 技巧。

  • 收听针对软件开发人员的有趣访谈和讨论,一定要访问 developerWorks podcasts。

  • 随时关注 developerWorks 的 技术活动和网络广播。

  • 查看免费的 developerWorks On demand demos 观看并了解 IBM 及开源技术和产品功能。

  • 查阅最近将在全球举办的面向 IBM 开放源码开发人员的研讨会、交易展览、网络广播和其他 活动。

  • 访问 developerWorks 开放源码专区 获得丰富的 how-to 信息、工具和项目更新,帮助您用开放源码技术进行开发,并与 IBM 产品结合使用。

获得产品和技术

  • 在 IBM alphaWorks 中查看最新的 Eclipse 技术下载。

  • 从 Eclipse Foundation 下载 Eclipse Platform 和其他项目 。

  • 下载 IBM 产品评估版,并开始使用来自 DB2?、Lotus?、Rational?、Tivoli? 和 WebSphere? 的应用程序开发工具和中间件产品。

  • 使用 IBM 试用软件 改进您的下一个开源开发项目,可以下载或从 DVD 获得。

讨论

  • Eclipse Platform 新闻组 是讨论关于 Eclipse 的问题的第一站(选择此链接将启动默认的 Usenet 新闻阅读器应用程序并打开 eclipse.platform)。

  • Eclipse 新闻组 中有很多参考资料适用于对使用和扩展 Eclipse 感兴趣的人员。

  • 参与 developerWorks blogs 并加入 developerWorks 社区。

关于作者

Matthew Scarpino 是 Eclipse Engineering LLC 的一名项目经理兼 Java 开发人员。他是 SWT/JFace in Action 的首席作家,并对标准部件工具包(Standard Widget Toolkit,SWT)做过一次较小的但非常重要的贡献。他喜欢爱尔兰民间音乐、马拉松赛跑、William Blake 的诗歌以及图形化编辑框架(Graphical Editing Framework,GEF)。