Dynamic Debugging#

Overview and usage#

Dynamic Debugging lets developers step through unoptimized code from optimized binaries. It largely removes the historical trade-off of runtime performance for improved debuggability by providing both at the cost of longer compile times and larger object files.

The feature is enabled from Clang with -fdynamic-debugging (disabled with -fno-dynamic-debugging) and requires a compatible linker such as LLD. No additional flags are required for linking dynamic debugging objects. Objects compiled with dynamic debugging can be linked with objects compiled without it. It also requires a compatible debugger (note that LLDB does not support the feature).

The full unoptimized binary is compiled ahead of time along with the optimized version, and all of its globals reference the optimized binary (both data and code - so unoptimized functions call optimized ones). While debugging, when a breakpoint is requested the debugger patches the optimized function entry to jump to the unoptimized version. Because unoptimized functions call optimized versions, continuing execution returns execution to the optimized binary.

Important info:

  • Compiler flags apply to both versions (except optimization level, which is set to 0 for the unoptimized version).

  • Preprocessor definitions apply to both versions so the unoptimized version may differ from a standalone “debug” build (consider debug and assert macros such as NDEBUG).

  • Some optimizations are suppressed so the optimized object may be different to an optimized object built without dynamic debugging.

  • There is a compile time and object file size impact.

  • Linking an object file that has no strong external symbols multiple times into the same ELF may result in a symbol redefinition linker error when built with this feature.

Support#

  • Currently the feature is only supported with the ELF file format. Non-x86 targets are not supported yet; although they should work the feature has only been tested on x86 platforms.

  • LLDB does not support Dynamic Debugging.

  • LLD does not support --wrap=<symbol> with Dynamic Debugging.

  • Dynamic Debugging with LTO is not supported.

When adding support for additional targets, prepareForDynamicDebugging needs to be taught the correct tail-pad-to-size and tail-pad-value attribute values, which would otherwise be omitted.

Nested object design#

The broad idea is that the unoptimized program (“inner object”) is compiled and stored in a new section in the optimized version (“outer object”). “Nesting” the objects allows build systems and other tools to continue to manage a single object file per translation unit.

High level nested object design:

  • In the outer (optimized) module, globals with internal linkage not in a COMDAT are “promoted” so they can be referenced by the inner (unoptimized) module. We implement this by introducing aliases with external linkage with suffixes unique to the translation unit. The suffix is “.dyndbg.<hash of directory, file, and driver flags>”.

  • The inner (unoptimized) module contains unoptimized copies of the outer module’s functions. Their names are prefixed with “__dyndbg.”

  • Declarations corresponding to the originally external and now-promoted globals in the outer module are added to the inner module. All global references (function and data) in the inner module refer to the outer module.

  • After linking, the inner ELF is still essentially an ET_REL object. It’s the debugger’s job to extract, load, and apply relocations for the inner ELF.

This diagram illustrates the result of compiling a simple source example:

Example object layout diagram