-
Notifications
You must be signed in to change notification settings - Fork 292
Debugging Child Processes
vadimcn edited this page Feb 1, 2026
·
1 revision
LLDB can debug only one process at a time after fork or posix_spawn. It can follow either the parent or the child via target.process.follow-fork-mode, detaching the other.
This script approximates multiprocess debugging in CodeLLDB by automatically attaching to child PIDs when the parent calls clone3.
Limitations:
- Linux only (glibc oriented), though a similar approach should be possible on other OSes.
- The child runs briefly before the debugger attaches, so breakpoints in early startup code may be missed.
Usage: Add "preRunCommands": ["command script import <script path>"] to your VS Code launch configuration.
import lldb
import codelldb
def __lldb_init_module(debugger, internal_dict):
target = debugger.GetTargetAtIndex(0)
# Break on libc wrapper of the clone3 syscall (https://github.com/bminor/glibc/blob/master/sysdeps/unix/sysv/linux/arm/clone3.S)
# This is where spawn*(), fork(), etc function calls end up just before plunging into the kernel.
bp = target.BreakpointCreateByName('__clone3')
bp.SetScriptCallbackFunction('debug_child_process.on_clone3')
def on_clone3(frame, bp_loc, internal_dict):
thread = frame.thread
process = thread.process
debugger = process.target.debugger
# thread_create() calls also end up here, however they do not create a new process, so we need to filter them out:
# Parameters are passed as a pointer to `clone_args` struct, the pointer itself being in the rdi register.
args_addr = frame.FindRegister('rdi').GetValueAsAddress()
flags = process.ReadUnsignedFromMemory(args_addr, 8, lldb.SBError()) # Read clone_args.flags field.
if flags & 0x00010000: # CLONE_THREAD
return False # Tell LLDB to skip this breakpoint
# Disable async execution.
# Normally CodeLLDB uses the async mode, so that calls like StepOut() or Continue()
# return immediately, while the debugger tracks execution via event notifications.
# In this case, though, we want StepOut() to block until the debuggee stops again.
try:
debugger.SetAsync(False)
# Execute the rest of the function, stop immediately after return.
thread.StepOut()
finally:
# Restore async mode.
debugger.SetAsync(True)
# The returned child process id is in the rax register.
child_pid = thread.GetFrameAtIndex(0).FindRegister('rax').GetValueAsUnsigned()
# Ask VSCode to attach to the child process.
codelldb.start_debugging('attach', {'pid': child_pid, 'stopOnEntry': True})
print('Attaching to', child_pid)
# Resume the debuggee.
process.Continue()
return False