Diagrams

This page collects every architecture and flow diagram used across the docs in one place. Each diagram links back to the page it illustrates, and each of those pages links here.

Framework build and run pipeline

How the assembly routines, C tests, build, and runner fit together — from asm-test.

        flowchart TB
    subgraph Author["You write"]
        ASM["Assembly routines<br/>examples/foo.s · foo.asm"]
        CT["C test cases<br/>TEST(...) + ASSERT_*"]
    end
    subgraph Build["Build (Makefile)"]
        AS["Assemble .s via cc (GAS)<br/>or nasm (Intel syntax)"]
        CC["Compile C tests +<br/>libasmtest runtime"]
        LD["Link one binary per suite<br/>build/test_foo"]
    end
    subgraph Run["Run the suite binary"]
        MAIN["Framework main()<br/>discover + filter + order tests"]
        ENG{"Execution engine"}
        NAT["Native tier — capture trampoline<br/>real CPU, real ABI"]
        EMU["Emulator tier (optional)<br/>Unicorn virtual CPU"]
        REP["Assert on return / registers /<br/>flags / memory / faults"]
        OUT["Colored TAP or JUnit XML<br/>+ nonzero exit on any failure"]
    end
    ASM --> AS
    CT --> CC
    AS --> LD
    CC --> LD
    LD --> MAIN --> ENG
    ENG -->|"ASM_CALLn"| NAT
    ENG -->|"emu_call"| EMU
    NAT --> REP
    EMU --> REP
    REP --> OUT
    

Test lifecycle states

The states each test moves through in the runner — from Writing tests.

        stateDiagram-v2
    [*] --> Registered
    Registered --> Selected : main() filters / shuffles the registry
    Selected --> Setup : run next test (forked child by default)
    Setup --> Body : SETUP(suite)
    Body --> Passed : all assertions held
    Body --> Failed : ASSERT_* failed, siglongjmp
    Body --> Skipped : SKIP(reason), siglongjmp
    Body --> Crashed : SIGSEGV / SIGBUS or alarm() timeout
    Passed --> Teardown
    Failed --> Teardown
    Skipped --> Teardown
    Crashed --> Teardown : parent rebuilds verdict from wait() status
    Teardown --> Report : TEARDOWN(suite)
    Report --> Selected : more tests remain
    Report --> Summary : registry exhausted
    Summary --> [*] : exit nonzero if any test failed
    

Runner fork-per-test lifecycle

How the parent runner and each forked child exchange a verdict — from The test runner.

        sequenceDiagram
    participant R as Runner (parent)
    participant C as Forked child
    participant T as Routine under test
    R->>C: fork() — one child per test
    Note over C: arm alarm(timeout) +<br/>SIGSEGV / SIGBUS handler
    C->>T: run test body (real ABI call)
    alt passes / fails / skips
        T-->>C: verdict via siglongjmp
        C->>R: write PASS/FAIL/SKIP + msg + location over pipe
    else infinite loop
        Note over C: alarm fires, SIGALRM
        C->>R: write "timed out"
    else hard crash (SIGABRT-class)
        Note over C: child dies before writing
        R->>R: synthesize result from wait() status
    end
    R->>R: reap_child(), record in registration order
    Note over R: -jN keeps N children in flight,<br/>report order stays deterministic
    

Capture trampoline

How ASM_CALLn runs a routine through the real ABI and snapshots register state — from ABI capture & registers.

        flowchart LR
    M["ASM_CALL2(r, fn, 2, 3)"] --> T
    subgraph T["capture trampoline — capture.s"]
        direction TB
        S1["Seed callee-saved regs<br/>with sentinels (0x1111…, 0x2222…)"]
        S2["Marshal args into ABI arg registers<br/>rdi,rsi,… / x0,x1,…"]
        S3["Real CALL into the routine"]
        S4["Snapshot all GP regs + RFLAGS<br/>(+ FP / vector file for _fp / _vec)"]
        S1 --> S2 --> S3 --> S4
    end
    T --> R["regs_t r<br/>ret · flags · callee-saved · fret · vec[]"]
    R --> A1["ASSERT_EQ(r.ret, 5)"]
    R --> A2["ASSERT_ABI_PRESERVED(r)"]
    R --> A3["ASSERT_FLAG_CLEAR(r, CF)"]
    

Register snapshot layouts across ABIs

