11
$\begingroup$

I've written a compiler, and it currently outputs assembly. I'd like to make it so that when I assemble it into a final binary, I can pass the binary to a debugger and view it with source mappings, so that debugging can be performed line-by-line.

How does the source file content and program structure/semantics get provided to a debugger so that it can properly step through statements, set breakpoints, etc?

$\endgroup$
5
  • 1
    $\begingroup$ Not really applicable to the OP's case, but the Java compiler puts debugging information, etc. into specially tagged parts of the binary $\endgroup$
    – Seggan
    Commented May 22, 2023 at 14:14
  • $\begingroup$ Yeah, interpreted languages have an easier time of it. $\endgroup$ Commented May 22, 2023 at 14:23
  • $\begingroup$ it works the same in compiled languages (at least in linux ELF). It also has sections specifically for debug-related data. I'm not positive all the debug data is in there or if some is in other locations $\endgroup$
    – mousetail
    Commented May 22, 2023 at 14:32
  • $\begingroup$ I will die proving Java is not interpreted lol $\endgroup$
    – Seggan
    Commented May 22, 2023 at 14:52
  • $\begingroup$ @Seggan It isn't really the point of the question so I'd prefer not to debate it here. $\endgroup$ Commented May 22, 2023 at 15:14

2 Answers 2

5
$\begingroup$

I'll focus on the linux-specific ELF binary file format, which is what you'll get after assembling. DWARF (https://dwarfstd.org/) is the name of a very common debugging information format that is typically used with ELF. The goal is to tell a debugger how the binary data relates to the original source data.

You can - of course - investigate how existing compilers emit this data, e.g. https://godbolt.org/z/GPvv1vT1T So, you can attach your debugging information by adding the necessary directives in your assembly. If you use LLVM, the easiest approach would be to use their DBG api: https://llvm.org/docs/SourceLevelDebugging.html

$\endgroup$
4
$\begingroup$

What "debug-info" to maintain and pass to the debugger?

Anything which is present in the source but not necessary to run the program. But most commonly:

  • Debug symbols: maps registers and obfuscated variable/method names to their names in the source code
  • Source-map: maps compiled code (bytecode, transpiled code, or obfuscated code) to the original source code.
    • This can be done at different levels of granularity, but most compilers more or less either map lines of bytecode to every line of source code (such as in C or Java), or map arbitrary ranges (such as in JavaScript).
    • A powerful enough source map can completely replace debug symbols, since you can just map every compiled variable to its original name in the source.
    • However, keep in mind that the more fine-grained the source mapping is, the harder it is to track within the compiler, and often source code doesn't map so close to the compiled output anyways. In particular, release-mode optimizations very often change order of instructions, elide variables, cache operations, and more.
  • Original source filename: technically part of the source-map. This along with line numbers are essential for the user to set breakpoints.
  • Debug-info version

For a new language, I recommend providing debug symbols, a line number source-map, the original source filename, and a debug-info version. These are both relatively easy to implement in the compiler and relatively easy to use in the debugger, and they enable "breakpoints" and "step in/out/over" within the compiled code (they also enable "watchpoints" if you test a variable on every instruction, but that can be expensive).

How to maintain debug-info throughout compilation?

This will depend on your compiler design, but generally, just make sure that your AST and each of your IRs are annotated with source ranges. Any decent parser-generator including ANTLR4 and tree-sitter will preserve source ranges on syntax nodes, and it's not hard to do for a hand-rolled parser. Then, when translating the AST into a new representation (for example, bytecode instructions), just make sure to propagate the source ranges. From the source ranges, you can get the original names of variables and line number of instructions.

Store debug-info in the binary or separately?

Some compilers add the debug symbols and the source-map to the binary, while others provide them in a separate file. The benefits of embedded debug-info are less clutter and chance of desyncing / losing the debug symbols, the benefits of separate debug-info are that you can withhold the info from end-users resulting in smaller binaries and obfuscation.

In JavaScript, CSS, and some other languages, source-maps are provided a separate .map file (where foo.map is the source map for foo). In Java, the source-map and debug symbols are provided directly in the class file (Java Class File Format). C and C++, and any other language which produces object code supports both: debug symbols and source-maps can be embedded in the compiled output (.o, .a, .dylib, etc.) or in a separate .dSYM file, usually via a compiler flag (read more in this answer)

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .