Skip to content

emproof-com/NyxstoneTricoreGCC

Repository files navigation

NyxstoneTricoreGCC

ci crates.io: nyxstone-tricore-gcc crates.io: nyxstone-tricore-gcc-ipc PyPI License: GPL-3.0-or-later

In-process TriCore assembler/disassembler. C++17 library + Rust and Python bindings. Built on the GNU assembler's TriCore encoder (md_assemble) and libopcodes' print_insn_tricore decoder from a pinned emproof-com/tricore-binutils-gdb fork, fully compatible with upstream EEESlab/tricore-binutils-gdb.

# Rust, pick your licensing mode (see "License modes" below):
cargo add nyxstone-tricore-gcc        # GPL-3.0-or-later, in-process, ~2.8 M ops/s
cargo add nyxstone-tricore-gcc-ipc    # MIT, IPC daemon, ~150 k ops/s

# Python
pip install nyxstone-tricore-gcc

Supported prebuilts: x86_64-linux-gnu, aarch64-linux-gnu. Other hosts build binutils-tricore from source automatically (see Binutils provisioning below).

The public API mirrors that of the sibling project Nyxstone, a different implementation built on LLVM-MC that covers the architectures LLVM supports. NyxstoneTricoreGCC is not a fork or downstream of Nyxstone; it's an independent codebase using GNU binutils to cover TriCore (which LLVM-MC doesn't). Both projects expose six assemble/disassemble methods with the same argument order (source / bytes, address, then either labels or count):

method purpose
assemble bytes only, labels resolved inline
assemble_to_instructions per-insn records, labels resolved inline
assemble_with_relocs bytes + relocations (gcc/gas -r equivalent)
assemble_to_instructions_with_relocs per-insn records + relocations
disassemble bytes → text
disassemble_to_instructions bytes → per-insn records
  • No fork/exec in the hot path. All work happens in the calling process (or in a long-lived per-instance daemon for the MIT-licensed Rust binding).
  • .text-only. Any directive that would switch the active section to anything other than .text makes assemble() fail.
  • Stock binutils. Uses an unmodified emproof-com/tricore-binutils-gdb build, no patches to gas, libbfd, or libopcodes.
  • Byte-equivalent to tricore-elf-as. PC-relative branches with forward references go through a mini relax + md_apply_fix pass; .align/.org padding is sized after relaxation, exactly like gas.
  • Loud failures, real diagnostics. gas's stderr is captured and embedded in the returned error (Error: Opcode/operand mismatch: mov %d0, 0x123456), never printed to the host process. In the plain assemble / assemble_to_instructions paths, references to labels that are neither defined in the source nor supplied via LabelDefinition are an error, not a silent zero displacement; same for .org moving backwards, unknown directives, and out-of-range branch displacements. The *_with_relocs variants are the link-later path: undefined references are not an error there -- each one comes back as a relocation record (zero placeholder bytes
    • R_TRICORE_* entry, gas -r style) for the linker to resolve.

License modes

The project ships two Rust crates with byte-identical public APIs so you can pick the licensing mode that fits your situation by changing one line in Cargo.toml:

crate license mode per-op typical use
nyxstone-tricore-gcc GPL-3.0-or-later in-process (links gas) ~360 ns open-source / GPL-compatible projects
nyxstone-tricore-gcc-ipc MIT IPC to GPL daemon ~6 µs closed-source / commercially permissive

The MIT crate spawns a separate daemon binary (nyxstone-tcd, GPL-3.0+) and talks to it over a socketpair(2) UNIX socket, one daemon per NyxstoneTricoreGCC instance, lazy-spawn, exiting when the instance is dropped or the parent dies (socket EOF; deliberately not PR_SET_PDEATHSIG, which is thread-scoped). Strict FIFO, 30 s request timeouts, automatic one-shot daemon respawn if it crashes mid-session. See bindings/rust-ipc/README.md for the binary dependency and three daemon-install methods (cargo install, cargo binstall, programmatic bootstrap).

The C++ and Python bindings only ship in GPL form today, they statically link gas in-process for full speed. If you need permissive licensing for C++ or Python too, use the Rust MIT crate via a thin FFI or open an issue.

Quick start

make Just Works™, it auto-extracts the committed binutils prebuilts (third_party/binutils-tricore-prebuilt/, ~3 MB across x86_64+aarch64, nopic+pic) on first run, no network access needed:

# 1. Build the C++ library + tests + examples (~5 s on first run).
make                    # or:  cmake -S . -B build && cmake --build build -j