The regs_t shape under each calling convention — from ABI capture & registers.

        flowchart LR
    subgraph SV["regs_t — x86-64 System V"]
        SVf["ret = rax (return value)<br/>rdx = second return register<br/>rbx, rbp, r12–r15 = callee-saved<br/>flags = RFLAGS (CF/PF/ZF/SF/OF)<br/>fret = xmm0 (FP return)<br/>vec[16] = xmm0–xmm15"]
    end
    subgraph W64["regs_t — x86-64 Win64"]
        W64f["ret, rdx = return registers<br/>rbx, rbp, r12–r15 = callee-saved<br/>rdi, rsi = callee-saved (Win64 only)<br/>flags = RFLAGS<br/>fret = xmm0<br/>vec[16] = xmm6–15 also callee-saved"]
    end
    subgraph A64["regs_t — AArch64 AAPCS64"]
        A64f["ret = x0 (return value)<br/>x19–x28, x29 = callee-saved<br/>flags = NZCV<br/>fret = d0 (FP return)<br/>vec[32] = v0–v31"]
    end
    

Assertion families

The six families of ASSERT_* macros — from Assertions.

        flowchart LR
    A["ASSERT_* families"] --> V["Value"]
    A --> MEM["Memory / string"]
    A --> REG["Register / flags / ABI"]
    A --> FPV["Floating-point / SIMD"]
    A --> PROP["Differential / property"]
    A --> EMU["Emulator"]
    V --> V1["ASSERT_TRUE / FALSE"]
    V --> V2["ASSERT_EQ/NE/LT/LE/GT/GE (signed)"]
    V --> V3["ASSERT_UEQ/UNE/ULT/ULE/UGT/UGE (unsigned hex)"]
    MEM --> M1["ASSERT_STREQ"]
    MEM --> M2["ASSERT_MEM_EQ (hexdump diff)"]
    REG --> R1["ASSERT_ABI_PRESERVED"]
    REG --> R2["ASSERT_FLAG_SET / CLEAR"]
    REG --> R3["ASSERT_REG_EQ"]
    FPV --> F1["ASSERT_FP_EQ / NEAR (ULP-aware)"]
    FPV --> F2["ASSERT_VEC_EQ + lane asserts<br/>ASSERT_DEQ/DNEAR/FEQ/FNEAR"]
    PROP --> P1["ASSERT_MATCHES_REF1 / 2 / 3"]
    EMU --> E1["ASSERT_NO_FAULT / FAULT / FAULT_AT"]
    EMU --> E2["ASSERT_EMU_REG_EQ / FP_EQ / VEC_EQ"]
    EMU --> E3["ASSERT_BLOCK_COVERED /<br/>ASSERT_BLOCKS_AT_LEAST"]
    

Property and differential testing loop

The generate → call → compare-against-reference loop — from Property / differential testing.

        flowchart TB
    SEED["seed — fixed by default,<br/>or ASMTEST_SEED"] --> RNG["splitmix64 RNG"]
    RNG --> GEN["gen(rng, args, cap)<br/>build one input tuple"]
    GEN --> CALL["call routine via real ABI<br/>(asm_call_capture_args)"]
    GEN --> REF["C reference model: ref(…)"]
    CALL --> CMP{"results equal?"}
    REF --> CMP
    CMP -->|"equal (silent)"| NEXT{"tried n inputs?"}
    NEXT -->|"no"| GEN
    NEXT -->|"yes"| PASS["test passes"]
    CMP -->|"mismatch"| FAIL["report input + both results + seed,<br/>then fail the test"]
    

Emulator guests

The five emulator guests and the shared *_open → *_call → result shape — from Emulator tier.

        flowchart TB
    G1["x86-64 System V"]
    G2["x86-64 Win64 ABI"]
    G3["AArch64 + NEON"]
    G4["RISC-V RV64"]
    G5["ARM32 A32 + NEON"]
    G1 & G2 & G3 & G4 & G5 --> OPEN["*_open(): map internal code + stack regions"]
    OPEN --> MAP["*_map / *_write:<br/>preload arbitrary guest memory"]
    MAP --> LOAD["Load routine bytes<br/>x86-64: copy from the built fn<br/>others: raw machine code"]
    LOAD --> ARGS["Marshal args into guest ABI registers<br/>scalar / _fp / _vec variants"]
    ARGS --> RUN["Run to the routine's ret<br/>OR stop after max_insns (mid-routine)"]
    RUN --> RES["emu_result_t:<br/>full register file + rip/flags + xmm/vec,<br/>faulted · fault_addr · fault_kind"]
    RUN -.->|"while running"| HOOKS["Hooks:<br/>invalid memory access becomes a fault,<br/>CODE/BLOCK becomes trace + coverage"]
    HOOKS -.-> RES
    

Emulator trace and coverage flow

