当您考虑调试嵌入式系统时,您会想到什么?我想这取决于您的背景和分配给工具的预算。您可能会考虑基于 JTAG/BDM 的调试端口、仿真器或逻辑分析仪、printf()或当今可用的许多复杂的源代码级调试器之一。每个解决方案都有自己的优点和缺点。有些非常强大,但代价高昂。有些与特定的编译器工具集相关,而另一些则仅在某些 CPU 系列上有用。基于 JTAG/BDM 的调试端口可能是现在常见的,因为它在成本和功能之间取得了良好的平衡。这些设备仍然需要花费几千美元,并且只有在连接到系统时才有用,通常需要一些笨重的吊舱挂在离目标相当近的地方。
本文的主题有点像是一门即将消亡的艺术:基于监视器的调试。什么是基于监视器的调试?首先,它假设您的应用程序正在使用某种监视器启动的系统上运行。启动监视器是应用程序驻留的基础平台,如果应用程序崩溃,监视器将接管。
当在 ROM 中时
基于监视器的调试依赖于处理器内置的功能,并且在大多数情况下(并非总是),这需要能够写入指令空间。通常,在监视器的命令行界面 (CLI) 中设置断点,然后将控制权移交给应用程序。如果执行设置断点的指令,则会从应用程序中取出一个分支,并将控制权返回到监视器的 CLI。现在,监视器具有显示内存的能力,可能是单步执行,甚至可能返回到断点所在位置的应用程序。
听起来不错吧?嗯,确实可以,但是可能会出现很多并发症,例如:
如何显示内存?通常,监视器可以将原始内存显示为 1、2 或 4 字节单元的块,但您必须以十六进制指定地址。由于您在目标的 CLI 上运行,因此您所能做的可能就是参考链接器生成的输出映射文件来确定符号在 data/bss 地址空间中的位置。您必须将符号与某个十六进制地址相关联。如果构建发生变化,内存映射也会发生变化,因此下次您想要查看同一块数据时,您必须再次进行地址到符号的关联。此外,监视器不知道如何以您想要的格式显示,例如短整型、长整型和字符串。忘记结构显示。
怎么设置断点呢?与上一个问题类似,首先查找函数的地址,然后发出一些命令,例如“b 0x123456”,其中 0x123456 是要设置断点的函数的地址。
一旦断点发生,监视器如何与串口通信?监视器因断点而接管;但应用程序现在拥有串行端口。如果监视器重新初始化串行端口以供使用,则应用程序和该端口之间的接口可能会混乱。这意味着将控制权返回给应用程序将非常困难。
监视器如何暂时关闭应用程序?当应用程序在 RTOS 上运行、启用中断并配置各种不同的外设时,这会变得很棘手。
单步执行现在处于汇编程序级别,而不是 C 语言级别。这对于程序员来说不是很有用。
也许这可以解释为什么基于监视器的调试不再流行。不过,在我们放弃之前,我想重新研究一下这个话题,看看这头老野兽是否还剩下一些气息。
调试理念
让我们从设定一些界限开始。我们必须接受这样一个事实:基于监视器的调试环境有局限性。在我们建立了一些指导方针并重新思考其中一些事情的完成方式之后,我想您会同意仍然有相当多的能力。那么让我们建立一个启动监视器的调试模型。我们得到什么,又牺牲什么?
对于内存显示,我们将能够显示“类似 C”的数据:字符串、字符、短整型、整数、长整型,甚至数据结构。数据结构可以单独显示、以表格或链接列表的形式显示。我们将能够以符号方式引用数据。这意味着名为“SysTick”的全局变量可以被引用为“SysTick”,而无需知道它在内存中的位置。我们将进行运行时分析,将断点作为其功能之一,而不是在典型断点的范围内进行思考。有终止应用程序执行的标准断点和用于运行时分析的“自动返回”断点。监视器捕获断点或异常后,可以进行符号堆栈跟踪。
我们将无法访问源代码行号信息;然而,如果编译器支持转储混合源代码和汇编代码的能力,那么我们就可以解决这个问题。我们不会考虑单步执行,因为它是汇编级别的;然而,如果我们实现其他一切,汇编级单步执行是一个方便的赠品。由于我们只能访问全局变量,因此堆栈跟踪将不会显示传递的参数,也无法从堆栈中检索本地数据。该调试器的一个也是重要的限制是,在硬断点之后控制权无法返回到正在运行的应用程序。
如果这些考虑因素可以接受,终的结果就是一个与应用程序一起存在的调试环境。它可以随产品一起提供并在现场使用,并且在某种程度上独立于编译器工具集、使用的 RTOS,并且在一定程度上独立于 CPU 和硬件。