| # Reactor Debug Info Generation | |
| ## Introduction | |
| Reactor produces Just In Time compiled dynamic executable code and can be used to JIT high performance functions specialized for runtime | |
| configurations, or to even build a compiler. | |
| In order to debug executable code at a higher level than disassembly, source code files are required. | |
| Reactor has two potential sources of source code: | |
| 1. The C++ source code of the program that calls into Reactor. | |
| 2. External source files read by the program and passed to Reactor. | |
| While case (2) is preferable for implementing a compiler, this is currently not | |
| implemented. | |
| Reactor implements case (1) and this can be used by GDB to single line step and | |
| inspect variables. | |
| ## Supported Platforms | |
| Currently: | |
| * Debug info generation is only supported on Linux with the LLVM 7 | |
| backend. | |
| * GDB is the only supported debugger. | |
| * The program must be compiled with debug info iteself. | |
| ## Enabling | |
| Debug generation is enabled with `REACTOR_EMIT_DEBUG_INFO` CMake flag (defaults | |
| to disabled). | |
| ## Implementation details | |
| ### Source Location | |
| All Reactor functions begin with a call to `RR_DEBUG_INFO_UPDATE_LOC()`, which calls into `rr::DebugInfo::EmitLocation()`. | |
| `rr::DebugInfo::EmitLocation()` calls `rr::DebugInfo::getCallerBacktrace()`, | |
| which in turn uses [`libbacktrace`](https://github.com/ianlancetaylor/libbacktrace) | |
| to unwind the stack and find the file, function and line of the caller. | |
| This information is passed to `llvm::IRBuilder<>::SetCurrentDebugLocation` | |
| to emit source line information for the next LLVM instructions to be built. | |
| ### Variables | |
| There are 3 aspects to generating variable debug information: | |
| #### 1. Variable names | |
| Constructing a Reactor `LValue`: | |
| ```C++ | |
| rr::Int a = 1; | |
| ``` | |
| Will emit an LLVM `alloca` instruction to allocate the storage of the variable, | |
| and emit another to initialize it to the constant `1`. While fluent, none of the | |
| Reactor calls see the name of the C++ local variable "`a`", and the LLVM `alloca` | |
| value gets a meaningless numerical value. | |
| There are two potential ways that Reactor can obtain the variable name: | |
| 1. Use the running executable's own debug information to examine the local | |
| declaration and extract the local variable's name. | |
| 2. Use the backtrace information to parse the name from the source file. | |
| While (1) is arguably a cleaner and more robust solution, (2) is | |
| easier to implement and can work for the majority of use cases. | |
| (2) is the current solution implemented. | |
| `rr::DebugInfo::getOrParseFileTokens()` scans a source file line by line, and | |
| uses a regular expression to look for patterns of `<type> <name>`. Matching is not | |
| precise, but is adequate to find locals constructed with and without assignment. | |
| #### 2. Variable binding | |
| Given that we can find a variable name for a given source line, we need a way of | |
| binding the LLVM values to the name. | |
| Given our trivial example: | |
| ```C++ | |
| rr::Int a = 1 | |
| ``` | |
| The `rr::Int` constructor calls `RR_DEBUG_INFO_EMIT_VAR()` passing the storage | |
| value as single argument. `RR_DEBUG_INFO_EMIT_VAR()` performs the backtrace | |
| to find the source file and line and uses the token information produced by | |
| `rr::DebugInfo::getOrParseFileTokens()` to identify the variable name. | |
| However, things get a bit more complicated when there are multiple variables | |
| being constructed on the same line. | |
| Take for example: | |
| ```C++ | |
| rr::Int a = rr::Int(1) + rr::Int(2) | |
| ``` | |
| Here we have 3 calls to the `rr::Int` constructor, each calling down | |
| to `RR_DEBUG_INFO_EMIT_VAR()`. | |
| To disambiguate which of these should be bound to the variable name "`a`", | |
| `rr::DebugInfo::EmitVariable()` buffers the binding into | |
| `scope.pending` and the last binding for a given line is used by | |
| `DebugInfo::emitPending()`. For variable construction and assignment, C++ | |
| guarantees that the LHS is the last value to be constructed. | |
| This solution is not perfect. | |
| Multi-line expressions, multiple assignments on a single line, macro obfuscation | |
| can all break variable bindings - however the majority of typical cases work. | |
| #### 3. Variable scope | |
| `rr::DebugInfo` maintains a stack of `llvm::DIScope`s and `llvm::DILocation`s | |
| that mirrors the current backtrace for function being called. | |
| A synthetic call stack is produced by chaining `llvm::DILocation`s with | |
| `InlinedAt`s. | |
| For example, at the declaration of `i`: | |
| ```C++ | |
| void B() | |
| { | |
| rr::Int i; // <- here | |
| } | |
| void A() | |
| { | |
| B(); | |
| } | |
| int main(int argc, const char* argv[]) | |
| { | |
| A(); | |
| } | |
| ``` | |
| The `DIScope` hierarchy would be: | |
| ```C++ | |
| DIFile: "foo.cpp" | |
| rr::DebugInfo::diScope[0].di: ↳ DISubprogram: "main" | |
| rr::DebugInfo::diScope[1].di: ↳ DISubprogram: "A" | |
| rr::DebugInfo::diScope[2].di: ↳ DISubprogram: "B" | |
| ``` | |
| The `DILocation` hierarchy would be: | |
| ```C++ | |
| rr::DebugInfo::diRootLocation: DILocation(DISubprogram: "ReactorFunction") | |
| rr::DebugInfo::diScope[0].location: ↳ DILocation(DISubprogram: "main") | |
| rr::DebugInfo::diScope[1].location: ↳ DILocation(DISubprogram: "A") | |
| rr::DebugInfo::diScope[2].location: ↳ DILocation(DISubprogram: "B") | |
| ``` | |
| Where '↳' represents an `InlinedAt`. | |
| `rr::DebugInfo::diScope` is updated by `rr::DebugInfo::syncScope()`. | |
| `llvm::DIScope`s typically do not nest - there is usually a separate | |
| `llvm::DISubprogram` for each function in the callstack. All local variables | |
| within a function will typically share the same scope, regardless of whether | |
| they are declared within a sub-block. | |
| Loops and jumps within a function add complexity. Consider: | |
| ```C++ | |
| void B() | |
| { | |
| rr::Int i = 0; | |
| } | |
| void A() | |
| { | |
| for (int i = 0; i < 3; i++) | |
| { | |
| rr::Int x = 0; | |
| } | |
| B(); | |
| } | |
| int main(int argc, const char* argv[]) | |
| { | |
| A(); | |
| } | |
| ``` | |
| In this particular example Reactor will not be aware of the `for` loop, and will | |
| attempt to create three variables called "`x`" in the same function scope for `A()`. | |
| Duplicate symbols in the same `llvm::DIScope` result in undefined behavior. | |
| To solve this, `rr::DebugInfo::syncScope()` observes when a function jumps | |
| backwards, and forks the current `llvm::DILexicalBlock` for the function. This | |
| results in a number of `llvm::DILexicalBlock` chains, each declaring variables | |
| that shadow the previous block. | |
| At the declaration of `i`, the `DIScope` hierarchy would be: | |
| ```C++ | |
| DIFile: "foo.cpp" | |
| rr::DebugInfo::diScope[0].di: ↳ DISubprogram: "main" | |
| ↳ DISubprogram: "A" | |
| | ↳ DILexicalBlock: "A".1 | |
| rr::DebugInfo::diScope[1].di: | ↳ DILexicalBlock: "A".2 | |
| rr::DebugInfo::diScope[2].di: ↳ DISubprogram: "B" | |
| ``` | |
| The `DILocation` hierarchy would be: | |
| ```C++ | |
| rr::DebugInfo::diRootLocation: DILocation(DISubprogram: "ReactorFunction") | |
| rr::DebugInfo::diScope[0].location: ↳ DILocation(DISubprogram: "main") | |
| rr::DebugInfo::diScope[1].location: ↳ DILocation(DILexicalBlock: "A".2) | |
| rr::DebugInfo::diScope[2].location: ↳ DILocation(DISubprogram: "B") | |
| ``` | |
| ### Debugger integration | |
| Once the debug information has been generated, it needs to be handed to the | |
| debugger. | |
| Reactor uses [`llvm::JITEventListener::createGDBRegistrationListener()`](http://llvm.org/doxygen/classllvm_1_1JITEventListener.html#a004abbb5a0d48ac376dfbe3e3c97c306) | |
| to inform GDB of the JIT'd program and its debugging information. | |
| More information [can be found here](https://llvm.org/docs/DebuggingJITedCode.html). | |
| LLDB should be able to support this same mechanism, but at the time of writing | |
| this does not appear to work. | |