acm-header
登录

ACM通信

实践

增强的跟踪调试


回到顶部

创建一个运行旧程序的模拟器是一项艰巨的任务。您需要全面了解目标硬件和模拟器要执行的原始程序的正确功能。除了功能正确之外,模拟器还必须达到以原始实时速度运行程序的性能目标。实现这些目标不可避免地需要大量的调试。对于老式街机游戏的玩家来说,这些bug是重现原始体验的重要组成部分,它们通常是模拟器本身的细微错误,但也可能是对目标硬件的误解或原始程序中实际已知的bug。(也有可能原始程序的二进制数据已经轻微损坏,或者不是预期的版本。)解决这些问题需要一些不同寻常的调试技术。

通过在CPU中设置断点,可以将主机系统的调试器用于目标(被模拟)系统进行的指令循环,并检查保存模拟CPU寄存器和主存内容的变量。这适用于较小且容易本地化的问题,如自检或初始化代码中的失败,但对于在执行的后期发生的问题没有太大用处。在这一点上,计划的组织并不明显。没有调试符号或源代码可以使用高级方法来解决问题。需要更强大的技术,并且内置到模拟器本身中。

显然,在可能出现的bug种类中存在着巨大的变异性。忽略主要的执行路径失败(例如,无限循环,程序计数器终止在非程序内存中),将在目标的视频输出中注意到典型的失败,如附件所示数字.在这个例子中,精灵的位置是绝对错误的。顶部精灵偏离了预期列的位置,位于13,11而不是24,11。我们知道外星人总是排成纵队攻击,所以模拟器出了问题。模拟器的内存状态中的一对值决定了精灵的位置。这个地址实际上是一个视频硬件寄存器的位置,它直接决定了显示的位置。您可以使用平台调试器来确认寄存器值实际上是13,11。不幸的是,即使您知道值是错误的,找到实际的错误也不容易。

错误的寄存器值是前面计算链的末尾(参见附带的侧栏“屏幕图像链”)。找到错误需要通过因果序列反向工作,以找到错误计算发生的位置。然而,调试器只向我们显示计算机的当前状态。如果有可能的话,重构事件链将涉及大量的探测工作。

回到顶部

救援的痕迹

也许有一条简单的捷径可以解决这个问题。如果您试图从已知可以工作的现有模拟器创建一个更快的模拟器,那么您可以使用正确的模拟器来调试不正确的模拟器。可以修改正确的仿真器以创建输入和CPU状态的跟踪,目标仿真器可以读取此跟踪文件(参见附带的侧栏“示例6502跟踪”)。输入值被用来代替真正的输入(操纵杆位置)。在每一个进行的指令调用时,当前机器状态与跟踪相匹配,并且在发散时停止执行。此时,您可以使用主机调试器详细检查问题并找到解决方案。

如果您没有引用跟踪(这种情况更有可能发生)怎么办?你仍然知道错误的最终来源是不正确的X值13被放置到内存位置0xc000中。您可以搜索跟踪文件以查找写入此值时的情况(例如,在BUS列中搜索C000w0d)。此时,您将看到来自寄存器的不正确值,并且只需往回看几行跟踪,就可以在影子[]记忆(“屏幕图像链侧边栏显示了所遵循的路径)。

此时,您可以对(现在)已知的影子内存地址应用相同的步骤,以找到跟踪中的点雪碧修改Struct。当您继续向后跟踪时,您会发现要跟踪的内存位置更多,或者可以手动验证代码段以查找违规模拟错误。这当然不像使用引用那么容易,但在不不断重新运行模拟器的情况下,很有可能发现错误。

回到顶部

使用痕迹

使用跟踪的模拟器可以在机器描述中查找问题,但是当问题存在于代码本身而不是机器描述中时,跟踪也会很有用。使用这样的跟踪可以使查找内存损坏问题更加容易。考虑一种特别讨厌的损坏:损坏的数据结构属于运行时内存分配器实现。例如,程序在调用时崩溃malloc ()当它遍历空闲列表时。实际的崩溃在调试器中很明显,但最终的原因却不是这样。

假设直接问题是在分配器的链表中位置为0x8004074的一个特定指针值被修改了。它包含0×1an无效且未对齐的内存引用,而不是貌似合理的值。在这一点上,标准调试器不能提供更多的帮助,因为它可以显示无效值,但不能显示无效值的时间。(您可以使用write -on-write,但如果损坏的地址在运行之间发生变化,那么这将不起作用。)

这就是跟踪解决问题的地方。向后搜索跟踪,查找对内存位置0x8004074的最后一次写操作。你可以找到以下条目:

ins01.gif

将给定的程序计数器(0x1072ab0)转换到源代码中的某个位置,将立即揭示损坏的可能原因。例如,现在很明显数组边界没有得到正确尊重。在数组中存储值为1的尝试会导致内存损坏。当然,就像在精灵的例子中一样,实际的损坏来源可能需要追溯到更远的地方。

回到顶部

