Skip to content

Emulator Building the HAL

pappadf edited this page Sep 14, 2026 · 1 revision

Building the HAL

This page owns: getting from a clean checkout to build/hal.dll, and what the build is doing that an ordinary one does not.

git clone https://github.com/pappadf/powermac-nt-hal.git
cd powermac-nt-hal
make

That is the whole thing. It takes a few seconds and produces build/hal.dll.

What follows is why that works at all, because building a Windows NT PowerPC binary with an open-source Linux toolchain is not something the toolchain was designed for.


1. Requirements

clang-18, lld-18 any recent LLVM works
python3 for the three build-time tools

No Microsoft toolchain. No DDK. No import libraries. That is deliberate: the historical route for building NT PowerPC code involves Microsoft's PowerPC assembler and the NT 4.0 DDK running under Wine, which works but makes the project impossible to contribute to casually. Everything here builds from packages your distribution already has.

The DDK contract — structure layouts, IRQL values, vector numbers — is restated in include/nt.h in this project's own words, with _Static_asserts where a wrong layout would otherwise be silent.


2. What the build actually does

  src/*.c  src/thunk.S
        │
        │  clang --target=powerpcle-unknown-linux-gnu -mcpu=604
        ▼
  ELF objects  ─────────────────┐
                                │  hal.imports / hal.exports
  tools/mkstubs.py  ────────────┤        │
        │                       │        ▼
        ▼                       │   imports.S / exports.S  (IAT slots, descriptors, thunks)
  ld.lld -T hal.ld --emit-relocs│
        ▼                       │
  build/hal.elf ────────────────┘
        │
        │  tools/elf2pe.py
        ▼
  build/hal.dll    ← an NT PowerPC PE

Three things are worth understanding.

mkstubs.py — the module boundary

NT PowerPC does not call functions by address; it calls them through function descriptors, and crossing a module boundary means changing the TOC register. mkstubs.py generates the assembly for both directions from two plain text files:

  • hal.imports — every kernel function the HAL calls. Produces an import-address-table slot and a stub per name.
  • hal.exports — every function the HAL publishes. Produces a descriptor per name, and a thunk that gives the kernel a stack frame of its own before calling our C.

That last clause is not optional, and the reason it exists is the most expensive bug this project has had. → The NT PowerPC ABI

elf2pe.py — ELF to PE

lld emits ELF; NT's loader wants a PE with machine type 0x1F0. elf2pe.py converts one to the other: function descriptors for the exports, an import directory naming ntoskrnl.exe with the IAT pre-filled the way NT's boot loader requires, and base relocations mapped one-to-one from the ELF relocation kinds.

The compiler flags that are not decoration

The build passes -mllvm -combiner-store-merging=false. That is not tuning — it stops clang merging adjacent byte stores into a single word store, which faults on a 604 in little-endian mode at a misaligned address. It is one of three shapes of the same trap, and the other two need volatile at the point of use rather than a flag. → Little-endian PowerPC


3. Checking the build is sound

Two checks worth running after any non-trivial change.

No byte-reversed accesses. The compiler will happily turn a hand-written endian conversion back into a single lwbrx, which faults at a misaligned address. This should print nothing:

llvm-objdump -d build/hal.elf | grep -E 'lwbrx|stwbrx|lhbrx|sthbrx'

No frame-ownership bugs. Disassemble any exported function and look at the first few instructions. If one stores to a positive offset from the incoming r1, it is writing into the caller's frame — which on NT PowerPC is where the caller's saved TOC lives:

llvm-objdump -d --disassemble-symbols=HalGetBusDataByOffset build/hal.elf | head

You should see the C function's own prologue reached through a thunk, never called directly by the kernel.


4. Reproducible builds

Two builds of the same tree are byte-identical. The PE timestamp, which would otherwise vary, comes from SOURCE_DATE_EPOCH and defaults to zero:

make && md5sum build/hal.dll
make clean && make && md5sum build/hal.dll      # same
SOURCE_DATE_EPOCH=$(date +%s) make              # if you want a real timestamp

This matters more than it usually does here, because the HAL's size is load-bearing in two separate places: the CD's directory record carries it for Setup's post-copy checksum, and every driver's load address shifts when it changes. → Making an OEM CD, Running text-mode Setup

So: whenever the HAL changes size, re-run mkoem.py.


5. What is in the source tree

src/init.c HAL initialisation phases, the configuration tree, memory descriptors
src/ints.c, irql.c interrupt dispatch and IRQL
src/clock.c the decrementer
src/pci.c Bandit configuration space, bus address translation, resource assignment
src/disk.c the partition-table exports NT puts on the HAL side
src/misc.c, arc.c the ARC environment, drive letters, the real-time clock
src/cuda.c Cuda transport, for ADB
src/vga.c, display.c the Cirrus console the HAL draws on
src/thunk.S the ABI glue, in both directions
include/nt.h the DDK contract, restated, with static asserts
include/ans.h the machine-specific addresses

Most of it is about the TNT chipset rather than the Network Server specifically; the ANS-only part is a short enumerated list — the external interrupt routing, the second Bandit, the two SCSI controllers, the on-board video and the timebase.


Next

Clone this wiki locally