Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/synth-backend-riscv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@
pub mod backend;
pub mod elf_builder;
pub mod encoder;
pub mod linker_script;
pub mod pmp;
pub mod register;
pub mod riscv_op;
pub mod selector;
pub mod startup;

pub use backend::RiscVBackend;
pub use elf_builder::{ElfMode, RiscVElfBuilder, RiscVElfFunction};
pub use encoder::{RiscVEncoder, RiscVEncodingError};
pub use linker_script::{LinkerScriptConfig, RiscVLinkerScriptGenerator};
pub use pmp::{PMPAllocator, PMPEntry, PMPError, PMPMode, PMPPermissions};
pub use register::{Reg, RegClass};
pub use riscv_op::{Branch, Csr, RiscVOp};
pub use selector::{RiscVSelection, SelectorError, select_simple};
pub use startup::{RiscVStartupGenerator, StartupConfig};
289 changes: 289 additions & 0 deletions crates/synth-backend-riscv/src/linker_script.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
//! GNU `ld` linker script generator for RISC-V targets.
//!
//! Mirrors `synth-backend::linker_script` (ARM) but produces RISC-V-flavored
//! sections: `.text._reset` is placed at the reset entry, `mtvec`'s alignment
//! is honored (4-byte for direct mode, 64-byte minimum for vectored), and
//! `__global_pointer$` / `__linear_memory_base` are exposed as weak symbols
//! so the startup code can `la` them with no relocation hassle.
//!
//! Memory model assumed:
//!
//! ```text
//! ┌──────────────────┐ flash_size
//! │ .data load │ ──▶ copied to _data_start at reset
//! │ .rodata │
//! │ .text │
//! │ .text._trap_entry│
//! │ .text._reset │ flash_origin (entry point)
//! └──────────────────┘
//!
//! ┌──────────────────┐ ram_origin + ram_size (= _stack_top)
//! │ stack ↓ │
//! │ … │
//! │ __linear_memory_base
//! │ .bss │
//! │ .data │ ram_origin
//! └──────────────────┘
//! ```
//!
//! The wasm linear-memory window lives between the `.bss` end and the stack —
//! the firmware author sets its size at link time via `--defsym` or by
//! editing the generated script.

use synth_core::HardwareCapabilities;

#[derive(Debug, Clone)]
pub struct LinkerScriptConfig {
/// Base address of the executable section (typically 0x0 for embedded
/// boot, 0x80000000 for SiFive HiFive-style boards).
pub flash_origin: u64,
/// Base address of writable RAM.
pub ram_origin: u64,
/// Number of bytes reserved for the wasm linear memory (multiple of 64 KiB).
/// 0 disables linear-memory pre-reservation entirely.
pub linear_memory_size: u64,
/// Number of bytes reserved for the call stack.
pub stack_size: u64,
}

impl Default for LinkerScriptConfig {
fn default() -> Self {
Self {
flash_origin: 0x0,
ram_origin: 0x8000_0000,
linear_memory_size: 64 * 1024, // 1 wasm page
stack_size: 4096,
}
}
}

pub struct RiscVLinkerScriptGenerator {
pub hw_caps: HardwareCapabilities,
pub config: LinkerScriptConfig,
}

