Debug Swift debugging with LLDB

Description: Learn how you can set up complex Swift projects for debugging. We'll take you on a deep dive into the internals of LLDB and debug info. We'll also share best practices for complex scenarios such as debugging code built on build servers or code from custom build systems.

What does LLDB need in order to show source code?

  • When the compiler compiles a function in a .swift file, it generates machine code (store in .o object files)
  • on debug builds, .o files come with a __debug_info segment, which contains addresses in the executable that can be mapped to a source file and line number, and vice versa
  • for archiving and distribution, debug info can be linked into .dSYM bundles
  • the debug info linker is called dsymutil

image and path remap

  • use image list nameOfFramework to check whether LLDB has found the debug dSYM of a third party framework embedded in our app
  • use image lookup 0xMemoryAddressHere to get more info about the current address
  • to remap source .dSYM paths, use settings set target.source-map old/path new/path
    • tip: instead of doing this at each session, create a lldb init file that is run at the beginning of each session (point to this file in your scheme)
  • alternatively, each .dSYM bundle comes with a UUID.plist where we can set a DBGSourcePathRemapping dictionary

Source path canonicalization

  • We can instruct the compiler to canonicalize source paths before putting them into the debug info
  • This is done using the -debug-prefix-map option
  • This way the machine-specific path prefix can be replaced by a unique, canonical placeholder name that can then be remapped to the local path in LLDB
// Clang:
-fdebug-prefix-map $PWD=/BUILDROOT

// Swift:
-debug-prefix-map $PWD=/BUILDROOT

po,p,expr vs v, frame

  • LLDB is both a debugger and a compiler
  • based on whether our LLDB command sides in the debugger or compiler side of LLDB, they might or or not fail
  • (debugger side) v and frame get their type information from LLDB Debug Info (which in turn gets types from Swift reflection metadata_
  • (compiler side) p, po, expr get type information from Modules
    • Modules are how the compiler organizes type declarations

How do we start diagnosing an issue that is happening on the compiler side?

  • new in Xcode 14, we have swift-healthcheck LLDB command
  • it helps understanding if and why a module import failed
  • it saves a .log of the Swift expression evaluator configuration

How LLDB's compiler finds Swift modules?

  • It's the build system's job to package up the modules so LLDB can find them
  • Modules from system frameworks stay in the SDK (anyone can find them via $ xcrun --show-sdk-path), LLDB will find a matching SDK to read them from as it's attaching to your program
  • when debugging straight from the .o object files, LLDB will find all non-SDK modules where they were at build time
  • dsymutil can package a debug info archive (.dSYM bundle) for every dynamic library, framework or dylib, and executable
    • each .dSYM bundle can contain binary Swift modules, which may contain bridging headers .h, textual Swift interface files .swiftinterface, and most importantly, debug info.
    • for static archives, a Swift module needs to be registered with the linker (ld ... -add-ast-path /path/to/My.swiftmodule)
      • for dynamic libraries and executables, the build system will do this automatically for you. But for static archives, this is needed because static archives are not produced by the linker
      • Xcode's build system should do this for you, but you need to be aware if you have your own build system

You can check your build log to verify that that everything is linked, or use dsymutil to dump the symbol table of your executable and grep for "swiftmodule":

dsymutil -s MyApp | grep .swiftmodule

In Linux the swift driver supports a -modulewrap flag that converts binary Swift module files into objects that you can link into your binary together with the rest of the debug info. LLDB will find it there

swiftc -modulewrap My.swiftmodule -o My.swiftmodule.o

Avoiding serialized search paths in Swift modules

  • The Swift compiler will serialize Clang header search paths and other related options into the binary .swiftmodule files
  • This is great, because it makes importing their Clang module dependencies just work during the build
  • But when building on a different machine, these local paths can be detrimental
  • Before shipping a binary .swiftmodule to another machine, set the -no-serialize-debugging-options compiler flag
  • In Xcode this is controlled via the SWIFT_SERIALIZE_DEBUGGING_OPTIONS setting
  • you can then reintroduce these search paths in LLDB with one of the following settings:
settings set target.swift-extra-clang-flags …
settings set target.swift-framework-search-paths …
settings set target.swift-module-search-paths …

Missing anything? Corrections? Contributions are welcome 😃

Related

Written by

Federico Zanetello

Federico Zanetello

Software engineer with a strong passion for well-written code, thought-out composable architectures, automation, tests, and more.