Kaleidoscope:Adding Debug Information

translate from: http://llvm.org/docs/tutorial/LangImpl09.html

唔,最后两个章节有点难产。

本章主要介绍:为Kaleidoscope添加调试信息支持。

9.1 第九章介绍 #

欢迎来到“使用LLVM来实现一门语言”教程的第九章。在第一章到第八章的内容中,我们已经用函数和变量构建了一个不错的编程语言。但是如果在程序出现问题的情况下,会发生什么呢?我们该如何调试我们的程序呢?

源码级的调试会使用格式化的数据,这些数据会帮助调试器将二进制的文件和机器的状态翻译回程序员编写的代码。在LLVM中,我们使用被叫做 DWARF。DWARF是一种紧凑的编码,它可以表示出变量类型、源代码位置和变量的位置。

本章的主要内容:为了获取到调试信息,并且将调试信息转换为DWARF格式,我们将看到各种各样你必须添加到编程语言中的内容。

警告:因为现在我们无法通过JIT来对程序进行调试,所以我们需要将我们的程序编译为小的并且独立的程序。作为其中的一部分,我们将对语言的运行以及程序的编译方式做一些修改。这意味着我们将拥有一个用Kaleidoscope编写的简单程序而不是交互式的JIT程序。但是它有一个限制,即我们一次只有一个“顶级”命令来减少必要的改变次数。

下面是我们将要编译的程序:

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

fib(10)

9.2 为什么这是个困难的问题 #

生成调试信息是一个困难的问题,主要有几个不同的原因 —- 这些原因大多数主要围绕优化代码。

  • 第一,编译器优化使想要保持源代码的位置不变更困难。在LLVM IR中,我们记录每个IR指令的源代码位置。优化 passes 应该记录新创建指令的对应的源代码位置。但是针对合并的指令,我们只能记录一个源代码位置 —- 当跳过优化程序时,这会引发一个程序跳转。
  • 第二,优化模块会使用很多种方式来移动变量(改变变量的位置),例如与其它变量共享内存。

为了达到本教程的目的,我们会避免优化(下面我们会采用一些方式来避免优化)。

9.3 提前编译模式 #

为了突出对源语言增加调试信息的各个方面,而不必担心JIT的复杂性,我们将对Kaleidoscope做一些改变来支持将前端产生的IR编译为一个简单的独立程序,你可以执行、调试和查看结果。

编译IR emitted by 前端 into 一个简单独立的程序that 你可以执行,调试 and see results。

首先,我们将包含顶级语句的匿名函数作为我们的"main"函数:

-    auto Proto = llvm::make_unique<PrototypeAST>("", std::vector<std::string>());
+    auto Proto = llvm::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();

最后我们将会禁用所有的优化passes和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语言编译为可执行程序:

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

运行这条命令,会在当前目录中生成a.out/ a.exe。

9.4 编译单元 #

DWARF中一段代码的顶级容器是一个编译单元。它包含单个翻译单元的类型和函数数据(读取:源代码的一个文件)。所以我们需要做的第一件事是为fib.ks文件构造一个编译单元。

9.5 DWARF Emission Setup #

IRBuilder类似,我们有一个 DIBuilder类,可以帮助构建LLVM IR文件调试所需的原始数据。DIBuilderIRBuilder类似,只是名字更好听一点。要想使用DIBuilder,需要你对DWARF有所了解,但是如果你仔细阅读有关 元数据格式的文档,那么就对DIBuilder会更加了解。我们将使用该类来构造所有的我们的IR级描述。它的构造需要一个模块,所以我们需要在构建模块后不久就构建它。我们会把它作为一个全局静态变量,这样会使它更容易使用。

接下来,我们将创建一个小容器来缓存我们的一些经常使用的数据。第一个就是我们的编译单元,我也会为我们的类型编写一些代码,因为我们不必担心多个类型表达式(我们的类型只有double):

static DIBuilder *DBuilder;

struct DebugInfo {
  DICompileUnit *TheCU;
  DIType *DblTy;

  DIType *getDoubleTy();
} KSDbgInfo;

