Skip to content

Repository files navigation

TinyCC for ARMv8-M — Tiny C Compiler fork for ARMv8-M (Cortex-M33, Cortex-M23)
=================================================================================
This is a fork of the Tiny C Compiler (TCC) by Fabrice Bellard, modified for
**ARMv8-M architecture** with a custom IR, register allocator, and Thumb-2
code generator.
Differences from Original TinyCC
--------------------------------
**1. Target Architecture**
Original TCC targets x86/x86_64/aarch64/riscv64 on desktop/server OSes.
This fork targets **ARMv8-M** microcontrollers (Cortex-M33, Cortex-M23, etc.)
with the Thumb-2 instruction set.
**2. Custom IR (Intermediate Representation)**
The original TCC uses a simple, direct translation to machine code. This fork
introduces a **three-address code IR** with explicit register operands, enabling
a separate front-end and back-end, and an SSA form built on top of it.
Public interface: `source/ir/tccir.h`; construction in `source/ir/gen/`.
**3. Optimizer**
Upstream TCC does not optimize. This fork carries a full optimizer under
`source/opt/`, driven by a pass pipeline table (`source/opt/engine/`) with two
phases — flat-IR peepholes/fusion (`opt/flat/`) and SSA passes (`opt/ssa/`) —
plus alias and def-use analysis, pre/post-RA passes, and a declarative pass DSL
(`opt/framework/`). `-O1` does scalar cleanup, ARM addressing-mode fusion,
constant/range propagation, loop rotation and light inlining; `-O2` adds the
heavy tier (loop unrolling, IV strength reduction, LICM, re-rolling, MUL+ADD→MLA
fusion, interprocedural constant propagation, full small-function inlining).
**4. Register Allocation**
An **SSA register allocator** (`source/ir/regalloc.c`, parameterized by
`RegAllocTarget`) with a linear-scan core (`source/machine/tccls.c`) performs
liveness analysis and assigns physical registers, including the VFP bank. The
original TCC uses a simpler approach without liveness analysis.
**5. Code Generation**
Instead of x86 code generation, this fork generates **Thumb-2 machine code**
via `source/backend/arch/arm/thumb/`: `arm-thumb-gen.c` drives the per-opcode
`thop_*.c` encoders (ALU, memory, branch, DSP, coprocessor, VFP, exclusive
access, …), which walk instruction variants narrow→wide and pick the first
whose constraints hold. Covers the ARMv8-M instruction set including DSP
extensions.
**6. Floating Point**
Hardware floating point is supported: VFP single-precision codegen, the full
AAPCS-VFP hard-float ABI, and inline RP2350 DCP sequences for `double`.
See the *Floating Point* section below for flags, coverage and limits.
**7. Library Mode**
Can be used as `libtcc.a` for **JIT compilation** in host applications.
**8. ARM-Specific Features**
- ARM Procedure Call Standard (AAPCS/AAPCS-VFP) support
- ARMv8-M EABI helper functions
- ARM assembler parser for inline assembly
- ARM-specific ELF linking (`source/backend/arch/arm/arm-link.c`)
**9. YAFF Output for YasOS**
Besides ELF, the linker emits **YAFF** (`source/obj/tccyaff.c`), a flat
position-independent module format for the YasOS loader, selected with
`-Wl,-oformat=yaff` (executables and shared libraries alike). The YasOS target
also defaults to PIC with text/data separation, shared read-only `.rodata`, and
SB-relative GOT addressing — `-no-share-rodata` and `-mno-sb-relative-got` opt
back out.
**10. Self-Hosting on Target**
The compiler builds for ARMv8-M itself and runs on-device under YasOS, so the
same sources produce both the host cross compiler and an on-target compiler
(`tests/selfhost/`).
**11. Runtime Library**
Includes a custom runtime library (`libtcc1`) with ARM EABI helpers in
`lib/armeabi.c` and `lib/armv8m_eabi.c`.
Project Structure
-----------------
All compiler code lives under `source/`, one directory per module. Each module
owns its headers, carries its own `Makefile` and builds to its own static
library; the top-level `Makefile` links them into `armv8m-tcc`. The repo root
holds only build inputs, generated headers and project metadata.
```
.
├── source/ # Compiler sources (one module per directory)
│ ├── include/ # tcc.h (umbrella header), tcctypes.h
│ ├── driver/ # tcc.c (CLI), libtcc.c (TCCState/options/driver), tcctools.c
│ ├── frontend/ # tccpp.c (preprocessor), tccasm.c (GAS asm), svalue.c
│ │ └── gen/ # Parser, type checker, IR emission (the former tccgen.c)
│ ├── ir/ # Target-independent IR
│ │ ├── tccir.h # Public IR interface (opcodes, builder)
│ │ ├── gen/ # IR construction (arith, control, float, params, vla, ...)
│ │ ├── cfg.c ssa.c # CFG and SSA form
│ │ ├── codegen.c # Two-pass dispatch (dry-run + real-run) to backend _mop handlers
│ │ ├── regalloc.c # SSA register allocator (parameterized by RegAllocTarget)
│ │ └── vreg.c stack.c # Virtual registers, stack frame layout
│ ├── opt/ # Optimizer
│ │ ├── engine/ # Pass registry, pipeline table, pass timing
│ │ ├── flat/ # Flat-IR passes (cfg, dce, fusion, loop, memory, scalar, ipa)
│ │ ├── ssa/ # SSA passes (cfg, dce, loop, memory, scalar, string)
│ │ ├── analysis/ # Alias analysis, def-use chains, memory SSA
│ │ ├── ra/ # Pre/post register-allocation passes
│ │ └── framework/ # Declarative pass DSL
│ ├── machine/ # Backend boundary: tccls.c (linear scan), tccmachine.c, tccabi.h
│ ├── backend/
│ │ ├── arch/arm/ # arm.c, arm_aapcs.c, arm-link.c, arm_regalloc.c, ssa_opt_arm.c
│ │ │ └── thumb/ # arm-thumb-gen.c, arm-thumb-asm.c, thop_*.c encoders
│ │ ├── arch/fpu/ # FPU capability descriptions
│ │ └── generators/ # Target-independent generator helpers
│ ├── obj/ # tccelf.c, tccld.c, tccyaff.c (YAFF), tccdbg.c (DWARF/STABS)
│ ├── support/ # tccdebug.c (gdb printers), logging, debug-env knobs
│ ├── memory/ # vector.c, unique_ptr.c, container headers
│ └── utils/ # Header-only helpers
│
├── lib/ # Runtime library sources (libtcc1)
│ ├── libtcc1.c # Core runtime functions
│ ├── armeabi.c # ARM EABI helpers
│ ├── armv8m_eabi.c # ARMv8-M EABI specific
│ ├── builtin.c # Freestanding builtin fallbacks
│ └── fp/ # Floating point libraries (see Floating Point)
├── include/ # Headers shipped with the compiler
│
├── tests/
│ ├── ir_tests/ # Primary suite: compile + run under QEMU (pytest)
│ ├── unit/ # Host unit tests for individual passes (make ut)
│ ├── thumb/armv8m/ # Thumb-2 encoder tests
│ ├── frontend/ linker/ # Per-subsystem pytest suites
│ ├── debug/ runtime/ # "
│ ├── selfhost/ # The compiler compiling itself
│ ├── gcctestsuite/ # GCC torture tests (fetched on demand)
│ ├── benchmarks/ # Performance, code size, FP conformance
│ ├── fuzz/ # Fuzzing and divergence triage
│ └── tests2/ pp/ # Upstream legacy suites
│
├── scripts/ # Bisect, reduce, disassembly diff, container runner
├── metrics/ # Code size / performance tracking, Grafana dashboards
├── docs/ # Design documents and plans
│
├── configure # Configuration script
├── Makefile # Top-level build rules (module Makefiles under source/*)
├── config.h / config.mak # Generated configuration
└── tcc-doc.texi # Texinfo documentation source
```
Build
-----
```bash
# Configure
./configure [options]
```
Frequently used `./configure` options:
| Flag | Description |
|------|-------------|
| `--enable-cross` | Build the ARMv8-M cross compiler (`armv8m-tcc`) |
| `--enable-O0` … `--enable-O3`, `--enable-Os`, `--enable-Og` | Optimization level TCC itself is built with (`-O2` default) |
| `--disable-asan` | **AddressSanitizer is ON by default**; disable it for fast or production builds |
| `--enable-ubsan`, `--enable-lsan` | Additional sanitizers |
| `--debug` | Include debug info in the resulting binaries |
Example:
```bash
./configure --enable-cross --disable-asan
make cross
```
Run `./configure --help` for the full list.
```bash
# Build ARMv8-M cross compiler
make cross
# Build everything including FP libraries
make cross fp-libs
# Run the full test suite
make test -j16
```
Two build-time knobs affect what ends up in the binary:
| Variable | Effect |
|----------|--------|
| `make cross CONFIG_minimal=yes` | Drops the `-dump-ir` / `-dump-ir-passes` machinery (`CONFIG_TCC_DEBUG`), which is on by default so IR tooling and the golden-IR tests work with a plain `make cross`. Generated code is unaffected either way. |
| `make cross CONFIG_debugenv=no` | Compiles out the getenv-driven debug/bisect knobs (`TCC_DISABLE_PASS`, `TCC_NO_COALESCE`, …); each folds to a compile-time constant. Worth ~17 KB on the device compiler, but disables the host bisect tooling. |
Floating Point
--------------
Both software and **hardware** floating point are supported. Two flags select
the behaviour:
| Flag | Values | Meaning |
|------|--------|---------|
| `-mfloat-abi=` | `soft`, `softfp` (default), `hard` | how FP values cross a call boundary |
| `-mfpu=` | `none`, `fpv4-sp-d16`, `fpv5-sp-d16`, `fpv5-d16`, `rp2350` (= `rp2350-dcp`) | which FP unit may be used inside a function |
The compile-time default can be changed for a whole toolchain with
`-DCONFIG_TCC_DEFAULT_FPU=ARM_FPU_...` (e.g. `ARM_FPU_RP2350`); an explicit
`-mfpu=` on the command line still wins, and the float ABI is untouched, so
objects stay link-compatible across the switch. Code that must stay soft-float
has to say `-mfpu=none` rather than rely on the default.
**What is emitted inline** (with `-mfloat-abi=softfp` or `hard`;
`-mfloat-abi=soft` disables all inline FP codegen regardless of `-mfpu`)
| Operation | `-mfpu=none` | `fpv4/fpv5-sp-d16` | `-mfpu=rp2350` |
|---|---|---|---|
| `float` add/sub/mul/div | `__aeabi_f*` | `vadd/vsub/vmul/vdiv.f32` | `vadd/vsub/vmul/vdiv.f32` |
| `float` compare/convert/negate | `__aeabi_*` | `__aeabi_*` | `__aeabi_*` |
| `double` add/sub/compare | `__aeabi_d*` | `__aeabi_d*` | inline DCP (CP4) sequences |
| `double` mul/div/convert | `__aeabi_d*` | `__aeabi_d*` | `__aeabi_d*` (DCP-backed library) |
**Hard-float ABI** (`-mfloat-abi=hard`) is complete: `float` arguments in
`s0-s15`, `double` arguments in `d0-d7` with AAPCS back-filling, returns in
`s0`/`d0`, VFP spills/reloads via `vldr`/`vstr`, and `.ARM.attributes`
(`Tag_FP_arch`, `Tag_ABI_HardFP_use`, `Tag_ABI_VFP_args`) matching
`arm-none-eabi-gcc` byte-for-byte. `softfp` keeps the base PCS at call
boundaries while still using the FPU internally, so it links against
soft-float libraries unchanged.
Note that AAPCS-VFP passes doubles in `d0-d7` even on a single-precision-only
FPU — the ABI says where arguments live, not which arithmetic exists, so a
callee unpacks `d0` into a GPR pair to call `__aeabi_dadd`.
**Runtime libraries** (`make fp-libs`, sources in `lib/fp/`):
| Library | `-mfpu` | Contents |
|---------|---------|----------|
| `libsoftfp` | `none` | pure software IEEE-754 |
| `libvfpv4sp` | `fpv4-sp-d16`, `fpv5-sp-d16` | float in HW, double in SW (self-contained) |
| `libvfpv5dp` | `fpv5-d16` | float and double in HW |
| `librp2350fp` | `rp2350` | double via the RP2350 DCP coprocessor |
**Testing**
```bash
# Float tests under one ABI (soft | softfp | hard)
make test-fp FLOAT_ABI=hard
# IEEE-754 conformance on real RP2350 silicon (inline DCP/VFP codegen,
# which QEMU cannot model); the same vectors run under QEMU as
# tests/ir_tests/421_fp_conformance.c
python3 tests/benchmarks/run_fp_conformance.py <host> --opt-level 1 \
--fp-lib rp2350fp --mfpu rp2350 --allow-ftz
```
`--allow-ftz` applies only to the DCP configuration: the coprocessor flushes
subnormals to zero and has no path that doesn't. The soft-float baseline (the
same script without those flags) stays held to full IEEE-754.
**Current limits**
- `float` compares, conversions and negate still go through `__aeabi_*`
helpers even with an FPU selected.
- No native `vadd.f64` on a double-precision unit (`-mfpu=fpv5-d16`); doubles
always call, or use DCP on RP2350.
- DCP `dmul`, `dneg` and the double conversions are not inlined yet.
- `.ARM.attributes` is written at link time only — object files carry none, so
GNU `ld` cannot ABI-check them.
- `-mfloat-abi=hard` on RP2350 silicon is untested (needs pico-sdk rebuilt for
the hard ABI); RP2350 uses `softfp` today.
- The DCP save/restore hooks exist in `dcp_aeabi.S` but the library entry
points do not use them, so an interrupt handler doing `double` math can
corrupt an interrupted user-space DCP sequence. The OS context switch half
is handled on YasOS.
Details: `docs/plan_vfp_hard_float.md` (VFP / hard-float ABI),
`docs/plan_rp2350_dcp.md` (DCP), `lib/fp/README.md` (runtime libraries).
Container
---------
A Dockerfile provides a reproducible build environment, published as
`ghcr.io/matgla/tinycc-armv8m`. The `run` target pulls it and mounts the repo
(`scripts/run_container.py` does the plumbing; podman by default, or
`CONTAINER_RUNTIME=docker`):
```bash
# Interactive shell in the container
make run
# Run one command inside it
make run CMD='make test -j16'
# Build the multi-arch image locally / push it
make build_container
make push_container
```
`container-build` / `container-pull` / `container-push` / `docker-build` /
`docker-start` remain as aliases.
Testing
-------
```bash
# Fetch the GCC torture tests (one-time). Only gcc.c-torture (~16 MB) is
# pulled, sparsely, not the whole gcc repo (~1.3 GB):
make download-gcc-tests # or: bash tests/gcctestsuite/download_gcc_tests.sh
# Full suite: IR tests + asm + unit tests + frontend/linker/debug/runtime/selfhost
make test -j16
# Same, but sequential — clean logs
make test-sequential
# Everything above plus the GCC torture tests
make test-all
```
Individual suites:
| Target | Scope |
|--------|-------|
| `make test-ir` | The primary QEMU corpus in `tests/ir_tests/` |
| `make ut` | Host unit tests for individual optimizer passes |
| `make test-asm` | Thumb-2 encoder tests (`tests/thumb/armv8m/`) |
| `make test-fp FLOAT_ABI=…` | Float tests under one ABI (see *Floating Point*) |
| `make test-selfhost` | The compiler compiling its own sources |
| `make test-frontend`, `test-linker`, `test-debug`, `test-runtime` | Per-subsystem suites |
| `make test-gcc-torture-compile`, `test-gcc-torture-execute` | GCC torture, split by phase |
| `make test-golden-ir` | Golden IR dumps (needs a `CONFIG_TCC_DEBUG` build) |
`make help` lists the rest.
Quick Test Runner
-----------------
```bash
cd tests/ir_tests
# Compile and run a single file
python run.py -c mytest.c
# With optimization
python run.py -c mytest.c --cflags="-O1"
# Dump IR
python run.py -c mytest.c --cflags="-O1" --dump-ir
# Compare against GCC, pass program arguments, or attach a debugger
python run.py -c mytest.c --gcc arm-none-eabi-gcc
python run.py -c mytest.c -a arg1 arg2
python run.py -c mytest.c --gdb
```
Debugging
---------
```bash
# Show IR output
./armv8m-tcc -dump-ir -c test.c
# Dump IR after each named optimizer pass (comma-separated, or 'all')
./armv8m-tcc -dump-ir-passes=all -O2 -c test.c
# Verbose: version banner plus every file read and written
./armv8m-tcc -vv -c test.c
```
Both dump options need a compiler built with `CONFIG_TCC_DEBUG`, which is the
default (see *Build*).
The getenv knobs described in `source/support/tccdbgenv.h` — `TCC_DISABLE_PASS`
and friends — bisect a miscompile to a single pass without rebuilding;
`scripts/bisect_pass.sh` and `scripts/opt_profile.py` drive them.
For extra compile-time logging (e.g. `-DTCC_LS_DEBUG` for the register
allocator), add the define to `CFLAGS` in `config.mak` or pass it to
`./configure --extra-cflags=`. Do **not** use `make CFLAGS+=-DFOO`: that
overrides the flags the sub-makes compute, so the affected TUs silently lose
`-fsanitize=address` and the resulting mixed binary crashes in unrelated code.
License
-------
TCC is distributed under the GNU Lesser General Public License (LGPL).
See the COPYING file for details.
This fork is maintained for ARMv8-M embedded development.

About

No description, website, or topics provided.

Resources

Stars

14 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages