Kernel

Debug the kernel

Attach lldb to a running darwinOS kernel over QEMU's gdb stub (or a real serial link) and step through XNU.

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-lldb or a recent upstream lldb)
  • 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
  • -s exposes the gdb stub on TCP port 1234
  • -S pauses 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:1234

kernel.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 --frame

Set breakpoints by symbol

(lldb) b kernel_bootstrap
(lldb) b arm_init
(lldb) b launchd_main
(lldb) c

Or by source line, if you have the source tree mounted where lldb expects it:

(lldb) b osfmk/kern/startup.c:142

If lldb complains it can’t find the source, tell it where the build happened:

(lldb) settings set target.source-map . /path/to/darwinos/xnu

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) showallthreads

showalltasks 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=1

Or from lldb itself:

(lldb) call (void)panic("debugger test")

Watchpoints

(lldb) watch set variable some_global
(lldb) watch set expression -w read -- 0xffffff8000001000

QEMU’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/ttyUSB0

Everything above works identically once you’re attached.

Next steps