Debugging LLDB with source stepping
Sometimes you want to (or need to) debug a program that you didn’t build yourself and you don’t even know how exactly it was built. Depending on the specifics of your setup that could mean many different things:
- Built on a build farm, running on a production server/container
- Installed via
apt
or similar, running locally - …
This post is inspired by my experience of debugging LLDB. Debugging the debugger is always interesting and tricky, even without the additional difficulties like trying to get the source stepping to work :)
The LLDB I was debugging was installed via apt
on Ubuntu (namely lldb-10
). Unfortunately, the binary I built from source myself didn’t reproduce the issue, so I was stuck with debugging the prebuilt one. At least the debug symbols are available, you just need to install them separately (you’re quite lucky if the already has them!). In my case that means liblldb-10-dbgsym
.
Now if you try debugging you’ll see that the debugger shows you an assembly rather than the source code, even though the debug info has the information about the context (source files, line numbers, etc):
(lldb) continue
Process 3096063 resuming
Process 3096063 stopped
* thread #1, name = 'exec', stop reason = breakpoint 2.2
frame #0: 0x00007fffefdcb750 liblldb-10.so.1`lldb::SBTarget::CreateValueFromData(char const*, lldb::SBData, lldb::SBType)
liblldb-10.so.1`lldb::SBTarget::CreateValueFromData:
-> 0x7fffefdcb750 <+0>: pushq %rbp
0x7fffefdcb751 <+1>: pushq %r15
0x7fffefdcb753 <+3>: pushq %r14
0x7fffefdcb755 <+5>: pushq %r13
This is because the debugger doesn’t have the source code! When you’re debugging the binary you’ve just built yourself this is not an issue, because the debug info references your local source code. But since the binary was built somewhere else, the original source code is not available anymore.
Luckily, all modern debuggers can do “source mapping” – you can tell where the source code is and the debugger will use it for all matched references. In LLDB one can use target.source-map
:
(lldb) settings set target.source-map /buildbot/path /my/path
First we need to figure out the original source path, i.e. the one used for building the target binary (/buildbot/path
in the example above). Since we didn’t build the binary, we don’t know the path. But that’s not a problem, because it’s saved somewhere in the debug info (otherwise the debuggers wouldn’t be able the source stepping at all).
The easiest way to extract it is to use image lookup
while debugging:
(lldb) image lookup -vn CreateValueFromData
2 matches found in /usr/lib/x86_64-linux-gnu/liblldb-10.so.1:
Address: liblldb-10.so.1[0x0000000000342750] (liblldb-10.so.1.PT_LOAD[0]..text + 1445344)
Summary: liblldb-10.so.1`::CreateValueFromData() at SBTarget.cpp:1488
Module: file = "/usr/lib/x86_64-linux-gnu/liblldb-10.so.1", arch = "x86_64"
CompileUnit: id = {0x00000031}, file = "/build/llvm-toolchain-10-GjIltB/llvm-toolchain-10-10.0.0/lldb/source/API/SBTarget.cpp", language = "c++14"
Function: id = {0x7fffffff002e1822}, name = "::CreateValueFromData()", range = [0x00007ffff712d750-0x00007ffff712db76)
FuncType: id = {0x7fffffff002e1822}, byte-size = 0, compiler_type = "void (void)"
Blocks: id = {0x7fffffff002e1822}, range = [0x7ffff712d750-0x7ffff712db76)
LineEntry: [0x00007ffff712d750-0x00007ffff712d77c): /build/llvm-toolchain-10-GjIltB/llvm-toolchain-10-10.0.0/lldb/source/API/SBTarget.cpp:1488
Symbol: id = {0x0000b0fc}, range = [0x00007ffff712d750-0x00007ffff712db76), name="lldb::SBTarget::CreateValueFromData(char const*, lldb::SBData, lldb::SBType)", mangled="_ZN4lldb8SBTarget19CreateValueFromDataEPKcNS_6SBDataENS_6SBTypeE"
...
(CreateValueFromData
is some random function from the binary we’re inspecting)
Bingo! The source path prefix we’re looking for is /build/llvm-toolchain-10-GjIltB/llvm-toolchain-10-10.0.0
.
Another way is to inspect the debug info directly.
Lookup the location of the debug info:
> readelf --debug-dump /usr/lib/llvm-10/lib/liblldb.so
Contents of the .gnu_debuglink section:
Separate debug info file: 617fb09b6875c831220b21d028cc7443dda882.debug
CRC value: 0x3e9d3f54
Peek into the actual DWARF:
> readelf --debug-dump=info /usr/lib/debug/.build-id/4b/617fb09b6875c831220b21d028cc7443dda882.debug
...
Contents of the .debug_info section:
Compilation Unit @ offset 0x0:
Length: 0xdd39 (32-bit)
Version: 4
Abbrev Offset: 0x0
Pointer Size: 8
<0><b>: Abbrev Number: 1 (DW_TAG_compile_unit)
<c> DW_AT_producer : (indirect string, offset: 0x3bded7): Debian clang version 10.0.0-4
<10> DW_AT_language : 33 (C++14)
<12> DW_AT_name : (indirect string, offset: 0x1de52): /build/llvm-toolchain-10-GjIltB/llvm-toolchain-10-10.0.0/lldb/source/API/SBAddress.cpp
<16> DW_AT_stmt_list : 0x0
<1a> DW_AT_comp_dir : (indirect string, offset: 0x4d5f6): /build/llvm-toolchain-10-GjIltB/llvm-toolchain-10-10.0.0/build-llvm/tools/clang/stage2-bins/
tools/lldb/source/API
<1e> DW_AT_low_pc : 0x0
<26> DW_AT_ranges : 0x3290
The path is in the DW_AT_name
attribute.
Now that we have a prefix we can remap the sources in the debugger via target.source-map
:
settings set target.source-map /build/llvm-toolchain-10-GjIltB/llvm-toolchain-10-10.0.0/ /home/werat/src/llvm-project/
… and starting debugging!
Process 1967456 stopped
* thread #1, name = 'main', stop reason = step over
frame #0: 0x00007ffff7039cec liblldb-10.so.1`::Initialize() at SBDebugger.cpp:152:3
149 }
150
151 void SBDebugger::Initialize() {
-> 152 LLDB_RECORD_STATIC_METHOD_NO_ARGS(void, SBDebugger, Initialize);
153 SBError ignored = SBDebugger::InitializeWithErrorHandling();
154 }
155
(lldb) n
Process 1967456 stopped
* thread #1, name = 'main', stop reason = step over
frame #0: 0x00007ffff7039d55 liblldb-10.so.1`::Initialize() at SBDebugger.cpp:153:21
150
151 void SBDebugger::Initialize() {
152 LLDB_RECORD_STATIC_METHOD_NO_ARGS(void, SBDebugger, Initialize);
-> 153 SBError ignored = SBDebugger::InitializeWithErrorHandling();
154 }
155
156 lldb::SBError SBDebugger::InitializeWithErrorHandling() {
(lldb) s
Process 1967456 stopped
* thread #1, name = 'main', stop reason = step in
frame #0: 0x00007ffff7039e1f liblldb-10.so.1`::InitializeWithErrorHandling() at SBDebugger.cpp:157
154 }
155
156 lldb::SBError SBDebugger::InitializeWithErrorHandling() {
-> 157 LLDB_RECORD_STATIC_METHOD_NO_ARGS(lldb::SBError, SBDebugger,
158 InitializeWithErrorHandling);
159
160 SBError error;
The steps above are quite typical when debugging non-locally-built binaries (e.g. debugging your payment service in production, fun 🤠 ). Depending on what kind of debug info is actually available you might not be able to do all the the usual debug operations you’re used to – for example, inspect local variables via frame variable foo
.
But hey, that’s still better than decoding the assembly yourself, right?