Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Debug Information

The compiler writes DWARF debug information into the artifact, so that a debugger can show source lines, variables, and types.

Generate debug information

OptionEffect
-g, --debugSource lines, variables, and types
--debug-variablesThe global variables only, without source lines
--gdwarf <2..5>The same as -g, with a fixed DWARF version
--gdwarf-variables <2..5>The same as --debug-variables, with a fixed DWARF version
plc -g main.st -o app --linker=cc

The four exclude each other, so give one of them. -g already covers the global variables, and the version it writes is DWARF 5. Use a fixed version when the debugger or the runtime on the target accepts one version only.

Stepping

A debugger stops once per statement, in source order, also when the compiler reorders the machine code of neighbouring statements due to optimizations. For this, the compiler marks the one place per statement where the debugger may stop (is_stmt in the line table). A debugger that ignores these marks, such as gdb before version 10, can still jump between lines.

With -O none, the machine code is still optimized a little, for example to reuse stack memory. That optimization may drop a write that a later statement overwrites before anything reads it, and the statement would then have no stop. So with -g and -O none, the compiler marks the write that completes each statement as volatile, which the optimization must keep, in source order. Every assignment, and every element of a chart that writes a value, keeps its stop, and at each stop the statements before it have written their results. This costs some speed, because the code reads such a value from memory again instead of keeping it in a register. A start value is not such a write: before the first assignment of a variable, a debugger can still show a value that the variable never had.

A chart has no source lines. Its network gets a debug file of its own, in which the line of an element is its priority.

Why paths matter

The debug information stores the path of every source file. For local work this is fine. For a shipped binary and for remote debugging you usually want two properties:

  • no local paths of the build machine inside the artifact
  • stable paths that an IDE or gdb can map to a local checkout

Without any option, the compiler writes compile units relative to the compilation directory where it can, and the compilation directory itself can stay absolute. This follows what clang does.

Rewrite the paths

--file-prefix-map OLD=NEW rewrites every path that starts with OLD, so that it starts with NEW. Repeat the option for more mappings.

plc -g \
  --file-prefix-map /home/alice/work/MyApp=/src/MyApp \
  --file-prefix-map /home/alice/work/MyApp/build=/build/MyApp \
  app.st
  • OLD resolves against the current directory, and is canonicalized where possible.
  • NEW is used as written, and normalized for the platform.
  • When two mappings match, the longest one wins.

--debug-prefix-map is another name for the same option. It exists so that build systems can use the same spelling as with GCC and Clang.

--debug-compilation-dir <dir> sets the compilation directory of the debug information.

plc -g \
  --file-prefix-map /home/alice/work/MyApp=/src/MyApp \
  --debug-compilation-dir /src/MyApp \
  app.st

The two options are independent. The prefix map rewrites the recorded source file paths, the compilation directory sets one field of the compile unit. With both, the debug information can hold two records for one file, one for the source and one for the compile unit:

!2  = !DIFile(filename: "main.st", directory: "/SOURCE_ROOT/...")
!10 = !DIFile(filename: "/SOURCE_ROOT/.../main.st", directory: "/BUILD_ROOT")

This is how clang behaves with -ffile-prefix-map and -fdebug-compilation-dir together. A tool resolves whichever record it reads. For one canonical path, choose a prefix map that puts the source below the compilation directory.

A convention for shipped builds

Use virtual roots that exist on no machine:

  • source root /src/<Product>
  • build root /build/<Product>
plc -g \
  --file-prefix-map /real/source/root=/src/MyApp \
  --file-prefix-map /real/build/root=/build/MyApp \
  --debug-compilation-dir /src/MyApp \
  ...

The same options work with the build subcommand:

plc build plc.json -g \
  --file-prefix-map /real/source/root=/src/MyApp \
  --debug-compilation-dir /src/MyApp

Note

On Windows, prefer virtual roots to paths with a drive letter. The drive of the build machine and the drive of the developer machine are often different, and a virtual root keeps the mapping stable.

Sources above the working directory

A build that runs in a subdirectory and compiles a file above it, for example ../main.st, keeps that form:

cd examples/test
plc -g ../main.st \
  --file-prefix-map "$(pwd)=/root" \
  --debug-compilation-dir "$(pwd)"

The compile unit then holds the directory /root and the name ../main.st, and no local path of the build machine.

Map the paths in the debugger

set substitute-path /src/MyApp /home/bob/dev/MyApp
set substitute-path /build/MyApp /home/bob/dev/MyApp/build

For a remote session:

file /path/to/local/unstripped/binary
set substitute-path /src/MyApp /home/bob/dev/MyApp
target remote <host>:<port>

Projects with C and Structured Text

If the project also builds C or C++ with clang, use the same virtual roots on both sides:

-ffile-prefix-map=<real-source-root>=/src/MyApp
-ffile-prefix-map=<real-build-root>=/build/MyApp

One convention for both compilers keeps the source lookup consistent in Eclipse, in gdb, and in every debug adapter.

What’s next

That is the toolchain. The next chapter connects a project to code that is not written in Structured Text: interoperability.