Compare Compiler Debug Information with llvm-debuginfo-analyzer

A debugger single-steps fine with one toolchain and loses the plot with another, and llvm-debuginfo-analyzer-18 is how you find out why. It turns the debug sections in an object file into a readable logical view, narrows that view to the elements you care about, and compares the semantics of two builds side by side. That last part matters most when a compiler change may have altered what a debugger can actually see.

Allow 15 to 20 minutes for the examples. You need LLVM 18, a C or C++ compiler, and object files built with debug information. The installed command on this Ubuntu system is LLVM 18.1.3 from package llvm-18. These checks only read object files and write to standard output, so nothing here needs elevated privileges.

1. Check the installed command

Confirm which executable will run and which LLVM build supplied it:

$ command -v llvm-debuginfo-analyzer-18
/usr/bin/llvm-debuginfo-analyzer-18
$ llvm-debuginfo-analyzer-18 --version
Ubuntu LLVM version 18.1.3
  Optimized build.

The manual calls the command llvm-debuginfo-analyzer, but this distribution installs the versioned executable. Use the versioned name in scripts whenever the workflow needs to stay tied to LLVM 18.

Checkpoint: if the command is missing, stop here and install the matching distribution package through your normal package-management process. Do not paper over a missing executable by assuming a different llvm-debuginfo-analyzer on the box has the same option set.

2. Build two debug objects

For a reproducible comparison, compile the same small source file with two compilers. The important flags are -g, which emits debug information, and -O0, which keeps this example easy to follow:

$ cat > /tmp/debug-view.cpp <<'EOF'
using INTPTR = const int *;
int foo(INTPTR ptr, unsigned count, bool enabled) {
  if (enabled) {
    typedef int INTEGER;
    const INTEGER value = 7;
    return value;
  }
  return (int)count;
}
EOF
$ clang++ -g -O0 -c /tmp/debug-view.cpp -o /tmp/debug-clang.o
$ g++ -g -O0 -c /tmp/debug-view.cpp -o /tmp/debug-gcc.o

Verify the objects exist before analysing them:

$ file /tmp/debug-clang.o /tmp/debug-gcc.o
/tmp/debug-clang.o: ELF 64-bit LSB relocatable, ...
/tmp/debug-gcc.o:   ELF 64-bit LSB relocatable, ...

The exact wording from file varies by release. The check that matters is that both paths are readable object files, not source files or stripped binaries.

3. Print a useful logical view

The default output is already a logical view, but explicit options make a saved diagnostic easier to read and repeat later. This command includes lexical levels, object formats and compiler producers, then prints scopes, symbols, types, lines and assembler instructions sorted by internal offset:

$ llvm-debuginfo-analyzer-18 \
    --attribute=level,format,producer \
    --output-sort=offset \
    --print=elements \
    /tmp/debug-clang.o

--print=elements is shorthand for instructions, lines, scopes, symbols and types. In the output, a file sits at level 0 and a compile unit at level 1; deeper levels are lexical nesting, and a row can carry a source line plus a named element such as a function, parameter, variable or type alias.

For a smaller first pass, print only symbols and types:

$ llvm-debuginfo-analyzer-18 \
    --attribute=level,format,producer \
    --output-sort=name \
    --print=symbols,types \
    /tmp/debug-clang.o

Sorting defaults to source line, but kind, name and offset are also available. An absent line number is not proof an element is missing: low-level debug formats simply do not all carry the same detail.

4. Select the elements that matter

Large object files produce noisy reports. The general --select pattern matches element names or line numbers; add --select-nocase for case-insensitive matching, and --select-regex when the patterns should be regular expressions:

$ llvm-debuginfo-analyzer-18 \
    --attribute=level,format \
    --select-nocase \
    --select=foo \
    --report=list \
    --print=scopes,symbols,types,summary \
    /tmp/debug-clang.o

The list report is tabular with no parent-child tree, and the manual says it is chosen automatically once a selection is supplied without an explicit report option. Naming it yourself stops a later edit from quietly changing the shape of a script's output.

These accepted kinds describe the debug format's logical model, not arbitrary source-language words, so reach for them when a plain name pattern is too broad.

Checkpoint: a selected report can include a summary such as Types ... Found or Printed. Use it as a quick sanity check that the filter matched anything at all, then inspect the actual rows before drawing a conclusion.

5. Compare two builds

Comparison is the useful diagnostic when both objects represent the same source but came from different compiler versions, options or debug formats. The first file supplied is the reference and the second is the target:

$ llvm-debuginfo-analyzer-18 \
    --attribute=level \
    --compare=types \
    --report=list \
    --print=symbols,types,summary \
    /tmp/debug-clang.o /tmp/debug-gcc.o

The result labels entries as missing or added and includes an expected, missing and added count. In this example, the same INTEGER alias can turn up at different lexical levels in Clang and GCC output: that is a semantic difference in the debug information, not necessarily a difference in the compiled program.

--compare=all covers lines, scopes, symbols and types together. Reach for --report=view when the surrounding parents and children matter, since a plain text diff cannot reliably compare DWARF against CodeView or account for the different internal layouts.

Reverse the two input paths to check whether the same change gets classified the same way in both directions. Do not call the target "wrong" just because it differs: the manual is explicit that comparison accuracy depends on how closely each format represents the original source.

6. Record warnings and deeper detail

Warnings are collected only when requested, then printed under the warnings output category:

$ llvm-debuginfo-analyzer-18 \
    --warning=all \
    --print=warnings \
    /tmp/debug-clang.o

The warning categories cover invalid symbol coverage, invalid locations, invalid code ranges and zero debug lines. A warning is evidence about the debug information; it is not automatically evidence that the executable is unsafe or the source program is wrong.

For a low-level investigation, --attribute=all and --print=all expose offsets, ranges, locations, linkage and more, and the output can get large fast. Redirect it to a new file rather than overwriting an existing report:

$ llvm-debuginfo-analyzer-18 \
    --attribute=all \
    --print=all \
    --output-file=/tmp/debug-clang-full.txt \
    /tmp/debug-clang.o
$ test -s /tmp/debug-clang-full.txt && echo 'report written'
report written

--output-file=- selects standard output. Split output is a separate mode: use --output=split with --output-folder=NAME to write one file per compilation unit. Create and inspect that folder first, because replacing an existing report directory is a destructive step that sits outside this command's own safety net.

7. Handle failures without guessing

The documented exit status is 0 when the input files are parsed and printed successfully, and 1 otherwise. A missing file looks like this:

$ llvm-debuginfo-analyzer-18 /tmp/does-not-exist.o
llvm-debuginfo-analyzer: error: File '/tmp/does-not-exist.o' does not exist..
$ printf '%s\n' "$?"
1

Check paths and read permissions before changing anything. If no input file is supplied, the command tries a.out. A single - reads from standard input. Response files are supported with @FILE, but keep them under review since they can hide options and filenames from the command line you are actually reading.

Warning: there is no reason to run this analyser as root. If it cannot read an object, fix ownership or permissions through your normal controlled process, then rerun the read-only command. Preserve the original objects and reports until you have captured the compiler versions, flags and comparison order.

Done means