EmbCCdocs Repository

Backends#

This page describes the code generators in src/arch/: the target model they share (target.c), the predefined-macro tables, the contract every backend implements, and then each backend in turn (x86-64, AArch64, Thumb, RISC-V, MIPS32 and AVR): how it lowers EmbIR, how it lays out a frame, how it implements its calling conventions, how its instruction encoder is checked, and what it does specially. It is written for people changing EmbCC. Register allocation, which all five share, is in Register allocation; the IR is in EmbIR; what each target supports from the user's side is in Targets.

Layout of src/arch/#

Path Contents
target.c, target.h the target model: triples and data model (read from the target database, src/targets/), ABI questions, relocation kinds
backend.h the contract a backend implements
code.c, code.h the machine-code buffer every encoder writes into
predef.c, predef.h which predefined-macro table the target uses
regalloc.c, regalloc.h the shared register allocator
backends.c the backend registry, a row per backend built in
common.c what every backend's lowering shares: the wide and float vreg classes, string-site resolution, the local-call rule, label marks
x86_64/ x86-64: codegen.c, emit.c, irgen.c (va_arg, extended asm), topasm.c (file-scope asm), as.c (EmbAS), disasm.c, predef*.c
aarch64/ AArch64: codegen.c, emit.c, asm.c (inline-asm assembler), irgen.c, predef*.c
thumb/ ARMv6-M, ARMv7-M and ARMv8-M Mainline: codegen.c (ARMv7-M/ARMv8-M selection, and the ABI, frame and allocator code all three share), v6m.c (ARMv6-M selection), cg.h (what the two share), emit.c, asm.c, attrs.c (build attributes), irgen.c, predef*.c
thumbv6m/, thumbv8m/ the ARMv6-M and ARMv8-M predefined-macro tables only
riscv/ RV32 and RV64, one backend: codegen.c, emit.c, asm.c, irgen.c
riscv32/, riscv64/ the two RISC-V predefined-macro tables only
mips/ MIPS32r2 (mipsel, o32): codegen.c, emit.c, asm.c, irgen.c
mips32/ its predefined-macro tables
loongarch/ LoongArch64 (LP64S): codegen.c, emit.c, asm.c, irgen.c
loongarch64/ its predefined-macro tables
avr/ AVR (ATmega328P): codegen.c, emit.c, asm.c, irgen.c, predef*.c

Each irgen.c holds the target's share of IR generation: va_arg, inline-asm operand resolution, and the *_imm_foldable predicate the optimizer asks before folding a constant into an instruction.

The target model#

One process compiles for one target. The driver selects it from --target= before the front end runs, and nothing in the compiler reads the host's architecture. Everything that depends on the target asks src/arch/target.h.

Triples#

A target has three independent dimensions: the architecture (enum target_arch: TARGET_X86_64, TARGET_AARCH64, TARGET_THUMB, TARGET_RISCV32, TARGET_RISCV64, TARGET_AVR, TARGET_MIPS32), the operating system (enum target_os: none, EmbLinkOS, Linux, Darwin, Windows), and the object format (enum target_fmt: ELF, Mach-O, COFF). The accepted triples are an explicit table, the TRIPLE rows of the target database (src/targets/<family>.def, read into g_triples[] in target.c), not a cross product, so a combination that does not exist cannot be accepted by accident. Each row is marked canonical (the spelling -dumpmachine and diagnostics print) or an alias. A name not in the table is refused.

The ARM rows also carry the sub-architecture (ARMv7-M, ARMv7E-M, ARMv8-M Mainline and Baseline, ARMv6-M, ARMv7-A) and whether the name is an -eabihf one, and a MIPS row its byte order. These are a level on one target, not separate targets, because the data model and calling convention are the same.

When no --target= is given, target_apply_default() applies, in order: EMBCC_DEFAULT_TARGET from the environment, the compiled-in EMBCC_DEFAULT_TARGET (make DEFAULT_TARGET=...), and x86_64-elf.

The data model#

g_model[] holds one row per architecture, the DATA_MODEL rows of the target database, and each question has a function:

Function x86-64 AArch64 Thumb RV32 RV64 AVR MIPS32
target_ptr_size() 8 8 4 4 8 2 4
target_long_size() 8 8 4 4 8 4 4
target_int_size() 4 4 4 4 4 2 4
target_double_size() 8 8 8 8 8 4 8
target_ldouble_size() 16 16 (8 on Darwin) 8 16 16 4 8
target_char_unsigned() no yes (no on Darwin) yes yes yes no no
target_wchar_unsigned() no yes (no on Darwin) yes no no no no
target_has_int128() yes yes no no yes no no
target_max_scalar_align() none none none none none 1 none
target_stack_align() 16 16 8 16 16 1 8