跟踪所有程序?

跟踪对于调试普通问题很有用,但是如何生成这些跟踪还不是很明显。与模拟器不同,你不能控制运行软件的微处理器的(硬件)实现。

有人可能会反对说,这种冗长的日志记录对于实际的程序来说并不特别可行。尽管硬盘存储的成本持续下降,I/O带宽确实有限制。如果存储空间确实有限,还有一些其他的方法。例如,快照之间的跟踪只能存储在RAM中,而不能写入日志文件。可以根据需要重新创建跟踪(通过从给定状态重新运行目标)。或者,只能将最近的跟踪存储在主存中,以便在必要时快速访问扫描跟踪。

在许多情况下,CPU实现的完全不可变是一种精心设计的假象。所有Java(和。net)程序实际上都运行在虚拟cpu上。修改这些虚拟机来记录跟踪信息和状态快照是可以想象的,因为不需要更改硬件。这仅仅是说服虚拟机实现的所有者进行必要的更改的问题。

甚至本地可执行程序也与真正的硬件相差一步。所有现代操作系统都强制用户和内核执行模式之间的分离。进程实际上是一个虚拟实体。快照进程执行状态的功能已经存在(Unix古老的核心转储)。有了一些额外的操作系统支持,获取这些快照状态并将它们还原为可以继续执行的真实进程是完全可行的。

实际上,整个被使用的机器可能是虚拟的。VMware和Parallels等产品通常使用机器状态快照,其中包括用户模式、内核模式、设备驱动程序甚至硬盘驱动器的整个状态。

回到顶部

更高级的痕迹

跟踪内存访问对于汇编程序或C程序是很有帮助的,内存和CPU传输很容易与实际的源代码相对应,但更常见的情况是使用与机器相隔一步的语言,其中C代码是实际使用的语言的解释器。在这种情况下,代码的低级操作不容易映射到解释语言中可识别的操作。

在现代系统中,I/O的使用也更加复杂。程序不直接读写硬件I/O位置。相反,设备交互是通过操作系统进行的。

可以以一种明显的方式将跟踪应用于这些高级程序:更改跟踪以记录高级事件。要捕获的确切事件将取决于程序的类型。GUI程序可能需要捕获鼠标、键盘和窗口事件。操作文件的程序将捕获打开/关闭和读/写操作。构建在数据库之上的代码可能会记录SQL语句和结果。

好的跟踪不同于简单的事件日志。跟踪必须提供足够的信息,以便仅使用跟踪就可以验证执行的正确性。应该可以构建一个参考实现它可以读取跟踪并自动验证是否做出了正确的决策。根据使用仿真器的经验,您可能首先编写参考实现的代码,然后使用它来验证生产代码。(您可以避免过早优化的陷阱,并发现简化的参考实现是一个令人满意的解决方案。)编写引用似乎与生产版本涉及的工作一样多,但总有一些需求不会改变功能输出。例如,生产代码可能需要一个持久的查找表,而引用可以使用一个更简单的内存哈希表。

回到顶部

结论

向现有调试环境中添加快照、跟踪和回放将显著减少查找和纠正顽固错误所需的时间。底层代码在这方面取得了一些进展;对于某些平台,GDB最近获得了反向执行的能力。由于CPU操作是不可逆转的,这意味着现在有方法为编译程序捕获跟踪信息。如果添加了保存和重新加载快照的功能,gdb就可以成为一个可跟踪的调试器。

详细的CPU状态跟踪对于优化和调试模拟器非常有帮助,但是该技术也可以应用于普通程序。如果有参考实现可供比较,则几乎可以直接应用该方法。如果不是这样,跟踪对于调试非本地问题仍然是有用的。在程序中添加跟踪工具的额外工作将减少调试时间。

ACM队列的q戳相关文章
queue.acm.org

没有源代码?没问题!
彼得·菲利普斯和乔治·菲利普斯
http://queue.acm.org/detail.cfm?id=945155

在生产环境中调试AJAX
埃里克·施洛克
http://queue.acm.org/detail.cfm?id=1515745

在异步世界中调试
迈克尔Donat说
http://queue.acm.org/detail.cfm?id=945134

回到顶部

作者

彼得·菲利普斯在英属哥伦比亚大学获得了计算机科学学士学位。他有15年的软件开发经验,其中10年致力于主机游戏。

回到顶部

脚注

DOI: http://doi.acm.org/10.1145/1735223.1735240

回到顶部

数据

UF1数字精灵放错地方了。

回到顶部

回到顶部


©2010 acm 0001-0782/10/0500 $10.00

允许为个人或课堂使用本作品的全部或部分制作数字或硬拷贝,但不得为盈利或商业利益而复制或分发,且副本在首页上附有本通知和完整的引用。以其他方式复制、重新发布、在服务器上发布或重新分发到列表,需要事先获得特定的许可和/或付费。

数字图书馆是由计算机协会出版的。版权所有©2010 ACM, Inc。


没有找到条目

Baidu
map