MIPS32 (mipsel, o32): the plan#
The target is 32-bit little-endian MIPS, MIPS32 Release 2, the o32 ABI
with soft float: mipsel-none-elf (also mipsel-unknown-elf). The same
core big-endian is mips-none-elf (mips-unknown-elf); what the byte
order changes -- the register pairs, sub-word composites in registers,
lwl/lwr, the objects and the image -- is in big-endian.md, and
everything below holds for both otherwise. It is the
core of Microchip's PIC32 parts. The board it is tested on is QEMU's
malta. Every fact below was read off clang 23
(--target=mipsel-unknown-elf -mcpu=mips32r2 -msoft-float), llvm-mc or
QEMU rather than remembered; clang with its integrated assembler is the
reference, as there is no MIPS gcc here.
The o32 calling convention#
- Registers.
$0zero,$1at (assembler temporary),$2-$3v0-v1 (results),$4-$7a0-a3 (arguments),$8-$15t0-t7,$16-$23s0-s7,$24-$25t8-t9,$26-$27k0-k1 (kernel, never touched),$28gp,$29sp,$30fp (s8),$31ra. Callee-saved: s0-s7, fp, gp, sp. - Arguments are laid out as if in a structure in memory: each at its
offset rounded up to its alignment (at least 4, at most 8), occupying
whole words. The first 16 bytes travel in a0-a3 and the rest on the
stack at the same offset from the caller's sp. So
f(int, long long)passes thelong longin a2:a3 and skips a1, andf(int, double)likewise;f(int, int, int, double)puts the double at sp+16. A composite of any size is passed by value in the same layout, split across a3 and the stack if it straddles them (no by-reference copies). A variadic argument follows exactly the same rules (afloatarrives as adouble). - The home area. The caller always reserves 16 bytes at the bottom of
its outgoing area, sp+0..15, for the callee to store a0-a3 into. A
variadic callee does exactly that, so its named and unnamed arguments
become one contiguous block at its entry sp, and
va_listis a bare pointer walking it;va_argof an 8-byte type rounds the pointer up to 8 first. So every function that makes a call -- a runtime helper included -- has an outgoing area of at least 16 bytes. - Results. An integer or pointer in v0; a
long longor a (soft)doublein v0:v1, low word in v0; afloatin v0. Every struct and union, whatever its size, is returned through a hidden pointer the caller passes in a0 (so the first argument moves to a1), and the callee returns that pointer in v0. A_Complex floatcomes back in v0 (real) and v1 (imaginary); clang returns a_Complex doublein v0:v1 and a0:a1. - Data model. ILP32: int, long and pointers are 4 bytes;
long longanddoubleare 8 and 8-aligned (also in structures and on the stack).long doubleisdouble. Plaincharis signed;wchar_tis a signedint. An unnamed bit-field does not raise a structure's alignment. The stack is 8-aligned. Soft float: a float is its bits in a GPR and every operation calls lib/rt (__addsf3,__adddf3, ..., libgcc's names).
Branch delay slots#
Every branch and jump executes the instruction after it, the delay slot,
before the transfer happens. The first backend fills every delay slot with
a nop (sll $0, $0, 0), so the code is correct by construction and
each transfer costs 8 bytes. The epilogue's addiu sp now fills the
jr ra slot, as clang's does (after the exec corpus passed). Filling the
other slots -- moving an independent instruction from before the branch
into it -- is an optimization still to do, and needs its own proof that the moved instruction
neither feeds the branch condition nor is a branch itself. MIPS32 has no
load delay slots, and Release 1 removed the HI/LO hazards, so there is
nothing else to schedule around.
Branches are PC-relative from the delay slot (target = PC + 4 + off16*4,
+-128 KiB). Inside a function every branch is resolved by the compiler,
with no relocation. One that does not reach takes the long form -- the
inverse branch over a j relocated against the function's own section
(R_MIPS_26, addend the label's offset) -- and the function is generated
again until nothing new fails. j/jal carry 26 bits of an absolute word
address within the current 256 MiB region and always take a relocation;
calls within the unit are jals relocated like any other.
Misaligned accesses#
A word or halfword access to an unaligned address traps (Address Error),
where x86, ARMv7-M and QEMU's RISC-V read it. C guarantees alignment
except for a packed structure's member, and irgen marks the accesses it
can promise (ir_ins.natural); every other load or store of 2 or 4 bytes
goes through lwl/lwr (swl/swr) or two byte accesses, and block
copies whose ends are not known to be word-aligned likewise. Wide string
literals are interned aligned to their element, which the other targets
had never needed.
HI/LO and mul#
MIPS32 has the three-operand mul rd, rs, rt (SPECIAL2), which writes
the low word directly (and leaves HI/LO undefined). A 64-bit product's
high word comes from multu/mult and mfhi. Division is
div/divu $zero, rs, rt followed by mflo (quotient) or mfhi
(remainder); it never traps. HI and LO never hold a value across another
instruction, so nothing is allocated to them.
Relocations#
o32 objects use REL relocations (.rel.text, the addend stored in
the field), and EmbCC writes them that way:
| Type | Field | Value |
|---|---|---|
R_MIPS_32 (2) |
a data word | S + A |
R_MIPS_26 (4) |
jal/j target, 26 bits |
(S + A) >> 2, within P+4's 256 MiB region; A = field << 2 (sign-extended for an external symbol) |
R_MIPS_HI16 (5) |
lui immediate |
(AHL + S + 0x8000) >> 16 |
R_MIPS_LO16 (6) |
addiu/load/store immediate |
AHL + S, low 16 bits |
R_MIPS_PC16 (10) |
branch offset | (S + A - P - 4) >> 2; A = sign-extended field << 2 |
The AHL rule. A HI16 and its LO16 carry one addend between them:
AHL = (AHI << 16) + (short)ALO, the HI16 field's value shifted up plus
the LO16 field's value sign-extended. So a HI16 relocation must be
followed, in the same relocation section, by a LO16 against the same
symbol, and a linker reads the pair together; the HI16 result is rounded
by 0x8000 because the LO16 half is sign-extended when it is added. EmbCC
writes each address as lui+addiu with the HI16 immediately followed
by its LO16, and the writer stores AHI = (A + 0x8000) >> 16 and
ALO = A & 0xffff. EmbLD finds the pairing LO16 for each HI16 (the next
LO16 against the same symbol in that section), and refuses an object
whose HI16 has none. The GOT/GP-relative relocations of PIC code
(R_MIPS_GOT16, R_MIPS_CALL16, R_MIPS_GPREL16) are refused by name;
R_MIPS_JALR is a hint and ignored.
ELF header and sections#
e_machine is EM_MIPS (8), class 32, little-endian. e_flags is
EF_MIPS_ARCH_32R2 | EF_MIPS_ABI_O32 | EF_MIPS_NOREORDER =
0x70001001, what clang writes with -mno-abicalls: noreorder because
the code is scheduled (the delay slots are filled, with nops), no
EF_MIPS_CPIC/EF_MIPS_PIC because the code is not abicalls/PIC, and
__mips_abicalls is not predefined for the same reason. Each object
carries a .MIPS.abiflags section as clang's does (ISA MIPS32r2, GPR
32, no FPR, FP ABI soft), so a linker can refuse to mix it with
hard-float code. EmbLD drops .MIPS.abiflags, .reginfo, .pdr and
.mdebug.* from images.
The board: QEMU malta#
- The reset vector is 0xbfc00000 in KSEG1 (physical 0x1fc00000, the boot
flash). QEMU's
-biosimage is loaded there word-swapped on a little-endian CPU, and code executing from that flash is re-fetched wrongly after an MMIO access (observed: a store to the UART made the next instruction read as0x420). So the harness uses-kernel: QEMU loads the ELF into RAM and writes its own reset-vector code at 0xbfc00000 that jumps to the ELF entry. Images are linked at 0x80100000 in KSEG0 (cached, unmapped RAM; QEMU keeps its boot environment below 0x80100000).-cpu 24Kc, a MIPS32r2 core with no FPU, makes any floating-point instruction trap. - Output. The malta FPGA has a 16550 UART at physical 0x1f000900,
KSEG1 0xbf000900, registers 8 bytes apart; a byte written to its THR
is printed. QEMU wires it to the third serial port, so the board runs
with
-serial null -serial null -serial stdio. - Ending a run. A bare-metal image never returns, so the harness
prints a sentinel (
==EXIT n==with main's result) and the runner (tests/harness/qrun.sh --until) stops at it. The harness then also writes 0x42 to the FPGA's SOFTRES register (0xbf000500), which with-no-rebootmakes QEMU exit at once.
What the first backend refuses#
By name, with the IR instruction: computed goto; atomics narrower or
wider than a word (ll/sc are word-sized); __builtin_frame_address
and __builtin_return_address (no frame-pointer chain); jump tables (a
dense switch stayed a decision tree until the table of bal-relative
offsets came, target_jump_tables()); __int128 (it does not exist on
ILP32); interrupt functions (naked functions, file-scope assembly with
instructions and .s files came with the GNU-syntax assembler); a
scalar local aligned beyond the 8-byte stack; C++ (the
C++ front end lays out LP64 only). Every MIPS flag other than the one
configuration emitted (MIPS32r2, little-endian, o32, soft float,
-mno-abicalls, -G0) is refused by the driver.
Tests#
| Test | What it checks |
|---|---|
tests/golden/mips-encoding.sh |
every encoder form against llvm-mc -show-encoding; mips_li executed; every range check |
tests/golden/mips-exec.sh |
tests/exec/*.c on the malta board at -O0, -O1, -O2 and -Os; the LP64-dependent programs against clang's result on the same board |
tests/golden/mips-abi.sh |
calls in both directions against clang, with the shared embedded ABI programs and the o32-specific ones |
tests/golden/mips-link.sh |
EmbLD's REL relocations, the AHL rule over every carry case, EmbCC's own far addends, and the link refusals |
tests/golden/mips-asm.sh |
the inline-asm vocabulary against llvm-mc, asm programs on the board, -S reassembled by llvm-mc |
tests/golden/mips-refuse.sh |
the object's header and flags, the accepted and refused options and constructs |