# 2. Run the test suite (162 + extra API tests).
make test               # or:  ctest --test-dir build --output-on-failure

# 3. Try an example.
./smoke                 # round-trip a handful of TriCore insns
./bench 2.0             # throughput benchmark, 2-second window

For the Rust binding (GPL in-process mode):

cd bindings/rust
NYX_LIB_DIR=.. cargo build --release
NYX_LIB_DIR=.. cargo test --release

For the Rust binding (MIT IPC mode):

cd bindings/rust && cargo build --release --bin nyxstone-tcd   # build the daemon
cd ../rust-ipc
NYXSTONE_TCD_PATH=../rust/target/release/nyxstone-tcd \
    cargo test --release

For the Python binding (requires pip install cffi setuptools):

# Python's CFFI extension needs -fPIC binutils objects; use the PIC variant.
NYX_BINUTILS_PIC=1 make
(cd bindings/python && python3 setup.py build_ext --inplace)
(cd bindings/python && PYTHONPATH=. python3 examples/smoke.py)

Binutils provisioning details

source trigger time
prebuilt (default, x86_64 / aarch64 Linux) make extracts third_party/binutils-tricore-prebuilt/$(uname -m)-linux-gnu/{nopic,pic}/lib.tar.xz automatically ~1 s
from source (other arches, or make fetch_binutils) scripts/fetch_binutils.sh, clone emproof-com fork, configure, build, stage ~75 s

Override the binutils tree wholesale with make NYX_BINUTILS=/path/to/tree.

API

