Skip to content

Instantly share code, notes, and snippets.

@theosp
Last active March 30, 2025 14:43
Show Gist options
  • Select an option

  • Save theosp/0c903c74422db47c32f458114b9dcba3 to your computer and use it in GitHub Desktop.

Select an option

Save theosp/0c903c74422db47c32f458114b9dcba3 to your computer and use it in GitHub Desktop.
installing-xv6-i386-on-mac-m-series-cpus

Running & Debugging xv6 on Apple Silicon (M1/M2/M3/M4) Macs

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.


1. Install Homebrew

Visit brew.sh and run the one-line install script if you haven’t already. Then verify:

brew --version

2. Install the i386 Cross-Compiler via nativeos/i386-elf-toolchain

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

3. Install QEMU

QEMU will emulate a 32-bit machine on Apple Silicon:

brew install qemu

Check:

qemu-system-i386 --version

4. Set Environment Variables

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

5. Fix Modern Compiler Warnings in xv6

Suppress Array-Bounds Warnings and the Infinite-Recursion Warnings

Find the CFLAGS = line and append to it:

CFLAGS += -Wno-array-bounds -Wno-infinite-recursion

6. Build and Run xv6

From the xv6 source directory:

  1. Clean the build:

     make clean
    
  2. Build:

     make
    
  3. 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.


7. Troubleshooting

  • Infinite Reboot: Ensure you’re really using qemu-system-i386 (not qemu-system-x86_64), and that your disk image’s boot sector is 512 bytes ending in 0x55aa.
  • “Command not found” for i386-elf-gcc: Update your PATH or run eval "$(/opt/homebrew/bin/brew shellenv)".
  • Still stuck? Do make clean, re-check your Makefile for the flags above, and verify the environment variables TOOLPREFIX and QEMU are 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.


8. Debugging xv6 in VS Code on Apple Silicon

By default, xv6 ships a .gdbinit (auto-generated from .gdbinit.tmpl) that automatically runs:

target remote localhost:<port>
symbol-file kernel

whenever 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.”

The Fix

  • 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 kernel

This removes the “auto-connect” in .gdbinit, so you can now safely rely on VS Code’s debugger.


1. Add VS Code Debug Config

Inside .vscode/launch.json, add a Remote GDB configuration for the Microsoft C/C++ Tools extension (type: "cppdbg"):

{
  "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"
    }
  ]
}

A few things to note:

  1. "miDebuggerPath" points to /opt/homebrew/bin/gdb on 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 .entitlements file. See various online guides for details.)

  2. "miDebuggerServerAddress": "localhost:25501" – That port should match the one QEMU actually listens on. If make qemu-nox-gdb defaults to :1234 or :26000, either pick one or update your Makefile to pass -gdb tcp::25501.


2. Automate QEMU Startup/Shutdown

Inside .vscode/tasks.json, you can define two tasks:

  • "build-xv6": kills any leftover QEMU processes, then runs make 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;


3. How the Debug Flow Works

  1. Open the xv6 folder in VS Code.
  2. Press F5 or click “Run and Debug” → “Debug xv6 Kernel (Remote GDB).”
  3. VS Code runs build-xv6 → which calls make qemu-nox-gdb, so QEMU starts listening on localhost:25501.
  4. VS Code’s GDB instance connects to that port, loads symbols from kernel, and sets breakpoints.
  5. Your .gdbinit no longer does the extra target remote or symbol-file, so no double attach.
  6. Debug xv6 as usual: you can set breakpoints in .c files, step, continue, etc.
  7. Stop debugging → VS Code kills QEMU.

4. Why .gdbinit Causes the Failure

Originally, .gdbinit forcibly did:

target remote localhost:1234
symbol-file kernel

VS 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment