Debugging C/C++ Libraries on iOS & Android with LLDB Source Mapping
This article explains why C/C++ binaries become black boxes on mobile platforms, how to inspect DWARF debug information, and provides step‑by‑step LLDB commands and a Python‑driven lldbinit script to automatically map remote source paths for seamless debugging on iOS and Android.
C/C++ offers natural cross‑platform capabilities, rich tooling, native performance, and a mature ecosystem, leading many mobile apps to embed high‑performance modules such as audio/video codecs, RPC libraries, databases, and neural‑network engines.
However, this brings hidden costs: the native toolchains (Makefile, CMake, GN, Ninja) are complex, and binaries are usually distributed as static or dynamic libraries, making the source code a black box for both low‑level module developers and business‑layer integrators.
When issues arise, developers can only rely on log statements because the provided binaries cannot be debugged directly, and the consuming side sees only assembly, severely reducing troubleshooting efficiency.
Source‑Level Debugging
LLDB can locate source files using DWARF debug information stored in __DWARF sections of Mach‑O binaries. Commands such as dwarfdump (for Mach‑O) or objdump (for ELF) reveal this information.
0x00000ed8: DW_TAG_inlined_subroutine
DW_AT_abstract_origin (0x00000ec6 "AbcAppContext")
DW_AT_ranges (0x000004e0 [0x00000506,0x00000560) [0x00000576,0x000005a2))
DW_AT_call_file ("/Users/abc/.abc/build/.../abc_engine_impl.cc")
DW_AT_call_line (37)Because the source paths embedded in DWARF do not exist locally, LLDB cannot enter source‑level debugging. Two solutions exist:
Recompile the library on the local machine so that the binary contains real source paths (requires replicating the complex build environment).
Perform “no‑compile” debugging by mapping the remote source paths to local ones.
No‑Compile Source Mapping
LLDB provides a settings set target.source-map command to remap paths. Manually entering this command each time is cumbersome, so the process can be automated with an lldbinit file and a Python script.
The script performs the following steps:
Use the LLDB Python API to locate a specified symbol and retrieve its source file path.
Assume the Python script resides in the project root to obtain the local source directory.
Iterate over the components of the remote path, constructing candidate local paths and checking for file existence to find a matching prefix.
Invoke settings set target.source-map with the discovered prefix and the local path.
Register the script as a stop‑hook in lldbinit so it runs automatically when the target stops on the chosen symbol.
# lldbinit
command script import -c debug.py
target stop-hook add -P debug.DebugHook -k "symbol" -v "::FuncAbc"Additional engineering tweaks improve the debugging experience:
Enable full debug information with the compiler flag -gfull to ensure DW_TAG_variable entries are present.
Disable stripping of symbols on Android by setting DoNotStrip for the shared library.
With these changes, C/C++ binaries can be debugged in standard IDEs on both iOS and Android, dramatically speeding up issue resolution and breaking the barrier between upstream and downstream developers.
The same approach works for Swift, Objective‑C, Rust, or any language that emits DWARF and can be debugged with LLDB.
Below are the key script snippets:
# debug.py (simplified)
import lldb, os
from pathlib import Path
class DebugHook:
def __init__(self, target, extra_args, internal_dict):
self.source_mapped = False
self.symbol_name = extra_args.GetValueForKey("symbol").GetStringValue(100)
def handle_stop(self, exe_ctx, stream):
if self.source_mapped: return
target = exe_ctx.GetTarget()
funcs = target.FindGlobalFunctions(self.symbol_name, 0, lldb.eMatchTypeNormal)
if funcs.GetSize() == 0: return
symbol = funcs.GetContextAtIndex(0)
symbol_file = "%s" % symbol.GetCompileUnit().GetFileSpec()
local_path = os.path.dirname(__file__)
comps = symbol_file.split("/")
for i in range(len(comps)):
if not comps[i]: continue
suffix = "/".join(comps[i:])
candidate = os.path.join(local_path, suffix)
if Path(candidate).is_file():
prefix = "/".join(comps[:i])
target.GetDebugger().HandleCommand(
"settings set target.source-map '%s' '%s'" % (prefix, local_path))
self.source_mapped = True
break
def __lldb_init_module(debugger, internal_dict):
print('source-debug enabled')By integrating these scripts and build‑time flags, developers gain a one‑click source‑level debugging experience for native libraries across platforms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