How emu_call_traced accumulates coverage and feeds the reporting/lcov helpers — from Emulator tier.

        flowchart TB
    IN["inputs: -5, 0, +7"] --> CALL["emu_call_traced(…, tr)"]
    CALL --> TR["emu_trace_t tr (APPENDS)<br/>insns[] ordered trace<br/>blocks[] distinct block offsets"]
    TR -->|"re-run unions coverage"| CALL
    TR --> Q{"every branch hit?"}
    Q --> A1["ASSERT_BLOCK_COVERED /<br/>ASSERT_BLOCKS_AT_LEAST"]
    Q --> A2["emu_trace_report /<br/>emu_coverage_uncovered"]
    Q --> A3["emu_trace_lcov<br/>(offset-level .info)"]
    MAP["line map: (offset, line) rows<br/>from objdump / DWARF, out-of-band"] --> SRC
    TR --> SRC["emu_trace_source_report /<br/>emu_trace_lcov_source"]
    SRC --> OUT["source-line coverage + lcov"]
    

Trace and coverage backends

Every trace backend — emulator, native DBI, and hardware — fills the same asmtest_trace_t sink, so a test switches backends without changing how it reads coverage — from Native runtime tracing, Execution traces, and Hardware tracing.

        flowchart TB
    RT["Routine under test<br/>(offsets from entry, 0 = first byte)"]
    subgraph Emu["Emulator tier — any host"]
        UC["Unicorn virtual CPU<br/>CODE/BLOCK hooks"]
    end
    subgraph DBI["Native software DBI"]
        DR["DynamoRIO client<br/>bb event, in-process<br/>Linux x86-64"]
    end
    subgraph HW["Native hardware / single-step — asmtest_hwtrace.h"]
        PT["Intel PT → libipt<br/>bare-metal Intel x86-64"]
        AMD["AMD LBR → built-in<br/>bare-metal Zen 4+"]
        CS["ARM CoreSight → OpenCSD<br/>AArch64 boards (scaffold)"]
        SS["Single-step EFLAGS.TF → #DB<br/>any x86-64 Linux (exact)"]
    end
    RT --> UC & DR & PT & AMD & CS & SS
    UC & DR & PT & AMD & CS & SS -->|"trace_append_insn /<br/>trace_append_block (dedup)"| SINK
    SINK["asmtest_trace_t (shared sink)<br/>insns[] ordered · blocks[] distinct<br/>insns_total · blocks_total · truncated"]
    SINK --> COV["Coverage helpers<br/>ASSERT_BLOCK_COVERED · _report · _lcov"]
    SINK --> ANN["Capstone annotation layer<br/>offsets → instruction text"]
    SINK -.->|"ptrace call descent:<br/>flat trace stays frame 0"| DESC["asmtest_descent_t<br/>edges + nested callee frames<br/>(separate opaque handle)"]
    

Portability across targets

How one source set reaches every target natively and via the emulator guests — from Portability.

        flowchart TB
    SRC["One source set<br/>foo.s (GAS) + foo.asm (NASM)<br/>ASM_FUNC abstracts ELF vs Mach-O"]
    SRC --> NATIVE["Native build — host architecture"]
    SRC --> EMUG["Emulator guests — any host"]
    NATIVE --> X86["x86-64"]
    NATIVE --> ARM["AArch64"]
    X86 --> X86OS["Linux · macOS"]
    ARM --> ARMOS["Linux · macOS (Apple Silicon)"]
    X86OS --> BK["Backend: GAS (default) or NASM"]
    ARMOS --> BKA["Backend: GAS only (NASM is x86-only)"]
    EMUG --> EG["x86-64 SysV · x86-64 Win64 ·<br/>AArch64 · RISC-V RV64 · ARM32"]
    

Language bindings architecture

The C core, the flat binding ABI, and the per-language modules that reproduce a shared conformance corpus — from Language bindings.

        flowchart TB
    subgraph Native["C core (this repo)"]
        H["Headers: asmtest.h / asmtest_emu.h<br/>regs_t + emu result structs<br/>_Static_assert layout guards"]
        LIB["libasmtest_emu (shared)<br/>binding ABI: capture entry points,<br/>verdict shims, opaque-handle accessors"]
        GEN["gen-manifest.c"]
        JSON["asmtest_abi.json<br/>struct sizes + field offsets"]
        H --> LIB
        H --> GEN --> JSON
    end
    subgraph Modules["Per-language modules — FFI kept inside"]
        PY["Python (ctypes)"]
        OTHERS["Go cgo · Rust · C++ · Zig · Node koffi ·<br/>Ruby Fiddle · Lua ffi · Java FFM · .NET P/Invoke"]
    end
    JSON -->|"struct layout"| PY
    LIB --> PY
    LIB -->|"opaque-handle accessors"| OTHERS
    PY --> CORP
    OTHERS --> CORP
    CORP["Conformance corpus<br/>canonical routines + expected captures<br/>SINGLE SOURCE OF TRUTH"] --> V{"every binding reproduces<br/>the same results?"}