Below is a concise guide for compiling and running the 32-bit xv6-public on Apple Silicon using Homebrew, a dedicated i386 cross-compiler, and QEMU. These steps address the typical “infinite reboot” and “array-bounds” errors that often occur on modern compilers and Apple hardware.
Visit brew.sh and run the one-line install script if you haven’t already. Then verify:
brew --version
Homebrew no longer ships an official i386 cross-compiler, but the tap nativeos/i386-elf-toolchain provides one. Install:
brew tap nativeos/i386-elf-toolchain
brew install nativeos/i386-elf-toolchain/i386-elf-binutils
brew install nativeos/i386-elf-toolchain/i386-elf-gcc
Check the toolchain is in your PATH:
which i386-elf-gcc
i386-elf-gcc -v
If brew can’t find it, ensure /opt/homebrew/bin (or /usr/local/bin on Intel Macs) is in your shell’s PATH. For example:
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zshrc
source ~/.zshrc
QEMU will emulate a 32-bit machine on Apple Silicon:
brew install qemu
Check:
qemu-system-i386 --version
In the Makefile, find the following vars, ensure to remove the # infront of them and set them to:
TOOLPREFIX=i386-elf-
QEMU=qemu-system-i386
Suppress Array-Bounds Warnings and the Infinite-Recursion Warnings
Find the CFLAGS = line and append to it:
CFLAGS += -Wno-array-bounds -Wno-infinite-recursion
From the xv6 source directory:
-
Clean the build:
make clean -
Build:
make -
Boot xv6 in QEMU:
make qemu-nox
(Or make qemu if you want a graphical window. qemu-nox runs xv6 with a serial console in your terminal.)
You should see xv6 load and drop you into its shell prompt.
- Infinite Reboot: Ensure you’re really using
qemu-system-i386(notqemu-system-x86_64), and that your disk image’s boot sector is 512 bytes ending in0x55aa. - “Command not found” for
i386-elf-gcc: Update your PATH or runeval "$(/opt/homebrew/bin/brew shellenv)". - Still stuck? Do
make clean, re-check yourMakefilefor the flags above, and verify the environment variablesTOOLPREFIXandQEMUare set.
Below is a new section you can add at the end of your existing “Running xv6 on Apple Silicon…” guide. It explains how to debug xv6 using GDB in Visual Studio Code—without the conflicts that arise when .gdbinit auto-attaches to QEMU.
By default, xv6 ships a .gdbinit (auto-generated from .gdbinit.tmpl) that automatically runs:
target remote localhost:<port>
symbol-file kernelwhenever you start GDB. That’s great if you manually run:
gdb kernel
(gdb) ...
But VS Code also issues its own target-select remote and symbol-file commands. When both happen, you get a “double attach” and QEMU typically responds with a timeout or “Terminated via GDBstub.”
- Remove (or comment out) those auto-attach lines in
.gdbinit.tmpl(which in turn regenerates.gdbinit). - Then let VS Code handle attaching and loading the kernel symbol file.
Example commit (showing the minimal changes):
--- a/.gdbinit.tmpl
+++ b/.gdbinit.tmpl
@@ -19,9 +19,3 @@ define hook-stop
end
set $lastcs = $cs
end
-
-echo + target remote localhost:1234\n
-target remote localhost:1234
-
-echo + symbol-file kernel\n
-symbol-file kernelThis removes the “auto-connect” in .gdbinit, so you can now safely rely on VS Code’s debugger.
Inside .vscode/launch.json, add a Remote GDB configuration for the Microsoft C/C++ Tools extension (type: "cppdbg"):
A few things to note:
-
"miDebuggerPath"points to/opt/homebrew/bin/gdbon Apple Silicon. By default, macOS requires gdb to be codesigned. If you see permissions errors, you need to sign gdb. For example:codesign -s gdb-cert --entitlements gdb-entitlement.xml /opt/homebrew/bin/gdb
(Use your own certificate name and
.entitlementsfile. See various online guides for details.) -
"miDebuggerServerAddress": "localhost:25501"– That port should match the one QEMU actually listens on. Ifmake qemu-nox-gdbdefaults to:1234or:26000, either pick one or update your Makefile to pass-gdb tcp::25501.
Inside .vscode/tasks.json, you can define two tasks:
"build-xv6": kills any leftover QEMU processes, then runsmake qemu-nox-gdb."kill-qemu": kills QEMU on debug exit.
For example:
{
"version": "2.0.0",
"tasks": [
{
"label": "build-xv6",
"type": "shell",
"command": "killall qemu-system-i386; killall qemu; make qemu-nox-gdb",
"group": { "kind": "build", "isDefault": true },
"isBackground": true,
"problemMatcher": {
"pattern": {
"regexp": "^.*$",
"file": 1,
"location": 2,
"message": 3
},
"background": {
"activeOnStart": true,
"beginsPattern": "^.*",
"endsPattern": "\\*\\*\\* Now run 'gdb'\\."
}
},
"presentation": {
"reveal": "always",
"panel": "shared"
}
},
{
"label": "kill-qemu",
"type": "shell",
"command": "pkill -f qemu; pkill -f qemu-system-i386",
"presentation": {
"reveal": "silent"
}
}
]
}- When you start debugging, VS Code runs
"preLaunchTask": "build-xv6". That kills old QEMU instances and launches a new one in GDB stub mode. - Once QEMU prints “Now run ‘gdb’.”, the background task is considered ready, and VS Code attaches GDB.
- When you stop debugging,
"postDebugTask": "kill-qemu"runs, cleaning up any leftover QEMU processes.
**BEFORE YOU USE THIS FLOW FOR THE FIRST TIME RUM: $ make clean;
- Open the xv6 folder in VS Code.
- Press F5 or click “Run and Debug” → “Debug xv6 Kernel (Remote GDB).”
- VS Code runs
build-xv6→ which callsmake qemu-nox-gdb, so QEMU starts listening onlocalhost:25501. - VS Code’s GDB instance connects to that port, loads symbols from
kernel, and sets breakpoints. - Your .gdbinit no longer does the extra
target remoteorsymbol-file, so no double attach. - Debug xv6 as usual: you can set breakpoints in
.cfiles, step, continue, etc. - Stop debugging → VS Code kills QEMU.
Originally, .gdbinit forcibly did:
target remote localhost:1234
symbol-file kernelVS Code also runs:
-target-select remote localhost:XXXX
-file-exec-and-symbols /path/to/kernel
Double-attaching to the same QEMU GDB stub triggers timeouts or “Terminated via GDBstub.” Removing those lines from .gdbinit ensures only one attach attempt.
If you still want .gdbinit to auto-attach for manual debugging, you can define a custom command:
define attach_xv6
target remote localhost:1234
symbol-file kernel
end…instead of calling target remote directly. Then, from a manual GDB session, you can type:
(gdb) attach_xv6
But for VS Code debugging, it’s recommended to keep the .gdbinit lines commented out, letting VS Code handle all the attach logic.
{ "version": "0.2.0", "configurations": [ { "name": "Debug xv6 Kernel (Remote GDB)", "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/kernel", "cwd": "${workspaceFolder}", "MIMode": "gdb", "miDebuggerPath": "/opt/homebrew/bin/gdb", "miDebuggerServerAddress": "localhost:25501", "externalConsole": false, // Add these lines to enable logging: "logging": { "engineLogging": true, "trace": true, "traceResponse": true }, "preLaunchTask": "build-xv6", "postDebugTask": "kill-qemu" }, // Dynamic configuration for debugging any xv6 user program { "name": "Debug xv6 User Program (Remote GDB)", "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/_${input:userProgramName}", "cwd": "${workspaceFolder}", "MIMode": "gdb", "miDebuggerPath": "/opt/homebrew/bin/gdb", "miDebuggerServerAddress": "localhost:25501", "externalConsole": false, "logging": { "engineLogging": true, "trace": true, "traceResponse": true }, "preLaunchTask": "build-xv6", "postDebugTask": "kill-qemu", "symbolLoadInfo": { "loadAll": true, "exceptionList": "" } } ], "inputs": [ { "id": "userProgramName", "type": "promptString", "description": "Enter the name of the xv6 user program to debug (without _ prefix)", "default": "ls" } ] }