C++ (#include <nyxstone/nyxstone.h>)

namespace nyxstone {

using Address = uint64_t;

struct RelocationSymbol { std::string name; Address address; };
struct RelocationInfo {
    Address offset;
    std::optional<int64_t> addend;
    RelocationSymbol symbol;
    uint32_t relocation_type;     // ELF R_TRICORE_*
};

class NyxstoneTricoreGCC {
public:
    struct LabelDefinition { std::string name; Address address; };
    struct Instruction     { Address address; std::string assembly; std::vector<uint8_t> bytes; };

    struct AssembleWithRelocsResult {
        std::vector<uint8_t> bytes;
        std::vector<RelocationInfo> relocations;
    };
    struct AssembleInstructionsWithRelocsResult {
        std::vector<Instruction> instructions;
        std::vector<RelocationInfo> relocations;
    };

    static tl::expected<std::unique_ptr<NyxstoneTricoreGCC>, std::string> create();

    tl::expected<std::vector<uint8_t>, std::string> assemble(
        const std::string& assembly, Address address,
        const std::vector<LabelDefinition>& labels) const;

    tl::expected<std::vector<Instruction>, std::string> assemble_to_instructions(
        const std::string& assembly, Address address,
        const std::vector<LabelDefinition>& labels) const;

    tl::expected<AssembleWithRelocsResult, std::string> assemble_with_relocs(
        const std::string& assembly, Address address,
        const std::vector<LabelDefinition>& labels) const;

    tl::expected<AssembleInstructionsWithRelocsResult, std::string>
    assemble_to_instructions_with_relocs(
        const std::string& assembly, Address address,
        const std::vector<LabelDefinition>& labels) const;

    tl::expected<std::string, std::string> disassemble(
        const std::vector<uint8_t>& bytes, Address address, size_t count) const;

    tl::expected<std::vector<Instruction>, std::string> disassemble_to_instructions(
        const std::vector<uint8_t>& bytes, Address address, size_t count) const;
};

}

tl::expected is the Sy Brand single-header (include/nyxstone/expected.hpp), no external dependency.

C ABI (#include <nyxstone_c.h>)

typedef struct nyxstone_handle nyxstone_handle_t;
typedef struct { const char* name;  uint64_t address; } nyxstone_label_def_t;
typedef struct { uint64_t address;  char* assembly;
                 uint8_t* bytes;    size_t bytes_len; } nyxstone_instruction_t;

nyxstone_handle_t* nyxstone_create(char** out_err);
void                        nyxstone_destroy(nyxstone_handle_t*);

int nyxstone_assemble(nyxstone_handle_t*, const char* src, size_t src_len,
                 uint64_t address,
                 const nyxstone_label_def_t* labels, size_t labels_len,
                 uint8_t** out_bytes, size_t* out_len,
                 char** out_err);
int nyxstone_assemble_to_instructions(/* same args, instr array out */);
int nyxstone_disassemble(nyxstone_handle_t*, const uint8_t* bytes, size_t bytes_len,
                    uint64_t address, size_t count,
                    char** out_text, char** out_err);
int nyxstone_disassemble_to_instructions(/* same args, instr array out */);

void nyxstone_free_bytes(uint8_t*);
void nyxstone_free_string(char*);
void nyxstone_free_instructions(nyxstone_instruction_t*, size_t);

Rust, same code, two crates

Both crates expose NyxstoneTricoreGCC with identical method signatures. Switching is a one-line Cargo.toml change:

// Same user code for both crates, only the use-path differs.
use nyxstone_tricore_gcc::{LabelDefinition, NyxstoneTricoreGCC};         // GPL crate
// or:
use nyxstone_tricore_gcc_ipc::{LabelDefinition, NyxstoneTricoreGCC};     // MIT crate

let nx = NyxstoneTricoreGCC::new()?;
let bytes  = nx.assemble("nop; ret", 0, &[])?;
let insns  = nx.disassemble_to_instructions(&bytes, 0x1000, 0)?;
let bytes2 = nx.assemble("nop; nop; j ext", 0x1000,
                         &[LabelDefinition::new("ext", 0x2000)])?;

// gcc/gas -r equivalent, external labels stay as relocs in the byte stream.
let (rel_bytes, relocs) = nx.assemble_with_relocs(
    "nop\n j ext\n", 0x1000,
    &[LabelDefinition::new("ext", 0x2000)])?;
// relocs[0] = { offset: 0x1002, symbol: { name: "ext", address: 0x2000 },
//               relocation_type: 3 /* R_TRICORE_24REL */, addend: Some(0) }
// offset is the absolute address of the reloc site: base 0x1000 + 2-byte nop.

The MIT crate additionally exposes three bootstrap helpers for daemon installation (install_daemon_if_missing, install_daemon, daemon_path) so you can avoid documenting a separate cargo install step in your own README:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Optional zero-config bootstrap (prefers `cargo binstall` if available).
    nyxstone_tricore_gcc_ipc::install_daemon_if_missing()?;
    let nx = nyxstone_tricore_gcc_ipc::NyxstoneTricoreGCC::new()?;
    // ... use nx ...
    Ok(())
}

See bindings/rust-ipc/README.md for full details on the IPC architecture, daemon lifecycle, and license boundary.

Python

from nyxstone_tricore_gcc import LabelDefinition, NyxstoneTricoreGCC
nx = NyxstoneTricoreGCC()
bytes_ = nx.assemble("nop; ret", address=0)
for ins in nx.disassemble_to_instructions(bytes_, address=0x1000):
    print(f"0x{ins.address:08x}  {ins.assembly}")
ext = nx.assemble("nop\n j ext\n",
                  address=0x1000,
                  labels=[LabelDefinition("ext", 0x2000)])

# gcc/gas -r equivalent: bytes + relocations.
rel_bytes, relocs = nx.assemble_with_relocs(
    "nop\n j ext\n",
    address=0x1000,
    labels=[LabelDefinition("ext", 0x2000)])
# relocs[0] == RelocationInfo(offset=0x1002, addend=0,   # absolute: base + nop
#                             symbol=RelocationSymbol(name="ext", address=0x2000),
#                             relocation_type=3)  # R_TRICORE_24REL

Benchmarks

Hot-cache throughput measured by examples/bench.cpp (single-threaded, 20-core x86_64 i7-1370P, gcc 14.2.0, -O3). Numbers are ops/second on the assemble / disassemble_to_instructions path, fully resetting gas's state and re-running the encoder on each call.

backend per-op (1-insn) ops/s (1-insn) insns/s (10-insn batch)
C++ in-process ~360 ns ~2.78 M ~4.84 M
Rust GPL (in-process) ~410 ns ~2.45 M ~4.70 M
Rust MIT (IPC daemon) ~6 µs ~167 k ~1.14 M
process-spawn (tricore-elf-as baseline) ~2.6 ms ~385

The MIT mode pays ~15× over the GPL in-process path at the 1-insn level for the license-clean process boundary. Batching client-side (e.g., 10 instructions per request) closes most of the gap.

Reproduce locally:

make && ./bench 2.0                                           # C++
cd bindings/rust     && cargo run --release --example bench   # GPL crate
cd bindings/rust-ipc && NYXSTONE_TCD_PATH=../rust/target/release/nyxstone-tcd \
                        cargo run --release --example bench   # MIT crate

Layout

NyxstoneTricoreGCC/
├── include/nyxstone/
│   ├── nyxstone.h                 Public C++ API
│   └── expected.hpp               Sy Brand's tl::expected (vendored, header-only)
├── c_api/nyxstone_c.h             Public C ABI
├── src/
│   ├── nyxstone.cpp               C++ implementation
│   ├── nyxstone_glue.c            The only TU that touches gas internals
│   └── nyxstone_c.cpp             C ABI wrapper
├── tests/tests.cpp                162-test matrix + stress + round-trip + API checks
├── examples/{smoke,bench}.cpp     Demo and throughput benchmark
├── bindings/
│   ├── rust/                      nyxstone-tricore-gcc (GPL) crate + nyxstone-tcd daemon
│   │   └── src/bin/nyxstone-tcd.rs  GPL daemon binary served via cargo install
│   ├── rust-ipc/                  nyxstone-tricore-gcc-ipc (MIT) IPC client crate
│   └── python/                    setuptools + CFFI extension
├── scripts/
│   ├── fetch_binutils.sh          Clone + build binutils from source
│   ├── build_prebuilts.sh         Refresh all 4 prebuilt tarball variants
│   └── extract_prebuilt.sh        Extract host-matching prebuilt into third_party/
├── third_party/
│   ├── binutils-tricore-prebuilt/ Committed prebuilts (~3 MB compressed)
│   └── binutils-tricore/          Extracted-on-demand working tree (gitignored)
├── docs/architecture.md           How the library is plumbed together
├── Makefile                       Simple build
└── CMakeLists.txt                 Production build with install

Tests

tests/tests.cpp ships a 162-test matrix split into twelve groups:

  • insn (47): every TriCore format we exercise (SR/SRR/SLR/SSR/SC/SRC/RC/ RR/RLC/B), various register kinds, immediate widths, signedness.
  • label (12): forward + backward branches, multi-label lines (a: b: nop), .L0/$x/_x naming variants, label-only sources.
  • relax (2): local branches shrink to their 2-byte short form in both directions.
  • data (36): every data directive Nyxstone supports, .byte / .half / .hword / .short / .2byte / .word / .int / .long / .4byte / .quad / .8byte / .ascii / .asciz / .string / .skip / .space / .zero / .org / .align / .balign.
  • mixed (4): instructions + labels + data interleaved.
  • edge (9): empty / comments-only / whitespace / ; separators.
  • forbid (7+3): .text-only restriction, .data / .bss / .section .foo / .pushsection must all reject; .text / .section .text* must accept.
  • quote (6): # / ; / // / /* */ inside string literals survive the tokenizer (.asciz "a#b" keeps its hash).
  • comment (6): /* */ block comments, including mid-line, whole-line, multi-line, and unterminated, are stripped like gas's input scrubber.
  • dirsem (13): .skip/.space/.align fill operands, .align max-skip, .p2align, .equ/.set constants and label expressions, numeric local labels (1: / 1b / 1f, including instance reuse).
  • alignrelax (4): .align/.org padding is sized after branch relaxation -- a shrinking branch before the directive must not shift the alignment.
  • error (13): undefined labels, unknown directives/mnemonics, duplicate labels, .org backwards, non-power-of-2 .balign, out-of-range immediates -- all must reject instead of emitting silently wrong bytes.

After the core matrix, post-matrix checks (194 assertions) cover:

  • 100× stress per test catches state-reset drift across consecutive assemble() calls.
  • Every BYTES test of ≥2 bytes is round-tripped through disassemble_to_instructions() (128 cases).
  • LabelDefinition external symbols (exact displacement bytes + address invariance), the address parameter propagating to Instruction.address, the count parameter, relocation records, and branch displacement semantics (decoded target == label address).
  • Resolution-path relocation encodings: every TriCore relocation the library resolves in place is a faithful transcription of gas's md_apply_fix, including the non-linear forms a naive mask heuristic silently mis-encoded -- the split B-format 24-bit displacement (24REL/ 24ABS), the carry-adjusted high half (HI/HIADJ, movh hi:), the permuted 16-bit offset (16OFF/LO2), and 18-bit absolute addressing (18ABS, lea/ld.w/st.w).
  • Relocation offsets are absolute (base + section position) and stay correct through relaxation, .align/.org padding, and interleaved data; the symbol.address hint resolves through every binding.
  • ISA level: the assembler accepts the v1.6/1.6.2 instruction set the disassembler decodes (fret, fcall, cmpswap.w, ...) -- gas's default v1.2 silently rejected them.
  • Disassembly is re-assemblable: the objdump-style <0x..> symbolic- address annotation (after a movh.a+lea/ld/st sequence) is stripped, and load/store/lea references to labels resolve to the right address.
  • The data fast-path (emit_int_list) is byte-checked against gas's cons for every value (overflow, sign, all bases).
  • An instruction idempotence corpus (one per major format) pins both the disassembler (bytes → text) and assembler/disassembler agreement (text → bytes → text), the property the round-trip fuzzer verified across the full 16-bit space and a 2.1M-case 32-bit sample.
  • Error-message quality (gas diagnostics + offending symbol names appear in the returned error; nothing leaks to the host stderr) and 32-bit masking of printed branch targets.

A separate roundtrip_all test (also run by make test) is exhaustive over the whole v1.6.2 instruction set: it drives all 398 instruction/format entries from the pinned binutils opcode table (extracted by scripts/gen_v162_corpus.py into tests/tricore_v162_insns.inc), synthesizing operands from each instruction's arg-spec across two parameter variants (low registers + mid immediates, and high registers + extreme immediates). Every instruction must assemble in both variants; every non-branch form must round-trip idempotently (disassemble → assemble → disassemble stable); and every branch/absolute/symbol operand must resolve both inline (label address recovered in the disassembly) and as a relocation (assemble_with_relocs yields the right symbol, absolute offset, and address). PC-relative branches are validated only via the reference path, since the disassembler prints an absolute target that gas re-reads as a displacement (upstream convention).

=== TriCore v1.6.2 exhaustive round-trip ===
instruction/format entries:  398
assembled (variant A):       398  (100.0%)
assembled (variant B):       398  (high regs / extreme immediates)
round-trip idempotent:       375 / 375 checked (non-branch)
reference/reloc resolved:    25 / 25
OK

The Rust crates run the same unit tests (verbatim sources, only the use line differs between nyxstone-tricore-gcc and nyxstone-tricore-gcc-ipc) plus one doctest each, and produce byte-identical output across both backends; the IPC crate adds daemon-resilience tests (kill-then-respawn, protocol frame caps).

$ ./run_tests
... (162 tests in 12 groups) ...
--- 100x stress (each non-MUST_FAIL test 100 iterations) ---
  PASS: no drift across 142 non-fail tests * 100 iterations
--- disassembly round-trip ---
  128 round-trip pass, 0 fail
--- resolution-path reloc encodings ---
  12 resolution-reloc pass, 0 fail
--- relocation offsets ---
  7 reloc-offset pass, 0 fail
--- data fast-path vs gas cons ---
  88 data-fastpath pass, 0 fail
--- idempotence corpus ---
  33 idempotence-corpus pass, 0 fail
--- v1.6 instruction set / annotation strip / load-store label resolution ---
  5 v1.6-isa, 4 annotation-strip, 9 load-store-label pass, 0 fail
  ... (external-label, relocs, branch, error-message, masking checks) ...
Summary: 162 passed, 0 failed, 0 drifts (of 162 tests);
         128 disasm round-trips passed, 0 failed;
         194 additional API checks passed, 0 failed

Threading

Single-threaded only at the gas layer. All gas globals are process-wide; concurrent calls from multiple threads would corrupt state.

  • The C++ API does not lock, callers must serialize.
  • The Rust GPL binding holds a global Mutex<()> per call so calls from multiple threads are safe (they're just serialized). NyxstoneTricoreGCC is both Send and Sync.
  • The Rust MIT binding holds a per-instance Mutex around the UDS socket, same FIFO contract, scoped per-handle since each MIT-mode instance has its own daemon process.
  • The Python binding holds a threading.Lock() per call.

License

GPL-3.0-or-later for the C++/C/Python parts and the nyxstone-tricore-gcc Rust crate. These statically link GNU assembler (gas) object files from binutils, which are GPL-3.0-or-later; that propagates to the combined library and to any binary distribution. libbfd and libopcodes (LGPL-3.0-or-later) are subsumed by the GPL term.

MIT for the nyxstone-tricore-gcc-ipc Rust crate, it ships zero GPL bytes and only talks to a separate nyxstone-tcd daemon binary (GPL-3.0+) over an IPC socket. Shipping the daemon alongside a commercial product follows the same well-trodden pattern as bundling gcc.exe next to a closed-source IDE.

See LICENSE, LICENSE-MIT, and bindings/rust-ipc/README.md for the full story on the license-mode split.

About

No description, website, or topics provided.

Resources

License

Unknown, Unknown licenses found

Licenses found

Unknown
LICENSE
Unknown
LICENSE-MIT

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors