内容


使用 CDT 调试器,第 2 部分

使用 Eclipse CDT 和 MI 访问 gdb

C/C++ Development Tooling 如何使用 C/C++ Debugger Interface 处理 GNU 调试器的机器接口

系列内容:

此内容是该系列 # 部分中的第 # 部分: 使用 CDT 调试器,第 2 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:使用 CDT 调试器,第 2 部分

敬请期待该系列的后续内容。

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 的第二个方法旨在通过软件与调试器交互:Machine Interface(MI)。调试器执行的任务和以前相同,但命令和输出响应有很大的不同。

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

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

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

清单 2. 调试会话
         $ gdb -q simple         (gdb) break main         (gdb) run
1	int main() {
(gdb) step
2	int x = 4;
(gdb) step
3	x += 6;     // x = 10
(gdb) print x
$1 = 4
(gdb) step
4	x *= 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 默认的调试器扩展
   <extension point="org.eclipse.cdt.debug.core.CDebugger">
      <debugger
            class="org.eclipse.cdt.debug.mi.core.GDBCDIDebugger2"
            cpu="native"
            id="org.eclipse.cdt.debug.mi.core.CDebuggerNew"
            modes="run,core,attach"
            name="gdb Debugger"
            platform="*">
         <buildIdPattern
               pattern="cdt\.managedbuild\.config\.gnu\..*">
         </buildIdPattern>
      </debugger>
   </extension>

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

gdb -q -nw -i mi-version -tty pty-slaveexecutable-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 调试透视图
CDT 调试透视图
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 添加新特性要比从头构建定制调试应用程序简单得多。


相关主题

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=326090
ArticleTitle=使用 CDT 调试器,第 2 部分: 使用 Eclipse CDT 和 MI 访问 gdb
publish-date=07312008