impl RiscVLinkerScriptGenerator {
pub fn new(hw_caps: HardwareCapabilities) -> Self {
Self {
hw_caps,
config: LinkerScriptConfig::default(),
}
}

pub fn with_config(mut self, config: LinkerScriptConfig) -> Self {
self.config = config;
self
}

pub fn generate(&self) -> String {
let mut out = String::new();
out.push_str("/* RISC-V Linker Script\n");
out.push_str(" * Generated by synth-backend-riscv\n");
out.push_str(&format!(
" * Flash: 0x{:08X} ({} KB), RAM: 0x{:08X} ({} KB)\n",
self.config.flash_origin,
self.hw_caps.flash_size / 1024,
self.config.ram_origin,
self.hw_caps.ram_size / 1024,
));
out.push_str(&format!(
" * Linear memory: {} KB, stack: {} KB\n",
self.config.linear_memory_size / 1024,
self.config.stack_size / 1024,
));
out.push_str(" */\n\n");

out.push_str("OUTPUT_ARCH(\"riscv\")\n");
out.push_str("ENTRY(_reset)\n\n");

out.push_str("MEMORY {\n");
out.push_str(&format!(
" flash (rx) : ORIGIN = 0x{:08X}, LENGTH = {}K\n",
self.config.flash_origin,
self.hw_caps.flash_size / 1024
));
out.push_str(&format!(
" ram (rwx) : ORIGIN = 0x{:08X}, LENGTH = {}K\n",
self.config.ram_origin,
self.hw_caps.ram_size / 1024
));
out.push_str("}\n\n");

out.push_str("SECTIONS {\n");

// ── .text — code in flash, with the reset vector first ───────
out.push_str(" .text : {\n");
out.push_str(" KEEP(*(.text._reset))\n");
// The trap entry must be 4-byte aligned for mtvec direct mode.
// For vectored mode (mtvec.MODE = 1) it must be 64-byte aligned —
// the conservative default below covers both.
out.push_str(" . = ALIGN(64);\n");
out.push_str(" KEEP(*(.text._trap_entry))\n");
out.push_str(" *(.text*)\n");
out.push_str(" } > flash\n\n");

// ── .rodata — read-only constants in flash ──────────────────
out.push_str(" .rodata : {\n");
out.push_str(" *(.rodata*)\n");
out.push_str(" *(.srodata*)\n");
out.push_str(" . = ALIGN(4);\n");
out.push_str(" PROVIDE(_data_load = .);\n");
out.push_str(" } > flash\n\n");

// ── .data — initialized RW data; load addr in flash, run addr in RAM ─
out.push_str(" .data : AT(_data_load) {\n");
out.push_str(" . = ALIGN(4);\n");
out.push_str(" PROVIDE(_data_start = .);\n");
out.push_str(" *(.data*)\n");
out.push_str(" *(.sdata*)\n");
out.push_str(" . = ALIGN(4);\n");
out.push_str(" PROVIDE(_data_end = .);\n");
out.push_str(" } > ram\n\n");

// ── .bss — zero-initialized RW data ────────────────────────
out.push_str(" .bss (NOLOAD) : {\n");
out.push_str(" . = ALIGN(4);\n");
out.push_str(" PROVIDE(_bss_start = .);\n");
out.push_str(" *(.bss*)\n");
out.push_str(" *(.sbss*)\n");
out.push_str(" *(COMMON)\n");
out.push_str(" . = ALIGN(4);\n");
out.push_str(" PROVIDE(_bss_end = .);\n");
out.push_str(" } > ram\n\n");

// ── Linear memory base — where the wasm heap starts ─────────
if self.config.linear_memory_size > 0 {
out.push_str(" /* Wasm linear memory — selector loads/stores use s11 = this */\n");
out.push_str(" .linear_memory (NOLOAD) : ALIGN(16) {\n");
out.push_str(" PROVIDE(__linear_memory_base = .);\n");
out.push_str(&format!(" . += {};\n", self.config.linear_memory_size));
out.push_str(" PROVIDE(__linear_memory_end = .);\n");
out.push_str(" } > ram\n\n");
} else {
out.push_str(" PROVIDE(__linear_memory_base = _bss_end);\n\n");
}

// ── Stack ────────────────────────────────────────────────
out.push_str(" /* Stack grows down from end of RAM */\n");
out.push_str(" .stack (NOLOAD) : ALIGN(16) {\n");
out.push_str(&format!(" . += {};\n", self.config.stack_size));
out.push_str(" PROVIDE(_stack_top = .);\n");
out.push_str(" } > ram\n\n");

// ── GP — small-data linker-relaxation anchor ─────────────
out.push_str(
" /* Global pointer — points 0x800 above .sdata for short-form RV access */\n",
);
out.push_str(" PROVIDE(__global_pointer$ = _data_start + 0x800);\n\n");

out.push_str("}\n");
out
}
}

