有人认为要是您在凌晨三点最终发现了困扰您多日可又一直找不到的程序错误,那真是再没有比这更高兴的编程经历了。请注意,我们的开心源于找到了漏洞,而不是解决问题。这是因为您一旦真正了解了为什么会存在程序错误,那么纠正错误就是一件小事了。如果您过去用了错误的方法,那么现在就要用正确方法;如果您的输入有误,现在就可以进行整理;如果您假定系统有错误,那么就改正它,并认真在整个代码中进行相应修改。
当然,当英雄的时候很风光,不过找程序错误并不是程序员真正的满意源。请设想您要在花园中挖一道沟,并在其中铺设喷水装置管线。如果您遇到一块石头挡住去路,那么我们可在石头四周挖土,让石头松动移位,这就解决了问题。如果石头太大,那么重新设计布局也很有意思,这样您就可以完全避开石头。不过移开石头的快乐并不是挖沟的目的所在。在认真想想,您的目的其实也不是安装喷水装置。您的目标是为您自己或为了让您的配偶高兴而建设美丽的花园。
编程的情况与之类似。实时系统故障排除的过程可能遇到很困难的程序错误。经验丰富的程序员知道不会有太多工具帮忙找到并了解程序错误。不幸的是,随着系统变得越发复杂,传统的故障排除工具不再像其过去那样有效。我们不妨设想开发实时视频处理设备(如摄像机)的情况。即便您采购到速度足够快的逻辑分析器,具备足够的通道来观察媒体处理器,但由于大部分重要工作都发生在专用引擎和内部高速缓存及存储器中,您还是难以对其进行跟踪。您对处理器的可视程度是有限的。即便您为处理器找到了电路内仿真器 (ICE),但要想在摄像机等小型便携式设备中安放探针插件仍是不可能的。
上述所有原因解释了许多处理器厂商已经开始在芯片上直接集成调试功能的原因。您可利用原型规划出您的设计,这样您就可以访问设备的各种组件;但是,仍会有一系列实时问题存在--也就是那些您的客户会遇到而您在实验室中又难以再现的问题,您只能在生产设备中才能进行观察。
在JTAG 基础上工作
集成调试外设已经推出一段时间了。举例来说,JTAG 仿真为观察处理器的内部实现了伪实时可视性,这就使开发人员能够读写存储器或寄存器,抑或控制/监控处理器的执行。但是,JTAG 的主要问题在于,它是用正在被测试的处理器进行上述操作的。即便像察看寄存器集或单个存储器地址这样的简单操作也会影响认真优化过的管道流程,感染缓存,破坏脆弱的实时定时,屏蔽甚至模拟错误,这就会大幅改变处理器的执行。
新系列处理器提供了更先进的调试外设,可为处理器内部进行的核心操作提供更高的可视性,从而增强了 JTAG 功能。外设不再需要接受测试的内核来执行额外的工作,而是自己与内核并行执行,通常可以访问全部系统寄存器、存储器,甚至可以控制处理器自身的执行。这样,我们就在不打扰操作的情况下获得了深入内核的可视性,乃至能够监控代码高度优化过的部分。
毫无疑问,软件开发人员最可怕的噩梦就是不断出现程序错误。不管出于何种原因,只要客户使用就出问题,但在实验室中却无法重复问题。开发人员常常甚至不能进行事后的调查分析重建崩溃情况,因为存储器内容已经删除,数据也被破坏。开发人员切实需要的是类似飞机上安装的"黑盒子",它能够在崩溃后幸存下来,记录下所有所需数据,并将时钟拨回原来位置。随着调试外设的最新发展,这种黑盒子现在已经成为可能。
一些实现黑盒子常见的调试外设可做到实时数据交换、复杂触发、多处理器支持、时间关键性中断屏蔽、自动跟踪等,并具备能够准确而细致地重建设备任何时间点状态的工具。
实时数据交换
一个简单而非常实用的工具就是能够读取或写入存储器,同时又不会导致被测试的处理器停止或中断其操作。在您放置断点时,是用断点操作码替代一个代码字节。断点很复杂时,不具干扰性的存储器存取非常有用;如果您希望在中断前执行x次代码,或者特定的变量为具体值或在某一范围内(监视点)时,那么这就非常有用了。如果处理器必须停下操作才能进行每次比较,那么不仅执行每次比较要占用循环,而且比较本身也会影响指令和存储器缓存。如果断点在一个时间关键性的优化环路中,那么其造成的缓存效率低下就会导致代码无法满足实时期限的要求,而这也正是它与有效代码的差距所在。如果监视点和其他系统组件争用存储器总线的话,那么就会造成更大干扰。
无干扰性存储器存取可实现更高效的断点。不用断点操作码,而是由调试外设来监控程序计数器并与处理器并行执行比较,同时不致影响缓存或程序定时。使用断点操作码的另一问题是指令缓存反映的是断点操作码而不是被替代的字节。当您恢复执行您同断点操作码所交换的字节时,整个缓存会因此而失效。由于监视点在寄存器或内部存储器上,调试外设会等待存储器总线再次变得可用,抑或也可具备其自己的专用总线。在上述两种情况中,外设都不会与测试中的系统发生竞争。
还要考虑采用反选监视点的情况。假设本地变量破坏的情况。我们设置标准监视点在每次变量修改时触发中断,这就让您必须察看所有有效的修改才能找到导致错误的因素。如果反选监视点,也就是说只跟踪或触发发生在代码功能外的修改,那么您就能大幅减少必须亲自评估的修改数量,这就提高了您找到出错修改的速度。
实时数据交换对于微调算法也是非常实用的。举例来说,根据特定的一组扬声器调节音频算法,或根据图像传感器调节视频算法,这比重新编译代码并重新下载要节约大量时间。您还可以手动破坏数据流或代码,抑或用已知有问题的值预先载入寄存器和存储器启动会话,从而测试代码的稳健性。举例而言,在视频应用中,您可以破坏一段进入的视频流,看看编码器或解码器如何应付违反预计格式的数据。同时改变大块数据直接而方便:将数据分组成单个对象中,由指针引用,改变对象的临时实例,随后改变指针,以刚输入的数据引用临时实例。
共同工作
随着处理器变得日益复杂,多处理器调试支持变得更为重要。举例而言,摄影机需要进行实时编码。这是挑战性很高的任务,可能要求处理器具备双内核或混合 DSP + RISC 的结合,将图像从控制/应用处理中划分出来。
但是,如果您将几个处理器或单个处理器与多个加速器或片上引擎相集成,那么调试本身就会变得更为困难。在双内核实施中,大多数调试工具在显示写入特定存储器位置的内容时不会指明具体核心。调试外设使您能够监控总线,使之像存储器一样共享资源,并为您提供明确执行写入的处理器所需的资源 。您应注意到,这种情况下,可视性增强也会加大复杂性,因此您需要能够为每个处理器交叉各个追踪缓冲区的调试环境。
支持总线监控的多处理器设备很可能也支持全局断点。标准断点机制只能在几个周期的等待时间后停止其他处理器。这使处理器彼此不同步,因为每个处理器都执行不同数量的周期,结果就好像周期是在不同时间运行的一样。任何处理器间的通信都会使断点进一步复杂化,特别是在两个处理器都传输数据的情况下更是如此;如果数据发送但并未接受,那么您可能不知不觉地丢失事务处理的一部分。如果不同步,那么您需要重设并重启两个处理器以及应用,以返回同步。如果程序错误的原因较复杂,那么您可能会发现很难成功进行断点,即便您知道造成错误的条件组合也一样无济于事。全局断点停止所有处理器和相同循环上的引擎,从而避免上述困难,这就保持了其相对位置。
现实编程
最难以重建并定位的程序错误是实时出现的错误。即便您停止了处理器,现实情况还会继续,这样就会出现问题。举例来说,您操纵马达工作(比如照相机的镜头缩放)或者通过网络连接传输数据,即便您仅是短时间地停止处理器,您也会丢失数据包的一部份,如果不停止系统的话,您就会丢失数据。即便您不担心数据本身,比如在有持续的视频源情况下,您仍然会丢失数据流中的部分。恢复执行时,如果应用不停止流处理等待再同步,那么视频流会被破坏并且发生错乱。
就马达而言,即便您停止处理器,驱动运动的电压也都会继续。数据损失与机械盲点截然不同。您通常可以重新发送数据,但没有监控运行的马达可能导致机型故障甚至损坏。举例来说,某些摄影机带有自动打开的镜头,只要镜头盖推向一边就会启动。如果您在处理器停止时无意中关闭了镜头盖,那么您就可能在恢复操作时导致镜头损坏。如果您在快门打开开始采集影像时无意停止了处理器,那么就会导致快门锁定打开。如果您没有关闭快门的自动机制,那么就会致使影像传感器过度曝光而在无意中遭到损坏。
管理和控制机械组件有两种基本方法:通过软件或通过硬件中断。例如,我们可用定期中断来管理马达控制缩放。每次触发中断时,处理器都会评估镜头的位置,确定马达相对于其应处目的地的位置,并据此调节驱动电压。马达在电压调节前不断运动。不过,这种方法的问题在于,如果您在缩放时停止了处理器,那么马达还会继续运动,这就可能导致马达损坏。
硬件中断的一个实例是在数码相机上设置打开/关闭机制。您不会希望用户能够立即关闭电源,因为照相机可能仍处在处理影像当中,还不能存储影像到非易失介质。当用户关闭照相机时,这会触发硬件中断,告诉照相机关闭。不过,如果您停止了处理器并尝试关闭照相机以节约电池电量,那么照相机还会保持打开,因为它还不能为中断提供服务。不妨设想这样的情况,您坐在电脑前准备从照相机传输影像。这时您突然要离开,就关闭了照相机。您回来时则发现电池已经没电了,所有数据全部丢失。
因此,我们可以看到,即便处理器已经停止,但还是存在时间关键性事件以及需要中断处理继续进行的情况。如果您就可靠硬件进行调试,那么通常就要处理该问题;换言之,如果您对硬件完全有信心并致力于应用问题时,就要处理上述问题。您确实只需停止系统的一部分。您希望机械作用在一定程度上继续进行,或许也希望网络和通信功能继续进行。如果您停止应用时不能让上述事件继续,那么您在调试系统、避免干扰实施部分工作时就有大量事情要做了。
停止的其他名称
正因为上述原因,目前处理器已开始支持时间关键性中断掩码。中断掩码标识出即便在处理器停止时仍需要继续接受服务的中断。调节掩码的功能相当重要,因为您可能需要停止某些基于具体应用的硬件定时器中断。因此,您并不受限于通用停止,而是可以很大程度上控制停止。请注意,"停止"这个词有了全新的含义,因为掩码时间关键性中断触发时,处理器开始执行适当的中断代码。举例而言,您会屏蔽处理网络连接的中断。尽管处理器停止了,但它仍会为通过网络连接的数据提供缓冲。
这又是一个调试变得更为复杂的实例。某些中断与定时器类似,执行后会自我重设,这样它们就能再次触发。就中断监控马达而言,这是我们所希望的行动。马达由于运动而处于可能面临风险的位置;不过,中断再次触发就可将马达带回其目的地从而避免风险。
不过,就调度程序中断而言,重设意味着调度程序将继续尝试处理队列中的下一个任务。如果已经计划安排了一系列运动--您打开照相机盖时,程序会设置一系列运动并伸出镜头--调度将持续进行,保证每个任务依次进行。此外,您除了关心完成当前任务之外,还希望清空整个任务队列。更现实地说,您可能希望根据任务将以上两项工作都完成。如果仅将一系列运动的一部分放在队列上,那么最后放置的任务实际上会使设备面临风险(如照相机快门打开)。
您如何处理上述问题取决于任务的粒度。如果您有高级别任务,如"打开镜头",那么上述任务结束后可能不致使设备面临风险。不过,如果您的任务粒度更强,那么在描述所有执行高级别任务所需的初始步骤并完成目前的低级别任务情况下,您的系统可能就会面临风险。 关键在于不要让设备面临风险,而是要继续执行任务,直到设备脱离风险为止(图)。在本例中,您的调度程序必须清楚它可在应用停止时继续执行。由于时间关键性中断掩码是系统 寄存器,因此您可在应用内对其进行存取,这就使您能够根据其状态有条件地进行操作,并使您有机会随时启用或禁用各种时间关键性中断。当确定如何进行下一个任务时,管理器必须检查以确保处理器是停止的,并确定系统目前是否面临风险。如果是的话,调度程序应执行下一个任务。如果否的话,调度程序可以选择停止。如果队列为空而系统面临风险,那么调度程序必须生成一个任务,保证让系统脱离风险。此外,如要取消其生成的额外任务,那么调度程序必须将系统恢复到面临风险的状态。
请注意,如果特殊中断(如将系统带回断电等已知安全状态的中断)依赖于应用代码的话,那么它们可能需要额外的修改。举例而言,我们照相机的打开/关闭按钮要求应用完成对当前已缓冲影像的处理。如果应用停止,那么处理就不能完成。中断实际上需要将处理器带出停止状态,从而让系统脱离面临风险的状态。
尽管时间关键性中断增加了系统设计的复杂性,但其也简化了某些类型的调试。假设您对应用很有信心,但需要调试与硬件相关的问题。一般说来,控制实际操作所需的代码量较小,至少与影像处理所需的代码相比如此。一个 50MHz 的处理器每秒可执行五千万个循环。举例来说,如果机械操作用两秒时间,那么您必须在 1 亿个周期的线迹缓冲中找到您需要的代码。不过,您也可以停止处理器,用屏蔽的时间关键性中断来操作硬件。这时,您的线迹缓冲就没有应用代码了,这就使查找程序错误简单得多。当然,这种方法不会暴露硬件和应用互动造成的错误。
自我启发
另一类调试外设是自跟踪处理器的外设。许多数据都可不经处理器处理。上述外设可过滤、压缩或评估数据,这样您就可通过有限的带宽总线(如 JTAG)发送更多的信息。
自跟踪外设的一个实例是不连续跟踪。它不是跟踪每个指令的指令指针值,而是仅在 IP 由分支等改变时跟踪。迭代数量可用环路计算,这就可更深入地了解执行路径。跟踪总的运动使您能够致力于线迹上的其他类型信息。
不连续跟踪可快速显示您的程序开始在代码外执行的点。您还可用线迹作为描述器,这就给出通路和支路之比,可用作条件或整体程序执行。
黑盒子
调试外设的演进自然使您获得"黑盒子"设计。利用完整的线迹和修改功能,我们可以创建线迹缓冲,帮助您了解系统在执行任何一点上的状态。如果您在停止系统时全面了解了系统的整个状态,那么您可利用线迹来正确重建以前的状态。这一功能正好像您可在调试环境中无数次点击"取消按钮"一样。
换言之,目前的处理器有着内置的黑盒子。主要限制在于跟踪存储。新型硬件工具提供了较大乃至无限的缓冲(如果附加硬盘的话)以在芯片外存储线迹缓冲,从而解决了这一问题。如果芯片外总线带宽足以发送您重建处理器任何时间状态所需的所有信息,那么您就拥有了黑盒子。
不妨设想这样的情况,客户系统总是不断崩溃,而且好像没办法重建崩溃。您可给系统添加跟踪存储工具,启用黑盒子。系统崩溃时,会告诉存储工具停止记录(如果系统不能自动做到这一点,您可通过按键进行)。随后客户给您打电话,您收取存储工具。这样您就得到了所需要的东西:故障的记录实例。您现在就可以致力于找到不断发生的问题并解决问题了,而不必再为重建问题而花费数周时间。 |