Five targets, cross-compiled from any host to any of them:
--target= |
format |
|---|---|
macos-aarch64 |
Mach-O |
linux-aarch64 |
ELF |
linux-x64 |
ELF |
windows-x64 |
PE32+ |
windows-arm64 |
PE32+ |
Executables are position-independent (ELF ET_DYN / PIE, matching Mach-O).
--freestanding drops the embedded startup runtime; the program supplies the
entry (__c5_entry, #pragma entrypoint or --entry). On Linux such an
image is placed at its link address (ET_EXEC), since nothing in it applies
load-time relocations, and carries no interpreter and no dynamic section
unless it binds a shared-library symbol, which the driver reports
(-Wfreestanding-import). It takes two PT_LOADs, read-execute and
read-write, as ld lays out a static executable. EFI images are supported
through the PE subsystem selector.
On x86_64 badc assumes x86-64-v3: AVX, AVX2, FMA3, BMI1, BMI2, LZCNT, MOVBE and
F16C on top of SSE4.2 and POPCNT, as in Intel Haswell, AMD Zen and later. On
AArch64 it assumes what the Apple M1 implements, less Apple’s own extensions:
ARMv8.4-A with FP16, DotProd, FHM, AES, PMULL, SHA3 and SHA512, plus FRINTTS,
FlagM2, SB and SSBS from ARMv8.5-A, without BTI, BF16 or I8MM. The emitted code
may use any instruction of the set at any optimization level, and a processor
that lacks one is not a target. -mno-sse / -mgeneral-regs-only take the
floating-point and vector registers out of the set (Hardening and code-model
knobs); nothing else narrows it, and the
driver refuses -march= and -mtune=. No instruction-set feature macro
(__SSE2__, __AVX2__, __ARM_NEON, …) is predefined but
__ARM_FEATURE_AES, __ARM_FEATURE_SHA2 and __ARM_FEATURE_CRYPTO, which
-mcpu=’s +aes, +sha2 and +crypto modifiers define.
A single badc invocation can mix .c sources, .o objects, and .a
archives:
badc -c foo.c bar.c # emits foo.o + bar.o (ELF64 ET_REL, target pinned)
badc -o app foo.o bar.o # links them into a final binary
badc --ar -o libfoo.a foo.c bar.c # bundles into a SysV ar(5) archive
badc -o app main.c -L. -l foo # link against libfoo.a, gcc-style
badc ships its own linker – there is no ld / lld / link.exe
dependency. Object files are standard ELF64 ET_REL relocatables: a .text
section of native machine code, .data / .bss for static storage,
.symtab / .strtab for the name table, and .rela.text carrying the
relocations the linker applies once each unit’s final position is known. The
target is pinned at -c time, and the objects are also linkable by ld /
lld. The address of – or a data load from – an external symbol routes
through the GOT, so a badc -c object links into a PIE produced by the system
toolchain. Archives are ar(5) with a SysV-style symbol index.
The full cargo feature gates the entire pipeline; library consumers that do
not need multi-TU artifacts can opt out via
default-features = false, features = ["std"].
-l<name> is resolved in the -L directories, then in the standard library
directories under --sysroot=<dir> (usr/lib, lib, their 64-bit and
multiarch variants on ELF; usr/lib and usr/local/lib on Mach-O). No other
directory is searched, on a native link too: the host’s /usr/lib is an input
only when the command names it (--sysroot=/), so one command emits one image
on every host. The same root supplies the system headers the bundled set lacks
(<zlib.h>), probed after the bundled headers so a standard header keeps the
embedded copy. For a Mach-O target $SDKROOT is the default sysroot, as for
the platform’s own tools; --sysroot= with no directory withdraws it.
Storage-class linkage follows C99 6.2.2: static at file scope is internal,
bare or extern declarations are external, and extern T x; with no defining
declaration becomes an unresolved external that the linker tries to satisfy
from the remaining objects or archive members.
The linker also takes GNU-ld-shaped work directly: linker scripts
(-T / --script), --emit-relocs, -z keywords,
--build-id, link maps (-Map, --print-map), --whole-archive spans, and
symbol-export control (--export-all, --export-data). Invoked as ld,
ld.badc, or with --ld, badc presents a GNU ld persona with its own flag
table, which is what lets it stand in for LD= in an existing build –
including the Linux kernel’s.
c5 covers most of C99, the C11 and C23 features real code gates on, and a wide
GCC extension surface. std-conformance.md enumerates
the rejected idioms, the divergent behavior, and the c5-only extensions.
The preprocessor predefines a standard set, double-underscore wrapped in the gcc / clang / msvc convention so it does not collide with user identifiers:
__BADC_VERSION__ <crate version> // string literal from Cargo.toml, e.g. "0.4.2"
__BADC_TARGET__ "macos-aarch64" // canonical target id (string literal)
__aarch64__ / __arm64__ // AArch64 targets
__x86_64__ / __amd64__ // x86_64 targets
_WIN32 / _WIN64 // Windows targets only
__BADC_WINDOWS__ // Windows targets only
__APPLE__ // macOS target only
__linux__ // Linux targets only
alongside the C99 / C11 set (__STDC__, __STDC_VERSION__, __SIZEOF_*__,
__BYTE_ORDER__, the __ATOMIC_* orders) and, under --gnu, the GCC
identity macros. std-conformance.md lists them all. __DATE__ and
__TIME__ are the time of translation in UTC, one instant for every unit
of an invocation; setting SOURCE_DATE_EPOCH fixes it, so a build that
expands either still emits the same bytes on every run.
Comparing the string-literal predefines with #if X == "..." / != is a c5
extension over C99, which restricts a #if controlling expression to an integer
constant expression; a string is admitted in no other operand position
(std-conformance.md states the rule).
The MSVC/MinGW mimicry surface (_MSC_VER / __MINGW32__ / __int64 /
__declspec / etc.) lives in libc/include/msvc_compat.h and is opted into
per translation unit with -include msvc_compat.h.
The header tells the compiler which dylibs / shared objects / DLLs the target offers and which local names resolve to which exported symbols:
#pragma dylib(libsystem, "/usr/lib/libSystem.B.dylib")
#pragma binding(libsystem::printf, "_printf")
int printf(char *fmt, ...);
The codegen drives its IAT / .got / DT_NEEDED records from these
declarations. When the source calls printf, the parser type-checks the call
against the prototype; the codegen looks up the binding to learn that the
loader should resolve _printf from libSystem.B.dylib. Switching target
swaps the header and the bindings change with it – printf lands on bare
printf from libc.so.6 on Linux, printf from msvcrt.dll on Windows.
Validation runs at codegen entry: every intrinsic the program references
must have a matching binding for the chosen target. An unused binding describes
the surface without pulling in what it names. A library a bundled header
declares reaches the image only when an
import binds through it, so including <math.h> without calling into it leaves
no DT_NEEDED behind, which is what ld --as-needed does. A #pragma dylib
in your own source is a load-time dependency and is recorded whether or not a
symbol binds through it: it is how a program names a library it reaches only by
runtime lookup, such as a framework whose initializer has to run before
dlsym or objc_getClass resolves a name.
#pragmabadc uses #pragmas to lighten the command line. Dylib bindings, exports,
alignment, the entry-point name, and the Windows subsystem each live next to
the code they configure, so the source carries enough context to build with a
bare badc <file>.
#pragma once // single-inclusion guard for headers.
#pragma dylib(libc, "libc.so.6") // declare a dylib c5 can bind into.
#pragma binding(libc::sin, "sin") // map a portable name to its dylib symbol.
#pragma export(my_api) // promote a function to a shared-object export.
#pragma pack(N) / pop / push // override the default 8-byte struct alignment.
#pragma entrypoint(WinMain) // override the default `main` entry point.
#pragma subsystem(windows) // pick the PE subsystem (console | windows | native | efi_*).
#pragma entrypoint(<name>) lets the source declare a non-main entry without
a build-driver flag; the compiler resolves the name through the same
symbol-table lookup it uses for main. #pragma subsystem(<kind>) drives the
PE optional-header Subsystem byte. The accepted kinds are console
(default, IMAGE_SUBSYSTEM_WINDOWS_CUI = 3), windows
(IMAGE_SUBSYSTEM_WINDOWS_GUI = 2), native (IMAGE_SUBSYSTEM_NATIVE = 1,
with nt / driver as aliases), and the EFI variants efi_application,
efi_boot_service_driver, efi_runtime_driver, and efi_rom; cui and
gui name console and windows, and every kind is taken in any case and
with - for _. --subsystem=<kind> takes the same set through the same
lookup and overrides the pragma. With console
/ windows, entrypoint(WinMain) plus subsystem(windows) is what a Win32
GUI app needs to skip the loader’s auto-attach to a console window. Non-PE
targets keep the default and ignore the directive, so the same source builds
for every OS.
Unknown #pragmas and unknown preprocessor directives warn rather than
failing the build. An #include that resolves through neither the search paths
nor the embedded headers is an error, as in gcc / clang; pass -H /
--show-includes for the gcc--H-shape resolution trace on stderr.
If something is not available, declare it yourself, or use runtime linking with
dlopen / dlsym (or LoadLibrary / GetProcAddress):
int main() {
int *h, *fn;
h = dlopen(0, 2); // RTLD_NOW
fn = dlsym(h, "strlen");
return fn("hello, world!"); // exits 13
}
dlopen(NULL, RTLD_NOW) returns the calling process’s symbol scope – libc on
POSIX, the loaded set on Windows.
What is reachable from each system:
dlsym(h, "objc_msgSend") gives the Objective-C runtime entry
point. The CoreFoundation / AppKit / Foundation surfaces are one
dlopen("/System/Library/.../X.framework/X") away.clock_gettime, nanosleep, pipe2, the entire pthread_*
family. Anything in /usr/lib’s sonames if you spell the path.dlopen resolves to LoadLibraryA, so
dlopen("user32.dll", 0) plus dlsym(h, "MessageBoxA") gives a callable
Win32 API entry point.--jit)Same encoder and relocations as the AOT path. badc mmaps the result
executable, resolves libc through a runtime-built fake GOT, and calls main
directly via a transmuted function pointer. Parse, lower and exec happen inside
the badc process, with no subprocess and no on-disk binary:
badc --jit tests/fixtures/c/c4.c hello.c # JIT'd c4 self-hosts hello.c
Five hosts are supported:
| host | mapping |
|---|---|
| Linux/aarch64 | mmap RW -> mprotect RX, manual dc cvau / ic ivau |
| Linux/x86_64 | mmap RW -> mprotect RX, hardware-coherent I-cache (no-op) |
| macOS/aarch64 | mmap RWX + MAP_JIT, pthread_jit_write_protect_np toggle |
| Windows/x86_64 | VirtualAlloc RW -> VirtualProtect RX, FlushInstructionCache (no-op) |
| Windows/aarch64 | VirtualAlloc RW -> VirtualProtect RX, FlushInstructionCache |
libc is bound at JIT time: a writable fake GOT gets one entry per resolved
import, and the codegen’s existing GOT relocations are patched against this
region. POSIX uses dlopen(NULL, RTLD_NOW) + dlsym to find each symbol in
the loaded process; Windows uses LoadLibraryA per declared dylib (kernel32,
msvcrt, ws2_32, …) + GetProcAddress. macOS uses Apple’s MAP_JIT plus the
per-thread W^X toggle the hardware requires on Apple Silicon.
The codegen always lowers through an SSA intermediate representation and a
graph-coloring register allocator. A handful of cheap rewrites run
unconditionally; --optimize adds a set of SSA passes on top.
Always on: drop self-movs and fuse compare + branch into cmp / b.cond
(or cmp / jcc) without materializing a 0/1 boolean in between. The
register allocator builds an interference graph over phi-congruence classes and
colors it greedily, spilling to frame slots only under pressure.
--optimize (-O, and the -O1/-O2/-O3/-Os/-Oz/-Ofast/-Og
spellings, which all select the same single level) runs mem2reg, inlining,
rotate and branch const-folding, and immediate dedup, and predefines NDEBUG=1
and __OPTIMIZE__=1.
-O contracts a*b+c into one fused multiply-add on both architectures, an
instruction of the baseline.
examples/bench.rs runs a few pure-computation workloads (fib32,
quicksort-50k, matmul-50) through the VM and the in-process JIT and reports
per-iteration timings:
cargo run --release --example bench -- --iter 10
Assembly and SSA snapshots of the test fixtures live under
tests/snapshots/, where a codegen change shows up as a reviewable diff.
For kernel and firmware work the driver accepts the shapes those builds
require: -mcmodel=small|kernel|tiny, -mno-sse / -mgeneral-regs-only
(keep codegen off the FP/SIMD register file), -mstrict-align,
-fPIC/-fpic/-fPIE/-fpie, -mindirect-branch= and -mfunction-return=
(retpolines), -mharden-sls=, -fcf-protection=branch (endbr64),
-mbranch-protection=none|bti|pac-ret|standard, and the stack-protector
family below. Options badc does not implement are rejected rather than
accepted and ignored, so a configure-time probe gets a truthful answer.
-ffixed-REG keeps a register out of the allocator, so no compiler-chosen
value lives in it – what the kernel asks for the shadow-call-stack register
(-ffixed-x18) and for the NEON registers a unit keeps guest or caller state
in (-ffixed-q16 .. -ffixed-q31). Every architectural spelling names the
register (x9 / w9, q16 / v16 / d16 / s16, rax / eax / ax /
al, r8 / r8d / r8w / r8b, xmm5). The ABI still passes arguments
and results through a reserved register, and an inline-asm operand, clobber
or register variable may still name it; the code generator’s own scratch
picks avoid it, and an FP scratch it would have used moves to another
register outside the allocator’s banks, into the callee-saved bank’s tail
when none is left there. The stack and frame pointers, the AArch64 link
register and the scratch registers the code generator cannot give up (x16,
x17, x19; r10, r11) are refused, as is a reservation that leaves a function
with floating-point work no scratch register at all.
-fstack-protector, -fstack-protector-strong and -fstack-protector-all
select which functions carry a stack canary; -fno-stack-protector (the
default) selects none. The per-function rule is gcc’s, read off the declared
automatic objects: a character array of at least --param ssp-buffer-size=
bytes (default 8) or a call to alloca for the plain form, plus any array,
any aggregate with an array member, and any object whose address the body
takes for strong. The canary occupies the frame’s topmost slot, between
the locals and the saved return address; the prologue stores the guard there
and every return path – including a tail call’s teardown – reloads it,
compares, and calls __stack_chk_fail on a mismatch.
-mstack-protector-guard=global|tls|sysreg says where the guard value comes
from, with -mstack-protector-guard-reg=, -mstack-protector-guard-offset= and
-mstack-protector-guard-symbol= as its operands. The default follows the
target: %fs:0x28 on Linux/x86-64, the C library’s __stack_chk_guard object
elsewhere. tls is the x86-64 segment-relative form the kernel selects
(-mstack-protector-guard=tls, -mstack-protector-guard-reg=gs,
-mstack-protector-guard-symbol=__ref_stack_chk_guard); sysreg is the
aarch64 form that reads a per-task offset above a system register
(-mstack-protector-guard-reg=sp_el0, -mstack-protector-guard-offset=N). The
family needs relocatable output – the failure branch is a relocation against
__stack_chk_fail – so --jit and --interp reject it, as do the Windows
targets, whose C library exports neither symbol.
-ftrivial-auto-var-init=uninitialized|zero|pattern (the kernel’s
CONFIG_INIT_STACK_ALL_ZERO passes zero) initializes every automatic
object declared without an initializer – scalars, aggregates, arrays and
variable-length arrays – where its storage is established: the value is
supplied in the front end as an ordinary initializer, so every output mode
carries it and the -O promotion treats it as any written one. pattern
stores the byte 0xFE gcc stores. A scalar that fits a register takes the
value as a literal of its own type; anything wider is filled byte-wise,
unrolled within the inline bound and as a store loop past it, and a
variable-length array’s loop follows its allocation.
__attribute__((uninitialized)) opts an object out, as does binding it to a
register with asm; a declaration a goto or switch jumps past is not
covered, as in gcc. -fzero-init-padding-bits=standard|unions|all is
accepted with every value and changes nothing: an automatic aggregate
initializer already zero-fills the whole object, padding included, before it
stores the members, for structs and unions alike.
-Wframe-larger-than=<n> reports a function whose stack frame exceeds n
bytes: what the prologue reserves below the return address, saved registers
and frame record included, alloca and variable-length arrays excluded. The
report breaks the size down by region: locals, spill slots, saved registers,
inline-asm scratch, an over-aligned region, the canary and the frame record. It
is the frame-larger-than row, so -Werror= makes it fatal and -Wno-
silences it, and n takes gcc’s byte-size suffixes (kB, KiB, MB,
MiB, …). Without the option no bound applies, as in gcc and clang; the
kernel passes CONFIG_FRAME_WARN through it.
pac-ret signs the return address of every function that stores the link
register: paciasp ahead of the prologue, autiasp after the last teardown
instruction of each epilogue, where sp – the signing modifier – holds its
function-entry value again. A frameless leaf stores no link register and is
left alone. standard is bti+pac-ret; a signed function opens with
paciasp, which is itself a landing pad for the branch types a function entry
is reached with, so it takes no separate BTI C. An aarch64 object built with
either claims the matching bits in a .note.gnu.property
GNU_PROPERTY_AARCH64_FEATURE_1_AND word. The linker intersects that word
across inputs, so the compiler sets a bit only where it emitted the
instructions.