#[cfg(test)]
mod tests {
use super::*;
use synth_core::{HardwareCapabilities, RISCVVariant, TargetArch};

fn rv32imac_caps() -> HardwareCapabilities {
HardwareCapabilities {
arch: TargetArch::RISCV(RISCVVariant::RV32IMAC),
has_mpu: false,
mpu_regions: 0,
has_pmp: true,
pmp_entries: 16,
has_fpu: false,
fpu_precision: None,
has_simd: false,
simd_level: None,
xip_capable: true,
flash_size: 64 * 1024,
ram_size: 64 * 1024,
}
}

#[test]
fn generates_well_formed_script() {
let g = RiscVLinkerScriptGenerator::new(rv32imac_caps());
let s = g.generate();
assert!(s.contains("OUTPUT_ARCH(\"riscv\")"));
assert!(s.contains("ENTRY(_reset)"));
assert!(s.contains("MEMORY {"));
// Flash and RAM regions present
assert!(s.contains("flash (rx)"));
assert!(s.contains("ram (rwx)"));
}

#[test]
fn places_reset_first_in_text() {
let g = RiscVLinkerScriptGenerator::new(rv32imac_caps());
let s = g.generate();
let text_start = s.find(".text :").unwrap();
let reset_first = s[text_start..].find(".text._reset").unwrap();
let trap_entry = s[text_start..].find(".text._trap_entry").unwrap();
let general_text = s[text_start..].find("*(.text*)").unwrap();
assert!(reset_first < trap_entry);
assert!(trap_entry < general_text);
}

#[test]
fn data_load_in_flash_run_in_ram() {
let g = RiscVLinkerScriptGenerator::new(rv32imac_caps());
let s = g.generate();
assert!(s.contains(".data : AT(_data_load)"));
}

#[test]
fn provides_global_pointer() {
let g = RiscVLinkerScriptGenerator::new(rv32imac_caps());
let s = g.generate();
assert!(s.contains("__global_pointer$"));
// GP convention: _data_start + 0x800 to put .sdata in short-form range
assert!(s.contains("_data_start + 0x800"));
}

#[test]
fn provides_linear_memory_base() {
let g = RiscVLinkerScriptGenerator::new(rv32imac_caps());
let s = g.generate();
assert!(s.contains("__linear_memory_base"));
}

#[test]
fn linear_memory_size_zero_falls_back_to_bss_end() {
let g = RiscVLinkerScriptGenerator::new(rv32imac_caps()).with_config(LinkerScriptConfig {
linear_memory_size: 0,
..Default::default()
});
let s = g.generate();
assert!(s.contains("PROVIDE(__linear_memory_base = _bss_end);"));
}

#[test]
fn trap_entry_aligned_64_bytes() {
let g = RiscVLinkerScriptGenerator::new(rv32imac_caps());
let s = g.generate();
// The 64-byte ALIGN comes immediately before the trap entry KEEP
let trap_idx = s.find("KEEP(*(.text._trap_entry))").unwrap();
let preamble = &s[..trap_idx];
assert!(preamble.rfind(". = ALIGN(64);").is_some());
}

#[test]
fn stack_top_provided() {
let g = RiscVLinkerScriptGenerator::new(rv32imac_caps());
let s = g.generate();
assert!(s.contains("PROVIDE(_stack_top"));
}

#[test]
fn custom_flash_origin() {
let g = RiscVLinkerScriptGenerator::new(rv32imac_caps()).with_config(LinkerScriptConfig {
flash_origin: 0x2000_0000,
..Default::default()
});
let s = g.generate();
assert!(s.contains("ORIGIN = 0x20000000"));
}
}
Loading
Loading