在编程语言中增加调试支持的方法

100 阅读3分钟

一个好的编程语言还应该能够支持调试和调试信息。在这篇文章中,我们增加了对调试和调试信息的支持。

目录

  1. 简介
  2. 超前编译(AOT)
  3. 总结
  4. 参考资料

先决条件

编译成目标代码。

介绍

到目前为止,我们可以在Kaleidoscope中使用函数和变量,然而,编译器的主要目的是报告错误,这样我们就可以在将代码运送给客户之前对其进行调试。

在这篇文章中,我们将讨论并为我们的编程语言添加调试信息,并将其翻译成DWARF。当调试程序时,我们希望调试信息是可读的,因此我们需要将源代码从二进制格式翻译成程序员以前写的源代码,这被称为源码级调试。LLVM使用DWARF格式来表示调试信息,它是一种紧凑的编码,表示类型、源和变量位置。

下面的简单程序计算了第n个斐波那契数,在这种情况下,函数返回55,因为它是第10个斐波那契数:

def fib(n)
  if n < 3 then
    1
  else
    fib(n - 1) + fib(n - 2);

fib(10)

超前编译

时前编译包括在执行前将高级编程语言编译成低级语言,这是在构建时完成的,它减少了运行时的工作。

我们将对Kaleidoscope进行修改,以支持将产生的IR编译成一个可以执行和调试的独立程序。

首先,我们制作一个匿名函数,包含我们的顶级语句main

-    auto Proto = std::make_unique<PrototypeAST>("", std::vector<std::string>());
+    auto Proto = std::make_unique<PrototypeAST>("main", std::vector<std::string>());

然后我们删除命令行代码,如下所示:

@@ -1129,7 +1129,6 @@ static void HandleTopLevelExpression() {
 /// top ::= definition | external | expression | ';'
 static void MainLoop() {
   while (1) {
-    fprintf(stderr, "ready> ");
     switch (CurTok) {
     case tok_eof:
       return;
@@ -1184,7 +1183,6 @@ int main() {
   BinopPrecedence['*'] = 40; // highest.

   // Prime the first token.
-  fprintf(stderr, "ready> ");
   getNextToken();

最后,我们关闭所有的优化通道和JIT(即时编译)。在完成解析和代码生成后,唯一可能发生的事情就是LLVM IR会转到标准错误:

@@ -1108,17 +1108,8 @@ static void HandleExtern() {
 static void HandleTopLevelExpression() {
   // Evaluate a top-level expression into an anonymous function.
   if (auto FnAST = ParseTopLevelExpr()) {
-    if (auto *FnIR = FnAST->codegen()) {
-      // We're just doing this to make sure it executes.
-      TheExecutionEngine->finalizeObject();
-      // JIT the function, returning a function pointer.
-      void *FPtr = TheExecutionEngine->getPointerToFunction(FnIR);
-
-      // Cast it to the right type (takes no arguments, returns a double) so we
-      // can call it as a native function.
-      double (*FP)() = (double (*)())(intptr_t)FPtr;
-      // Ignore the return value for this.
-      (void)FP;
+    if (!F->codegen()) {
+      fprintf(stderr, "Error generating code for top level expr");
     }
   } else {
     // Skip token for error recovery.
@@ -1439,11 +1459,11 @@ int main() {
   // target lays out data structures.
   TheModule->setDataLayout(TheExecutionEngine->getDataLayout());
   OurFPM.add(new DataLayoutPass());
+#if 0
   OurFPM.add(createBasicAliasAnalysisPass());
   // Promote allocas to registers.
   OurFPM.add(createPromoteMemoryToRegisterPass());
@@ -1218,7 +1210,7 @@ int main() {
   OurFPM.add(createGVNPass());
   // Simplify the control flow graph (deleting unreachable blocks, etc).
   OurFPM.add(createCFGSimplificationPass());
-
+  #endif
   OurFPM.doInitialization();

   // Set the global so the code gen can use this.

现在我们可以将Kaleidoscope编译成一个可执行程序a.out或*.exe*。为此我们执行以下命令:

Kaleidoscope-Ch9 < fib.ks | & clang -x ir -

总结

为一种语言创建调试信息被认为是一个困难的问题,原因如下:

  • 代码需要被优化--优化后保持源码位置是很难的
  • 优化会移动变量,它们要么被优化掉,要么在内存中共享,要么难以追踪 在这篇文章中,我们已经学会了如何

    Kaleidoscope编程语言添加对调试和调试信息的支持。正如我们前面所说,这是任何好的编程语言的一个重要方面

参考资料

调试DWARF, 函数, 来源位置, 变量.