-fsigned-char and -funsigned-char override the char column through target_set_char_signed(). target_xlen() is the register width in bits (RISC-V's XLEN).

Other questions are asked by name, each a switch with no default so that a new architecture fails to compile until it answers:

Function Meaning
target_va_list_is_pointer() va_list is a bare pointer (Thumb, RISC-V, MIPS32, AVR, Win64, Darwin AArch64) rather than a pointer to a tag (System V, AAPCS64); decides what va_copy copies
target_anon_bitfield_aligns() an unnamed bit-field raises the struct's alignment (AAPCS, AAPCS64; not Apple arm64, which lays them out as x86-64 does)
target_widen_unsigned_fp_cvt() an unsigned 32-bit integer is widened to 64 bits before a floating-point conversion (x86-64, AArch64)
target_jump_tables() a dense switch may become IR_SWITCH (every target but AVR)
target_win64_abi() x86-64 uses the Microsoft x64 convention (the Windows triples)
target_op_calls_helper() the current backend lowers this IR instruction to a runtime-helper call. The driver registers the backend's predicate with target_set_calls_helper() once, for the target finally chosen (from --target= or the configured default): t_op_calls_helper, rv_op_calls_helper or a64_op_calls_helper; x86-64 and AVR have none
thumb_imm_foldable(), riscv_imm_foldable(), a64_imm_foldable() whether the optimizer may fold a constant into an instruction's immediate
target_mul_shift_add() whether a multiply by c is a shifted add or reverse subtract, for AArch64 and Thumb
target_thumb_*(), target_pcs_vfp(), target_pcs_differs() the Thumb level, FPU, float ABI and pcs attribute
target_riscv_rvc() RISC-V emits compressed instructions; always 1, read by the encoder and the ELF flags
target_insn_len() the length of the instruction at a byte pointer, for -S grouping; 0 on x86-64, where the disassembler decides

target.c is also linked into tools that carry no backend (the encoding checkers, embls), so it must not name a backend's functions directly; the backend predicates are declared in target.h and defined in the backends.

Relocation kinds#

A backend records a machine-neutral enum reloc_kind at each patch site (RK_CALL, RK_TAIL, RK_PCREL32, RK_ADR_HI21/RK_ADD_LO12, RK_GOT_PAGE/RK_GOT_LO12, RK_THM_MOVW/RK_THM_MOVT, RK_RISCV_PCREL_HI20/RK_RISCV_PCREL_LO12_I, RK_RISCV_CALL, RK_MIPS_HI16/RK_MIPS_LO16, RK_MIPS_TEXT26, the RK_AVR_* kinds, RK_ABS64, RK_ABS32, RK_DATA_PREL32 and the TLS kinds). The driver turns a kind into a concrete relocation:

  • target_reloc_type(arch, kind) gives the ELF type (-1 when the kind does not apply to that architecture, which is a codegen bug). RK_TAIL becomes RK_CALL except on Thumb (R_ARM_THM_JUMP24) and AArch64 (R_AARCH64_JUMP26).
  • target_reloc_addend(arch, kind, bias) gives the ELF addend; x86-64's PC-relative fields are biased by -4, because they are measured from the end of the instruction.
  • target_macho_reloc() and target_coff_reloc() give the Mach-O and COFF spellings; both formats store the addend in the patched field, so the -4 bias does not apply.

target_elf_machine() and target_elf_flags() give e_machine and e_flags (EF_ARM_EABI_VER5 on Thumb, EF_RISCV_RVC on RISC-V, EF_AVR_ARCH_AVR5 on AVR, MIPS32r2 | o32 | noreorder on MIPS32). target_elf_uses_rel() says the target's objects carry REL relocations (MIPS32), and target_rel_put_addend() stores an addend in the field the way that relocation type's linker reads it back.

Adding a target#

  1. Its family file in the target database, src/targets/<family>.def, with a FAMILY row first, a DATA_MODEL row and its TRIPLE rows, and the file's line in src/targets/targets.def. Then an answer in every switch in target.c (the build fails until each is given), and the relocation mappings.
  2. Its place in make TARGETS= (see Building for some targets): the family's EMBCC_FAMILY_<name> default in src/arch/target.h, its name in the Makefile's TARGET_FAMILIES, its sources in FAMILY_SRCS_<name> and its runtime triples in FAMILY_RT_<name>. Every reference shared code makes into the new backend (a registry row, an irgen or asm dispatch, a predef table, the linker's relocations) goes under #if FAMILY_BUILT(name).
  3. A directory with codegen.c, emit.c and irgen.c, and a codegen_unit_* entry point.
  4. Its row in the backend registry, src/arch/backends.c (see The backend registry), and its GCC -m options in src/arch/<arch>/options.c. The driver reads the row; it has no per-target chain to extend.
  5. A predefined-macro table from tools/gen-predef.sh (below).
  6. An encoding referee for emit.c (see Encoders and their referees).
  7. A QEMU harness under tests/harness/<arch>/, and the target in the test suite. See Testing.
  8. A new source file also has to be added to the build lists, including EMBLS_SRCS; otherwise make test fails at its build step.

Predefined macros#

Each architecture's predefined-macro table is generated from a reference compiler and checked in, so building EmbCC needs no cross toolchain. tools/gen-predef.sh ARCH regenerates src/arch/ARCH/predef.c and predef_cxx.c (ARCH is x86_64, aarch64, thumb, thumbv8m, riscv32, riscv64 or avr; with no argument, all of them). The files must not be edited by hand.

Table Reference
x86_64 x86_64-elf-gcc -dM -E
aarch64 aarch64-elf-gcc -dM -E
thumb clang -target thumbv7m-none-eabi -ffreestanding
thumbv8m clang -target thumbv8m.main-none-eabi -mfloat-abi=soft -ffreestanding
riscv32 clang -target riscv32-unknown-elf -march=rv32imac -mabi=ilp32 -mcmodel=medany -ffreestanding
riscv64 clang -target riscv64-unknown-elf -march=rv64imac -mabi=lp64 -mcmodel=medany -ffreestanding
mips32 clang -target mipsel-unknown-elf -mcpu=mips32r2 -msoft-float -mno-abicalls -ffreestanding
avr clang -target avr -mmcu=atmega328p -ffreestanding

EMBCC_REF_GCC_<ARCH> (for example EMBCC_REF_GCC_THUMB) names a different reference compiler. The C++ tables come from the same compiler's C++ driver with -std=gnu++20. The generated file's header records the compiler and version used.

The script removes macros that would claim something EmbCC is not or does not do: the __GNUC__, __VERSION__, __clang__ and __llvm__ families (EmbCC is neither), __STDC* (defined by the preprocessor itself), __BITINT_MAXWIDTH__, __riscv_v_intrinsic, __ARM_FEATURE_CMSE, and on RISC-V the __GCC_HAVE_SYNC_COMPARE_AND_SWAP_* sizes the backend refuses. The C++ tables also drop the __cpp_* feature-test macros and the types EmbCC's C++ does not implement.

src/arch/predef.c chooses the table at run time and adds what the generated tables cannot know:

  • the operating system's macros (__emblink__, __linux__, __APPLE__/__MACH__, _WIN32/_WIN64/__MINGW32__/__MINGW64__, and the __unix__ family where it applies);
  • Apple's arm64 data model, replacing the generated AArch64 values for long double, wchar_t and wint_t and dropping __CHAR_UNSIGNED__;
  • the Thumb FPU macros (__ARM_FP 0x4, the __ARM_VFPV*__ set, __ARM_FPV5__ on ARMv8-M, __ARM_PCS_VFP for the hard-float ABI), with __SOFTFP__, the generated __ARM_FP and (for hard float) __ARM_PCS removed;
  • __ELF__ removed for Mach-O and COFF.

embcc --dump-predef prints the table in use. tests/golden/predef.sh compares it with tools/gen-predef.sh --reference ARCH when the reference compiler is installed.

The backend registry#

src/arch/backends.c has one row per enum target_arch (a struct backend_desc, declared in backend.h), holding what the driver needs to know about the code generator behind it:

Field What it says
family The family's name in messages ("RX", "PowerPC")
codegen The code generator's entry point (below)
ra_at_o0 The register allocator also runs at -O0, for each expression's temporaries (EMBCC_O0_NORA=1 turns it off)
op_calls_helper Whether an IR instruction becomes a runtime-helper call on this target, which the optimizer asks; NULL for none
unwind_unwritten The unwind tables the target needs and EmbCC does not write, as a refusal names them ("RISC-V .eh_frame"); NULL where eh_emit writes them
cxx_exceptions Whether a C++ unit may use exceptions: BACKEND_CXX_EXC_OK, _NONE, or _BIG_ENDIAN (MIPS64)
firmware The driver links firmware for it with embld, and its file-scope asm blocks and naked functions are read by the .s assembler
ld_scripts embld lays out a GNU linker script for it (-T)
call_insn, call_delay_slot A naked function's argument-less call, and whether a nop fills a delay slot after it (SPARC)
sym_prefix What a C name is called in the object and in assembly (_ on RX)
imm_prefixed An asm operand that is a constant is written #5 (RX, ColdFire)
text_p2align The alignment of a naked function's body (AVR: 1)
no_asm_text Why -S writes no text for it, as the refusal says; NULL when it does
option The target's own command-line options, in src/arch/<arch>/options.c: returns 1 when it handled the argument (accepted it, or refused it by name), 0 to let the driver try the rest. Each handler lists its options as an exact and a prefix table, checked with option_listed()

backend_get(arch) returns the row, and stops the compiler if a target has none. The driver selects from the row the code generator, the helper predicate, the unwind and exception refusals, whether and how it links firmware, how a naked function is written out, and whether -S has text. Before the registry, these were chains of if (ta == TARGET_...) in the driver, nine of them for the unwind refusals alone, and a new backend had to extend each one. More of the driver's per-target knowledge moves into the row as the redesign proceeds (Redesign).

Building for some targets#

make TARGETS=arm,riscv builds a compiler with those families' backends and no other (Choosing the targets is the user's side). A family is one file of the target database, and its FAMILY row gives the name, the line --version prints and the default triple of a compiler built with it alone; the DATA_MODEL rows after it are its architectures (target_family_of).

The Makefile compiles only the selected families' FAMILY_SRCS_<name>, into $(BUILD)/targets-<names>, and defines EMBCC_FAMILY_<name>=0 for each family left out. src/arch/target.h defaults every one to 1, so every other build of these sources (build.ebm, the self-host, embld and embls, which link some of them) has all of them, and SRCS stays the full list those builds read. Shared code that names a backend's symbol does so under #if FAMILY_BUILT(name):

Where What is guarded
src/arch/backends.c the registry rows and the ELF notes
src/arch/predef.c the predefined-macro tables
src/ir/irgen.c the va_arg and asm dispatch
src/opt/immfold.c each backend's foldable immediates
src/sema/sema.c the register names of register ... asm("r") (stubbed: never asked)
src/as/gas.c the gas_target rows; ARM's and Xtensa's inline helpers (stubbed)
src/link/link.c each machine's relocations and entry stub; add_object refuses a machine whose family is not built
src/driver/main.c, asmout.c the x86-64 assembler and disassembler, the encoder settings for ARM and MIPS, AVR's -mmcu=

Two files serve two families: x86_64/topasm.c (file-scope asm on x86-64 and AArch64) and the variadic lowering at the top of riscv/irgen.c (RISC-V and LoongArch; the rest of that file is under FAMILY_BUILT(riscv)). Both are in both families' source lists. The helpers every backend calls are in src/arch/common.c.

The driver checks target_arch_built once every way of choosing a target has been applied (--target=, the default, the name it was called by, -arch), and refuses an unbuilt one before anything asks for its row. tests/golden/target-select.sh builds TARGETS=arm and checks that no other backend's symbol is in it and that its objects are byte-identical to the full compiler's; EMBCC_TARGETS_EVERY=1 builds all 13 one-family compilers, which is where a new unguarded reference shows up as a link error or an unused-function error.

The backend contract#

backend.h declares one entry point per backend, all with the same signature: codegen_unit (x86-64), codegen_unit_arm64, codegen_unit_thumb, codegen_unit_riscv (both widths), codegen_unit_mips (both widths), codegen_unit_loongarch, codegen_unit_tricore, codegen_unit_xtensa, codegen_unit_ppc, codegen_unit_rx, codegen_unit_sparc, codegen_unit_coldfire and codegen_unit_avr. Each takes the optimized struct ir_unit and returns:

  • the unit's .text in a struct code, with each function's code_off and code_len filled in;
  • struct extcall sites: calls to functions not defined in the unit (tail set for a tail call);
  • struct strsite, struct gsite and struct fsite sites: the places that need a string's, a global's or a function's address, each with a relocation kind (one site on x86-64, two for a two-instruction address on AArch64, Thumb and RISC-V, one per byte on AVR).

The driver turns the sites into relocations for the object writer. Nothing downstream knows which backend produced the image.

The flags passed in are want_debug (-g: collect line tables and variable locations), optimize (-O1 and above), no_sse (x86-64 -mno-sse) and regalloc (also -O1 and above).

Shared helpers:

  • cg_wide_vregs(fn) marks vregs holding a value too wide for a register (a 16-byte long double, an __int128, a vector).
  • cg_float_vregs(fn) marks the floats and doubles, for a backend with a floating-point register class.
  • cg_resolve_strsites() turns each string site's index into its .rodata offset.
  • cg_call_local() says whether a callee is defined in the unit.
  • code_byte, code_u16, code_u32, code_patch32 and code_align write and patch the buffer (little-endian). code_mark_data(c, start, end) records a range of data inside .text (a jump table); the ELF writer puts ARM mapping symbols around it ($d, then $t or $x) on Thumb and AArch64.

What the backends have in common#

All six backends share a shape:

  • Every vreg has one home: the register the allocator assigned, or a frame slot. Instructions read operands through accessor functions that return the register a value is in, or load it into a scratch register from its slot, and write results the same way. A lowering that bypasses the accessors and reads a slot directly is reading memory that nothing wrote when the value has a register; x86-64 and AArch64 catch that at encoding time (the dead-slot guards below).

  • Scratch registers outside the allocator's pool carry operands of spilled values, addresses and intermediate results. Which ones, and why, is listed per backend in Register allocation.

  • Fusions look at neighbouring instructions. A compare whose only use is the next branch becomes a compare-and-branch; an address computation whose only use is the next load or store becomes an addressing mode. The optimizer's last pass (pass_sinkaddr) moves address arithmetic next to its access so these fire.

  • No code after an unconditional transfer up to the next label, and no jump to the label that immediately follows.

  • Tail calls from -O1 when the call is direct, every argument is in a register, and nothing in the frame outlives the call. Each backend adds its own conditions. EMBCC_NO_TAILCALL turns them off on AArch64, Thumb, RISC-V, MIPS32 and AVR.

  • Branch relaxation (all but AArch64) by generating a function more than once: each branch starts in one form, and a later pass shrinks or grows it according to the distances measured.

  • Refusals. An operation a backend cannot lower ends the compilation with a message naming it, never with wrong code. The embedded backends use one format:

    embcc: FILE:LINE: error: the ARMv7-M backend cannot lower WHAT yet (function NAME) [OP w=W size=S]
    

    with the RV32 backend, the RV64 backend or the AVR backend in place of the ARMv7-M backend. An encoder asked to encode an operand it cannot represent stops with an internal error rather than emit a different instruction.

Encoders and their referees#

Each backend assembles its own instructions, so no assembler checks them. Each encoder is therefore compared with an independent one by a golden test: a small program built from the encoder emits every form the backend uses, and the test compares the bytes, or the disassembly of the bytes, with an LLVM or GNU tool.

Encoder Test Driver program Reference What is compared
AArch64 emit.c tests/golden/aarch64/arm64-encoding.sh tools/a64check/a64check.c aarch64-elf-objdump disassembly text of each word against the intended instruction
AArch64 asm.c tests/golden/aarch64/arm64-asm.sh tools/a64check/a64asmcheck.c aarch64-elf-as -march=armv8.2-a bytes, for the kernel's templates and every vocabulary entry
Thumb emit.c tests/golden/thumb-encoding.sh tools/thumbcheck llvm-objdump disassembly text; --immediates sweeps every modified immediate
Thumb VFP tests/golden/thumb-vfp.sh tools/vfpcheck llvm-mc -triple=thumbv7em-none-eabihf, with -mattr=+vfp4 and again with -mattr=+fp-armv8d16 bytes
Thumb-1 (ARMv6-M) t1_* forms tests/golden/thumb-v6m-encoding.sh tools/t1check (--refuse) llvm-mc -triple=thumbv6m-none-eabi -mcpu=cortex-m0 bytes of every operand combination, including the ARMv7-M encoders ARMv6-M code reuses (16-bit branches, mov, ldr literal, BL, system); --refuse checks out-of-field operands write nothing
Thumb asm.c tests/golden/thumb-asm.sh tools/tasmcheck llvm-mc bytes, for every entry of the assembler's own tables
RISC-V emit.c tests/golden/riscv-encoding.sh tools/riscvcheck (--rv32, --rv64) llvm-mc --disassemble -mattr=+m disassembly text; --li32/--li64 execute rv_li sequences in an interpreter; --refuse checks the range checks fire
RISC-V compression tests/golden/riscv-compressed.sh tools/riscvcheck (--csweep32, --csweep64) llvm-mc -mattr=+m,+c bytes of rv_compress against llvm-mc's compression of the same instruction
RISC-V asm.c tests/golden/riscv-asm.sh tools/rvasmcheck llvm-mc -mattr=+m bytes
MIPS emit.c tests/golden/mips-encoding.sh tools/mipscheck (--vocab, --li, --refuse) llvm-mc -triple=mipsel-unknown-elf -mcpu=mips32r2 -show-encoding each word, every register in every field; --li executes mips_li sequences; --refuse checks the range checks fire
MIPS asm.c tests/golden/mips-asm.sh tools/mipsasmcheck llvm-mc bytes; and -S reassembled by llvm-mc against -c's object
LoongArch emit.c tests/golden/loongarch-encoding.sh tools/lacheck (--vocab, --li, --run-li, --refuse) llvm-mc --triple=loongarch64 -show-encoding each word, every register in every field; --li against llvm-mc's li.d; --run-li executes the sequences; --refuse checks the range checks fire
LoongArch asm.c tests/golden/loongarch-asm.sh tools/laasmcheck llvm-mc each statement's words, pseudos included; and -S reassembled
AVR emit.c tests/golden/avr-encoding.sh tools/avrcheck llvm-mc -triple=avr -mcpu=atmega328p bytes for the vocabulary; PC-relative forms disassembled and compared as text; --writes checks the decoder avr_insn_writes
AVR asm.c tests/golden/avr-asm.sh tools/avrasmcheck llvm-mc bytes; PC-relative forms as text

The vocabularies are generated from the encoders' and assemblers' own tables where possible, so a new form cannot escape the check. Comparing disassembly text, not only bytes, is what catches a condition code or register field that encodes cleanly but means something else. A form the driver program does not list is not checked; add each new instruction form to it. On Thumb, cbz, the IT form and the wide patchers are covered only by execution tests (thumb-relax.sh, thumb-exec.sh); the 16-bit branches and their patchers are checked by thumb-v6m-encoding.sh at every offset they can hold.

x86-64's emit.c has no per-instruction referee. Its coverage is the execution tests, tools/x86-identity.sh (byte-identical objects against a baseline revision, for changes that must not alter x86-64 output), and the inline-asm tests (tests/golden/x86_64/inline-asm*.sh), which check the separate inline-asm assembler against objdump.

tests/golden/asm-S.sh and tests/golden/asmout-roundtrip.sh check that -S output reassembles to the same object as -c. They test the -S writer, not the encoders: -S prints the encoded bytes.

The toolchain the tests use can be overridden: EMBCC_LLVM_MC, EMBCC_LLVM_OBJDUMP, EMBCC_LLVM_OBJCOPY, EMBCC_AARCH64_OBJDUMP, EMBCC_AARCH64_OBJCOPY. A test whose tool is missing is skipped.

x86-64#

src/arch/x86_64/codegen.c, entry point codegen_unit. Targets x86_64-elf (the default), x86_64-emblink, x86_64-linux-gnu, x86_64-apple-darwin (Mach-O) and x86_64-windows-gnu (COFF).

Lowering#

gen_func lowers a function; its per-instruction switch is preceded by the __int128 lowering (gen_i128), the x87 long double lowering (gen_x87), and the read-modify-write fusion. At -O0 every value lives in an rbp-relative slot and operations go through rax and rcx. At -O1 the backend keeps a residency cache: a value just computed into rax is not reloaded. With the allocator on, values live in their allocated registers, and a zero-extended value in rax also serves a wider zero-extending read of the same temp -- but only when rax holds the whole value, as after a store of a 4-byte result, whose slot's upper bytes are then zero. After a narrow load of a wider value rax holds only its low bytes, and only a read of exactly that width may reuse it.

Instruction selection:

  • Compare and branch fuse when the compare's only use is the next branch. A float compare branches on the flags; == and != add a jp for the unordered case.
  • Addressing modes. An ADD whose only use is the next load or store becomes [base+disp] or [base+index*scale], with a shl by 1 to 3 as the scale. The address of a local folds into an rbp displacement.
  • lea is the three-address add (d = a + imm, d = a + b); multiply by a constant is the three-operand imul.
  • Read-modify-write. A load, an operation and a store back to the same address become one instruction with a memory destination (x86_rmw_find; EMBCC_NO_RMW turns it off). Its address folds in too (rmw_addr_load): s[sp - 1] += s[sp] is one add %x, (%base,%index,4), the add and the shift emitting nothing and the base and the index held, not combined, across the loads of the right-hand side. A constant stored to memory is one mov $imm, mem.
  • IR_SELECT is test of the condition at its own width (size, 4 or 8) and cmovne.
  • Division puts the dividend in rax, extends it into rdx (cqo, cdq, or xor edx), and divides; the remainder is read from rdx. A variable shift count goes through cl.
  • Block copies and clears (IR_MEMCPY, IR_MEMZERO, struct arguments) over 256 bytes use rep movsq or rep stosq; smaller ones move 16 bytes at a time through xmm7 (8 bytes through rax under -mno-sse), then the remainder.
  • Vectors. The vectorizer's operations (IR_VLOAD, IR_VSTORE, IR_VBIN, IR_VSPLAT, IR_VWIDEN, IR_VREDADD) are SSE2 only, with xmm7 as the working register and accumulators in xmm8-xmm11.
  • __int128: add, subtract, bitwise operations, negation and comparisons are inline (adc/sbb); multiply, divide, remainder, shifts and conversions call libgcc (__multi3, __divti3, __udivti3, __modti3, __umodti3, __ashlti3, __ashrti3, __lshrti3, __floattisf and the others). IR_CAS16 is lock cmpxchg16b.
  • long double is the x87 80-bit format in a 16-byte slot. Values are never in registers; each operation loads onto the x87 stack and stores back (fld/fstp, faddp and the others, fucomip), and conversions to integers use fistp under a truncating control word.
  • Atomics are xchg, lock xadd, lock cmpxchg, and a compare-and-swap loop for the other read-modify-write operations, reading their operands where the allocator put them (atomic_in_reg): rax is the one fixed register, and an operand left in a slot is loaded into rcx (an address), rdx (a desired value) or rsi (an IR_ARMW operand). __atomic_compare_exchange writes the value seen back through expected only on a miss. mfence is the fence.
  • Thread-local storage is local-exec: mov %fs:0, reg and an add of an RK_TPOFF32 offset.

Frame layout#

The frame is rbp-based. Below rbp, layout_frame places, in order: the callee-saved register saves, the sret pointer slot (when the function returns a struct in memory), the locals, the shared temp slots, the 16-byte slots of wide temps, scratch space for struct-return temporaries and Win64 by-reference copies, the variadic register save area (176 bytes) and __va_list_tag (24 bytes), and the outgoing argument area at rsp. The total is a multiple of 16. There is no red zone.

Locals share slots when their scopes, or (when their address is not taken) their live ranges, do not overlap (coalesce_locals); this is off under -g, with a computed goto, and with exception regions. Temps share slots through ra_coalesce_temps. A local whose slot is not needed (unreferenced, in a register, built in place, or an incoming memory-class struct parameter used where it arrives) gets no slot.

A local aligned beyond what the stack guarantees does not reach the backend as a slot: sema gives such an aggregate storage carved from the stack at function entry and turns the local into a pointer to it (var_indirect), and refuses such a scalar. layout_frame still stops on a slot needing more than 16-byte alignment (a local in 'NAME' needs N-byte alignment, exceeding the 16-byte stack alignment EmbCC can guarantee) as a guard.

Frameless and push-only functions (from -O1, not under -g). A function with an empty frame, no saved registers, no calls other than tail calls, at most four scalar parameters, and none of alloca, varargs, sret, inline asm, va_start or frame-address operations has no prologue at all. A function that meets the same conditions but makes calls or saves registers, and has no value in a stack slot, no outgoing stack arguments and no exception regions, gets only push instructions (plus a pad push when needed for alignment) and no frame pointer. This is ELF-only (Mach-O's compact unwind expects an rbp frame) and not used under Win64.

The dead-slot guard. A slot that should never be accessed (a local in a register, a temp that appears nowhere, a value with an FP home) is given the displacement X86_DEAD_SLOT (-0x40000000). Every rbp-relative ModRM goes through no_dead_slot, which stops the compilation if that displacement is used, or if any frame access is made in a function with no frame pointer:

internal error: a value was read from a stack slot it does not have -- it lives in a register, and the lowering of `OP` does not know that
internal error: a frame access in a function that has no frame pointer -- rbp is the caller's here, and the lowering of `OP` does not know that

OP is the IR operation being lowered (x86_lowering_op). The guard also fires on writes.

Prologue and epilogue#

Each function is aligned to 16 bytes with nop padding, except at -Os. The prologue is push rbp; mov rbp, rsp, the callee-saved pushes, and sub rsp, N. A variadic function then saves rdi-r9 and (unless -mno-sse) xmm0-xmm7 to the save area; under Win64 it spills rcx-r9 to the home area. Incoming parameters are moved to their allocated registers with one parallel move per class.

The epilogue restores the callee-saved registers (lea to the save area and pops, or a load when there is one) and returns with leave; ret (or ret). With registers to restore and more than one return, every return jumps to one shared epilogue.

Unwind information is .eh_frame (src/debug/eh.c) on ELF and __compact_unwind on Mach-O. C++ exceptions are refused for Windows, because .pdata/.xdata are not written.

Tail calls require a direct call with no struct return, a caller that is not variadic, has no alloca, no exception regions, takes no address of a local and uses no computed goto, no stack or by-reference arguments, the Win64 ABI not in use, and the call's result returned directly. The call becomes the epilogue followed by jmp.

Calling conventions#

System V. Arguments are classified in the shared IR generator (src/ir/irgen.c, using ty_classify in src/sema/type.c), which records for each argument its class, register or stack offset, and whether it is passed by reference. Integer arguments use rdi, rsi, rdx, rcx, r8, r9; floating-point arguments xmm0-xmm7, each file with its own counter. A struct goes on the stack whole unless every eightbyte fits in registers. MEMORY-class results are returned through a hidden pointer in rdi, which is returned in rax. A variadic call sets al to the number of vector registers used. va_list points at the 24-byte __va_list_tag; va_arg is generated by irg_va_arg_sysv in x86_64/irgen.c. A structure is classified as an argument would be: when every eightbyte finds an unread register of its class, the INTEGER eightbytes are read from the general-register part of the save area and the SSE ones from the vector part, 16 bytes apart; otherwise, and always for a MEMORY structure, it is the next run of the overflow area. Its bytes are copied into a slot semantic analysis gives the va_arg expression, as on every target.

Microsoft x64 (target_win64_abi()). Four argument slots, rcx, rdx, r8, r9, with one position counter shared by integer and floating-point arguments; 32 bytes of shadow space are always reserved, so the first stack argument is at rbp+48. Aggregates other than 1, 2, 4 or 8 bytes are copied by the caller and passed by address. A variadic double is also passed in the integer register of its slot. A variadic callee spills rcx-r9 into the home area its caller reserved; va_list is a char * that walks 8-byte slots from the first unnamed one, va_arg of a double reads its slot, and va_copy is an assignment. va_arg of a structure, of long double or of __int128 is refused. EmbCC's Windows target does not yet preserve rsi, rdi, xmm6 and xmm7 across calls, and its long and wchar_t sizes are those of System V; the -Wwindows-abi warning says so on every Windows compile.

Darwin uses System V; the backend differences are no push-only frames, offset jump tables and compact unwind.

Branches and jump tables#

Branch relaxation: codegen_unit generates each function repeatedly. Every branch starts in its short (rel8) form; one that does not reach is marked long and the function is generated again. After 12 rounds every remaining branch is long. A switch's default branch, a compare-and-swap loop's branch, jumps to the shared epilogue and tail jumps are always rel32. tests/golden/branches.sh checks convergence and displacements at the limits.

IR_SWITCH is cmp $n, %rax; jae default; lea table(%rip), %rcx, with the table in .text right after the dispatch:

  • ELF: jmp *(%rcx,%rax,8) through a table of 8-byte absolute addresses, each an RK_ABS64 relocation against the function's own symbol with the case label's offset as addend. EmbCC's code is position-dependent (-fPIC is refused), so the relocations cost nothing at run time.
  • Mach-O and COFF: 4-byte offsets from the table, read with movsxd, added to the table address, and jumped through; no relocations.

The IR generator decides which switches become tables (switch_dense in src/ir/irgen.c, the same for every target that has them): at least 4 cases, a range of at most 4096 values, and the range at most 4n+4 for n cases (at -Os, at least 6 cases and a range at most 2n). A switch on a value wider than a pointer stays a compare tree.

Inline and file-scope assembly#

Extended inline asm (AT&T syntax) is assembled by x86_64/irgen.c (irg_asm_x86, asm_assemble). Operands are resolved from their constraints (fixed a, b, c, d, S, D; allocatable r from rax, rcx, rdx, rbx, rsi, rdi, r8-r11; x for xmm0-xmm7; m; i). Clobbered registers and registers the template names are not used for operands. A callee-saved register an operand names is marked used, so the prologue saves it.

File-scope __asm__ is assembled by topasm.c on x86-64 and AArch64: the directives (.global, labels, .byte, .long, .quad) work on both; the mnemonic half (and, call, jmp, ret) is x86-64 only. On Cortex-M, RISC-V, MIPS32 and AVR the driver hands each block to the GNU-syntax assembler instead (gas_assemble_block in src/as/gas.c), which returns its bytes, labels, relocations with their ELF types, alignment and data ranges in the same struct topasm. A naked function becomes such a block before code generation (naked_to_blocks in src/driver/main.c): its label, then its asm statements with their constant operands written in.

as.c is EmbAS, the NASM-syntax assembler behind embas and embcc -c FILE.asm; see embas. disasm.c is the AT&T disassembler used by -S and EmbDBG.

AArch64#

src/arch/aarch64/codegen.c, entry point codegen_unit_arm64. Targets aarch64-elf, aarch64-emblink, aarch64-linux-gnu and aarch64-apple-darwin (Mach-O). There is no Windows on ARM target.

Lowering#

gen_func handles the frame and the per-instruction switch; the binary128 long double lowering (gen_a64_ld) and the __int128 lowering (gen_a64_i128) run before it. Operands are read with rd/rd_ext/rd_b and written with wr/wrote (and frd/fwr for floats); a spilled value is loaded into x9 (or x10 for the second operand, v16/v17 for floats).

Instruction selection:

  • Immediates. Constants are built with movz/movk or movn/movk, whichever is shorter; add/sub/cmp take a 12-bit immediate, optionally shifted by 12 (a negative one flips the operation); logical operations take bitmask immediates; float constants use fmov where the 8-bit form fits, fmov d, xzr for +0.0, and otherwise -- when movz/movk would take three or more instructions for a double, two for a float -- ldr dN, <literal> from the function's literal pool: each distinct value once, eight-aligned after the epilogue, marked as data ($d), and the loads patched once it is laid out (a64_lit_load, a64_lit_flush, a64_fldr_lit).
  • Compare and branch. An ==/!= compare with zero whose only use is the next branch is cbz/cbnz; any other such compare is b.cond.
  • IR_SELECT is cmp of the condition with zero at its own width (size) and csel.
  • Shifted and extended operands. A shift by a constant whose only use is the next add, subtract or logical operation becomes its shifted register operand, and an extension feeding an add or subtract becomes its extended register operand (uxtw, sxtw and the others). A multiply by (2^k ± 1) << j is a shifted add or a shift and subtract.
  • Addressing. An add whose only use is the next access becomes a register-offset address ([xn, xm], or [xn, xm, lsl #k] when the shift matches the access size); a constant offset folded by ra_fold_memoff uses the scaled 12-bit form or, for -256..255, ldur/stur.
  • Division is sdiv/udiv; the remainder is the quotient in x11 followed by msub.
  • Multiply-accumulate. A product whose only reader is the add or subtract right after it is one madd (c + a*b) or msub (c - a*b), at 4 or 8 bytes; a*b - c stays two instructions.
  • binary128 long double calls libgcc (__addtf3, __subtf3, __multf3, __divtf3, the __eqtf2 family, __floatditf, __fixtfdi, the extend and truncate helpers), with values in q0/q1.
  • __int128: add, subtract, bitwise operations, negation and comparisons are inline; multiply, divide, remainder, shifts and conversions call libgcc. IR_CAS16 is an ldxp/stxp loop.
  • Atomics are ldxr/stxr retry loops between dmb ish barriers; IR_FENCE is dmb ish.
  • Addresses of strings are adrp + add :lo12:. A global or function that is undefined and either weak or on Darwin is reached through the GOT (adrp + ldr). TLS is local-exec (mrs tpidr_el0 and two adds).
  • Vector operations are not lowered. They reach the default case and stop with aarch64 codegen: unhandled IR op N in 'NAME'; the vectorizer is on only for x86-64.
  • Under -mgeneral-regs-only, any floating-point operation stops with floating point used under -mgeneral-regs-only (in 'NAME').

Frame layout#

Slots are addressed from the frame base with non-negative offsets: sp, or x19 in a function with alloca (which moves sp). From the frame base upward, layout_frame places the outgoing argument area, the by-reference copy area, struct-return scratch space, the sret slot (which keeps x8), the saved x19 (with alloca), the variadic save areas (64 bytes for x0-x7, 128 bytes for q0-q7 unless -mgeneral-regs-only, and the 32-byte va_list record; not on Darwin), the locals, the shared temp slots, and the 16-byte slots of wide temps. The callee-saved save area follows, and the frame record (x29, x30) sits above it; incoming stack arguments are at x29+16. Over-aligned locals are handled in sema as on x86-64, with the same guard in layout_frame.

An offset that does not fit the scaled 12-bit form or the 9-bit unscaled form is built in x12 (mov x12, #off; add x12, base, x12).

The dead-slot guard. A slot that should never be accessed gets the offset A64_DEAD_SLOT (0x40000000). a64_no_dead_slot checks every load, store and address computation based on the frame base, x29 or sp, and stops on an offset in [0x40000000, 0x40010000) (the range covers field offsets within the dead slot):

internal error: aarch64: a value was read or written at a stack slot it does not have -- it lives in a register, and some lowering path does not know that

Frameless leaves. From -O1 without -g, a function with an empty frame, no saved registers, no alloca, no varargs, no exception regions, no calls other than tail calls, no helper calls, no inline asm and no stack or by-reference parameters gets no frame record: no prologue, and ret as its epilogue.

Prologue and epilogue#

Each function is aligned to 16 bytes with nop, except at -Os. The prologue is stp x29, x30, [sp, #-16]!, mov x29, sp, and sub sp, sp, #N (through x12 when N does not fit). Then the callee-saved registers are stored (stp pairs where the offset allows), x19 is set up for alloca, the variadic registers are saved, x8 is stored to the sret slot, and parameters are moved to their homes: first the register-arrived ones (one integer and one floating-point parallel move), then the stack-arrived ones.

There is one epilogue at the end of the function: restore the callee-saved registers, reset sp (add sp, or mov sp, x29), ldp x29, x30, [sp], #16, ret.

CFI (.eh_frame) describes the frame record and each saved register; a frameless function's FDE has no instructions. Mach-O gets __compact_unwind entries instead.

Tail calls (from -O1, without -g) require a direct, non-variadic call with no struct or float result, a caller that returns no struct or float, is not variadic, has no alloca and no exception regions, takes no local's address, does not use va_start, and every argument in a register; and the caller has at most one return, unless its frame is the frame record alone (no locals, no callee-saved registers), when each tail call's own teardown is one ldp -- or nothing, when every call is a tail call and the function needs no frame. The call becomes the callee-saved restores, the frame teardown and b.

Calling conventions#

AAPCS64. Placement (a64_place_info, a64_place_arg) is computed by the backend, shared by call sites, the prologue, va_start and the allocator's hints. Eight x and eight v registers with separate counters, and no back-filling once one is exhausted. A float takes the next v register; a homogeneous floating-point aggregate (1 to 4 members of one floating type) takes one v register per member if all fit; a composite over 16 bytes that is not an HFA is passed as a pointer to a caller-made copy; an __int128 takes an even-numbered x pair; a composite with 16-byte natural alignment starts at an even register; other composites take consecutive x registers. Stack slots are at least 8 bytes, aligned by natural alignment. A result over 16 bytes is returned through x8. Variadic functions fill the 32-byte va_list record (__stack, __gr_top, __vr_top, __gr_offs, __vr_offs). va_arg of a structure (va_struct_aapcs) reads an HFA from the v save area, one member per 16-byte register slot; a composite over 16 bytes as the pointer it was passed as; any other from the x save area, starting at an even register when its natural alignment is 16; and one that no longer fits in registers from the stack, in whole doublewords.

Darwin arm64. Named stack arguments are packed at their own size and alignment, there is no even-register rounding, every variadic argument goes on the stack in 8-byte slots (aligned to 16 for a type with 16-byte alignment), and va_list is a char *, so va_copy is an assignment. The data model differs too (signed char, int wchar_t, 8-byte long double), and an unnamed bit-field does not raise a structure's alignment. irg_va_arg_darwin walks the pointer in 8-byte slots; a structure takes whole doublewords, starts at a 16-byte boundary when its type is 16-byte aligned, and is read through a pointer when it is over 16 bytes.

Branches and jump tables#

There is no branch relaxation. b.cond and cbz/cbnz reach ±1 MiB, b and bl ±128 MiB; a displacement out of range stops with an internal error (aarch64 conditional branch displacement cannot encode N). tbz/tbnz are not used.

IR_SWITCH is cmp, b.hs default, adr x11, table, ldrsw x12, [x11, wI, uxtw #2], add x12, x11, x12, br x12, with the table of 4-byte offsets from its start inline in .text and no relocations. The table is marked with code_mark_data.

Inline asm#

aarch64/irgen.c (irg_asm_arm64) resolves operands: constraints r, g, i, n, register variables bound with register ... __asm__("xN") (x0-x11 and x13-x15 only), and the template forms %N, %wN, %xN, %[name]. Operand registers come from x9, x10, x11, x13, x14, x15, then x0-x8, never a callee-saved register; a template that names or clobbers a callee-saved register is refused. asm.c assembles the template text: hints, barriers, mrs/msr with a table of system registers, tlbi, dc, ic, exception-generating instructions, ldr/str, and .inst. Labels in inline asm are refused.

Thumb (ARMv7-M and ARMv8-M Mainline)#

src/arch/thumb/codegen.c, entry point codegen_unit_thumb. Targets thumbv7m-none-eabi, thumbv7em-none-eabi, thumbv8m.main-none-eabi and their -eabihf forms. Code generation is the same at both architecture levels; ARMv8-M differs in its build attributes, its predefined-macro table and its FPU (fpv5-sp-d16). ARMv6-M (thumbv6m-none-eabi, Cortex-M0/M0+/M1) is a third level with its own instruction selection, src/arch/thumb/v6m.c: see ARMv6-M below and armv6m-plan.md.

Lowering#

gen_ins lowers 32-bit operations, gen_ins64 64-bit ones, and cmp64 64-bit comparisons. Operands are read with rdr/rd and written with wreg/wr/wrote; rd64/wr64 handle register pairs.

  • 16-bit forms. Flags are dead between IR instructions, so the encoder uses the 16-bit flag-setting forms (movs, adds, subs, register ALU operations, shifts, muls) whenever the operands are low registers. The low-scratch mechanism (lo_free) computes in a free low register to make that possible.
  • Constants are movs, movw, mov.w with a modified immediate, or movw+movt. There are no literal pools. Addresses are always movw/movt (RK_THM_MOVW/RK_THM_MOVT).
  • Immediates: add and subtract use the 16-bit forms, then addw/subw (±4095), then a modified immediate; an AND mask of low bits that is not a modified immediate becomes ubfx.
  • Shifted operands and addressing. A constant shift with one use followed by an add, subtract or logical operation is its shifted operand (rsb for (a<<k)-b); with LSL up to 3 and an add used only as the next access's address it becomes ldr/str rt, [rn, rm, lsl #k]. An add of two registers used only as the next access's address, with no folded constant offset, is ldr/str rt, [rn, rm] (16-bit in low registers). Multiply by (2^k ± 1) << j uses add/rsb with a shifted operand.
  • Compare and branch fuse when the compare's only use is the next branch. IR_BRZ/IR_BRNZ on a low register whose target is 0 to 126 bytes ahead is cbz/cbnz.
  • A comparison's 0/1 into a low register is ite c; mov d, #1; mov d, #0 (six bytes). This is the only IT block the backend emits; IR_SELECT uses a compare and branches, testing the condition at its own width (size): an 8-byte condition is the OR of its two halves.
  • Divide is sdiv/udiv; the remainder is the quotient followed by mls.
  • Multiply-accumulate. A 32-bit product whose only reader is the add or subtract right after it is one mla (c + a*b) or mls (c - a*b); a*b - c stays two instructions.
  • 64-bit integers use register pairs: adds/adc, subs/sbc, per-half logic, umull/mla for multiply, shifts by a constant or (branching on count ≥ 32) by a variable. A 64-bit divide calls __divdi3, __udivdi3, __moddi3 or __umoddi3. An AND, OR or XOR with a constant takes each half on its own (logic_half): all ones or zero is a copy, a zero or a mvn; a modified immediate or its complement is and/orr/eor or bic/orn; a mask of low bits is ubfx; anything else is built in r10. A shift by a constant goes from the operand's pair straight into the result's, the bits crossing between the words being an orr's shifted operand. A 64-bit shift right by 32 or more whose only reader is a 32-bit AND with a low mask is one ubfx of the high word, and so is a 32-bit shift right followed by such a mask. A branch on a 64-bit value is one orrs of the halves where they live. EMBCC_T_NOWIDEIMM=1 turns these off. A 64-bit compare with a constant K keeps K in the instructions when its halves are modified immediates (thumb_cmp64_imm, which the optimizer's immediate fold asks too): subs; sbcs of x - K for < and >=, and of x - (K + 1) for > and <=; rsbs; mvn; adcs of K - x when K + 1 does not encode (SBC is AddWithCarry(x, ~y, C), so adcs of ~x's high half leaves the flags sbcs would); cmp lo; it eq; cmpeq hi for equality. Two values in registers are compared where they live, > and <= by swapping the register names. EMBCC_T_NOCMP64IMM=1 keeps the constant in a register pair.
  • Soft float calls the libgcc names (not __aeabi_*): __addsf3/__adddf3 and the rest of the arithmetic, the __eqsf2/__eqdf2 family followed by a compare of r0 with 0, and the __float*, __fix*, __extendsfdf2, __truncdfsf2 conversions. Negation flips the sign bit inline. __builtin_sqrt without an FPU is refused.
  • With an FPU (-mfpu=fpv4-sp-d16 or fpv5-sp-d16 with -mfloat-abi=softfp or hard, or an -eabihf triple), single-precision arithmetic, negation, square root, comparisons (vcmp/vcmpe and vmrs APSR_nzcv) and int32 conversions run on the FPU, on values in s16-s31. Doubles still use the soft-float helpers.
  • With the double-precision unit (-mfpu=fpv5-d16, the Cortex-M7's; target_thumb_fpu_dp()), fp_on_vfp also takes the eight-byte arithmetic, comparisons, IR_SQRT, the int32 conversions and IR_F2F, as .f64 instructions with d0/d1 as scratch. A double the FPU computes with gets a d register of its own: t_double_map picks them (operands and results of those, call arguments and results, double locals, and the bit casts and and/xor of the sign bit that fabs and negation become, which are vabs.f64/vneg.f64 there), and an allocation pass over d8-d15 (T_DPOOL, THUMB_DRA) runs before the floats' pass, which ra_reserve keeps off the s registers each double holds over its live range. floc names a double's low single, so t_dreg is floc >> 1, and every slot check covers doubles too. rd64/wr64 reach a d register with one vmov r, r, d, so every 64-bit integer path works on one; copies, IR_SELECT, loads and stores (vldr/vstr, with memoff folded for eight-byte accesses), constants (vmov.f64 #imm where VFPExpandImm reaches) and the prologue's parameters go to and from d registers directly. The 64-bit integer conversions are still helper calls. The prologue saves d8 up as vpush {d8-...}.
  • Atomics are dmb, an ldrex/strex loop (status in lr), dmb; compare-and-swap adds clrex on failure. Atomics wider than four bytes are refused (an atomic wider than four bytes (ARMv7-M has no doubleword exclusive; GCC calls libatomic for these)).
  • IR_ALLOCA (VLAs, __builtin_alloca, and locals aligned beyond 8 bytes) subtracts the size rounded to 8 from sp.
  • Byte swaps arrive as shifts and masks: IR generation emits no IR_BSWAP for ARMv7-M.
  • Computed goto: &&label is pc-relative with no relocation, movw/movt rD, #(label | 1) - (pc) then add rD, pc (in ARM state add rD, pc, rD and no Thumb bit; on ARMv6-M and v8-M Baseline the distance is a literal-pool word), patched once the label is placed; goto *p is bx p.
  • Refused: exception landing pads, 128-bit values, long double operations the target lacks, and anything else not handled, with the backend's refusal message. IR_UD2 is udf #0.

Frame layout#

The frame is sp-relative. A function with alloca uses r7 as the frame base (mov r7, sp after the frame is set up). From sp upward (layout): the outgoing argument area, the shared 4-byte temp slots, 64-bit temps that did not get a pair, locals of up to 8 bytes, then larger locals, scratch space, and the sret slot; the total is a multiple of 8. This order keeps the most-used slots within the reach of the 16-bit ldr/str rt, [sp, #imm] forms (1020 bytes). Above the frame are the pushed registers and, in a variadic function, r0-r3.

Slots beyond an instruction's reach are addressed by building the address (add sp, addw, or movw and add); an access that still cannot be made is an internal error rather than a dropped access. Block copies are straight-line up to 4092 bytes and a loop beyond.

Prologue and epilogue#

Functions are halfword-aligned (4-byte aligned when they contain inline asm), padded with nop. The prologue is push {r0-r3} in a variadic function, push {mask}, vpush {s16-...} with an FPU (vpush {d8-...} with the double-precision one), sub sp, #N, mov r7, sp with alloca, the sret pointer stored, and the parameters placed. The push mask contains the used scratch registers among r9-r11, the allocator's callee-saved registers, and lr; when the register count is odd, r3 is added so that sp stays 8-byte aligned. push and pop use the 16-bit form when the mask allows. The epilogue undoes the frame and returns with pop {..., pc} (pop, add sp, #16, bx lr in a variadic function).

A leaf that saves nothing, has no frame, is not variadic and has no alloca emits no push at all and returns with bx lr.

Tail calls (from -O1, without -g) require a direct, non-variadic call with no struct, float or VFP result, a caller with no struct or float result, not variadic, no alloca, no exception regions, no IR_ADDR and no va_start, every argument in registers, and the result (at most 4 bytes) returned directly. The tail call is emitted as b.w only in a function that pushes nothing.

Interrupt handlers. __attribute__((interrupt)) is accepted and changes nothing: the Cortex-M hardware stacks r0-r3, r12, lr, pc and xPSR on exception entry, so an ordinary function is a handler and bx lr with EXC_RETURN is the interrupt return.

Calling convention#

AAPCS32 core (place_arg): r0-r3, then the stack. A value with 8-byte alignment starts at an even register and an 8-aligned stack offset. A composite may be split between r3 and the stack only while nothing is on the stack yet. A composite's alignment is its natural alignment. A composite over 4 bytes is returned through a hidden pointer in r0; one of at most 4 bytes in r0.

AAPCS-VFP (hard-float ABI, or pcs("aapcs-vfp")). Floats, doubles and homogeneous aggregates of 1 to 4 of them go in s0-s15 (d0-d7), allocated lowest-first with back-filling; after one spills to the stack, no further one uses VFP registers. Results go in s0/d0 (s0-s3/d0-d3 for an aggregate). A variadic call always uses the base convention. pcs("aapcs") selects the base convention for one function; pcs("aapcs-vfp") without an FPU is an error.

Float ABI selection (thumb_options_done, src/arch/thumb/options.c): -mfloat-abi=soft (the default) emits no FPU instructions; softfp uses the FPU with the base convention; hard uses the FPU and AAPCS-VFP. -mfpu= accepts fpv4-sp-d16 and fpv5-d16 (ARMv7E-M; the latter only with -mcpu=cortex-m7 when a CPU is named, and implied by -mcpu=cortex-m7 with an -eabihf triple) and fpv5-sp-d16 (ARMv8-M).

Register arguments, pair halves, and the operands of 64-bit and soft-float helper calls are placed with ra_parallel_move (r9 breaks cycles). va_list is a bare pointer; va_arg walks it in 4-byte steps, and an argument with 8-byte alignment (for a structure, its natural alignment) starts at a doubleword boundary.

Encoder and build attributes#

emit.c writes halfwords; a 32-bit instruction is two halfwords. Each encoder picks the 16-bit form when the operands fit. Modified immediates are found by searching all encodings. The VFP encoder handles the opposite register-number splits of singles and doubles.

attrs.c writes .ARM.attributes: the CPU name and architecture (7-M, 7E-M or 8-M.MAIN), the M profile, Thumb-only ISA use, the FP architecture and HardFP_use when there is an FPU, ABI_VFP_args for the hard-float ABI, and the ABI tags (wchar_t 4, 8-byte alignment needed and preserved, enum_size, R9 use, unaligned access). EmbLD checks these when it links Thumb objects.

Branches and jump tables#

Relaxation (from -O1) runs up to three passes. Pass 0 emits every branch in its 32-bit form and saves all of r9-r11; it records which branches would fit 16 bits (b: -2048 to +2046; b<c>: -256 to +254) or a cbz. Pass 1 emits those short and saves only the scratch registers pass 0 used. Pass 2 runs only if pass 1 used a scratch register it did not save. After allocation, b<c> L1; b L2; L1: is merged into b<!c> L2 (invert_last_bcond); a merged branch is never a cbz. If any conditional branch is beyond ±1 MiB, every conditional jump becomes b<!c> over an unconditional b (far mode). tests/golden/thumb-relax.sh and thumb-far.sh test the limits.

IR_SWITCH is cmp rI, #n; bhs default, then:

  • when every case label lies ahead: tbh [pc, rI, lsl #1] and a table of halfword offsets (an entry that cannot reach restarts the function without tbh for that switch);
  • otherwise: adr.w r11, table; ldr.w r12, [r11, rI, lsl #2]; add r12, r11; bx r12, a nop to align, and a table of word offsets with the Thumb bit set.

Tables are marked with code_mark_data, and the object carries $t and $d mapping symbols around them.

ARMv6-M#

src/arch/thumb/v6m.c, entered from gen_func_best (v6_gen_func) when target_thumb_arch() is 6. ARMv6-M is Thumb-1 plus BL, MRS, MSR and the barriers, so it has its own instruction selection, which emits only through the t1_* encoders and the ARMv7-M encoders emit.h lists as already Thumb-1. It shares with codegen.c, through cg.h, AAPCS32 (place_one), the frame layout (layout), the site lists and the allocator setup; codegen.c's ARMv7-M lowering is unchanged by it.

  • Registers. r0-r5 are the allocator's (T6_POOL; pairs r0:r1, r2:r3, r4:r5); r6 and r7 are the two scratch registers, saved by the prologue when a pass used them, as r9-r11 are on ARMv7-M. A lowering that needs a third register calls tmp_get, which pushes one of r0-r5 the instruction does not touch and pops it after (F->spb corrects every sp-relative offset meanwhile). r12 breaks parallel-move cycles and holds an indirect call's target. r5 is the frame base of a function with a VLA.
  • Flags. Every low-register ALU instruction sets them; the reads (v_rd, fr_ld, fr_addr) never do, building an offset from a literal, so a lowering may read operands between a compare and the branch, and between ADDS and ADCS.
  • Constants are MOVS, MOVS+MVNS, MOVS+LSLS, MOVS+ADDS or MOVS+NEGS (k32), else a literal-pool word; every symbol address is a pool word with an R_ARM_ABS32 site.
  • Literal pools. A load is ldr rX, [pc, #off], forward and within 1020 bytes. The pool is dumped at pool points: each IR instruction's start, each word of a long struct copy, before a switch table. The first pass of a layout decides at each point whether the code to come could carry the first pending load out of reach (an island with a branch around it) or, after an unconditional transfer, whether the pool is half way there (an island without one); later passes replay the decisions by point. Code between a load and its island only shrinks from pass to pass, and literals only drop out.
  • Branches have three sizes: b<c> (±256), b<!c> over b (±2 KB), b<!c> over bl (±16 MB; lr is then saved). The first pass uses the middle form (a far form everywhere, restarted, when a target is past 2 KB); later passes shrink each branch by its measured distance, with two bytes of slack per alignment pad between, until nothing changes.
  • Switch: adr rB, table; ldrb rT, [rB, rI]; lsls rT, #1; add pc, rT with byte entries, halfword entries (lsls rT, rI, #1; ldrh), or, for a backward case or one too far, a word table taken with bx.
  • Calls the lowering makes: 32-bit / and % (__aeabi_idiv, __aeabi_idivmod and the unsigned pair), 64-bit * (__aeabi_lmul) and variable shifts (__aeabi_llsl, llsr, lasr), block copies and clears over 8 bytes (__aeabi_memcpy, __aeabi_memclr), and every atomic read-modify-write (__atomic_*_N, with a compare-exchange's two memory orders pushed as stack arguments). v6_op_calls_helper tells the allocator and the optimizer which instructions these are. 64-bit add, subtract, logic, compare, negate and constant shifts are inline.
  • Alignment. Nothing here makes an unaligned access: a load or store whose address is not natural is done a byte at a time, an inline block copy uses words only between frame slots, and a struct argument or a small struct result is read by its alignment. Wide string literals are aligned to their element (target_string_align) and static arrays of four bytes or more to a word (target_object_align), on ARMv6-M only.
  • Inline asm is ARMv7-M's (ra_target.asm_in_reg): an "=r" output is a value (ir_asm_op.val, a second one a continuation), the inputs and the "m" and "+" addresses move into r0-r3 and r12 as one parallel move and the values out as another, and an output through an address is stored after the template. r12, which only MOV and ADD reach, goes through a low register; a cycle is broken through r12, or r6 when r12 is an operand. A value live across the asm keeps out of ir_asm.clob, and the asm makes its function a non-leaf only when the template calls or names lr. thumbv6m-asm-values.sh runs it.
  • -O0 runs the allocator for the temporaries, with every source variable pinned to its slot (tcg_o0, codegen.c's g_t_o0), as on ARMv7-M, and a function past ra_o0_too_big compiles with every temporary in a slot, as there.
  • No low-register rename. gen_func_best tries pairs on and off as for ARMv7-M, but not t_lowregs: every register ARMv6-M computes in is a low one, so there is no 16-bit form to win.
  • The scan. v6_scan decodes every finished function and refuses it, by name, if anything but an ARMv6-M instruction is in it -- an inline asm template included.
  • Not yet: tail calls, IR_IGOTO, exception landing pads, 8-byte atomics and 128-bit values are refused by name.

Inline asm#

Operands come from r0-r3 and r12; naming or clobbering r4-r11 in a template is refused. asm.c assembles the CMSIS vocabulary: mrs/msr with the M-profile special registers, cpsid/cpsie, barriers, hints, bkpt, ALU and shift instructions, loads and stores, ldrex/strex, multiply and divide, clz/rbit/rev, compares, branches, push/pop, movw/movt.

RISC-V#

src/arch/riscv/codegen.c, entry point codegen_unit_riscv, one backend for riscv32-unknown-elf (ILP32) and riscv64-unknown-elf (LP64). Where the widths differ it reads target_xlen(). The ISA is RV32IMAC or RV64IMAC: integer, multiply and divide, atomics, compressed instructions. There is no -march= option and no hardware floating point; the ABI is soft-float (ilp32, lp64). .riscv.attributes records the ISA string (rv32i2p1_m2p0_a2p1_c2p0 or the RV64 equivalent) and the 16-byte stack alignment.

Lowering#

gen_ins dispatches each instruction: floating-point operations become helper calls, operations wider than 64 bits are refused, and at RV32 64-bit operations go to gen_ins64. At RV64 a 32-bit operation uses the W forms (addw, subw, sllw, mulw, divw and the others). Operands are read with rdr and written with wreg/wrote; rd64/wr64 handle RV32 pairs.

  • Constants (rv_li): addi for 12 bits, lui+addi (or addiw at RV64) for 32 bits, a shift-and-add chain beyond.
  • Addresses of strings, globals and functions are always auipc+addi with R_RISCV_PCREL_HI20/R_RISCV_PCREL_LO12_I, at both widths: the medany code model. lui sign-extends bit 31 at RV64 and cannot reach an image at 0x80000000. The LO12 relocation names the auipc's own address (against the .text section symbol), as the psABI requires.
  • Calls to a function in the same unit are jal ra while every such call reaches (±1 MiB); if one does not, the whole unit is regenerated with auipc+jalr. Calls to other units are auipc ra; jalr ra with R_RISCV_CALL_PLT. EMBCC_RV_LONG_CALLS always uses auipc+jalr, and EMBCC_RV_JAL_RANGE reduces the assumed jal reach, for testing.
  • Compare and branch fuse when the compare's only use is the next branch; a compare with 0 uses x0. A compare whose result is kept as a value takes a folded constant that fits 12 bits as an immediate (cmp_imm_to_reg): slti/sltiu, x <= k as x < k + 1 and x > k its inverse, == and != an xori and a seqz/snez. Against zero that is the seqz, snez or slt alone. EMBCC_RV_NOCMPIMM=1 loads the constant into a register instead.
  • 64-bit operations with a constant at RV32 take each half on its own (logic_half): all ones or zero is a copy, a zero or an li -1; a 12-bit immediate is andi/ori/xori; a mask of low bits is slli+srli, and one of high bits srli+slli; anything else is built in t4. A shift by a constant goes from the operand's pair straight into the result's (shift64_imm_to). A branch or select on a 64-bit value ORs the halves where they live. A 64-bit compare with zero whose only reader is the next branch is that OR and a beqz/bnez, or, for a signed < or >=, a bltz/bgez of the high word. EMBCC_RV_NOWIDEIMM=1 turns these off.
  • Multiply and divide use the M extension (mul, div, rem and the W forms). At RV32 a 64-bit multiply is mul/mulhu; a 64-bit divide calls __divdi3, __udivdi3, __moddi3 or __umoddi3.
  • Soft float: arithmetic calls __addsf3/__adddf3 and the rest, comparisons the __eqsf2 family, conversions __floatsisf, __fixdfsi and the others; negation flips the sign bit inline. __builtin_sqrt and long double arithmetic are refused.
  • Atomics (A extension): amoswap.aqrl, amoadd.aqrl, amoand/amoor/amoxor.aqrl; NAND and compare-and-swap use lr.aq/sc.rl loops. Atomics must be register-width (or 4 bytes at RV64); narrower ones are refused. IR_FENCE is fence rw, rw.
  • IR_ALLOCA (VLAs, __builtin_alloca, and locals aligned beyond 16 bytes) subtracts the size rounded to 16 from sp.
  • IR_SELECT loads one arm or the other around a branch on the condition, tested at its own width: at RV32 an 8-byte condition is the OR of its halves, and at RV64 a 4-byte one is read through rd32.
  • Byte swaps are IR_BSWAP only at RV64; at RV32 IR generation builds them from shifts and masks.
  • Block copies are straight-line up to 2040 bytes and a loop beyond.
  • Computed goto: &&label is auipc/addi (never compressed) patched with the label's distance, no relocation; goto *p is jalr zero, 0(p).
  • Refused: 128-bit values. IR_UD2 is unimp.

32-bit values at RV64. The psABI keeps a 32-bit value in a register as its sign extension, unsigned values included (0xffffffffu is all ones). The W forms and lw produce that, but EmbIR reads a narrowed value as the same temp at width 4, and lwu, zero-extending reads and zero extensions do not. Each reader that compares or passes all 64 bits takes a width-4 operand through rd32, which adds sext.w unless the value is already sign-extended: compares and fused compare-and-branch, IR_BRZ/IR_BRNZ, an IR_SELECT condition of 4 bytes, the bound check of IR_SWITCH, the expected value of a compare-and-swap (compared with what lr.w sign-extended), an int converted to floating point, a 4-byte return value, and 4-byte integer arguments in registers and on the stack, as clang and gcc pass them. sext_map decides which values are already sign-extended, to a fixed point: the W arithmetic, compares, constants that fit 32 bits signed, signed 4-byte and all narrower loads and extensions, a call returning an integer of at most 4 bytes, __fix*si results, an and with a constant from 0 to 2^31-1 (whatever its other operand), and and/or/xor/not, copies and selects of such values.

Frame layout#

The frame is sp-relative; s0 is the frame base only in a function with alloca. From sp upward (layout): the outgoing argument area, the by-reference copy area, the shared temp slots, RV32 64-bit temps without a pair, locals of up to two registers' width, larger locals (arrays last), scratch space, the sret slot, the callee-saved saves, ra (absent in a leaf), and in a variadic function the a0-a7 save area at the top, next to the caller's stack arguments. The frame is a multiple of 16. Values with a register home get no slot; a lowering that asks for the slot of such a value stops with riscv: NAME: a path addresses vreg N's slot, and it has none. Offsets beyond 12 bits are built in t6.

A leaf (no non-tail calls, no inline asm, no helper calls) saves no ra and, with nothing else in the frame, never moves sp.

Prologue and epilogue#

The prologue adjusts sp, stores ra and the callee-saved registers, sets up s0 for alloca, spills a0-a7 in a variadic function, and places parameters with one ra_parallel_move (t4 breaks cycles) followed by loads of stack-arrived parameters. Every return jumps to the single epilogue, or falls into it when nothing but labels lies between; the epilogue restores, adjusts sp and returns with ret (c.jr ra). It is omitted when the function ends in a tail call and nothing jumps to it. Functions are not padded when the C extension is on (always, at present).

Tail calls (from -O1, without -g) require a direct, non-variadic call with no struct or float result, a caller that returns no struct or float, has at most one return, no alloca, no varargs, no exception regions, no IR_ADDR and no va_start, and every argument in registers. They are jal x0 within the unit or auipc t1; jalr x0.

Interrupt handlers are refused on RISC-V (__attribute__((interrupt)) is not supported: ...).

Calling convention#

place_arg implements the RISC-V psABI integer convention: a0-a7, then the stack. A struct larger than two registers is passed by reference to a caller-made copy. A two-register scalar or aggregate takes two consecutive registers; only a variadic argument with 2×XLEN alignment is rounded to an even register (and to 2×XLEN on the stack), a named one is not. A value split by the end of the register file puts its low word in a7 and its high word on the stack. A stack argument is aligned to its type, between XLEN and 16 bytes. Small aggregates are packed by bytes into registers. A composite over two registers is returned through a hidden pointer in a0; smaller ones and 64-bit scalars at RV32 come back in a0/a1. At a call, by-reference copies and stack words come first, then one parallel move for register arguments, then struct words, and the sret pointer last. va_list is a bare pointer. va_arg of a structure of up to two registers copies its words from the walk; a larger one was passed by reference, and its pointer is followed.

Encoder#

emit.c has one packer per instruction format (R, I, S, B, U, J), each with range checks that stop with an internal error rather than truncate. Every instruction passes through rv_w, which substitutes the compressed form from rv_compress when one exists and compression is on (c.addi, c.li, c.mv, c.lui, c.add, c.sub and the other register forms, c.lw/c.ld/c.sw/c.sd and their sp forms, c.addi16sp, c.addi4spn, c.jr/c.jalr, and others). Compression is suppressed where a fixed size is needed: branch and jump placeholders, auipc pairs (so the LO12 relocation's instruction is 4 bytes), call placeholders, jump table dispatch, and unimp.

Branches and jump tables#

Pass 0 emits every jump as jal x0 and every conditional branch as an inverted branch over a jal (8 bytes). From the measured distances, pass 1 (when the allocator is on and something can shrink) uses c.j (±2 KiB), c.beqz/c.bnez (a compare with zero of x8-x15, ±256 bytes), a plain branch (±4 KiB), or keeps the long form. A relaxed branch that no longer reaches is an internal error. tests/golden/riscv-relax.sh sweeps the limits.

IR_SWITCH is a bounds check (bgeu to the default), then auipc t1, 0; slli t2, rI, 2; add t2, t2, t1; lw t2, off(t2); add t2, t2, t1; jr t2, and a 4-byte-aligned table of 32-bit offsets from the auipc in .text, resolved by the compiler with no relocations.

Inline asm#

Operands use ABI register names and come from t0-t6, then a0-a7. Naming or clobbering a callee-saved register is refused. asm.c assembles system instructions (ecall, ebreak, mret, sret, wfi, fences, fence.i), the CSR instructions with a table of named CSRs (RV32-only ones refused at RV64), integer, multiply, load, store and branch instructions with numeric offsets, and the common pseudo-instructions. AMOs and lr/sc are not in the vocabulary.

Relocations#

RK_CALL and RK_TAIL become R_RISCV_CALL_PLT; the address pair R_RISCV_PCREL_HI20/R_RISCV_PCREL_LO12_I; data R_RISCV_32 or R_RISCV_64. EmbCC emits no R_RISCV_RELAX or R_RISCV_ALIGN.

MIPS32#

src/arch/mips/codegen.c, entry point codegen_unit_mips, for mipsel-none-elf: MIPS32 Release 2, little-endian, the o32 ABI with soft float. The design notes, with every ABI fact and where it was read off, are in the MIPS32 plan. The backend has RV32's shape (one home per vreg, rdr/wreg/wrote, register pairs for 64-bit values, helper calls for floating point and 64-bit division), with the differences below.

Lowering#

  • Delay slots. A return's slot holds the frame's release: mips_restore emits jr $ra before addiu $sp, $sp, N. Every other transfer to a label (branch_to), call (call_sym, the indirect jalr) and frameless return takes the instruction emitted just before it into its slot when take_slot allows: the instruction decodes as an ordinary computation, load or store (slot_decode; never a transfer, trap, sync, ll/sc or a nop), the transfer does not read what it writes, a linking transfer (jal, jalr) finds it neither reading nor writing $ra, it carries no relocation site, and nothing lies between it and the transfer that something could jump to -- F->barrier is raised past every label, landing, transfer, asm block and the prologue, and past the epilogue's label when a branch goes there. Otherwise the slot is a nop (put_slot); the branches inside one lowering (br_place) always take a nop. Under -g nothing moves (the line rows name offsets), and EMBCC_MIPS_NO_FILL turns the filling off. MIPS32 has no load delay slots and no HI/LO hazards.
  • Jump tables. IR_SWITCH is sltiu $at, rI, n and a branch to the default, then bal to the next-but-one instruction with sll t2, rI, 2 in its slot, addu/lw/addu of the entry against $ra, and jr t2: the table follows, of 32-bit offsets from the address bal returns (FX_TAB fixes, patched when the function ends), so it carries no relocation. A leaf keeps its live $ra in t6 around the bal.
  • Branches compare two registers only for == and !=; an ordered comparison against zero has bltz/bgez/blez/bgtz, and anything else is slt/sltu into $at and a beq/bne of it (branch_if). A fused compare against a 16-bit constant is slti/sltiu into $at.
  • Long branches. Each branch is first emitted short (±128 KiB). One that does not reach is given the long form -- the inverse branch over a j relocated against the function's section (RK_MIPS_TEXT26, an R_MIPS_26 whose addend is the label's offset) -- and the function is generated again until nothing new fails (gen_func).
  • Immediates. addiu, slti, sltiu, loads and stores take a sign-extended 16-bit field; andi, ori and xori a zero-extended one. mips_imm_foldable tells the optimizer which constants fold. An AND with a run of low bits is ext, with all but a run of low bits ins of $0.
  • Multiply and divide. mul is three-operand (Release 2); a 64-bit product's high word comes from multu and mfhi. Division is div/divu $zero then mflo or mfhi; it never traps.
  • Addresses are absolute: lui and addiu with RK_MIPS_HI16 and RK_MIPS_LO16, noted high first so the relocation table pairs them.
  • Calls are jal with R_MIPS_26 to any function, in the unit or not (a jal holds an absolute word index only the linker knows); an indirect call is jalr through $t9.
  • Misaligned accesses trap. A 2- or 4-byte load or store irgen did not mark natural goes through ld_any/st_any: lwl/lwr and swl/swr for a word, two bytes for a halfword. Block copies whose ends are not known to be word-aligned (IR_MEMCPY without a natural alignment) use the same, and so do a struct argument's words.
  • Atomics are ll/sc loops on words, bracketed by sync; a narrower or wider one is refused. IR_FENCE is sync.
  • Select is movn. Byte swap is wsbh and rotr.
  • IR_ALLOCA rounds to 16 and keeps sp 16-aligned, and the frame is addressed from fp in such a function.
  • Computed goto: &&label is the function's own address as IR_FADDR takes it (lui/addiu, or MIPS64's four pieces) plus the label's offset as the relocation's addend; goto *p is jr p and a nop.
  • Refused: IR_FRAMEADDR, 128-bit values.

Frame and calling convention#

From sp upward: the outgoing argument block (at least the 16-byte home area in any function that calls, a helper included), the shared temp slots, 64-bit temps without a pair, the locals, the struct-return scratch, the sret pointer, the callee-saved registers and ra. A multiple of 8. A variadic function stores a0-a3 into its caller's home area, at the top of its own frame, so va_list walks one block. place_arg lays every argument out as o32 does: an offset rounded up to the argument's alignment (4 to 8) in a block whose first 16 bytes ride in a0-a3; nothing goes by reference. Results come back in v0:v1, every struct through a hidden a0 handed back in v0, a _Complex in v0..a1.

The allocator's pool is v0, v1, a0-a3, t7, t8, then s0-s7 (a0-a3 withheld in a variadic function); the pair pool a0:a1, a2:a3, v0:v1, s0:s1 .. s6:s7. t0-t6 are scratch, $at the branch comparison, $t9 an indirect call's target; fp is the frame base under alloca, and gp, k0, k1 are never touched. Each function is generated with and without the pair pass and the shorter kept, as at RV32.

Encoder and inline asm#

emit.c packs the R, I and J formats once; every instruction is its opcode, function code and fields, with the range of its immediate's extension checked. tools/mipscheck encodes every form with every register in every field and both ends of every immediate, and tests/golden/mips-encoding.sh compares each word with llvm-mc -show-encoding. asm.c is the inline-asm vocabulary ($ register names, numeric branch displacements from the delay slot, the pseudo-instructions move, li, not, negu, b; gas's macros -- neg, div with a destination -- refused), checked against llvm-mc by tests/golden/mips-asm.sh. Operands come from t0-t9, v0-v1 and a0-a3; a template that names a callee-saved register is refused.

Relocations and EmbLD#

The objects are REL: elfw stores each addend in its field (target_rel_put_addend), the HI16 half rounded by 0x8000 so that it and the sign-extended LO16 add back to the addend. apply_mips in src/link/link.c reads REL and RELA, applies a HI16 with the next LO16 against the same symbol in its section (the AHL rule), checks that a jal's target is in its 256 MiB region, checks .MIPS.abiflags across inputs and drops it and .reginfo, and refuses the GOT and gp-relative relocations by name. -Tstack emits li sp and a jr to the entry through $t9.

LoongArch64#

src/arch/loongarch/codegen.c, entry point codegen_unit_loongarch, for loongarch64-unknown-elf: LA64, LP64S (soft float). The facts it rests on are in the LoongArch64 plan. The file began as a copy of RV64's code generator, because the LP64 calling conventions are the same rule for rule, with the RV32 machinery removed (register pairs, 64-bit-at-RV32 lowering, RV32's long double, the C extension) and every instruction re-selected; a copy, so that nothing here can move RISC-V.

  • Registers. t0, t1, t2, t4, t5 and t6 are the scratches (t6 only for sp plus a far offset); the allocator's pool is a0-a7, t3, t7, t8, then fp and s0-s8. r21 and tp are never named. A function with a VLA addresses its frame from fp.
  • Immediates. addi, slti, sltui, the loads and stores take a signed 12-bit field; andi, ori and xori an unsigned one, so la_imm_foldable folds an AND/OR/XOR constant only in 0..4095, not is nor rd, rj, zero, and an alloca rounds down with two shifts. Constants are LoongArchMatInt's sequences (la_li).
  • 32-bit values stay sign-extended (sext_map, rd32), as at RV64; div.w/mod.w/div.wu/mod.wu read their operands through rd32 because they are undefined otherwise.
  • Branches are emitted short (beq-family +-128 KiB, beqz/bnez +-4 MiB); a function in which one does not reach is generated again with that one the long form (the inverse branch over a b).
  • Calls are bl (R_LARCH_B26), within the unit patched directly; addresses pcalau12i/addi.d (RK_LA_PCALA_HI20/LO12, both against the symbol). A jump table is found by pcaddi and indexed by alsl.d.
  • Select is maskeqz/masknez/or; bswap is revb plus a bstrpick.
  • Atomics are am*_db and ll/sc loops between dbar 0s; a one- or two-byte one is an ll.w/sc.w loop on its word (la_atomic_narrow).
  • Inline asm is substituted in loongarch/irgen.c and assembled by loongarch/asm.c; the operand moves are RISC-V's.

tools/lacheck and tools/laasmcheck referee emit.c and asm.c against llvm-mc (tests/golden/loongarch-encoding.sh, loongarch-asm.sh).

AVR#

src/arch/avr/codegen.c, entry point codegen_unit_avr. Target avr (the ATmega328P). Registers are 8 bits, pointers 16 bits, int 2 bytes, and double and long double are the 4-byte float. Code and data are separate address spaces.

Lowering#

The IR computes at 4 bytes and int is 2. IR generation keeps every int value equal to its 16-bit extension (an IR_EXT after each operation that can carry out of 16 bits; see EmbIR), so a 4-byte read sees the C value. The backend computes only the bytes something reads: avr_demand propagates, backward to a fixpoint, how many bytes of each value are needed, and comparisons of extended values are made at their original width (cmp_width). A compare whose 0/1 is used only by the next branch fuses with it.

  • Constants (ldi4): a zero byte is mov r, r1; registers r16 and up take ldi; a nonzero byte for r0-r15 goes through r31 (push r31; ldi r31, b; mov r, r31; pop r31).
  • Multiply by a constant below 2^17 is shift-and-add in place; otherwise it calls __mulsi3. The mul instruction is not emitted by code generation.
  • Divide and remainder call __divsi3, __udivsi3, __modsi3 and __umodsi3 (arguments in r22-r25 and r18-r21, result in r22-r25); the 64-bit forms __muldi3, __divdi3 and the others.
  • Shifts by a constant move whole bytes, then shift single bits; by a variable they loop.
  • Floats are all helper calls: __addsf3, __subsf3, __mulsf3, __divsf3, the __eqsf2 family, and the __floatsisf/__fixsfsi family. Negation flips the sign bit inline; float % is refused.
  • 64-bit values are processed a byte at a time.
  • Loads and stores through a pointer use Z; constant offsets up to 63 (folded by ra_fold_memoff) go into ldd/std. Stores write the high byte first. Z keeps a pointer between accesses while nothing has written it (z_holds, checked by decoding the bytes emitted since).
  • IR_SELECT is a branch and copies. The condition's low four bytes are ORed and tested whatever its size, so the high half of an 8-byte condition is ignored (a defect; see Status). IR_MEMCPY/IR_MEMZERO are byte loops with Z as the destination and X as the source. IR_FENCE emits nothing; IR_UD2 is rjmp .-2.
  • Refused with the backend's message: 128-bit values, a memory access wider than four bytes, a VLA, an exception region, an aligned local, returns wider than 8 bytes, and every operation without a lowering by its IR name (switch, bswap, atomics wider than one byte). IR generation does not emit IR_BSWAP for AVR; a byte swap arrives as shifts and masks.
  • Computed goto: &&label is a word address, as a function pointer is: ldi lo8(gs(f+L)), ldi hi8(gs(f+L)) against the function's own symbol with the label's byte offset L as the addend; goto *p loads Z and is ijmp.

The compiler never emits lpm: the linker places .rodata in the data segment, and the startup code copies .data and .rodata from flash.

Frame layout#

Y (r28:r29) is the frame pointer and points one byte below the frame, so slots are at Y+1 to Y+frame. Y is set up only when the function has a frame, is variadic, has an sret slot, has stack-passed parameters, or is an interrupt handler; a frame access in a function without it is an internal error (avr: NAME: a frame access in a function whose frame pointer was not set up).

The outgoing argument area comes first, at Y+1. Locals and temp slots are then placed densest first: each object is weighted by the bytes of code that access it, so the most-used ones fall within ldd/std reach. Four-byte temps share slots through ra_coalesce_temps; eight-byte temps share by an interval sweep. Incoming stack arguments are at Y+frame+5+2*nsave.

Far slots. ldd/std reach Y+0 to Y+63. A slot ending by Y+127 is reached by copying Y into Z, adiw Z, min(off, 63), and ldd/std with the rest of the offset. Beyond that, Y is copied into a walker pointer (Z, or X when the value being moved occupies Z), the offset is added with subi/sbci, and the bytes are moved with post-increment. Both clobber SREG, so the _cc variants, used where flags must survive, save SREG in r0 around the access.

SP is written from Y with interrupts disabled for the high byte: in r0, SREG; cli; out SPH, r29; out SREG, r0; out SPL, r28.

Prologue, epilogue and interrupts#

The prologue pushes the call-saved pairs the function uses, pushes Y (when used), reads SP into Y, subtracts the frame size, writes SP, then places parameters: those without a home are stored to their slots and those with a home are moved with one ra_parallel_move, emitted with movw where two byte moves form an aligned pair. The epilogue (a_teardown) reverses it and returns with ret. Every function also saves the call-saved registers that loading a call's arguments would overwrite (r8-r17, down to the lowest argument register used).

Interrupt handlers. __attribute__((signal)) and __attribute__((interrupt)) are accepted. The handler saves r0, SREG, r1, r18-r27, r30, r31 and Y, clears r1, and (for interrupt) executes sei; it restores them and returns with reti. A handler with parameters or a return value is refused. The compiler emits no vector table; the startup code provides one (the test harness's boot.S has weak __vector_N entries).

Tail calls require a direct, non-variadic, non-float call with no struct result and every argument in registers; a caller that is not an interrupt handler, returns no struct or float, is not variadic, has no alloca, no exception regions, no IR_ADDR and no va_start; and the result (at most 8 bytes) returned directly. They are made only when the function allocated at least one home, when no argument register is one the epilogue pops, and, if the function has other returns, when the teardown is empty. The sequence is the teardown and jmp.

Calling convention#

The avr-gcc convention (place_arg). A cursor starts at r26 and moves down by each argument's size rounded up to even, and the argument occupies the registers from the new cursor upward: a 2-byte first argument is in r24:r25, a 4-byte one in r22-r25. The cursor stops at r8; an argument that does not fit, and every one after it, goes on the stack, packed at natural size, with no back-filling. A variadic call passes every argument on the stack. A result's size is padded to a power of two, and it is in r24 (up to 2 bytes), r22-r25 (up to 4) or r18-r25 (up to 8); a composite over 8 bytes is returned through a hidden pointer passed as the first argument and returned in r24:r25; the callee copies the value into that buffer with the source in Z and the buffer in X, both walked with post-increment, so the size is not limited by ldd's reach. The caller extends a narrow result as far as it is read. Struct arguments are loaded through Z into registers, or copied to the stack; a stack copy beyond std's reach walks the source in X and the destination in Z. All calls are call with a relocation, even within the unit; indirect calls are icall. va_list is a bare pointer, and va_arg walks it with no padding, structures included.

Encoder#

emit.c appends little-endian halfwords; a 32-bit instruction is two. Operand ranges are checked, and an out-of-range register or immediate stops with avr: WHAT (got N) -- a wrong register here encodes a DIFFERENT one and assembles cleanly, because AVR's split operand fields turn a wrong value into a different valid instruction. avr_insn_writes decodes an instruction and reports which registers it writes; the allocator's proof and the Z cache use it, and the referee tests it. Function addresses are word addresses (RK_AVR_LO8_LDI_GS, RK_AVR_HI8_LDI_GS, RK_AVR_ABS16_PM).

Branches#

jump_to and jump_if measure backward targets directly: a conditional branch within -64 to +63 words, rjmp within ±2048 words, jmp beyond. A forward jump starts long (an inverted branch over jmp, or a bare jmp); jmp carries an absolute address, relocated with RK_AVR_TEXT_CALL against .text plus the label offset. gen_relaxed then regenerates the function up to 16 times, giving each site a hint (long, rjmp, branch, or omitted when it jumps to the next instruction); a short site that turns out not to reach is pinned long.

No jump tables#

target_jump_tables() is false for AVR, so the IR generator lowers every switch as a balanced compare tree (equality tests at each node, chains of up to four cases at the leaves). An IR_SWITCH that reaches the backend is refused.

Inline asm#

Constraint letters follow avr-gcc: d (r16 and up), a (r16-r23), w (r24, r26, r30), e (X or Z), b (Z), r, x, z, and the constant letters. y is refused, because Y is the frame pointer. Operands come from r18-r27 and r30-r31; r2-r17, r28 and r29 are refused in templates, operands and clobbers. Modifiers %A to %D name an operand's bytes and %a names it as a pointer. asm.c assembles the AVR5 instruction set with lo8, hi8, pm_lo8, pm_hi8 and gs() symbol operands.

Debugging a backend#

Variable Backend Effect
EMBCC_NO_TAILCALL AArch64, Thumb, RISC-V, MIPS32, AVR no tail calls
EMBCC_NO_MEMOFF Thumb, RISC-V, MIPS32, AVR no constant-offset folding into accesses
EMBCC_NO_RMW x86-64 no read-modify-write fusion
EMBCC_NO_MLA AArch64, Thumb no multiply-accumulate fusion
EMBCC_T_NOLO Thumb no low-register scratch
EMBCC_T_NOREGOFF Thumb no [rn, rm] register-offset addressing
EMBCC_T_FPU Thumb override the FPU setting
EMBCC_T_NOWIDEIMM Thumb build a 64-bit AND/OR/XOR constant whole
EMBCC_T_NOCMP64IMM Thumb keep a 64-bit compare's constant in a register pair
EMBCC_T_EXT Thumb 0: no attempts with r9-r11 allocatable; 1: keep one when it succeeds
EMBCC_RV_NOWIDEIMM RISC-V the same at RV32
EMBCC_RV_NOCMPIMM RISC-V load a value compare's constant into a register
EMBCC_RV_LONG_CALLS RISC-V never use jal for calls
EMBCC_RV_JAL_RANGE RISC-V assume a shorter jal reach

The register-allocation knobs (EMBCC_RA_*, EMBCC_T_*, EMBCC_RV_*, EMBCC_AVR_*) are listed in Register allocation.

embcc -S prints the object's bytes as .byte directives, one instruction per line (grouped by target_insn_len, or by the disassembler on x86-64), with each relocation as an explicit .reloc; assembling it reproduces the -c object.