Skip to content
ALB Wiki

ALB NASM backend

The ALB NASM backend is the second assembly-native emitter in AdaLogic BASIC's toolchain: same x86-64 Windows target as FASM, different assembler. Where FASM emits its own format PE64 program complete with import tables, NASM emits a COFF object file (nasm -f win64) that gets linked the traditional way — which makes it the backend you reach for when you want the object, the link step, and everything that exposes.

Why a second assembler backend

FASM assembles and links in one self-contained step — beautiful, but its PE64 emission is monolithic. The NASM route exists for the opposite reason: it produces a standard .obj you can inspect, feed to any linker, or combine with objects from other languages. It's the same emitter design ported to a different dialect — and porting it surfaced things worth seeing.

Same emitted text, two assemblers

The NASM emitter inherits the whole FASM program shape — the infix expression style, the invoke calling convention, the hardened traps, the runtime library — but NASM doesn't speak FASM's dialect. The solution is a 167-line include file, win64nasm.inc, that reimplements the FASM call dialect inside NASM's preprocessor:

%macro invoke 1-*
  %assign %%space 32
  %if %%argc > 4
    %assign %%space (((%%argc + 1) & ~1) * 8)
  %endif
  sub rsp, %%space
  __win64_movarg rcx, %2
  __win64_movarg rdx, %3
  __win64_movarg r8,  %4
  __win64_movarg r9,  %5
  …args 5+ spill to [rsp+32], [rsp+40]…
  call %1
  add rsp, %%space
%endmacro

A complete Microsoft x64 calling-convention shim — shadow space, register order, stack spill — written in preprocessor macros. It even parses argument text: addr Symbol becomes lea, and a trailing d on a register name (eax-style) triggers the zero-extending 32-bit move. The include also declares the whole extern surface (ExitProcess, CreateFileA, printf, PlaySoundA, LoadLibraryA, GetProcAddress…) and stubs out library/import.

The payoff is architectural: the emitter can write one source text that assembles under either dialect. FASM reads invoke natively; NASM reads it through this shim. One AST, two assemblers, zero duplicated codegen.

The honest gap

FASM's signature trick — the ALB_LET match-macro that fuses emitted infix expressions into tight instruction sequences at assemble time — isn't portable. NASM's preprocessor can inspect tokens (%ifidn, %substr) but can't rewrite instruction sequences the way match does. The NASM emitter says so itself, in a comment:

; NASM: passthrough macro (FASM match/rewrite not ported).
%macro ALB_LET 1+
  %1
%endmacro

The macro still exists — emitted source keeps the ALB_LET marker so the two dialects share text — but on NASM it's a documented passthrough, not an optimizer. The peephole fusion is a FASM superpower, not a NASM limitation to hide.

What it emits

The file opens in NASM's own voice:

bits 64
default rel
global start
%include "win64nasm.inc"
  • default rel — RIP-relative addressing by default, which modern linkers and ASLR want; FASM's flat model doesn't need it, COFF does
  • struc/endstruc — NASM's native record syntax carries the same runtime layouts: ALB_Fact (hash, arg hash, value, active byte) and ALB_GC_Node (alive, refs, two children)
  • Sections — .text, .data, .bss as real COFF sections instead of PE-embedded areas
  • global start — the entry is exported for the linker to find, rather than declared inside the format header

Beyond the framing, the same hardened runtime is ported wholesale: traps, strings, files, the Prolog knowledge base, temporal variables, reversible blocks, windows and pixels and sound — the feature list tracks the FASM page.

The build is the interesting part

--build on NASM is a three-tool pipeline, and each step has a story:

  1. nasm.exe -f win64 — assembles to .obj (COFF)
  2. The CRT trampoline — albnasm writes a tiny C file, alb_crt_main.c, containing int main(void) { start(); return 0; }, and compiles it. Why? The source documents the scar: "-nostdlib + custom start entry crashes inside Wicked/msvcrt bring-up (ntdll AV)" — going straight into start skipped runtime initialization that msvcrt needed, so a four-line C shim owns main and calls ALB's start after the CRT wakes up.
  3. gcc link — -Wl,--image-base,0x400000 with the same 16 MiB stack and heap as the FASM builds (-Wl,--stack,16777216 -Wl,--heap,16777216), linking kernel32/user32/gdi32/winmm/advapi32 plus SDL3 and ws2_32 — matching the feature surface, not just the frame.

The driver itself — albnasm.adb, ~41,000 lines, a full albt-scale front end — stages win64nasm.inc into the build directory, resolves nasm.exe/gcc.exe through the toolchain's bundled-tools logic, and emits the same family of build_*.bat / build_shared_* scripts the C backend stages. --dll produces a shared library through the same COFF path.

--gfx — the renderer door

albnasm also accepts --gfx=<backend> with a real roster — opengl, vulkan, d3d6…d3d12 — wired to the alb_gfx_<backend>.dll runtime. The assembly backend doesn't draw; it links to the renderer you ask for, which is exactly what a hardware-abstraction layer is for.

Under the hood

Emit_Native_NASM (~7,300 lines, SPARK-annotated) implements the extended Emit_* surface — the one shared with C: file suite, PEEK/POKE, FFI header includes and the loader prelude, the Prolog cut operator, draw/fill/blit calls, and native C blocks alongside native ASM blocks — polyglot source lowered through the assembly path.

See also

Categories: AdaLogic BASIC · Backends
This page was last edited 2026-10-16 04:34:06 UTC by ALB Team. What links here