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_TAILbecomesRK_CALLexcept 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()andtarget_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#
- Its family file in the target database,
src/targets/<family>.def, with aFAMILYrow first, aDATA_MODELrow and itsTRIPLErows, and the file's line insrc/targets/targets.def. Then an answer in everyswitchintarget.c(the build fails until each is given), and the relocation mappings. - Its place in
make TARGETS=(see Building for some targets): the family'sEMBCC_FAMILY_<name>default insrc/arch/target.h, its name in the Makefile'sTARGET_FAMILIES, its sources inFAMILY_SRCS_<name>and its runtime triples inFAMILY_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). - A directory with
codegen.c,emit.candirgen.c, and acodegen_unit_*entry point. - Its row in the backend registry,
src/arch/backends.c(see The backend registry), and its GCC-moptions insrc/arch/<arch>/options.c. The driver reads the row; it has no per-target chain to extend. - A predefined-macro table from
tools/gen-predef.sh(below). - An encoding referee for
emit.c(see Encoders and their referees). - A QEMU harness under
tests/harness/<arch>/, and the target in the test suite. See Testing. - A new source file also has to be added to the build lists, including
EMBLS_SRCS; otherwisemake testfails 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_tandwint_tand dropping__CHAR_UNSIGNED__; - the Thumb FPU macros (
__ARM_FP0x4, the__ARM_VFPV*__set,__ARM_FPV5__on ARMv8-M,__ARM_PCS_VFPfor the hard-float ABI), with__SOFTFP__, the generated__ARM_FPand (for hard float)__ARM_PCSremoved; __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
.textin astruct code, with each function'scode_offandcode_lenfilled in; struct extcallsites: calls to functions not defined in the unit (tailset for a tail call);struct strsite,struct gsiteandstruct fsitesites: 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-bytelong 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.rodataoffset.cg_call_local()says whether a callee is defined in the unit.code_byte,code_u16,code_u32,code_patch32andcode_alignwrite 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$tor$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
-O1when 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_TAILCALLturns 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 backendorthe AVR backendin place ofthe 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 ajpfor the unordered case. - Addressing modes. An
ADDwhose only use is the next load or store becomes[base+disp]or[base+index*scale], with ashlby 1 to 3 as the scale. The address of a local folds into anrbpdisplacement. leais the three-address add (d = a + imm,d = a + b); multiply by a constant is the three-operandimul.- 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_RMWturns it off). Its address folds in too (rmw_addr_load):s[sp - 1] += s[sp]is oneadd %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 onemov $imm, mem. IR_SELECTistestof the condition at its own width (size, 4 or 8) andcmovne.- Division puts the dividend in rax, extends it into rdx (
cqo,cdq, orxor 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 userep movsqorrep 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,__floattisfand the others).IR_CAS16islock cmpxchg16b.long doubleis 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,faddpand the others,fucomip), and conversions to integers usefistpunder 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 (anIR_ARMWoperand).__atomic_compare_exchangewrites the value seen back throughexpectedonly on a miss.mfenceis the fence. - Thread-local storage is local-exec:
mov %fs:0, regand an add of anRK_TPOFF32offset.
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 anRK_ABS64relocation against the function's own symbol with the case label's offset as addend. EmbCC's code is position-dependent (-fPICis 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/movkormovn/movk, whichever is shorter;add/sub/cmptake a 12-bit immediate, optionally shifted by 12 (a negative one flips the operation); logical operations take bitmask immediates; float constants usefmovwhere the 8-bit form fits,fmov d, xzrfor +0.0, and otherwise -- whenmovz/movkwould 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 iscbz/cbnz; any other such compare isb.cond. IR_SELECTiscmpof the condition with zero at its own width (size) andcsel.- 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,sxtwand the others). A multiply by(2^k ± 1) << jis 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 byra_fold_memoffuses the scaled 12-bit form or, for -256..255,ldur/stur. - Division is
sdiv/udiv; the remainder is the quotient in x11 followed bymsub. - Multiply-accumulate. A product whose only reader is the add or
subtract right after it is one
madd(c + a*b) ormsub(c - a*b), at 4 or 8 bytes;a*b - cstays two instructions. - binary128
long doublecalls libgcc (__addtf3,__subtf3,__multf3,__divtf3, the__eqtf2family,__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_CAS16is anldxp/stxploop.- Atomics are
ldxr/stxrretry loops betweendmb ishbarriers;IR_FENCEisdmb 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_el0and twoadds). - Vector operations are not lowered. They reach the
defaultcase and stop withaarch64 codegen: unhandled IR op N in 'NAME'; the vectorizer is on only for x86-64. - Under
-mgeneral-regs-only, any floating-point operation stops withfloating 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.wwith a modified immediate, ormovw+movt. There are no literal pools. Addresses are alwaysmovw/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 becomesubfx. - Shifted operands and addressing. A constant shift with one use
followed by an add, subtract or logical operation is its shifted
operand (
rsbfor(a<<k)-b); with LSL up to 3 and an add used only as the next access's address it becomesldr/str rt, [rn, rm, lsl #k]. An add of two registers used only as the next access's address, with no folded constant offset, isldr/str rt, [rn, rm](16-bit in low registers). Multiply by(2^k ± 1) << jusesadd/rsbwith a shifted operand. - Compare and branch fuse when the compare's only use is the next
branch.
IR_BRZ/IR_BRNZon a low register whose target is 0 to 126 bytes ahead iscbz/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_SELECTuses 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 bymls. - Multiply-accumulate. A 32-bit product whose only reader is the add
or subtract right after it is one
mla(c + a*b) ormls(c - a*b);a*b - cstays two instructions. - 64-bit integers use register pairs:
adds/adc,subs/sbc, per-half logic,umull/mlafor multiply, shifts by a constant or (branching on count ≥ 32) by a variable. A 64-bit divide calls__divdi3,__udivdi3,__moddi3or__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 amvn; a modified immediate or its complement isand/orr/eororbic/orn; a mask of low bits isubfx; 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 anorr'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 oneubfxof the high word, and so is a 32-bit shift right followed by such a mask. A branch on a 64-bit value is oneorrsof the halves where they live.EMBCC_T_NOWIDEIMM=1turns 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; sbcsof x - K for<and>=, and of x - (K + 1) for>and<=;rsbs; mvn; adcsof K - x when K + 1 does not encode (SBC is AddWithCarry(x, ~y, C), soadcsof ~x's high half leaves the flagssbcswould);cmp lo; it eq; cmpeq hifor equality. Two values in registers are compared where they live,>and<=by swapping the register names.EMBCC_T_NOCMP64IMM=1keeps the constant in a register pair. - Soft float calls the libgcc names (not
__aeabi_*):__addsf3/__adddf3and the rest of the arithmetic, the__eqsf2/__eqdf2family followed by a compare of r0 with 0, and the__float*,__fix*,__extendsfdf2,__truncdfsf2conversions. Negation flips the sign bit inline.__builtin_sqrtwithout an FPU is refused. - With an FPU (
-mfpu=fpv4-sp-d16orfpv5-sp-d16with-mfloat-abi=softfporhard, or an-eabihftriple), single-precision arithmetic, negation, square root, comparisons (vcmp/vcmpeandvmrs 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_vfpalso takes the eight-byte arithmetic, comparisons,IR_SQRT, the int32 conversions andIR_F2F, as.f64instructions with d0/d1 as scratch. A double the FPU computes with gets a d register of its own:t_double_mappicks them (operands and results of those, call arguments and results, double locals, and the bit casts andand/xorof the sign bit that fabs and negation become, which arevabs.f64/vneg.f64there), and an allocation pass over d8-d15 (T_DPOOL,THUMB_DRA) runs before the floats' pass, whichra_reservekeeps off the s registers each double holds over its live range.flocnames a double's low single, sot_dregisfloc >> 1, and every slot check covers doubles too.rd64/wr64reach a d register with onevmov r, r, d, so every 64-bit integer path works on one; copies,IR_SELECT, loads and stores (vldr/vstr, withmemofffolded for eight-byte accesses), constants (vmov.f64 #immwhere 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 asvpush {d8-...}. - Atomics are
dmb, anldrex/strexloop (status in lr),dmb; compare-and-swap addsclrexon 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 fromsp.- Byte swaps arrive as shifts and masks: IR generation emits no
IR_BSWAPfor ARMv7-M. - Computed goto:
&&labelis pc-relative with no relocation,movw/movt rD, #(label | 1) - (pc)thenadd rD, pc(in ARM stateadd rD, pc, rDand no Thumb bit; on ARMv6-M and v8-M Baseline the distance is a literal-pool word), patched once the label is placed;goto *pisbx p. - Refused: exception landing pads, 128-bit values,
long doubleoperations the target lacks, and anything else not handled, with the backend's refusal message.IR_UD2isudf #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 withouttbhfor that switch); - otherwise:
adr.w r11, table; ldr.w r12, [r11, rI, lsl #2]; add r12, r11; bx r12, anopto 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 callstmp_get, which pushes one of r0-r5 the instruction does not touch and pops it after (F->spbcorrects 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 anR_ARM_ABS32site. - 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>overb(±2 KB),b<!c>overbl(±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, rTwith byte entries, halfword entries (lsls rT, rI, #1; ldrh), or, for a backward case or one too far, a word table taken withbx. - Calls the lowering makes: 32-bit
/and%(__aeabi_idiv,__aeabi_idivmodand 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_helpertells 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
naturalis 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 ofir_asm.clob, and the asm makes its function a non-leaf only when the template calls or names lr.thumbv6m-asm-values.shruns it. - -O0 runs the allocator for the temporaries, with every source
variable pinned to its slot (
tcg_o0, codegen.c'sg_t_o0), as on ARMv7-M, and a function pastra_o0_too_bigcompiles with every temporary in a slot, as there. - No low-register rename.
gen_func_besttries pairs on and off as for ARMv7-M, but nott_lowregs: every register ARMv6-M computes in is a low one, so there is no 16-bit form to win. - The scan.
v6_scandecodes 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):addifor 12 bits,lui+addi(oraddiwat RV64) for 32 bits, a shift-and-add chain beyond. - Addresses of strings, globals and functions are always
auipc+addiwithR_RISCV_PCREL_HI20/R_RISCV_PCREL_LO12_I, at both widths: the medany code model.luisign-extends bit 31 at RV64 and cannot reach an image at0x80000000. TheLO12relocation names theauipc's own address (against the.textsection symbol), as the psABI requires. - Calls to a function in the same unit are
jal rawhile every such call reaches (±1 MiB); if one does not, the whole unit is regenerated withauipc+jalr. Calls to other units areauipc ra; jalr rawithR_RISCV_CALL_PLT.EMBCC_RV_LONG_CALLSalways usesauipc+jalr, andEMBCC_RV_JAL_RANGEreduces the assumedjalreach, 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 <= kasx < k + 1andx > kits inverse,==and!=anxoriand aseqz/snez. Against zero that is theseqz,snezorsltalone.EMBCC_RV_NOCMPIMM=1loads 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 anli -1; a 12-bit immediate isandi/ori/xori; a mask of low bits isslli+srli, and one of high bitssrli+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 abeqz/bnez, or, for a signed<or>=, abltz/bgezof the high word.EMBCC_RV_NOWIDEIMM=1turns these off. - Multiply and divide use the M extension (
mul,div,remand theWforms). At RV32 a 64-bit multiply ismul/mulhu; a 64-bit divide calls__divdi3,__udivdi3,__moddi3or__umoddi3. - Soft float: arithmetic calls
__addsf3/__adddf3and the rest, comparisons the__eqsf2family, conversions__floatsisf,__fixdfsiand the others; negation flips the sign bit inline.__builtin_sqrtandlong doublearithmetic are refused. - Atomics (A extension):
amoswap.aqrl,amoadd.aqrl,amoand/amoor/amoxor.aqrl; NAND and compare-and-swap uselr.aq/sc.rlloops. Atomics must be register-width (or 4 bytes at RV64); narrower ones are refused.IR_FENCEisfence rw, rw. IR_ALLOCA(VLAs,__builtin_alloca, and locals aligned beyond 16 bytes) subtracts the size rounded to 16 fromsp.IR_SELECTloads 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 throughrd32.- Byte swaps are
IR_BSWAPonly 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:
&&labelisauipc/addi(never compressed) patched with the label's distance, no relocation;goto *pisjalr zero, 0(p). - Refused: 128-bit values.
IR_UD2isunimp.
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_restoreemitsjr $rabeforeaddiu $sp, $sp, N. Every other transfer to a label (branch_to), call (call_sym, the indirectjalr) and frameless return takes the instruction emitted just before it into its slot whentake_slotallows: the instruction decodes as an ordinary computation, load or store (slot_decode; never a transfer, trap,sync,ll/scor 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->barrieris 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 anop(put_slot); the branches inside one lowering (br_place) always take anop. Under-gnothing moves (the line rows name offsets), andEMBCC_MIPS_NO_FILLturns the filling off. MIPS32 has no load delay slots and no HI/LO hazards. - Jump tables.
IR_SWITCHissltiu $at, rI, nand a branch to the default, thenbalto the next-but-one instruction withsll t2, rI, 2in its slot,addu/lw/adduof the entry against$ra, andjr t2: the table follows, of 32-bit offsets from the addressbalreturns (FX_TABfixes, patched when the function ends), so it carries no relocation. A leaf keeps its live$rain t6 around thebal. - Branches compare two registers only for
==and!=; an ordered comparison against zero hasbltz/bgez/blez/bgtz, and anything else isslt/sltuinto$atand abeq/bneof it (branch_if). A fused compare against a 16-bit constant isslti/sltiuinto$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
jrelocated against the function's section (RK_MIPS_TEXT26, anR_MIPS_26whose 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,oriandxoria zero-extended one.mips_imm_foldabletells the optimizer which constants fold. An AND with a run of low bits isext, with all but a run of low bitsinsof$0. - Multiply and divide.
mulis three-operand (Release 2); a 64-bit product's high word comes frommultuandmfhi. Division isdiv/divu $zerothenmfloormfhi; it never traps. - Addresses are absolute:
luiandaddiuwithRK_MIPS_HI16andRK_MIPS_LO16, noted high first so the relocation table pairs them. - Calls are
jalwithR_MIPS_26to any function, in the unit or not (ajalholds an absolute word index only the linker knows); an indirect call isjalrthrough$t9. - Misaligned accesses trap. A 2- or 4-byte load or store irgen did
not mark
naturalgoes throughld_any/st_any:lwl/lwrandswl/swrfor a word, two bytes for a halfword. Block copies whose ends are not known to be word-aligned (IR_MEMCPYwithout anaturalalignment) use the same, and so do a struct argument's words. - Atomics are
ll/scloops on words, bracketed bysync; a narrower or wider one is refused.IR_FENCEissync. - Select is
movn. Byte swap iswsbhandrotr. IR_ALLOCArounds to 16 and keepssp16-aligned, and the frame is addressed fromfpin such a function.- Computed goto:
&&labelis the function's own address asIR_FADDRtakes it (lui/addiu, or MIPS64's four pieces) plus the label's offset as the relocation's addend;goto *pisjr pand anop. - 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.
r21andtpare 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_foldablefolds an AND/OR/XOR constant only in 0..4095,notisnor 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 throughrd32because 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; addressespcalau12i/addi.d(RK_LA_PCALA_HI20/LO12, both against the symbol). A jump table is found bypcaddiand indexed byalsl.d. - Select is maskeqz/masknez/or; bswap is revb plus a bstrpick.
- Atomics are
am*_dbandll/scloops betweendbar 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.cand assembled byloongarch/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 ismov r, r1; registers r16 and up takeldi; 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. Themulinstruction is not emitted by code generation. - Divide and remainder call
__divsi3,__udivsi3,__modsi3and__umodsi3(arguments in r22-r25 and r18-r21, result in r22-r25); the 64-bit forms__muldi3,__divdi3and 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__eqsf2family, and the__floatsisf/__fixsfsifamily. 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 intoldd/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_SELECTis a branch and copies. The condition's low four bytes are ORed and tested whatever itssize, so the high half of an 8-byte condition is ignored (a defect; see Status).IR_MEMCPY/IR_MEMZEROare byte loops with Z as the destination and X as the source.IR_FENCEemits nothing;IR_UD2isrjmp .-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 emitIR_BSWAPfor AVR; a byte swap arrives as shifts and masks. - Computed goto:
&&labelis 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 offsetLas the addend;goto *ploadsZand isijmp.
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.