What had to change
The port is a stack of branches on one pinned upstream commit. Each branch owns one concern. The sections follow the branches; each compare link shows that branch’s changes only.
Portable footing
mac8/portable-host-footing
· compare ↗
mac8/x64-cpuid-host-cleanup
· compare ↗
Upstream assumes an x64 host.
-mcx16 -msse2moved from common build flags to x64-host-only flags.- x86 intrinsic headers guarded behind
ARCH_X64;ARCH_64BITwidened to include ARM64. build.shgained an env-bash shebang and a Bash ≥ 4.2 check — macOS ships 3.2.- Host CPUID compiles and executes only on x64.
POSIX parity
mac8/portable/posix-base-parity
· compare ↗
mac8/portable/posix-linker-helper-parity
· compare ↗
Brings the POSIX base layer up to parity with the Win32 one.
- File-read EOF guards, directory-iteration hardening, write-open truncation.
- Linker and helper parity so the portable linker torture tests run.
DWARF, RDI, and Mach-O
mac8/portable/dwarf-rdi-correctness
· compare ↗
mac8/portable/macho-dwarf-tools
· compare ↗
The debugger consumes symbols in its own RDI format; on macOS the platform format is DWARF in Mach-O binaries and dSYM bundles.
DW_OP_breg0–31read the wrong operand (operand_s64s[1]for[0]), so two distinct ARM64 locals reported the same wrong stack address. The fix was masked at first by a stale RDI cache — the cache key does not include converter identity.- Line-table paths were built as
dir + "/" + file; DWARF 5 makes directory entry zero the compilation directory and resolves other relative directories against it. DW_AT_high_pcaddress-versus-size handling was swapped — differently — in two places; now one helper.- Reference forms:
DW_FormKind_RefAddrresolves section-relative,RefUDataunit-relative; both were previously unresolved TODOs. - vaddr→voff rebasing no longer underflows, and inlinee and lexical-block ranges are now rebased at all.
- Line sequences split only on emitting rows; line-zero rows are kept; empty parent frame-base location lists fall back instead of iterating zero times.
New: Mach-O DWARF section extraction, radbin Mach-O and dSYM
discovery, and MACHO32/MACHO64 as radbin input formats.
The AppKit shell
mac8/mac/appkit-shell · compare ↗
A native AppKit application: bundle, windows, menus, file dialogs, input.
- The native menu bar (off by default) is generated from the debugger’s command metadata; picks return as ordinary command events. Recorded as a compromise: a second representation of the menu structure, not a shared schema with RAD’s drawn menus - this is to aid fork maintenance.
- Chrome ownership is explicit — native decorations or RAD’s custom titlebar — switched as one operation covering style mask, traffic lights, drag regions, and renderer inputs. Traffic-light spacing is measured from AppKit at runtime.
- The red traffic light closes the window while others are open and exits on the last; the mapping stays in the macOS layer instead of changing core close semantics.
- No system install: the
.appbundle is the installation unit. - RAD’s base layer claims
internal,global, andFileInfo; AppKit imports are wrapped in push/pop macro guards, and the unity build compiles the whole tree as Objective-C. - ⌘ is folded into Ctrl, so the Windows-derived keybind table works unchanged — at the cost of ⌘ and ⌃ being indistinguishable. Another compromise for fork maintenance.
CoreText and Metal
mac8/mac/coretext-metal · compare ↗
- Windows and Linux lay out in physical pixels, scaled by DPI, backing
scale 1. macOS lays out in Cocoa points, layout scale 1, backing
scale =
backingScaleFactor. - Glyphs rasterize at backing-scale resolution, keyed so 1× and 2× atlases cannot collide. Two rejected alternatives are on record: linear texture filtering (blurs the symptom, supplies no sharper glyphs) and multiplying font size by backing scale (changes layout).
- CoreText emits premultiplied, LCD-smoothed RGB; the renderer expects a coverage mask in alpha, as DWrite and FreeType provide. The provider forces grayscale AA and stores coverage in alpha — this fixed text smearing on non-Retina displays.
- D3D11 blends through an sRGB view on a UNORM swapchain; the first
Metal backend was plain
BGRA8Unormend to end, darkening every background. The shader linearizes by hand. - Redraw is now triggered using
CADisplayLinkto enable live resize.
ARM64 core and disassembly
mac8/portable/arm64-core
· compare ↗
mac8/portable/arm64-disasm
· compare ↗
Upstream understands x64 and, partially, x86; the port adds ARM64 as a first-class architecture.
- Trap and control-flow support, RDI and DWARF register mappings, vector-register metadata.
- Disassembly vendors Binary Ninja’s ARM64 disassembler at a pinned stable
tag under
src/third_party/bn_arm64_disasm: decode and format files only, MIT license alongside, upstream symbols mechanically prefixed, a small bridge on top. Chosen as the smallest self-contained disassembler covering the required instructions; swappable behind the bridge. By volume this vendoring is most of the port’s total diff: of the branch’s 104,968 added lines, about 100,000 are unmodified upstream decoder source, 4,000 a generated symbol-prefix header, and about 900 the bridge, integration, and tests. - An undecodable instruction no longer truncates the listing: on a
fixed-width ISA the view resyncs to the next word and emits
<invalid>. - Auto-disassembly no longer hard-codes
rip; the IP register comes from the process architecture, and the range is clamped to the containing module.
Darwin process control
mac8/darwin/process-control-x64 · compare ↗
The debugger’s low-level backend exists upstream for Windows and Linux. The Darwin implementation:
- Mach exceptions alongside ptrace: launch is
PT_TRACE_MEplus a task exception port, attach isPT_ATTACHEXC. Signals arrive as soft-signal exceptions that must be answered before the stopped thread can move. The target’s own exception ports are saved and restored on detach. - Trap writes that fail walk the region map, unprotect, and retry with
VM_PROT_COPYto break copy-on-write pages; ARM64 flushes the instruction cache and reads back to verify. - dyld gets two hidden traps: the notification hook — its address arrives PAC-signed and must be stripped — and a bootstrap trap on the entry point for the window before dyld has published anything.
- Data breakpoints: DR0–DR3 on x64; WVR/WCR on ARM64 with the slot count probed at runtime. New threads inherit watchpoints.
- Task ports need debugger entitlements plus an authorization requested before launching the target.
The ARM64 runtime, target calls, and TLS
mac8/darwin/arm64-runtime
· compare ↗
mac8/darwin/target-call-tls
· compare ↗
On top of process control: ARM64 registers, traps, dyld fixups, compact unwind with fallback, function-start reads, continuation traps, and runtime tests. Then target calls and TLS:
- Darwin C/C++ thread-locals are Mach-O TLV descriptors, and reading memory is not enough to locate one. The debugger has to run code inside the debugged process: call the descriptor’s thunk on the target thread, then read back the per-thread address it returns. Emulating the thunk instead was rejected — it means decoding instruction streams and second-guessing dyld.
- The call machinery reserves three pages in the target, with the middle page as the return trap. Saved registers are always restored, and “returned” is reported separately from “registers restored”.
- Target calls are used only for TLS resolution so far. The expression evaluator does not yet make arbitrary calls into the target; that is a possible extension.
- Compact unwind entries are encoding ranges, not function ranges.
Function boundaries come from
LC_FUNCTION_STARTS, and an encoding that defers to DWARF carries the offset of its exact FDE.
Fork policy
mac8/portable/metagen-preserve-line-endings
· compare ↗
mac8/portable/crash-dump-format-guards
· compare ↗
- The code generator preserves each generated file’s existing line-ending convention, so cross-platform regeneration produces no diff churn.
- Unsupported or incomplete crash-dump formats are rejected before the debugger creates a process entity for them.