DIType *DebugInfo::getDoubleTy() {
  if (DblTy)
    return DblTy;

  DblTy = DBuilder->createBasicType("double", 64, dwarf::DW_ATE_float);
  return DblTy;
}

然后在我们构建模块时的’main’中:

DBuilder = new DIBuilder(*TheModule);

KSDbgInfo.TheCU = DBuilder->createCompileUnit(
    dwarf::DW_LANG_C, DBuilder->createFile("fib.ks", "."),
    "Kaleidoscope Compiler", 0, "", 0);

这里有几点需要注意。

  • 第一,当我们为一种叫做Kaleidoscope的语言生成一个编译单元时,我们会用到C语言常量。这是因为调式器不一定理解它无法识别的语言的调用约定或默认ABI,并且我们在LLVM代码生成中遵循C ABI约定,因此它是最准确的。这确保了我们可以实际调用来自调试器的函数,并且让他们执行。
  • 第二,你将会在调用 createCompileUnit 时看到 “fib.ks”。这是一个默认的硬编码值,因为我们使用shell重定向来将我们的源码放入Kaleidoscope编译器中。在前端中,你通常有一个输入文件名,并且它最终即会转到那里。

作为通过 DIBuilder 生成调试信息的一部分,最后一件事情是我们需要’finalize’调试信息。原因是DIBuilder底层API的一部分,但请确保在main结束时执行此操作:

DBuilder->finalize();

9.6 函数 #

现在我们有了我们的编译单元和源码的位置,我们可以在调试信息中添加函数定义。所以在 PrototypeAST::codegen()中,我们添加了几行代码来描述子程序的上下文,在该例子中上下文是"File",以及函数本身的实际定义。

所以上下文:

DIFile *Unit = DBuilder->createFile(KSDbgInfo.TheCU.getFilename(),
                                    KSDbgInfo.TheCU.getDirectory());

给我们一个 DIFile 并且询问我们上面创建的编译单元,来获取我们当前所在的目录和文件名字。然后,现在,我们使用 0 的一些源位置(因为我们的AST当前没有源位置信息)并且构造我们的函数定义:

DIScope *FContext = Unit;
unsigned LineNo = 0;
unsigned ScopeLine = 0;
DISubprogram *SP = DBuilder->createFunction(
    FContext, P.getName(), StringRef(), Unit, LineNo,
    CreateFunctionType(TheFunction->arg_size(), Unit),
    false /* internal linkage */, true /* definition */, ScopeLine,
    DINode::FlagPrototyped, false);
TheFunction->setSubprogram(SP);

我们现在有了一个 DISubprogram,它包含了对该函数的所有元数据的引用。

9.7 源代码位置 #

对于调试信息来说,最重要的事情是准确的源代码位置 – 这样就可以将源代码映射回来。我们现在有一个问题就是Kaleidoscope 在词法分析器和解析器中没有记录任何源代码位置信息,所以我们需要添加它:

struct SourceLocation {
  int Line;
  int Col;
};
static SourceLocation CurLoc;
static SourceLocation LexLoc = {1, 0};

static int advance() {
  int LastChar = getchar();

  if (LastChar == '\n' || LastChar == '\r') {
    LexLoc.Line++;
    LexLoc.Col = 0;
  } else
    LexLoc.Col++;
  return LastChar;
}

在这组代码中,我们添加了一些关于如何跟踪“源文件”的行和列的功能。因为我们词法分析其会记录每个token,所以我们设置我们目前的“词汇定位”设置为token开头的行和列。我们通过使用新的advance() 重载所有之前对 getchar() 的调用来跟踪信息,然后我们将所有AST类添加到源位置:

class ExprAST {
  SourceLocation Loc;

