11

这是一个关于宏的更具理论性的问题(我认为)。我知道宏获取源代码并生成目标代码而不对其进行评估,从而使程序员能够创建更通用的句法结构。如果我必须对这两个宏系统进行分类,我会说有“C 风格”宏和“Lisp 风格”宏。

调试宏似乎有点棘手,因为在运行时,实际运行的代码与源代码不同。

调试器如何根据预处理的源代码跟踪程序的执行?是否必须设置特殊的“调试模式”才能捕获有关宏的额外数据?

在 C 语言中,我可以理解您会为调试设置编译时开关,但解释性语言(例如某些形式的 Lisp)如何做到这一点?

很抱歉没有尝试这个,但 lisp 工具链需要的时间比我花费的时间要多。

4

6 回答 6

4

我认为“C 风格”和“Lisp 风格”宏的编译方式没有根本区别。两者都在编译器正确看到它之前转换源。最大的区别在于 C 的宏使用 C 预处理器(一种较弱的辅助语言,主要用于简单的字符串替换),而 Lisp 的宏是用 Lisp 本身编写的(因此可以做任何事情)。

(顺便说一句:我已经有一段时间没有看到未编译的 Lisp 了……当然不是自世纪之交以来。但如果有的话,被解释似乎会使宏调试问题更容易,而不是更难,因为你有更多的信息。)

我同意 Michael 的观点:我根本没有见过处理宏的 C 语言调试器。使用宏的代码会在任何事情发生之前进行转换。用于编译 C 代码的“调试”模式通常只意味着它存储函数、类型、变量、文件名等——我认为它们中的任何一个都不存储有关宏的信息。

  • 对于使用宏的调试程序,Lisp 与此处的 C 几乎相同:您的调试器看到的是编译后的代码,而不是宏应用程序。通常,宏保持简单,并在使用前独立调试,以避免需要这样做,就像 C 一样。

  • 为了调试宏 本身,在你去某个地方使用它之前,Lisp 确实具有比 C 更容易的特性,例如 repl 和 macroexpand-1(尽管在 C 中显然有一种方法可以一次完全地宏扩展整个文件)。编写宏扩展时,您可以直接在编辑器中查看宏扩展的前后变化。

我不记得有什么时候遇到过调试宏定义本身很有用的情况。要么是宏定义中的错误,在这种情况下macroexpand-1会立即隔离问题,要么是低于此的错误,在这种情况下,正常的调试工具可以正常工作,我不在乎在调用堆栈的两帧之间发生宏扩展.

于 2010-07-09T18:02:35.507 回答
3

LispWorks开发人员可以使用Stepper 工具

LispWorks 提供了一个步进器,可以在其中逐步完成完整的宏扩展过程

于 2010-07-09T21:26:34.607 回答
2

您应该真正研究一下Racket对使用宏调试代码的支持。正如 Ken 提到的,这种支持有两个方面。一方面是调试宏的问题:在 Common Lisp 中,最好的方法是手动扩展宏形式。对于 CPP,情况类似但更原始——您只需通过 CPP 扩展运行代码并检查结果。然而,对于更多涉及的宏来说,这两者都不够,这就是在 Racket 中使用宏调试器的动机——它一一向您展示了语法扩展步骤,以及针对绑定标识符等内容的额外基于 gui 的指示。

使用宏方面,Racket 一直比其他 Scheme 和 Lisp 实现更先进。这个想法是每个表达式(作为一个句法对象)是代码加上包含其源位置的附加数据。这样,当表单是宏时,包含来自宏的部分的扩展代码将具有正确的源位置——来自宏的定义,而不是来自它的使用(表单并不真正存在的地方)。正如 dmitry-vk 所提到的,一些 Scheme 和 Lisp 实现将使用子表单的身份实现对此的限制。

于 2010-07-09T18:18:50.267 回答
1

我不了解 lisp 宏(我怀疑它可能与 C 宏完全不同)或调试,但许多 - 可能是大多数 - C/C++ 调试器不能很好地处理 C 预处理器宏的源代码级调试。

通常,C/C++ 调试器不会“步入”宏定义。如果一个宏扩展为多个语句,那么对于每个调试器“步骤”操作,调试器通常只会停留在同一源代码行(调用宏的位置)上。

这可能会使调试宏比其他方式更痛苦 - 这是在 C/C++ 中避免使用它们的另一个原因。如果一个宏以一种真正神秘的方式出现异常,我将进入汇编模式来调试它或扩展宏(手动或使用编译器的开关)。你必须走到那个极端是非常罕见的。如果您正在编写如此复杂的宏,那么您可能采用了错误的方法。

于 2010-07-09T17:26:38.957 回答
1

通常在 C 源代码级调试中具有行粒度(“下一个”命令)或指令级粒度(“步入”)。宏处理器将特殊指令插入到已处理的源代码中,允许编译器将已编译的 CPU 指令序列映射到源代码行。

在 Lisp 中,宏和编译器之间没有约定来跟踪源代码到已编译代码的映射,因此并不总是可以在源代码中进行单步执行。

显而易见的选择是在宏扩展代码中进行单步执行。编译器已经看到最终的、扩展的代码版本,并且可以跟踪源代码到机器代码的映射。

其他选择是使用操作期间的 lisp 表达式具有标识的事实。如果宏很简单并且只是将代码解构并粘贴到模板中,那么扩展代码的某些表达式将与从源代码中读取的表达式相同(相对于 EQ 比较)。在这种情况下,编译器可以将一些表达式从扩展代码映射到源代码。

于 2010-07-09T18:18:46.580 回答
0

简单的答案是它很复杂;-) 有几种不同的东西有助于调试程序,甚至更多有助于跟踪宏。

在 C 和 C++ 中,预处理器用于扩展宏并包含到实际的源代码中。使用#line 指令在这个扩展的源文件中跟踪原始文件名和行号。

http://msdn.microsoft.com/en-us/library/b5w2czay(VS.80).aspx

在启用调试的情况下编译 C 或 C++ 程序时,汇编器会在目标文件中生成附加信息,用于跟踪源代码行、符号名称、类型描述符等。

http://sources.redhat.com/gdb/onlinedocs/stabs.html

操作系统具有使调试器可以附加到进程并控制进程执行的功能;暂停、单步等。

当调试器附加到程序时,它通过在调试信息中查找程序地址的含义,将进程堆栈和程序计数器转换回符号形式。

动态语言通常在虚拟机中执行,无论是解释器还是字节码 VM。VM 提供了钩子以允许调试器控制程序流和检查程序状态。

于 2010-07-09T21:47:34.103 回答