Kernel debugging on darwinOS mirrors debugging on macOS — same KDP (Kernel Debugging Protocol), same lldb, similar workflow. The mechanical differences are all about the transport: over a serial cable on real hardware, or over QEMU’s built-in gdb stub in a VM.
This guide focuses on the QEMU path because it’s what you’ll use most often. The hardware flow is a variation covered briefly at the end.
Prerequisites
- An LLDB build that matches your target architecture (either the SDK’s
darwinos-lldbor a recent upstreamlldb) - A debug-profile kernel build (
./configure --profile=debug— the release kernel strips frame pointers and makes stepping unpleasant) - The QEMU guide setup working
Launch QEMU with the gdb stub
Add two flags to your usual QEMU invocation:
qemu-system-aarch64 \
… \
-s -S-sexposes the gdb stub on TCP port 1234-Spauses the guest at the first instruction so you can set breakpoints before anything runs
The VM will appear frozen — that’s expected. It’s waiting for a debugger.
Attach lldb
From a second terminal:
darwinos-lldb build/arm64-debug/kernel.elf
(lldb) gdb-remote localhost:1234kernel.elf is the unstripped kernel with full DWARF. Don’t feed lldb the bootable kernel.img — it’s a stripped Mach-O without debug info.
Once attached you’re at the kernel’s entry point, before even _start:
(lldb) register read pc
pc = 0x0000000040000000
(lldb) disassemble --frameSet breakpoints by symbol
(lldb) b kernel_bootstrap
(lldb) b arm_init
(lldb) b launchd_main
(lldb) cOr by source line, if you have the source tree mounted where lldb expects it:
(lldb) b osfmk/kern/startup.c:142If lldb complains it can’t find the source, tell it where the build happened:
(lldb) settings set target.source-map . /path/to/darwinos/xnuPrint task and thread state
darwinOS ships a set of lldb macros that mirror the kgmacros from classical XNU work:
(lldb) command script import ~/.darwinos-sdk/share/lldb/xnu_debug.py
(lldb) showalltasks
(lldb) showtask 0xffffff8000a40000
(lldb) showallthreadsshowalltasks lists every Mach task, showtask dumps a specific one, showallthreads walks every thread on every task. The rest of the macros — showvmem, showpcreg, showallkexts — follow the same pattern and are in the same script.
Panic handling
When the kernel panics, XNU drops into the kernel debugger by default. If you’re attached, lldb takes over at the panic site; if not, you’ll see the panic text on the console and the VM will hang.
To force a panic for testing, call the panic() syscall via sysctl:
sysctl debug.panic_test=1Or from lldb itself:
(lldb) call (void)panic("debugger test")Watchpoints
(lldb) watch set variable some_global
(lldb) watch set expression -w read -- 0xffffff8000001000QEMU’s gdb stub supports hardware watchpoints, so these cost nothing at runtime.
Debugging on real hardware
On real hardware the transport is serial (or KDP-over-ethernet once that lands). Wire the host to the target’s serial port, boot the target with debug=0x14e in the boot args, and point lldb at a serial device:
(lldb) gdb-remote /dev/ttyUSB0Everything above works identically once you’re attached.
Next steps
- Add a syscall — make the kernel do something new and step through it.
- Trace a syscall — non-invasive observation with DTrace.
- Write a kext — the driver-side equivalent of this flow.