  public:
    ExprAST(SourceLocation Loc = CurLoc) : Loc(Loc) {}
    virtual ~ExprAST() {}
    virtual Value* codegen() = 0;
    int getLine() const { return Loc.Line; }
    int getCol() const { return Loc.Col; }
    virtual raw_ostream &dump(raw_ostream &out, int ind) {
      return out << ':' << getLine() << ':' << getCol() << '\n';
    }

当我们创建一个新的表达式时,我们会传递下去:

LHS = llvm::make_unique<BinaryExprAST>(BinLoc, BinOp, std::move(LHS),
                                       std::move(RHS));

为我们的每个表达式和变量提供了位置信息。

无论什么时候我们到了一个新的位置时,为了确保每条指令都获得合适的定位信息,我们必须要告诉 Builder。这时我们会使用一个小的辅助函数:

void DebugInfo::emitLocation(ExprAST *AST) {
  DIScope *Scope;
  if (LexicalBlocks.empty())
    Scope = TheCU;
  else
    Scope = LexicalBlocks.back();
  Builder.SetCurrentDebugLocation(
      DebugLoc::get(AST->getLine(), AST->getCol(), Scope));
}

这既会告诉主要的IRBuilder我们现在在哪,也会告诉它我们当前的作用域范围。作用域范围可以是编译单元级别的,也可以是最近的封闭词汇块,如当前函数。为了表示这个,我们会创建一个栈来记录范围:

std::vector<DIScope *> LexicalBlocks;

当我们开始为每个函数生成代码时,将范围(函数)推送到堆栈的顶部:

KSDbgInfo.LexicalBlocks.push_back(SP);

同时,我们还要在函数代码产生的末尾弹出作用域:

// Pop off the lexical block for the function since we added it
// unconditionally.
KSDbgInfo.LexicalBlocks.pop_back();

然后,我们会确保每次我们开始为新的AST对象产生代码时emit位置:

KSDbgInfo.emitLocation(this);

9.8 变量 #

现在我们有了函数,我们需要能够打印作用域中的变量。让我们设置我们的函数参数,这样我们就可以获得不错的 backtraces(回溯) 并且看看我们的函数是如何被调用的。这并不需要写很多代码,我们通常当我们在 FunctionAST::codegen 创建参数allocas时处理它。

// Record the function arguments in the NamedValues map.
NamedValues.clear();
unsigned ArgIdx = 0;
for (auto &Arg : TheFunction->args()) {
  // Create an alloca for this variable.
  AllocaInst *Alloca = CreateEntryBlockAlloca(TheFunction, Arg.getName());

  // Create a debug descriptor for the variable.
  DILocalVariable *D = DBuilder->createParameterVariable(
      SP, Arg.getName(), ++ArgIdx, Unit, LineNo, KSDbgInfo.getDoubleTy(),
      true);

  DBuilder->insertDeclare(Alloca, D, DBuilder->createExpression(),
                          DebugLoc::get(LineNo, 0, SP),
                          Builder.GetInsertBlock());

  // Store the initial value into the alloca.
  Builder.CreateStore(&Arg, Alloca);

  // Add arguments to variable symbol table.
  NamedValues[Arg.getName()] = Alloca;
}

在这里我们首先会创建变量,给它作用域范围(SP),名称,源位置,类型,因为它是一个参数,还需要有参数索引。下一步,我们创建一个 lvm.dbg.declare 调用来指示在 IR 级别我们在 alloca 中有一个变量(并且它给出了变量的起始位置),并在申明上为该范围的开头设置了一个源位置。

此刻需要注意的一件有趣的事情是,各种调试器会根据过去为它们生成代码和调试信息进行假设。在这个例子中,我们需要做一些hack来避免产生为函数prologue产生行信息,以便于当设置断点时,调试器知道跳过这些指令。所以我们在 FunctionAST::CodeGen 添加一些更多的行:

// Unset the location for the prologue emission (leading instructions with no
// location in a function are considered part of the prologue and the debugger
// will run past them when breaking on a function)
KSDbgInfo.emitLocation(nullptr);

然后我们实际开始为函数体生成代码时emit一个新的位置:

KSDbgInfo.emitLocation(Body.get());

有了这个,我们有足够的调试信息来设置函数中的断点,打印参数变量,并且调用函数。只需要几行简单的代码就行了!

9.9 全部代码 #

# Compile
$clang++ -g toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core mcjit native` -O3 -o toy
# Run
$./toy

http://llvm.org/docs/tutorial/LangImpl09.html#full-code-listing