diff --git a/Cargo.lock b/Cargo.lock index ff92bd7..529ce79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -525,6 +525,10 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mini-template" +version = "0.1.0" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1167,7 +1171,6 @@ dependencies = [ "zeroos-device-urandom", "zeroos-device-zero", "zeroos-foundation", - "zeroos-libunwind", "zeroos-macros", "zeroos-os-linux", "zeroos-rng", @@ -1222,6 +1225,7 @@ dependencies = [ "clap", "dirs", "log", + "mini-template", "parse-size", "serde", "serde_json", @@ -1279,10 +1283,6 @@ dependencies = [ "zeroos-debug", ] -[[package]] -name = "zeroos-libunwind" -version = "0.1.0" - [[package]] name = "zeroos-macros" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index eb15f32..33aa4dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "xtask", "crates/cargo-matrix", "crates/htif", + "crates/mini-template", "crates/zeroos", "crates/zeroos-foundation", "crates/zeroos-debug", @@ -13,7 +14,6 @@ members = [ "crates/zeroos-runtime-musl", "crates/zeroos-runtime-gnu", "crates/zeroos-runtime-nostd", - "crates/zeroos-libunwind", "crates/zeroos-build", "crates/zeroos-allocator-bump", "crates/zeroos-allocator-linked-list", @@ -42,6 +42,7 @@ description = "ZeroOS" [workspace.dependencies] # Internal workspace crates cargo-matrix = { path = "crates/cargo-matrix" } +mini-template = { path = "crates/mini-template" } zeroos = { path = "crates/zeroos" } foundation = { path = "crates/zeroos-foundation", package = "zeroos-foundation" } debug = { path = "crates/zeroos-debug", package = "zeroos-debug" } @@ -51,7 +52,6 @@ os-linux = { path = "crates/zeroos-os-linux", package = "zeroos-os-linux" } runtime-musl = { path = "crates/zeroos-runtime-musl", package = "zeroos-runtime-musl" } runtime-gnu = { path = "crates/zeroos-runtime-gnu", package = "zeroos-runtime-gnu" } runtime-nostd = { path = "crates/zeroos-runtime-nostd", package = "zeroos-runtime-nostd" } -libunwind = { path = "crates/zeroos-libunwind", package = "zeroos-libunwind" } allocator-linked-list = { path = "crates/zeroos-allocator-linked-list", package = "zeroos-allocator-linked-list" } allocator-bump = { path = "crates/zeroos-allocator-bump", package = "zeroos-allocator-bump" } allocator-buddy = { path = "crates/zeroos-allocator-buddy", package = "zeroos-allocator-buddy" } diff --git a/build-c-smoke.sh b/build-c-smoke.sh index c32b334..1b4a489 100755 --- a/build-c-smoke.sh +++ b/build-c-smoke.sh @@ -21,6 +21,7 @@ cargo spike build \ --memory-size 128Mi \ --heap-size 64Mi \ --stack-size 2Mi \ + -- \ --quiet \ --profile "${PROFILE}" diff --git a/build-fibonacci.sh b/build-fibonacci.sh index 455d42a..2ef9558 100755 --- a/build-fibonacci.sh +++ b/build-fibonacci.sh @@ -13,7 +13,7 @@ TARGET_TRIPLE="riscv64imac-unknown-none-elf" OUT_DIR="${ROOT}/target/${TARGET_TRIPLE}/$([ "$PROFILE" = "dev" ] && echo debug || echo "$PROFILE")" BIN="${OUT_DIR}/fibonacci" -cargo spike build -p fibonacci --target "${TARGET_TRIPLE}" --quiet --features=debug --profile "${PROFILE}" +cargo spike build -p fibonacci --target "${TARGET_TRIPLE}" -- --quiet --features=debug --profile "${PROFILE}" OUT_NOSTD="$(mktemp)" OUT_STD="$(mktemp)" trap 'rm -f "${OUT_NOSTD}" "${OUT_STD}"' EXIT @@ -28,7 +28,7 @@ TARGET_TRIPLE="riscv64imac-zero-linux-musl" OUT_DIR="${ROOT}/target/${TARGET_TRIPLE}/$([ "$PROFILE" = "dev" ] && echo debug || echo "$PROFILE")" BIN="${OUT_DIR}/fibonacci" -cargo spike build -p fibonacci --target "${TARGET_TRIPLE}" --mode std --quiet --features=std,debug --profile "${PROFILE}" +cargo spike build -p fibonacci --target "${TARGET_TRIPLE}" --mode std -- --quiet --features=std,debug --profile "${PROFILE}" RUST_LOG=debug cargo spike run "${BIN}" --isa RV64IMAC --instructions 100000000 | tee "${OUT_STD}" grep -q "fibonacci(10) = 55" "${OUT_STD}" grep -q "Test PASSED" "${OUT_STD}" diff --git a/build-std-smoke.sh b/build-std-smoke.sh index ad8e047..e9105ff 100755 --- a/build-std-smoke.sh +++ b/build-std-smoke.sh @@ -2,6 +2,7 @@ set -euo pipefail export RUSTUP_NO_UPDATE_CHECK=1 + TARGET_TRIPLE="riscv64imac-zero-linux-musl" PROFILE="dev" ROOT="$(git rev-parse --show-toplevel 2>/dev/null || (cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd))" @@ -10,7 +11,7 @@ BIN="${OUT_DIR}/std-smoke" cd "${ROOT}" echo "Building std-smoke example..." -cargo spike build -p std-smoke --target "${TARGET_TRIPLE}" --mode std --quiet --features=std --profile "${PROFILE}" +cargo spike build -p std-smoke --target "${TARGET_TRIPLE}" --mode std --backtrace=enable -- --quiet --features=std,backtrace --profile "${PROFILE}" echo "Running on Spike simulator..." OUT="$(mktemp)" diff --git a/build-syscall-cycles.sh b/build-syscall-cycles.sh index c6ae0f5..47d67fa 100755 --- a/build-syscall-cycles.sh +++ b/build-syscall-cycles.sh @@ -18,9 +18,9 @@ if [[ "${PROFILE}" = "release" ]]; then CARGO_PROFILE_RELEASE_STRIP=none \ CARGO_PROFILE_RELEASE_LTO=true \ CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 \ - cargo spike build -p syscall-cycles --target "${TARGET_TRIPLE}" --mode std --quiet --features=std --profile "${PROFILE}" + cargo spike build -p syscall-cycles --target "${TARGET_TRIPLE}" --mode std -- --quiet --features=std --profile "${PROFILE}" else - cargo spike build -p syscall-cycles --target "${TARGET_TRIPLE}" --mode std --quiet --features=std --profile "${PROFILE}" + cargo spike build -p syscall-cycles --target "${TARGET_TRIPLE}" --mode std -- --quiet --features=std --profile "${PROFILE}" fi # Persist logs under target/ so they survive script exit and are easy to share/debug. diff --git a/crates/mini-template/Cargo.toml b/crates/mini-template/Cargo.toml new file mode 100644 index 0000000..afc9926 --- /dev/null +++ b/crates/mini-template/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "mini-template" +version.workspace = true +edition.workspace = true +description = "Minimal, auditable template renderer for ZeroOS build-time scripts" + +[lib] +name = "mini_template" +path = "src/lib.rs" + +[dependencies] + + diff --git a/crates/mini-template/src/lib.rs b/crates/mini-template/src/lib.rs new file mode 100644 index 0000000..58cc67a --- /dev/null +++ b/crates/mini-template/src/lib.rs @@ -0,0 +1,247 @@ +//! `mini-template`: a tiny, auditable template renderer intended for build-time scripts. +//! +//! Supported syntax (Jinja-like subset): +//! - `{% if %} ... {% else %} ... {% endif %}` +//! +//! Only boolean identifiers are supported; no expressions, no filters, no loops. + +use std::collections::BTreeMap; + +#[derive(Debug, Clone, Default)] +pub struct Context { + bools: BTreeMap, + strs: BTreeMap, +} + +impl Context { + pub fn new() -> Self { + Self::default() + } + + pub fn insert_bool(&mut self, name: impl Into, value: bool) { + self.bools.insert(name.into(), value); + } + + pub fn with_bool(mut self, name: impl Into, value: bool) -> Self { + self.insert_bool(name, value); + self + } + + pub fn insert_str(&mut self, name: impl Into, value: impl Into) { + self.strs.insert(name.into(), value.into()); + } + + pub fn with_str(mut self, name: impl Into, value: impl Into) -> Self { + self.insert_str(name, value); + self + } + + fn get_bool(&self, name: &str) -> Option { + self.bools.get(name).copied() + } + + fn get_str(&self, name: &str) -> Option<&str> { + self.strs.get(name).map(|s| s.as_str()) + } +} + +#[derive(Debug, Clone)] +pub struct RenderError { + pub message: String, + pub byte_offset: usize, +} + +impl std::fmt::Display for RenderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} (at byte {})", self.message, self.byte_offset) + } +} + +impl std::error::Error for RenderError {} + +#[derive(Debug)] +struct Frame { + cond_true: bool, + in_else: bool, +} + +fn should_emit(stack: &[Frame]) -> bool { + // Emit only if every active frame selects this branch. + stack + .iter() + .all(|f| if f.in_else { !f.cond_true } else { f.cond_true }) +} + +/// Render `template` using `ctx`. +pub fn render(template: &str, ctx: &Context) -> Result { + let mut out = String::with_capacity(template.len()); + let mut stack: Vec = Vec::new(); + + let mut i = 0; + while i < template.len() { + let rest = &template[i..]; + let next_ctrl = rest.find("{%"); + let next_expr = rest.find("{{"); + let open = match (next_ctrl, next_expr) { + (None, None) => None, + (Some(a), None) => Some((a, true)), + (None, Some(b)) => Some((b, false)), + (Some(a), Some(b)) => Some(if a <= b { (a, true) } else { (b, false) }), + }; + + if let Some((open, is_ctrl)) = open { + let text = &rest[..open]; + if should_emit(&stack) { + out.push_str(text); + } + i += open; + + let rest2 = &template[i..]; + if is_ctrl { + let close = rest2.find("%}").ok_or_else(|| RenderError { + message: "Unclosed template tag".to_string(), + byte_offset: i, + })?; + + let tag = rest2[2..close].trim(); + let tag_offset = i; + i += close + 2; + + if tag == "else" { + let top = stack.last_mut().ok_or_else(|| RenderError { + message: "{% else %} without matching {% if ... %}".to_string(), + byte_offset: tag_offset, + })?; + if top.in_else { + return Err(RenderError { + message: "Duplicate {% else %} in the same {% if %} block".to_string(), + byte_offset: tag_offset, + }); + } + top.in_else = true; + continue; + } + + if tag == "endif" { + if stack.pop().is_none() { + return Err(RenderError { + message: "{% endif %} without matching {% if ... %}".to_string(), + byte_offset: tag_offset, + }); + } + continue; + } + + if let Some(cond) = tag.strip_prefix("if ") { + let ident = cond.trim(); + if ident.is_empty() { + return Err(RenderError { + message: "Empty identifier in {% if %}".to_string(), + byte_offset: tag_offset, + }); + } + let cond_true = ctx.get_bool(ident).ok_or_else(|| RenderError { + message: format!("Unknown boolean identifier in template: {}", ident), + byte_offset: tag_offset, + })?; + + stack.push(Frame { + cond_true, + in_else: false, + }); + continue; + } + + return Err(RenderError { + message: format!("Unknown template tag: {{% {} %}}", tag), + byte_offset: tag_offset, + }); + } else { + let close = rest2.find("}}").ok_or_else(|| RenderError { + message: "Unclosed template expression".to_string(), + byte_offset: i, + })?; + let expr = rest2[2..close].trim(); + let expr_offset = i; + i += close + 2; + + if should_emit(&stack) { + let ident = expr; + if ident.is_empty() { + return Err(RenderError { + message: "Empty identifier in {{ ... }}".to_string(), + byte_offset: expr_offset, + }); + } + let val = ctx.get_str(ident).ok_or_else(|| RenderError { + message: format!("Unknown string identifier in template: {}", ident), + byte_offset: expr_offset, + })?; + out.push_str(val); + } + continue; + } + } else { + if should_emit(&stack) { + out.push_str(rest); + } + break; + } + } + + if !stack.is_empty() { + return Err(RenderError { + message: "Unclosed {% if %} block(s)".to_string(), + byte_offset: template.len(), + }); + } + + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn if_true_emits_then_branch() { + let ctx = Context::new().with_bool("backtrace", true); + let s = "a{% if backtrace %}b{% else %}c{% endif %}d"; + assert_eq!(render(s, &ctx).unwrap(), "abd"); + } + + #[test] + fn if_false_emits_else_branch() { + let ctx = Context::new().with_bool("backtrace", false); + let s = "a{% if backtrace %}b{% else %}c{% endif %}d"; + assert_eq!(render(s, &ctx).unwrap(), "acd"); + } + + #[test] + fn if_without_else() { + let ctx = Context::new().with_bool("x", false); + let s = "a{% if x %}b{% endif %}c"; + assert_eq!(render(s, &ctx).unwrap(), "ac"); + } + + #[test] + fn nesting_works() { + let ctx = Context::new().with_bool("a", true).with_bool("b", false); + let s = "{% if a %}A{% if b %}B{% else %}C{% endif %}D{% endif %}"; + assert_eq!(render(s, &ctx).unwrap(), "ACD"); + } + + #[test] + fn unknown_identifier_errors() { + let ctx = Context::new(); + let err = render("{% if nope %}x{% endif %}", &ctx).unwrap_err(); + assert!(err.message.contains("Unknown boolean identifier")); + } + + #[test] + fn string_interpolation() { + let ctx = Context::new().with_str("MEMORY_ORIGIN", "0x80000000"); + let s = "ORIGIN={{ MEMORY_ORIGIN }}"; + assert_eq!(render(s, &ctx).unwrap(), "ORIGIN=0x80000000"); + } +} diff --git a/crates/zeroos-build/Cargo.toml b/crates/zeroos-build/Cargo.toml index b61c428..affa068 100644 --- a/crates/zeroos-build/Cargo.toml +++ b/crates/zeroos-build/Cargo.toml @@ -22,3 +22,4 @@ anyhow.workspace = true serde.workspace = true serde_json.workspace = true parse-size.workspace = true +mini-template.workspace = true diff --git a/crates/zeroos-build/linker.ld b/crates/zeroos-build/linker.ld deleted file mode 100644 index 816345b..0000000 --- a/crates/zeroos-build/linker.ld +++ /dev/null @@ -1,109 +0,0 @@ -OUTPUT_ARCH(riscv) -ENTRY(_start) - -MEMORY -{ - RAM (rwx) : ORIGIN = {MEMORY_ORIGIN}, LENGTH = {MEMORY_SIZE} -} - -PHDRS -{ - text PT_LOAD FLAGS(5); /* R-X */ - rodata PT_LOAD FLAGS(4); /* R-- */ - data PT_LOAD FLAGS(6); /* RW- */ - tls PT_TLS; /* TLS metadata */ -} - -SECTIONS -{ - . = {MEMORY_ORIGIN}; - PROVIDE_HIDDEN(__ehdr_start = .); - - .text : { - *(.text.boot) - *(.text .text.*) - . = ALIGN(4); - } > RAM : text - - .rodata : { - *(.rodata .rodata.*) - *(.srodata .srodata.*) - . = ALIGN(8); - } > RAM : rodata - - .init_array : { - PROVIDE_HIDDEN(__init_array_start = .); - KEEP(*(SORT_BY_INIT_PRIORITY(.init_array.*))) - KEEP(*(.init_array)) - PROVIDE_HIDDEN(__init_array_end = .); - } > RAM : data - - .fini_array : { - PROVIDE_HIDDEN(__fini_array_start = .); - KEEP(*(SORT_BY_INIT_PRIORITY(.fini_array.*))) - KEEP(*(.fini_array)) - PROVIDE_HIDDEN(__fini_array_end = .); - } > RAM : data - - .data : { - *(.data .data.*) - *(.sdata .sdata.*) - . = ALIGN(8); - } > RAM : data - - .tdata : ALIGN(16) { - PROVIDE_HIDDEN(__tdata_start = .); - *(.tdata .tdata.*) - /* Force non-empty TLS section */ - . = . + 8; - PROVIDE_HIDDEN(__tdata_end = .); - } > RAM : data : tls - - .tbss : ALIGN(16) { - PROVIDE_HIDDEN(__tbss_start = .); - *(.tbss .tbss.*) - *(.tcommon) - /* Force non-empty TLS section */ - . = . + 8; - PROVIDE_HIDDEN(__tbss_end = .); - } > RAM : data : tls - - .bss : { - PROVIDE(__bss_start = .); - *(.bss .bss.*) - *(COMMON) - *(.sbss .sbss.*) - . = ALIGN(8); - PROVIDE(__bss_end = .); - - /* HTIF sections must be in writable memory */ - KEEP(*(.tohost)); - . = ALIGN(8); - PROVIDE_HIDDEN(tohost = . - 8); - KEEP(*(.fromhost)); - . = ALIGN(8); - PROVIDE_HIDDEN(fromhost = . - 8); - } > RAM : data - - /* Stack and Heap setup */ - /* Stack is at the top of RAM */ - __stack_size = {STACK_SIZE}; - __stack_top = ORIGIN(RAM) + LENGTH(RAM); - __stack_bottom = __stack_top - __stack_size; - - /* Heap starts after BSS and goes up to Stack */ - .heap : { - . = ALIGN(4096); - PROVIDE(__heap_start = .); - . = __stack_bottom; - PROVIDE(__heap_end = .); - } > RAM : data - - ASSERT(__heap_end >= __heap_start, "Error: memory overflow (heap size negative)") - - /DISCARD/ : { - *(.eh_frame) - *(.comment) - } -} - diff --git a/crates/zeroos-build/src/cmds/build.rs b/crates/zeroos-build/src/cmds/build.rs index 36bccca..c51133c 100644 --- a/crates/zeroos-build/src/cmds/build.rs +++ b/crates/zeroos-build/src/cmds/build.rs @@ -4,17 +4,30 @@ use std::fs; use std::path::{Path, PathBuf}; use std::process::{exit, Command}; +use crate::spec::TargetRenderOptions; + #[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] pub enum StdMode { Std, NoStd, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] +pub enum BacktraceMode { + Auto, + Enable, + Disable, +} + #[derive(clap::Args, Debug, Clone)] pub struct BuildArgs { #[arg(long, short = 'p')] pub package: String, + /// Backtrace policy for the guest. + #[arg(long, value_enum, default_value = "auto")] + pub backtrace: BacktraceMode, + #[arg(long, default_value = "0x80000000")] pub memory_origin: String, @@ -42,7 +55,11 @@ pub struct BuildArgs { #[arg(long, env = "RISCV_GCC_PATH")] pub gcc_lib_path: Option, - #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + /// Arguments after `--` are forwarded to the underlying `cargo build` invocation. + /// + /// Example: + /// `cargo spike build -p foo --target ... --mode std -- --release --quiet` + #[arg(trailing_var_arg = true)] pub cargo_args: Vec, } @@ -90,6 +107,7 @@ pub fn build_binary( let target_dir = crate::project::get_target_directory(workspace_root)?; let profile = crate::project::detect_profile(&args.cargo_args); + let backtrace_enabled = should_enable_backtrace(args, &profile); debug!("target_dir: {}", target_dir.display()); debug!("target: {}", target); @@ -103,7 +121,8 @@ pub fn build_binary( let config = crate::linker::LinkerConfig::new() .with_memory(memory_origin, memory_size) .with_stack_size(stack_size) - .with_heap_size(heap_size); + .with_heap_size(heap_size) + .with_backtrace(backtrace_enabled); let config = if let Some(template) = linker_template { config.with_template(template) @@ -120,7 +139,14 @@ pub fn build_binary( .or_else(|| { if args.mode == StdMode::Std && target == TARGET_STD { let target_spec_path = crate_out_dir.join(format!("{}.json", target)); - write_target_spec(target_spec_path, target).ok(); + write_target_spec( + target_spec_path, + target, + TargetRenderOptions { + backtrace: backtrace_enabled, + }, + ) + .ok(); Some(crate_out_dir.clone()) } else { None @@ -156,6 +182,13 @@ pub fn build_binary( .map(|s| s.split('\x1f').map(|s| s.to_string()).collect()) .unwrap_or_default(); + // In unwind-table-based backtraces, we need DWARF CFI tables even with `panic=abort`. + // This forces `.eh_frame` emission for Rust code when backtraces are enabled. + if args.mode == StdMode::Std && backtrace_enabled { + rustflags_parts.push("-C".to_string()); + rustflags_parts.push("force-unwind-tables=yes".to_string()); + } + for arg in &link_args { rustflags_parts.push("-C".to_string()); rustflags_parts.push(format!("link-arg={}", arg)); @@ -210,13 +243,17 @@ pub fn build_binary( fn write_target_spec( target_spec_path: impl AsRef, target: &str, + render_opts: TargetRenderOptions, ) -> Result<(), anyhow::Error> { let path = target_spec_path.as_ref(); debug!("Writing target spec to: {}", path.display()); - let target_spec_json = crate::cmds::generate_target_spec(&GenerateTargetArgs { - profile: Some(target.to_string()), - ..Default::default() - }) + let target_spec_json = crate::cmds::generate_target_spec( + &GenerateTargetArgs { + profile: Some(target.to_string()), + ..Default::default() + }, + render_opts, + ) .map_err(|e| anyhow::anyhow!("Failed to generate target spec: {}", e)) .unwrap(); fs::write(path, target_spec_json)?; @@ -293,3 +330,16 @@ pub fn parse_address(s: &str) -> Result { use crate::cmds::GenerateTargetArgs; pub use crate::project::find_workspace_root; + +fn should_enable_backtrace(args: &BuildArgs, profile: &str) -> bool { + match args.backtrace { + BacktraceMode::Enable => true, + BacktraceMode::Disable => false, + BacktraceMode::Auto => { + // Default split: + // - debug/dev profiles: on + // - release/other profiles: off + matches!(profile, "debug" | "dev") + } + } +} diff --git a/crates/zeroos-build/src/cmds/linker.rs b/crates/zeroos-build/src/cmds/linker.rs index 719fa05..30b0f2c 100644 --- a/crates/zeroos-build/src/cmds/linker.rs +++ b/crates/zeroos-build/src/cmds/linker.rs @@ -14,6 +14,9 @@ pub struct GenerateLinkerArgs { #[arg(long, default_value = "2Mi")] pub stack_size: String, + #[arg(long, default_value_t = true, action = clap::ArgAction::Set)] + pub backtrace: bool, + #[arg(long, default_value = "_start")] pub entry_point: String, } @@ -44,13 +47,15 @@ pub fn generate_linker_script(args: &GenerateLinkerArgs) -> Result, } -pub fn generate_target_spec(args: &GenerateTargetArgs) -> Result { +pub fn generate_target_spec( + args: &GenerateTargetArgs, + render_opts: TargetRenderOptions, +) -> Result { let (config, arch_spec, mut llvm_config) = if let Some(profile_name) = &args.profile { let profile = load_target_profile(profile_name).ok_or_else(|| { format!( @@ -89,7 +94,7 @@ pub fn generate_target_spec(args: &GenerateTargetArgs) -> Result llvm_config.data_layout = data_layout.clone(); } - let json_content = config.render(&arch_spec, &llvm_config); + let json_content = config.render(&arch_spec, &llvm_config, render_opts)?; Ok(json_content) } diff --git a/crates/zeroos-build/src/files/generic-linux.json.template b/crates/zeroos-build/src/files/generic-linux.json.template index 5ad18ec..627f6ba 100644 --- a/crates/zeroos-build/src/files/generic-linux.json.template +++ b/crates/zeroos-build/src/files/generic-linux.json.template @@ -1,20 +1,20 @@ { - "arch": "{ARCH}", - "cpu": "{CPU}", - "features": "{FEATURES}", - "llvm-target": "{LLVM_TARGET}", - "llvm-abiname": "{ABI}", - "data-layout": "{DATA_LAYOUT}", - "target-endian": "{ENDIAN}", - "target-pointer-width": "{POINTER_WIDTH}", - "os": "{OS}", - "env": "{ENV}", - "vendor": "{VENDOR}", - "abi": "{ABI}", + "arch": "{{ ARCH }}", + "cpu": "{{ CPU }}", + "features": "{{ FEATURES }}", + "llvm-target": "{{ LLVM_TARGET }}", + "llvm-abiname": "{{ ABI }}", + "data-layout": "{{ DATA_LAYOUT }}", + "target-endian": "{{ ENDIAN }}", + "target-pointer-width": "{{ POINTER_WIDTH }}", + "os": "{{ OS }}", + "env": "{{ ENV }}", + "vendor": "{{ VENDOR }}", + "abi": "{{ ABI }}", "target-family": [ "unix" ], - "max-atomic-width": {MAX_ATOMIC_WIDTH}, + "max-atomic-width": {{ MAX_ATOMIC_WIDTH }}, "singlethread": true, "has-thread-local": false, "disable-redzone": true, @@ -38,13 +38,13 @@ "supported-split-debuginfo": [ "off" ], - "eh-frame-header": false, + "eh-frame-header": {{ BACKTRACE }}, "metadata": { "description": "RISC-V 64-bit with minimal musl-like environment", "tier": 3, "host-tools": false, "std": true }, - "requires-uwtable": false, - "default-uwtable": false + "requires-uwtable": {{ BACKTRACE }}, + "default-uwtable": {{ BACKTRACE }} } \ No newline at end of file diff --git a/crates/zeroos-build/src/files/linker.ld.template b/crates/zeroos-build/src/files/linker.ld.template index 9e7cef3..a22de11 100644 --- a/crates/zeroos-build/src/files/linker.ld.template +++ b/crates/zeroos-build/src/files/linker.ld.template @@ -3,12 +3,12 @@ ENTRY(_start) MEMORY { - RAM (rwx) : ORIGIN = {MEMORY_ORIGIN}, LENGTH = {MEMORY_SIZE} + RAM (rwx) : ORIGIN = {{ MEMORY_ORIGIN }}, LENGTH = {{ MEMORY_SIZE }} } /* Reserve heap and stack sizes */ -__heap_size = {HEAP_SIZE}; -__stack_size = {STACK_SIZE}; +__heap_size = {{ HEAP_SIZE }}; +__stack_size = {{ STACK_SIZE }}; PHDRS { @@ -20,7 +20,7 @@ PHDRS SECTIONS { - . = {MEMORY_ORIGIN}; + . = {{ MEMORY_ORIGIN }}; PROVIDE_HIDDEN(__ehdr_start = .); .text : { @@ -34,6 +34,27 @@ SECTIONS *(.srodata .srodata.*) . = ALIGN(8); } > RAM : rodata + + {% if backtrace %} + .eh_frame_hdr : ALIGN(4) { + PROVIDE_HIDDEN(__eh_frame_hdr_start = .); + KEEP(*(.eh_frame_hdr)) + PROVIDE_HIDDEN(__eh_frame_hdr_end = .); + . = ALIGN(8); + } > RAM : rodata + + .eh_frame : ALIGN(8) { + PROVIDE_HIDDEN(__eh_frame_start = .); + KEEP(*(.eh_frame)) + KEEP(*(.eh_frame.*)) + /* GCC/libgcc expects .eh_frame to be terminated by a 0-length entry. + * Without this, __register_frame may walk past the end and fault. + */ + LONG(0) + PROVIDE_HIDDEN(__eh_frame_end = .); + . = ALIGN(8); + } > RAM : rodata + {% endif %} /* Constructor/destructor arrays (used by musl's __libc_start_init) * GNU convention: these come BEFORE .data section @@ -96,23 +117,28 @@ SECTIONS PROVIDE_HIDDEN(fromhost = . - 8); } > RAM : data - /* Heap section - must be PROGBITS not NOLOAD so Spike loads it */ - .heap : ALIGN(4096) { - PROVIDE(__heap_start = .); - . = . + __heap_size; - PROVIDE(__heap_end = .); - } > RAM : data - - /* Stack section - must be PROGBITS not NOLOAD so Spike loads it */ - .stack : ALIGN(16) { - PROVIDE(__stack_bottom = .); - . = . + __stack_size; - PROVIDE(__stack_top = .); - } > RAM : data + /* Heap/stack reservation without file bloat. + * + * In zkVM/unikernel settings, heap and stack are *reservations* in RAM, not initialized data. + * Encoding them as sections (even NOLOAD/NOBITS) advances the linker location counter and can + * force huge file offsets for non-alloc sections (e.g. .symtab), inflating the ELF on disk. + * + * Instead, compute heap/stack boundaries as linker symbols near the top of RAM, without + * emitting any heap/stack sections at all. + */ + /* Align down to 16 bytes: ALIGN(x - 15, 16) == floor(x/16)*16 */ + PROVIDE(__stack_top = ALIGN((ORIGIN(RAM) + LENGTH(RAM)) - 15, 16)); + PROVIDE(__stack_bottom = __stack_top - __stack_size); + + /* Align heap end to a page boundary below the stack, leaving a 1-page guard gap. */ + PROVIDE(__stack_guard_size = 4096); + PROVIDE(__heap_end = ALIGN((__stack_bottom - __stack_guard_size) - 4095, 4096)); + PROVIDE(__heap_start = ALIGN((__heap_end - __heap_size) - 4095, 4096)); + + /* Safety: ensure the reserved regions do not overlap the loaded image. */ + ASSERT(__heap_start >= __bss_end, "heap overlaps .bss/.data") + ASSERT(__heap_end <= __stack_bottom, "heap overlaps stack") + ASSERT(__heap_end + __stack_guard_size <= __stack_bottom, "heap/stack guard gap violated") - /DISCARD/ : { - *(.eh_frame) - *(.comment) - } } diff --git a/crates/zeroos-build/src/host/backtrace.rs b/crates/zeroos-build/src/host/backtrace.rs new file mode 100644 index 0000000..a448d9f --- /dev/null +++ b/crates/zeroos-build/src/host/backtrace.rs @@ -0,0 +1,113 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// Resolve an addr2line executable path. +/// +/// - Uses `explicit` if provided +/// - Else tries `riscv64-unknown-elf-addr2line`, then `llvm-addr2line` in PATH +pub fn resolve_addr2line(explicit: Option<&Path>) -> Option { + if let Some(p) = explicit { + return Some(p.to_path_buf()); + } + find_in_path("riscv64-unknown-elf-addr2line").or_else(|| find_in_path("llvm-addr2line")) +} + +fn find_in_path(bin: &str) -> Option { + let path = std::env::var_os("PATH")?; + for dir in std::env::split_paths(&path) { + let cand = dir.join(bin); + if cand.is_file() { + return Some(cand); + } + } + None +} + +/// Find an executable in PATH (simple `which`). +pub fn which(bin: &str) -> Option { + find_in_path(bin) +} + +/// Parse a Rust `stack backtrace:` frame line. +/// +/// Example line: +/// ` 19: 0x80019fa2 - ` +/// +/// Returns `(frame_no, hex_addr_without_0x)` for frames that are ``. +pub fn parse_backtrace_unknown_frame(line: &str) -> Option<(usize, String)> { + if !line.contains(" - ") { + return None; + } + let (left, _) = line.split_once(':')?; + let frame_no: usize = left.trim().parse().ok()?; + let hex_pos = line.find("0x")?; + let after = &line[hex_pos + 2..]; + let hex: String = after + .chars() + .take_while(|c| c.is_ascii_hexdigit()) + .collect(); + if hex.is_empty() { + return None; + } + Some((frame_no, hex)) +} + +pub fn parse_hex(s: &str) -> usize { + usize::from_str_radix(s, 16).unwrap_or(0) +} + +/// Symbolize a single PC using addr2line. +/// +/// Returns a single-line string like: +/// `my_func at /path/file.rs:123` +pub fn symbolize_addr(bin: &Path, addr2line: &Path, addr: &str) -> Option { + let output = Command::new(addr2line) + .args(["-e"]) + .arg(bin) + .args(["-f", "-C", "-p"]) + .arg(addr) + .output() + .ok()?; + + if !output.status.success() { + return None; + } + let s = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if s.is_empty() { + return None; + } + + // addr2line output can include an address prefix; keep the RHS if present. + let s = if let Some((_, rhs)) = s.split_once(": ") { + rhs.to_string() + } else { + s + }; + + // We prefer `fn at file:line`, but for early boot / assembly stubs we may only be able to + // recover a symbol name with unknown location (e.g. `foo at ??:?`). Keep the symbol name in + // that case instead of reporting ``. + let (func, loc) = s.split_once(" at ").unwrap_or((s.as_str(), "")); + let func = func.trim(); + if func.is_empty() || func == "??" { + return None; + } + if loc.contains("??:0") || loc.contains("??:?") || loc.is_empty() { + return Some(func.to_string()); + } + Some(format!("{func} at {loc}")) +} + +/// Best-effort symbolize an unknown backtrace PC, with a RISC-V `pc-4` fallback. +pub fn symbolize_pc_with_fallback( + bin: &Path, + addr2line: &Path, + addr_hex_no_0x: &str, +) -> Option { + let addr = format!("0x{}", addr_hex_no_0x); + symbolize_addr(bin, addr2line, &addr).or_else(|| { + let pc = parse_hex(addr_hex_no_0x); + let addr_m4 = format!("0x{:x}", pc.saturating_sub(4)); + symbolize_addr(bin, addr2line, &addr_m4) + }) +} diff --git a/crates/zeroos-build/src/host/mod.rs b/crates/zeroos-build/src/host/mod.rs new file mode 100644 index 0000000..b853436 --- /dev/null +++ b/crates/zeroos-build/src/host/mod.rs @@ -0,0 +1 @@ +pub mod backtrace; diff --git a/crates/zeroos-build/src/lib.rs b/crates/zeroos-build/src/lib.rs index 43592a9..2d25d3d 100644 --- a/crates/zeroos-build/src/lib.rs +++ b/crates/zeroos-build/src/lib.rs @@ -1,5 +1,6 @@ pub mod cmds; pub mod files; +pub mod host; pub mod linker; pub mod project; pub mod spec; diff --git a/crates/zeroos-build/src/linker.rs b/crates/zeroos-build/src/linker.rs index eaf0c88..dcb5e50 100644 --- a/crates/zeroos-build/src/linker.rs +++ b/crates/zeroos-build/src/linker.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use mini_template as ztpl; use std::fs; use std::path::Path; @@ -12,6 +13,8 @@ pub struct LinkerConfig { pub stack_size: usize, + pub backtrace: bool, + template: Option, } @@ -28,6 +31,7 @@ impl LinkerConfig { memory_size: DEFAULT_MEMORY_SIZE, heap_size: None, stack_size: DEFAULT_STACK_SIZE, + backtrace: false, template: None, } } @@ -53,6 +57,11 @@ impl LinkerConfig { self } + pub fn with_backtrace(mut self, backtrace: bool) -> Self { + self.backtrace = backtrace; + self + } + pub fn heap_size(&self) -> usize { self.heap_size .unwrap_or_else(|| self.memory_size.saturating_sub(self.stack_size)) @@ -76,12 +85,14 @@ impl LinkerConfig { .as_deref() .or(self.template.as_deref()) .unwrap_or(LINKER_SCRIPT_TEMPLATE); - - template - .replace("{MEMORY_ORIGIN}", &origin) - .replace("{MEMORY_SIZE}", &mem_size) - .replace("{HEAP_SIZE}", &heap_size) - .replace("{STACK_SIZE}", &stack_size) + let ctx = ztpl::Context::new() + .with_bool("backtrace", self.backtrace) + .with_str("MEMORY_ORIGIN", origin) + .with_str("MEMORY_SIZE", mem_size) + .with_str("HEAP_SIZE", heap_size) + .with_str("STACK_SIZE", stack_size); + + ztpl::render(template, &ctx).unwrap_or_else(|_| template.to_string()) } } diff --git a/crates/zeroos-build/src/main.rs b/crates/zeroos-build/src/main.rs index 280b8fb..6e25f7d 100644 --- a/crates/zeroos-build/src/main.rs +++ b/crates/zeroos-build/src/main.rs @@ -63,6 +63,9 @@ struct ZeroosGenerateTargetArgs { #[command(flatten)] base: zeroos_build::cmds::GenerateTargetArgs, + #[arg(long, default_value_t = true, action = clap::ArgAction::Set)] + backtrace: bool, + #[arg(long, short = 'o')] output: Option, } @@ -337,7 +340,7 @@ Environment variables: fn generate_target_command(cli_args: ZeroosGenerateTargetArgs) -> Result<()> { use zeroos_build::cmds::generate_target_spec; - use zeroos_build::spec::{load_target_profile, parse_target_triple}; + use zeroos_build::spec::{load_target_profile, parse_target_triple, TargetRenderOptions}; debug!("Generating target spec with args: {:?}", cli_args.base); @@ -354,8 +357,13 @@ fn generate_target_command(cli_args: ZeroosGenerateTargetArgs) -> Result<()> { return Err(anyhow::anyhow!("Either --profile or --target is required")); }; - let json_content = - generate_target_spec(&cli_args.base).map_err(|e| anyhow::anyhow!("{}", e))?; + let json_content = generate_target_spec( + &cli_args.base, + TargetRenderOptions { + backtrace: cli_args.backtrace, + }, + ) + .map_err(|e| anyhow::anyhow!("{}", e))?; let output_path = cli_args .output diff --git a/crates/zeroos-build/src/spec/mod.rs b/crates/zeroos-build/src/spec/mod.rs index 3e13268..e94696b 100644 --- a/crates/zeroos-build/src/spec/mod.rs +++ b/crates/zeroos-build/src/spec/mod.rs @@ -10,6 +10,6 @@ pub use profiles::{ list_profiles, load_target_profile, TargetProfile, PROFILE_RISCV64IMAC_ZERO_LINUX_MUSL, }; pub use target::TargetConfig; -pub use utils::parse_target_triple; +pub use utils::{parse_target_triple, TargetRenderOptions}; const GENERIC_LINUX_TEMPLATE: &str = include_str!("../files/generic-linux.json.template"); diff --git a/crates/zeroos-build/src/spec/utils.rs b/crates/zeroos-build/src/spec/utils.rs index 90f60ac..e0cbc36 100644 --- a/crates/zeroos-build/src/spec/utils.rs +++ b/crates/zeroos-build/src/spec/utils.rs @@ -2,6 +2,18 @@ use super::target::TargetConfig; use super::GENERIC_LINUX_TEMPLATE; use crate::spec::llvm::LLVMConfig; use crate::spec::ArchSpec; +use mini_template as ztpl; + +#[derive(Debug, Clone, Copy)] +pub struct TargetRenderOptions { + pub backtrace: bool, +} + +impl Default for TargetRenderOptions { + fn default() -> Self { + Self { backtrace: true } + } +} pub fn parse_target_triple(target: &str) -> Option { // Parse target triple: {arch}-{vendor}-{sys}[-{abi}] @@ -31,24 +43,30 @@ pub fn parse_target_triple(target: &str) -> Option { } impl TargetConfig { - pub fn render(&self, arch_spec: &ArchSpec, llvm_config: &LLVMConfig) -> String { + pub fn render( + &self, + arch_spec: &ArchSpec, + llvm_config: &LLVMConfig, + opts: TargetRenderOptions, + ) -> Result { let template = GENERIC_LINUX_TEMPLATE; - template - .replace("{ARCH}", arch_spec.arch) - .replace("{CPU}", arch_spec.cpu) - .replace("{FEATURES}", &llvm_config.features) - .replace("{LLVM_TARGET}", &llvm_config.llvm_target) - .replace("{ABI}", &llvm_config.abi) - .replace("{DATA_LAYOUT}", &llvm_config.data_layout) - .replace("{POINTER_WIDTH}", arch_spec.pointer_width) - .replace("{ENDIAN}", arch_spec.endian) - .replace("{OS}", &self.os) - .replace("{ENV}", &self.abi) - .replace("{VENDOR}", &self.vendor) - .replace( - "{MAX_ATOMIC_WIDTH}", - &arch_spec.max_atomic_width.to_string(), - ) + let ctx = ztpl::Context::new() + .with_str("ARCH", arch_spec.arch) + .with_str("CPU", arch_spec.cpu) + .with_str("FEATURES", &llvm_config.features) + .with_str("LLVM_TARGET", &llvm_config.llvm_target) + .with_str("ABI", &llvm_config.abi) + .with_str("DATA_LAYOUT", &llvm_config.data_layout) + .with_str("POINTER_WIDTH", arch_spec.pointer_width) + .with_str("ENDIAN", arch_spec.endian) + .with_str("OS", &self.os) + .with_str("ENV", &self.abi) + .with_str("VENDOR", &self.vendor) + .with_str("MAX_ATOMIC_WIDTH", arch_spec.max_atomic_width.to_string()) + // JSON booleans (rendered without quotes in template) + .with_str("BACKTRACE", if opts.backtrace { "true" } else { "false" }); + + ztpl::render(template, &ctx).map_err(|e| e.to_string()) } } diff --git a/crates/zeroos-libunwind/Cargo.toml b/crates/zeroos-libunwind/Cargo.toml deleted file mode 100644 index 9fb9bbd..0000000 --- a/crates/zeroos-libunwind/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "zeroos-libunwind" -version.workspace = true -edition.workspace = true -description = "Minimal unwind stubs for panic=abort environments" - -[lib] -name = "zeroos_libunwind" -path = "src/lib.rs" diff --git a/crates/zeroos-libunwind/src/lib.rs b/crates/zeroos-libunwind/src/lib.rs deleted file mode 100644 index d4e2ca3..0000000 --- a/crates/zeroos-libunwind/src/lib.rs +++ /dev/null @@ -1,76 +0,0 @@ -#![no_std] - -extern "C" { - fn platform_exit(code: i32) -> !; -} - -#[inline(always)] -fn unwind_abort() -> ! { - unsafe { platform_exit(101) } -} - -// `_Unwind_Reason_Code` values (GCC/libunwind ABI). -const _URC_END_OF_STACK: i32 = 5; - -#[no_mangle] -pub extern "C" fn _Unwind_Resume(_exception: *mut u8) -> ! { - unwind_abort() -} - -#[no_mangle] -pub extern "C" fn _Unwind_Backtrace( - _trace_fn: extern "C" fn(*mut u8, *mut u8) -> i32, - _trace_argument: *mut u8, -) -> i32 { - _URC_END_OF_STACK -} - -#[no_mangle] -pub extern "C" fn _Unwind_GetIP(_context: *mut u8) -> usize { - unwind_abort() -} - -#[no_mangle] -pub extern "C" fn _Unwind_GetIPInfo(_context: *mut u8, _ip_before_insn: *mut i32) -> i32 { - unwind_abort() -} - -#[no_mangle] -pub extern "C" fn _Unwind_GetCFA(_context: *mut u8) -> usize { - unwind_abort() -} - -#[no_mangle] -pub extern "C" fn _Unwind_GetLanguageSpecificData(_context: *mut u8) -> *mut u8 { - unwind_abort() -} - -#[no_mangle] -pub extern "C" fn _Unwind_GetRegionStart(_context: *mut u8) -> usize { - unwind_abort() -} - -#[no_mangle] -pub extern "C" fn _Unwind_GetTextRelBase(_context: *mut u8) -> usize { - unwind_abort() -} - -#[no_mangle] -pub extern "C" fn _Unwind_GetDataRelBase(_context: *mut u8) -> usize { - unwind_abort() -} - -#[no_mangle] -pub extern "C" fn _Unwind_SetGR(_context: *mut u8, _index: i32, _value: usize) { - unwind_abort() -} - -#[no_mangle] -pub extern "C" fn _Unwind_SetIP(_context: *mut u8, _value: usize) { - unwind_abort() -} - -#[no_mangle] -pub extern "C" fn _Unwind_FindEnclosingFunction(_pc: *mut u8) -> *mut u8 { - core::ptr::null_mut() -} diff --git a/crates/zeroos-runtime-musl/Cargo.toml b/crates/zeroos-runtime-musl/Cargo.toml index 78a6053..534cdc0 100644 --- a/crates/zeroos-runtime-musl/Cargo.toml +++ b/crates/zeroos-runtime-musl/Cargo.toml @@ -17,3 +17,4 @@ foundation.workspace = true default = [] debug = ["debug/debug"] bounds-checks = [] +backtrace = [] diff --git a/crates/zeroos-runtime-musl/src/eh_frame_register.rs b/crates/zeroos-runtime-musl/src/eh_frame_register.rs new file mode 100644 index 0000000..94191f5 --- /dev/null +++ b/crates/zeroos-runtime-musl/src/eh_frame_register.rs @@ -0,0 +1,27 @@ +//! Register `.eh_frame` with libgcc's unwinder. +//! +//! We do this from `.init_array` so it runs after musl's `__init_libc` (malloc/env are ready) +//! but before `main`, avoiding early-boot allocations/faults. +#![cfg(feature = "backtrace")] + +extern "C" { + // libgcc frame registration API (DWARF2 unwinder) + fn __register_frame(begin: *const u8); + // Provided by the linker script (we KEEP .eh_frame and export these). + static __eh_frame_start: u8; + static __eh_frame_end: u8; +} + +#[no_mangle] +extern "C" fn __zeroos_register_eh_frame() { + let start = core::ptr::addr_of!(__eh_frame_start) as *const u8; + let end = core::ptr::addr_of!(__eh_frame_end) as *const u8; + if start != end { + unsafe { __register_frame(start) }; + } +} + +// Place a pointer to our init function in `.init_array` so musl calls it from `__libc_start_init`. +#[used] +#[link_section = ".init_array"] +static __ZEROOS_EH_FRAME_INIT: extern "C" fn() = __zeroos_register_eh_frame; diff --git a/crates/zeroos-runtime-musl/src/lib.rs b/crates/zeroos-runtime-musl/src/lib.rs index d79a269..5986586 100644 --- a/crates/zeroos-runtime-musl/src/lib.rs +++ b/crates/zeroos-runtime-musl/src/lib.rs @@ -1,5 +1,7 @@ #![no_std] +#[cfg(feature = "backtrace")] +mod eh_frame_register; mod lock_override; mod stack; diff --git a/crates/zeroos-runtime-musl/src/riscv64/bootstrap.rs b/crates/zeroos-runtime-musl/src/riscv64/bootstrap.rs index 2db9361..798e159 100644 --- a/crates/zeroos-runtime-musl/src/riscv64/bootstrap.rs +++ b/crates/zeroos-runtime-musl/src/riscv64/bootstrap.rs @@ -13,7 +13,6 @@ extern "C" { ldso_dummy: Option, ) -> i32; - static __ehdr_start: u8; } #[no_mangle] @@ -40,9 +39,8 @@ unsafe fn build_musl_in_buffer() -> usize { let buffer_ptr = core::ptr::addr_of_mut!(MUSL_BUILD_BUFFER) as *mut usize; let buffer_bottom = buffer_ptr as usize; let buffer_top = buffer_ptr.add(MUSL_BUFFER_SIZE) as usize; - let ehdr_start = core::ptr::addr_of!(__ehdr_start) as usize; - let size = build_musl_stack(buffer_top, buffer_bottom, ehdr_start, PROGRAM_NAME); + let size = build_musl_stack(buffer_top, buffer_bottom, PROGRAM_NAME); if size > MUSL_BUFFER_BYTES { panic!( diff --git a/crates/zeroos-runtime-musl/src/stack.rs b/crates/zeroos-runtime-musl/src/stack.rs index 078de39..cd5d274 100644 --- a/crates/zeroos-runtime-musl/src/stack.rs +++ b/crates/zeroos-runtime-musl/src/stack.rs @@ -71,6 +71,47 @@ impl DownwardStack { } } +impl DownwardStack { + /// Push raw bytes onto the stack, rounded up to `align` bytes. + /// Returns a pointer (address) to the start of the bytes. + #[inline(always)] + #[cfg(feature = "backtrace")] + fn push_bytes_aligned(&mut self, bytes: &[u8], align: usize) -> usize { + debug_assert!(align.is_power_of_two()); + let len = bytes.len(); + let rounded = (len + (align - 1)) & !(align - 1); + self.sp -= rounded; + + #[cfg(feature = "bounds-checks")] + { + if self.sp < self.buffer_bottom { + #[cfg(feature = "debug")] + debug::writeln!( + "Stack overflow! SP=0x{:x} below stack bottom=0x{:x}, top=0x{:x}", + self.sp, + self.buffer_bottom, + self.buffer_top + ); + + panic!( + "Stack overflow! SP=0x{:x} below stack bottom=0x{:x}, top=0x{:x}", + self.sp, self.buffer_bottom, self.buffer_top + ); + } + } + + unsafe { + core::ptr::copy_nonoverlapping(bytes.as_ptr(), self.sp as *mut u8, len); + // Zero any padding bytes so the stack contents are deterministic. + for i in len..rounded { + core::ptr::write((self.sp + i) as *mut u8, 0); + } + } + + self.sp + } +} + #[inline] fn generate_random_bytes(entropy: &[u64]) -> (u64, u64) { let mut state = 0x123456789abcdef0u64; @@ -95,19 +136,19 @@ fn generate_random_bytes(entropy: &[u64]) -> (u64, u64) { pub unsafe fn build_musl_stack( stack_top: usize, stack_bottom: usize, - _ehdr_start: usize, program_name: &'static [u8], ) -> usize { let mut ds = DownwardStack::::new(stack_top, stack_bottom); - // Zero-auxv approach: Tell musl that program headers are not available. - // This avoids expensive ELF parsing at runtime (critical for zkVM cycle efficiency). - // Musl will skip PT_TLS parsing and use builtin TLS, which is sufficient for - // single-threaded bare-metal execution. - let at_phdr = 0; - let at_phent = 0; - let at_phnum = 0; - let at_entry = 0; + // Optional environment variables for musl's `__libc_start_main`: + // it computes envp = argv + argc + 1. + #[cfg(feature = "backtrace")] + let rust_backtrace_ptr = + ds.push_bytes_aligned(b"RUST_BACKTRACE=full\0", core::mem::align_of::()); + + // In ZeroOS we run as a single static image with no dynamic loader; musl startup does not + // require AT_PHDR/AT_PHNUM/AT_PHENT/AT_ENTRY for correctness, so we set them to 0. + let (at_phdr, at_phent, at_phnum, at_entry) = (0usize, 0usize, 0usize, 0usize); // Prepare auxiliary vector entries let auxv_entries = [ @@ -156,8 +197,13 @@ pub unsafe fn build_musl_stack( ds.push(key); } + // envp terminator (always present) ds.push(0); + // envp[0] (optional) + #[cfg(feature = "backtrace")] + ds.push(rust_backtrace_ptr); + // argv terminator ds.push(0); ds.push(program_name.as_ptr() as usize); @@ -178,7 +224,7 @@ mod tests { let program_name = b"test\0"; unsafe { - let new_sp = build_musl_stack(stack_top, stack_top - 4096, 0, program_name); + let new_sp = build_musl_stack(stack_top, stack_top - 4096, program_name); assert_eq!(new_sp % 16, 0, "Stack pointer must be 16-byte aligned"); @@ -194,7 +240,7 @@ mod tests { let program_name = b"myprogram\0"; unsafe { - let new_sp = build_musl_stack(stack_top, stack_top - 4096, 0, program_name); + let new_sp = build_musl_stack(stack_top, stack_top - 4096, program_name); let argc_ptr = new_sp as *const usize; let argc = *argc_ptr; diff --git a/crates/zeroos/Cargo.toml b/crates/zeroos/Cargo.toml index a207454..a4e9506 100644 --- a/crates/zeroos/Cargo.toml +++ b/crates/zeroos/Cargo.toml @@ -33,7 +33,9 @@ os-linux = ["dep:os-linux", "foundation/trap"] runtime-nostd = ["dep:runtime-nostd"] runtime-musl = ["dep:runtime-musl"] runtime-gnu = ["dep:runtime-gnu"] -libunwind = ["dep:libunwind"] + +# Backtraces +backtrace = ["runtime-musl?/backtrace"] # Capabilities ## Memory @@ -64,7 +66,6 @@ zeroos-macros.workspace = true foundation = { workspace = true } arch-riscv = { workspace = true, optional = true } os-linux = { workspace = true, optional = true } -libunwind = { workspace = true, optional = true } runtime-nostd = { workspace = true, optional = true } runtime-musl = { workspace = true, optional = true } diff --git a/crates/zeroos/src/lib.rs b/crates/zeroos/src/lib.rs index b39f1a2..9cebe4d 100644 --- a/crates/zeroos/src/lib.rs +++ b/crates/zeroos/src/lib.rs @@ -25,9 +25,6 @@ pub extern crate runtime_musl; #[cfg(feature = "runtime-gnu")] pub extern crate runtime_gnu; -#[cfg(feature = "libunwind")] -extern crate libunwind; - #[cfg(feature = "memory")] pub use foundation::register_memory; diff --git a/examples/std-smoke/Cargo.toml b/examples/std-smoke/Cargo.toml index d2d7b91..93987e4 100644 --- a/examples/std-smoke/Cargo.toml +++ b/examples/std-smoke/Cargo.toml @@ -29,6 +29,7 @@ memory = ["platform/memory"] vfs = ["platform/vfs"] bounds-checks = ["platform/bounds-checks"] thread = ["platform/thread"] +backtrace = ["platform/backtrace"] [[bin]] name = "std-smoke" diff --git a/examples/syscall-cycles/Cargo.toml b/examples/syscall-cycles/Cargo.toml index 5b7670e..7cca0e3 100644 --- a/examples/syscall-cycles/Cargo.toml +++ b/examples/syscall-cycles/Cargo.toml @@ -15,7 +15,6 @@ std = [ # minimal std(musl) runtime support without enabling `platform/std` (which pulls in threads) "platform/os-linux", "platform/runtime-musl", - "platform/libunwind", "memory", "platform/vfs", "platform/vfs-device-console", diff --git a/matrix.yaml b/matrix.yaml index 1748178..fbc0862 100644 --- a/matrix.yaml +++ b/matrix.yaml @@ -42,6 +42,10 @@ entries: target: - *host_targets + - package: mini-template + target: + - *host_targets + - package: htif target: - *targets_none_elf_imac @@ -98,10 +102,6 @@ entries: target: - *targets_linux_musl_gc - - package: zeroos-libunwind - target: - - *targets_linux_musl_gc - - package: zeroos-allocator-bump target: - *guest_targets @@ -162,7 +162,6 @@ entries: - arch-riscv - os-linux - runtime-musl - - libunwind - [alloc-linked-list, alloc-buddy, alloc-bump] - vfs-device-console - vfs-device-null @@ -211,7 +210,6 @@ entries: - std - os-linux - runtime-musl - - libunwind - memory - vfs - vfs-device-console @@ -221,7 +219,7 @@ entries: target: riscv64imac-unknown-none-elf commands: build: >- - cargo spike build --package {package} --target "{target}" {features_flag} --quiet + cargo spike build --package {package} --target "{target}" -- {features_flag} --quiet features: - with-spike @@ -230,7 +228,7 @@ entries: - *targets_linux_musl_gc commands: build: >- - cargo spike build --package {package} --target "riscv64imac-zero-linux-musl" --mode std {features_flag} --quiet + cargo spike build --package {package} --target "riscv64imac-zero-linux-musl" --mode std -- {features_flag} --quiet features: - with-spike - std @@ -240,7 +238,7 @@ entries: - *targets_linux_musl_gc commands: build: >- - cargo spike build --package {package} --target "riscv64imac-zero-linux-musl" --mode std {features_flag} --quiet + cargo spike build --package {package} --target "riscv64imac-zero-linux-musl" --mode std -- {features_flag} --quiet features: - with-spike - std @@ -250,4 +248,4 @@ entries: - *targets_linux_musl_gc commands: build: >- - cargo spike build --package {package} --target "riscv64imac-zero-linux-musl" --mode std {features_flag} --quiet + cargo spike build --package {package} --target "riscv64imac-zero-linux-musl" --mode std -- {features_flag} --quiet diff --git a/platforms/platform/Cargo.toml b/platforms/platform/Cargo.toml index a4af68a..2822622 100644 --- a/platforms/platform/Cargo.toml +++ b/platforms/platform/Cargo.toml @@ -22,7 +22,7 @@ bounds-checks = ["spike-platform?/bounds-checks"] std = ["spike-platform?/std"] os-linux = ["spike-platform?/os-linux"] runtime-musl = ["spike-platform?/runtime-musl"] -libunwind = ["spike-platform?/libunwind"] +backtrace = ["spike-platform?/backtrace"] vfs = ["spike-platform?/vfs"] vfs-device-console = ["spike-platform?/vfs-device-console"] diff --git a/platforms/spike-build/src/cmds/build.rs b/platforms/spike-build/src/cmds/build.rs new file mode 100644 index 0000000..615cccb --- /dev/null +++ b/platforms/spike-build/src/cmds/build.rs @@ -0,0 +1,287 @@ +use anyhow::{Context, Result}; +use clap::Args; +use log::debug; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use build::cmds::build::{TARGET_NO_STD, TARGET_STD}; +use build::cmds::{BuildArgs, StdMode}; + +#[derive(Args, Debug)] +pub struct SpikeBuildArgs { + #[command(flatten)] + pub base: BuildArgs, + + /// Emit the fully-rendered linker script used for this build. + /// + /// Supports placeholders: ``, ``, ``, ``. + #[arg( + long, + value_name = "PATH", + default_missing_value = "/linker.ld." + )] + pub emit_linker_script: Option, + + /// Overwrite the emitted linker script if it already exists. + #[arg(long)] + pub force: bool, +} + +pub fn build_command(args: SpikeBuildArgs) -> Result<()> { + debug!("build_command: {:?}", args); + + let workspace_root = build::cmds::find_workspace_root()?; + debug!("workspace_root: {}", workspace_root.display()); + + let linker_tpl_path = find_spike_platform_linker_template(&workspace_root)?; + let linker_tpl = std::fs::read_to_string(&linker_tpl_path).with_context(|| { + format!( + "Failed to read spike-platform linker template: {}", + linker_tpl_path.display() + ) + })?; + + let fully = args.base.mode == StdMode::Std || args.base.fully; + + let toolchain_paths = if args.base.mode == StdMode::Std || fully { + let tc_cfg = build::toolchain::ToolchainConfig::default(); + let install_cfg = build::toolchain::InstallConfig::default(); + let paths = match build::toolchain::get_or_install_toolchain( + args.base.musl_lib_path.clone(), + args.base.gcc_lib_path.clone(), + &tc_cfg, + &install_cfg, + ) { + Ok(p) => (p.musl_lib, p.gcc_lib), + Err(e) => { + eprintln!("Toolchain install failed: {}", e); + eprintln!("Falling back to building toolchain from source..."); + build::cmds::get_or_build_toolchain( + args.base.musl_lib_path.clone(), + args.base.gcc_lib_path.clone(), + fully, + )? + } + }; + Some(paths) + } else { + None + }; + + build::cmds::build_binary( + &workspace_root, + &args.base, + toolchain_paths, + Some(linker_tpl), + )?; + + if let Some(out_tpl) = &args.emit_linker_script { + emit_linker_script(&workspace_root, &args.base, out_tpl, args.force)?; + } + + Ok(()) +} + +fn emit_linker_script( + workspace_root: &Path, + base: &BuildArgs, + out_tpl: &str, + force: bool, +) -> Result<()> { + let target = base.target.as_deref().unwrap_or(match base.mode { + StdMode::Std => TARGET_STD, + StdMode::NoStd => TARGET_NO_STD, + }); + let profile = build::project::detect_profile(&base.cargo_args); + + let target_dir = build::project::get_target_directory(&workspace_root.to_path_buf())?; + let generated_linker = target_dir + .join(target) + .join(&profile) + .join("zeroos") + .join(&base.package) + .join("linker.ld"); + + if !generated_linker.exists() { + anyhow::bail!( + "Generated linker script not found (expected {}); was the build successful?", + generated_linker.display() + ); + } + + let out_path_str = expand_emit_path( + out_tpl, + workspace_root, + &resolve_package_dir(workspace_root, &base.package)?, + target, + &profile, + &base.package, + ); + let out_path_raw = PathBuf::from(out_path_str); + let out_path = out_path_raw; + + if out_path.exists() && !force { + anyhow::bail!( + "Refusing to overwrite existing linker script: {} (use --force)", + out_path.display() + ); + } + if let Some(parent) = out_path.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("Failed to create {}", parent.display()))?; + } + + std::fs::copy(&generated_linker, &out_path).with_context(|| { + format!( + "Failed to copy linker script from {} to {}", + generated_linker.display(), + out_path.display() + ) + })?; + + Ok(()) +} + +fn expand_emit_path( + template: &str, + workspace: &Path, + package_dir: &Path, + target: &str, + profile: &str, + _package: &str, +) -> String { + template + .replace("", &workspace.display().to_string()) + .replace("", &package_dir.display().to_string()) + .replace("", target) + .replace("", profile) +} + +fn resolve_package_dir(workspace_root: &Path, package_name: &str) -> Result { + let output = Command::new("cargo") + .args(["metadata", "--format-version", "1", "--no-deps"]) + .arg("--manifest-path") + .arg(workspace_root.join("Cargo.toml")) + .output() + .context("Failed to run `cargo metadata`")?; + + if !output.status.success() { + anyhow::bail!( + "`cargo metadata` failed:\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + + let v: serde_json::Value = + serde_json::from_slice(&output.stdout).context("Failed to parse cargo metadata JSON")?; + + let packages = v + .get("packages") + .and_then(|p| p.as_array()) + .ok_or_else(|| anyhow::anyhow!("cargo metadata JSON: missing `packages` array"))?; + + for pkg in packages { + if pkg.get("name").and_then(|n| n.as_str()) != Some(package_name) { + continue; + } + let manifest = pkg + .get("manifest_path") + .and_then(|m| m.as_str()) + .ok_or_else(|| anyhow::anyhow!("cargo metadata JSON: package missing manifest_path"))?; + let manifest_dir = Path::new(manifest) + .parent() + .ok_or_else(|| anyhow::anyhow!("Invalid manifest_path for {}", package_name))?; + return Ok(manifest_dir.to_path_buf()); + } + + anyhow::bail!("Package not found in cargo metadata: {}", package_name) +} + +fn find_file_named( + root: &std::path::Path, + file_name: &str, + max_depth: usize, +) -> Result> { + if max_depth == 0 { + return Ok(None); + } + + let mut entries: Vec<_> = std::fs::read_dir(root) + .with_context(|| format!("Failed to list directory: {}", root.display()))? + .collect::, _>>()?; + + entries.sort_by_key(|e| e.file_name()); + + for entry in entries { + let path = entry.path(); + if path.is_file() && path.file_name().and_then(|n| n.to_str()) == Some(file_name) { + return Ok(Some(path)); + } + if path.is_dir() { + if let Some(found) = find_file_named(&path, file_name, max_depth - 1)? { + return Ok(Some(found)); + } + } + } + + Ok(None) +} + +fn find_spike_platform_linker_template(workspace_root: &std::path::Path) -> Result { + let output = Command::new("cargo") + .args(["metadata", "--format-version", "1", "--no-deps"]) + .arg("--manifest-path") + .arg(workspace_root.join("Cargo.toml")) + .output() + .context("Failed to run `cargo metadata`")?; + + if !output.status.success() { + anyhow::bail!( + "`cargo metadata` failed:\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + + let v: serde_json::Value = + serde_json::from_slice(&output.stdout).context("Failed to parse cargo metadata JSON")?; + + let packages = v + .get("packages") + .and_then(|p| p.as_array()) + .ok_or_else(|| anyhow::anyhow!("cargo metadata JSON: missing `packages` array"))?; + + for pkg in packages { + if pkg.get("name").and_then(|n| n.as_str()) != Some("spike-platform") { + continue; + } + + let manifest = pkg + .get("manifest_path") + .and_then(|m| m.as_str()) + .ok_or_else(|| { + anyhow::anyhow!("cargo metadata JSON: spike-platform missing manifest_path") + })?; + + let manifest_dir = std::path::Path::new(manifest) + .parent() + .ok_or_else(|| anyhow::anyhow!("Invalid manifest_path for spike-platform"))?; + + let src_tpl = manifest_dir.join("src/linker.ld.template"); + if src_tpl.exists() { + return Ok(src_tpl); + } + + if let Some(found) = find_file_named(manifest_dir, "linker.ld.template", 3)? { + return Ok(found); + } + + anyhow::bail!( + "spike-platform linker template not found under {} (expected `src/linker.ld.template`)", + manifest_dir.display() + ); + } + + anyhow::bail!("spike-platform package not found in `cargo metadata` output") +} diff --git a/platforms/spike-build/src/cmds/generate.rs b/platforms/spike-build/src/cmds/generate.rs new file mode 100644 index 0000000..8b90476 --- /dev/null +++ b/platforms/spike-build/src/cmds/generate.rs @@ -0,0 +1,90 @@ +use anyhow::{Context, Result}; +use clap::{Args, Subcommand}; +use log::info; +use std::fs; +use std::path::PathBuf; + +#[derive(Subcommand, Debug)] +pub enum GenerateCmd { + Target(SpikeGenerateTargetArgs), + Linker(SpikeGenerateLinkerArgs), +} + +#[derive(Args, Debug)] +pub struct SpikeGenerateTargetArgs { + #[command(flatten)] + pub base: build::cmds::GenerateTargetArgs, + + #[arg(long, short = 'o')] + pub output: Option, +} + +#[derive(Args, Debug)] +pub struct SpikeGenerateLinkerArgs { + #[command(flatten)] + pub base: build::cmds::GenerateLinkerArgs, + + #[arg(long, short = 'o', default_value = "linker.ld")] + pub output: PathBuf, +} + +pub fn generate_target_command(cli_args: SpikeGenerateTargetArgs) -> Result<()> { + use build::cmds::generate_target_spec; + use build::spec::{load_target_profile, parse_target_triple}; + + let target_triple = if let Some(profile_name) = &cli_args.base.profile { + load_target_profile(profile_name) + .ok_or_else(|| anyhow::anyhow!("Unknown profile: {}", profile_name))? + .config + .target_triple() + } else if let Some(target) = &cli_args.base.target { + parse_target_triple(target) + .ok_or_else(|| anyhow::anyhow!("Cannot parse target triple: {}", target))? + .target_triple() + } else { + anyhow::bail!("Either --profile or --target is required"); + }; + + let json_content = + generate_target_spec(&cli_args.base, build::spec::TargetRenderOptions::default()) + .map_err(|e| anyhow::anyhow!("{}", e))?; + + let output_path = cli_args + .output + .unwrap_or_else(|| PathBuf::from(format!("{}.json", target_triple))); + + if let Some(parent) = output_path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("Failed to create output directory: {}", parent.display()))?; + } + + fs::write(&output_path, &json_content) + .with_context(|| format!("Failed to write target spec to {}", output_path.display()))?; + + info!("Generated target spec: {}", output_path.display()); + info!("Target triple: {}", target_triple); + + Ok(()) +} + +pub fn generate_linker_command(cli_args: SpikeGenerateLinkerArgs) -> Result<()> { + use build::cmds::generate_linker_script; + + let result = generate_linker_script(&cli_args.base)?; + + if let Some(parent) = cli_args.output.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("Failed to create output directory: {}", parent.display()))?; + } + + fs::write(&cli_args.output, &result.script_content).with_context(|| { + format!( + "Failed to write linker script to {}", + cli_args.output.display() + ) + })?; + + info!("Generated linker script: {}", cli_args.output.display()); + + Ok(()) +} diff --git a/platforms/spike-build/src/cmds/mod.rs b/platforms/spike-build/src/cmds/mod.rs new file mode 100644 index 0000000..f498e18 --- /dev/null +++ b/platforms/spike-build/src/cmds/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod generate; +pub mod run; diff --git a/platforms/spike-build/src/cmds/run.rs b/platforms/spike-build/src/cmds/run.rs new file mode 100644 index 0000000..0d9bd27 --- /dev/null +++ b/platforms/spike-build/src/cmds/run.rs @@ -0,0 +1,193 @@ +use anyhow::{Context, Result}; +use clap::Args; +use log::debug; +use std::path::{Path, PathBuf}; +use std::process::{exit, Command, Stdio}; +use std::{io::BufRead, io::BufReader, io::Write}; + +use build::host::backtrace as sym; + +#[derive(Args, Debug)] +pub struct RunArgs { + #[arg(value_name = "BINARY")] + pub binary: PathBuf, + + /// Path to spike executable (defaults to `spike` in PATH; falls back to common install dirs) + #[arg(long, env = "SPIKE_PATH")] + pub spike: Option, + + #[arg(long, default_value = "RV64IMAC")] + pub isa: String, + + #[arg(long, short = 'n', default_value = "1000000")] + pub instructions: u64, + + /// Symbolize `stack backtrace:` frame addresses using addr2line on the host + #[arg(long, default_value_t = true)] + pub symbolize_backtrace: bool, + + /// Path to addr2line binary (defaults to `riscv64-unknown-elf-addr2line` if found) + #[arg(long, env = "RISCV_ADDR2LINE")] + pub addr2line: Option, + + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + pub spike_args: Vec, +} + +pub fn run_command(args: RunArgs) -> Result<()> { + if !args.binary.exists() { + anyhow::bail!("Binary not found: {}", args.binary.display()); + } + + debug!("Running binary: {}", args.binary.display()); + debug!("ISA: {}", args.isa); + debug!( + "Instructions: {}", + if args.instructions == 0 { + "unlimited".to_string() + } else { + args.instructions.to_string() + } + ); + + let spike_path = resolve_spike(args.spike.as_deref()) + .ok_or_else(|| anyhow::anyhow!("spike not found (set SPIKE_PATH or add it to PATH)"))?; + + let mut spike_cmd = Command::new(&spike_path); + spike_cmd.arg(format!("--isa={}", args.isa)); + + if args.instructions > 0 { + spike_cmd.arg(format!("--instructions={}", args.instructions)); + } + + spike_cmd.args(&args.spike_args); + spike_cmd.arg(&args.binary); + + let args_vec: Vec = spike_cmd + .get_args() + .map(|s| s.to_string_lossy().to_string()) + .collect(); + let spike_cmd_str = format!("{} {}", spike_path.display(), args_vec.join(" ")); + debug!("Spike command: {}", spike_cmd_str); + + // Stream spike output so we can optionally symbolize backtraces. + spike_cmd.stdout(Stdio::piped()); + spike_cmd.stderr(Stdio::inherit()); + + let mut child = spike_cmd + .spawn() + .context("Failed to execute spike (is it installed?)")?; + + let stdout = child + .stdout + .take() + .ok_or_else(|| anyhow::anyhow!("Failed to capture spike stdout"))?; + + let mut reader = BufReader::new(stdout); + let mut out = std::io::stdout().lock(); + + let addr2line = if args.symbolize_backtrace { + sym::resolve_addr2line(args.addr2line.as_deref()) + } else { + None + }; + + // Backtrace symbolization state: buffer contiguous frame lines and rewrite them. + let mut pending_frames: Vec<(usize, String)> = Vec::new(); // (frame_no, addr_hex) + let mut in_backtrace = false; + + let mut line = String::new(); + loop { + line.clear(); + let n = reader + .read_line(&mut line) + .context("Failed to read spike stdout")?; + if n == 0 { + break; + } + + if line.trim_end() == "stack backtrace:" { + in_backtrace = true; + pending_frames.clear(); + out.write_all(line.as_bytes()).ok(); + out.flush().ok(); + continue; + } + + if in_backtrace { + if let Some((frame_no, addr_hex)) = sym::parse_backtrace_unknown_frame(&line) { + pending_frames.push((frame_no, addr_hex)); + continue; + } + + if !pending_frames.is_empty() { + flush_symbolized_frames( + &mut out, + &args.binary, + addr2line.as_deref(), + &pending_frames, + ); + pending_frames.clear(); + } + in_backtrace = false; + } + + out.write_all(line.as_bytes()).ok(); + out.flush().ok(); + } + + if in_backtrace && !pending_frames.is_empty() { + flush_symbolized_frames( + &mut out, + &args.binary, + addr2line.as_deref(), + &pending_frames, + ); + } + + let status = child.wait().context("Failed to wait for spike process")?; + + if !status.success() { + exit(status.code().unwrap_or(1)); + } + + Ok(()) +} + +fn resolve_spike(explicit: Option<&Path>) -> Option { + if let Some(p) = explicit { + return Some(p.to_path_buf()); + } + // Prefer PATH. + if let Some(p) = sym::which("spike") { + return Some(p); + } + // Common install locations (keep this list aligned with team conventions). + let home = std::env::var_os("HOME").map(PathBuf::from); + let candidates: Vec = [ + home.as_ref().map(|h| h.join(".local/bin/spike")), + Some(PathBuf::from("/opt/riscv/bin/spike")), + ] + .into_iter() + .flatten() + .collect(); + + candidates.into_iter().find(|p| p.is_file()) +} + +fn flush_symbolized_frames( + out: &mut dyn Write, + bin: &Path, + addr2line: Option<&Path>, + frames: &[(usize, String)], +) { + for (frame_no, addr_hex) in frames { + let addr = format!("0x{}", addr_hex); + let sym_str = addr2line + .and_then(|a2l| sym::symbolize_pc_with_fallback(bin, a2l, addr_hex)) + .unwrap_or_else(|| "".to_string()); + + let _ = writeln!(out, "{:>4}: {:>18} - {}", frame_no, addr, sym_str); + } + let _ = out.flush(); +} diff --git a/platforms/spike-build/src/main.rs b/platforms/spike-build/src/main.rs index bcb3abb..567d197 100644 --- a/platforms/spike-build/src/main.rs +++ b/platforms/spike-build/src/main.rs @@ -1,99 +1,8 @@ -use anyhow::{Context, Result}; -use clap::Parser; -use log::{debug, info}; -use std::fs; -use std::path::PathBuf; -use std::process::{exit, Command}; - -use build::cmds::{BuildArgs, StdMode}; - -fn find_file_named( - root: &std::path::Path, - file_name: &str, - max_depth: usize, -) -> Result> { - if max_depth == 0 { - return Ok(None); - } - - let mut entries: Vec<_> = std::fs::read_dir(root) - .with_context(|| format!("Failed to list directory: {}", root.display()))? - .collect::, _>>()?; - - entries.sort_by_key(|e| e.file_name()); - - for entry in entries { - let path = entry.path(); - if path.is_file() && path.file_name().and_then(|n| n.to_str()) == Some(file_name) { - return Ok(Some(path)); - } - if path.is_dir() { - if let Some(found) = find_file_named(&path, file_name, max_depth - 1)? { - return Ok(Some(found)); - } - } - } - - Ok(None) -} - -fn find_spike_platform_linker_template(workspace_root: &std::path::Path) -> Result { - let output = Command::new("cargo") - .args(["metadata", "--format-version", "1", "--no-deps"]) - .arg("--manifest-path") - .arg(workspace_root.join("Cargo.toml")) - .output() - .context("Failed to run `cargo metadata`")?; +mod cmds; - if !output.status.success() { - anyhow::bail!( - "`cargo metadata` failed:\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - } - - let v: serde_json::Value = - serde_json::from_slice(&output.stdout).context("Failed to parse cargo metadata JSON")?; - - let packages = v - .get("packages") - .and_then(|p| p.as_array()) - .ok_or_else(|| anyhow::anyhow!("cargo metadata JSON: missing `packages` array"))?; - - for pkg in packages { - if pkg.get("name").and_then(|n| n.as_str()) != Some("spike-platform") { - continue; - } - - let manifest = pkg - .get("manifest_path") - .and_then(|m| m.as_str()) - .ok_or_else(|| { - anyhow::anyhow!("cargo metadata JSON: spike-platform missing manifest_path") - })?; - - let manifest_dir = std::path::Path::new(manifest) - .parent() - .ok_or_else(|| anyhow::anyhow!("Invalid manifest_path for spike-platform"))?; - - let src_tpl = manifest_dir.join("src/linker.ld.template"); - if src_tpl.exists() { - return Ok(src_tpl); - } - - if let Some(found) = find_file_named(manifest_dir, "linker.ld.template", 3)? { - return Ok(found); - } - - anyhow::bail!( - "spike-platform linker template not found under {} (expected `src/linker.ld.template`)", - manifest_dir.display() - ); - } - - anyhow::bail!("spike-platform package not found in `cargo metadata` output") -} +use clap::Parser; +use log::debug; +use std::process::exit; #[derive(Parser)] #[command(name = "cargo-spike")] @@ -106,58 +15,10 @@ enum Cli { #[derive(clap::Subcommand, Debug)] enum SpikeCmd { - Build(SpikeBuildArgs), - - Run(RunArgs), - + Build(cmds::build::SpikeBuildArgs), + Run(cmds::run::RunArgs), #[command(subcommand)] - Generate(GenerateCmd), -} - -#[derive(clap::Subcommand, Debug)] -enum GenerateCmd { - Target(SpikeGenerateTargetArgs), - - Linker(SpikeGenerateLinkerArgs), -} - -#[derive(clap::Args, Debug)] -struct SpikeBuildArgs { - #[command(flatten)] - base: BuildArgs, -} - -#[derive(clap::Args, Debug)] -struct RunArgs { - #[arg(value_name = "BINARY")] - binary: PathBuf, - - #[arg(long, default_value = "RV64IMAC")] - isa: String, - - #[arg(long, short = 'n', default_value = "1000000")] - instructions: u64, - - #[arg(trailing_var_arg = true, allow_hyphen_values = true)] - pub spike_args: Vec, -} - -#[derive(clap::Args, Debug)] -struct SpikeGenerateTargetArgs { - #[command(flatten)] - base: build::cmds::GenerateTargetArgs, - - #[arg(long, short = 'o')] - output: Option, -} - -#[derive(clap::Args, Debug)] -struct SpikeGenerateLinkerArgs { - #[command(flatten)] - base: build::cmds::GenerateLinkerArgs, - - #[arg(long, short = 'o', default_value = "linker.ld")] - output: PathBuf, + Generate(cmds::generate::GenerateCmd), } fn main() { @@ -168,179 +29,22 @@ fn main() { debug!("cargo-spike starting"); - if let Err(e) = run() { - eprintln!("Error: {:#}", e); - exit(1); - } -} - -fn run() -> Result<()> { let Cli::Spike(cmd) = Cli::parse(); - - match cmd { - SpikeCmd::Build(args) => build_command(args), - SpikeCmd::Run(args) => run_command(args), + let result = match cmd { + SpikeCmd::Build(args) => cmds::build::build_command(args), + SpikeCmd::Run(args) => cmds::run::run_command(args), SpikeCmd::Generate(gen_cmd) => match gen_cmd { - GenerateCmd::Target(args) => generate_target_command(args), - GenerateCmd::Linker(args) => generate_linker_command(args), - }, - } -} - -fn build_command(args: SpikeBuildArgs) -> Result<()> { - debug!("build_command: {:?}", args); - - let workspace_root = build::cmds::find_workspace_root()?; - debug!("workspace_root: {}", workspace_root.display()); - - let linker_tpl_path = find_spike_platform_linker_template(&workspace_root)?; - let linker_tpl = std::fs::read_to_string(&linker_tpl_path).with_context(|| { - format!( - "Failed to read spike-platform linker template: {}", - linker_tpl_path.display() - ) - })?; - - let fully = args.base.mode == StdMode::Std || args.base.fully; - - let toolchain_paths = if args.base.mode == StdMode::Std || fully { - let tc_cfg = build::toolchain::ToolchainConfig::default(); - let install_cfg = build::toolchain::InstallConfig::default(); - let paths = match build::toolchain::get_or_install_toolchain( - args.base.musl_lib_path.clone(), - args.base.gcc_lib_path.clone(), - &tc_cfg, - &install_cfg, - ) { - Ok(p) => (p.musl_lib, p.gcc_lib), - Err(e) => { - eprintln!("Toolchain install failed: {}", e); - eprintln!("Falling back to building toolchain from source..."); - build::cmds::get_or_build_toolchain( - args.base.musl_lib_path.clone(), - args.base.gcc_lib_path.clone(), - fully, - )? + cmds::generate::GenerateCmd::Target(args) => { + cmds::generate::generate_target_command(args) } - }; - Some(paths) - } else { - None - }; - - build::cmds::build_binary( - &workspace_root, - &args.base, - toolchain_paths, - Some(linker_tpl), - )?; - - Ok(()) -} - -fn run_command(args: RunArgs) -> Result<()> { - if !args.binary.exists() { - anyhow::bail!("Binary not found: {}", args.binary.display()); - } - - debug!("Running binary: {}", args.binary.display()); - debug!("ISA: {}", args.isa); - debug!( - "Instructions: {}", - if args.instructions == 0 { - "unlimited".to_string() - } else { - args.instructions.to_string() - } - ); - - println!("Running on Spike simulator...\n"); - - let mut spike_cmd = Command::new("spike"); - spike_cmd.arg(format!("--isa={}", args.isa)); - - if args.instructions > 0 { - spike_cmd.arg(format!("--instructions={}", args.instructions)); - } - - spike_cmd.args(&args.spike_args); - - spike_cmd.arg(&args.binary); - - let args_vec: Vec = spike_cmd - .get_args() - .map(|s| s.to_string_lossy().to_string()) - .collect(); - let spike_cmd_str = format!("spike {}", args_vec.join(" ")); - debug!("Spike command: {}", spike_cmd_str); - - let status = spike_cmd - .status() - .context("Failed to execute spike (is it installed?)")?; - - if !status.success() { - exit(status.code().unwrap_or(1)); - } - - Ok(()) -} - -fn generate_target_command(cli_args: SpikeGenerateTargetArgs) -> Result<()> { - use build::cmds::generate_target_spec; - use build::spec::{load_target_profile, parse_target_triple}; - - let target_triple = if let Some(profile_name) = &cli_args.base.profile { - load_target_profile(profile_name) - .ok_or_else(|| anyhow::anyhow!("Unknown profile: {}", profile_name))? - .config - .target_triple() - } else if let Some(target) = &cli_args.base.target { - parse_target_triple(target) - .ok_or_else(|| anyhow::anyhow!("Cannot parse target triple: {}", target))? - .target_triple() - } else { - return Err(anyhow::anyhow!("Either --profile or --target is required")); + cmds::generate::GenerateCmd::Linker(args) => { + cmds::generate::generate_linker_command(args) + } + }, }; - let json_content = - generate_target_spec(&cli_args.base).map_err(|e| anyhow::anyhow!("{}", e))?; - - let output_path = cli_args - .output - .unwrap_or_else(|| PathBuf::from(format!("{}.json", target_triple))); - - if let Some(parent) = output_path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("Failed to create output directory: {}", parent.display()))?; - } - - fs::write(&output_path, &json_content) - .with_context(|| format!("Failed to write target spec to {}", output_path.display()))?; - - info!("Generated target spec: {}", output_path.display()); - info!("Target triple: {}", target_triple); - - Ok(()) -} - -fn generate_linker_command(cli_args: SpikeGenerateLinkerArgs) -> Result<()> { - use build::cmds::generate_linker_script; - - let result = generate_linker_script(&cli_args.base)?; - - if let Some(parent) = cli_args.output.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("Failed to create output directory: {}", parent.display()))?; + if let Err(e) = result { + eprintln!("Error: {:#}", e); + exit(1); } - - fs::write(&cli_args.output, &result.script_content).with_context(|| { - format!( - "Failed to write linker script to {}", - cli_args.output.display() - ) - })?; - - info!("Generated linker script: {}", cli_args.output.display()); - - Ok(()) } diff --git a/platforms/spike-platform/Cargo.toml b/platforms/spike-platform/Cargo.toml index 661dd01..9b5116d 100644 --- a/platforms/spike-platform/Cargo.toml +++ b/platforms/spike-platform/Cargo.toml @@ -26,7 +26,6 @@ std = [ "arch-riscv", "os-linux", "runtime-musl", - "libunwind", "vfs-device-console", "memory", @@ -40,7 +39,7 @@ bounds-checks = ["zeroos/bounds-checks"] arch-riscv = ["zeroos/arch-riscv"] os-linux = ["zeroos/os-linux"] runtime-musl = ["zeroos/runtime-musl"] -libunwind = ["zeroos/libunwind"] +backtrace = ["runtime-musl", "zeroos/backtrace"] memory = ["zeroos/alloc-linked-list"] vfs = ["zeroos/vfs"] diff --git a/platforms/spike-platform/src/linker.ld.template b/platforms/spike-platform/src/linker.ld.template index 9e7cef3..e259eeb 100644 --- a/platforms/spike-platform/src/linker.ld.template +++ b/platforms/spike-platform/src/linker.ld.template @@ -3,12 +3,12 @@ ENTRY(_start) MEMORY { - RAM (rwx) : ORIGIN = {MEMORY_ORIGIN}, LENGTH = {MEMORY_SIZE} + RAM (rwx) : ORIGIN = {{ MEMORY_ORIGIN }}, LENGTH = {{ MEMORY_SIZE }} } /* Reserve heap and stack sizes */ -__heap_size = {HEAP_SIZE}; -__stack_size = {STACK_SIZE}; +__heap_size = {{ HEAP_SIZE }}; +__stack_size = {{ STACK_SIZE }}; PHDRS { @@ -20,7 +20,7 @@ PHDRS SECTIONS { - . = {MEMORY_ORIGIN}; + . = {{ MEMORY_ORIGIN }}; PROVIDE_HIDDEN(__ehdr_start = .); .text : { @@ -34,6 +34,27 @@ SECTIONS *(.srodata .srodata.*) . = ALIGN(8); } > RAM : rodata + + {% if backtrace %} + .eh_frame_hdr : ALIGN(4) { + PROVIDE_HIDDEN(__eh_frame_hdr_start = .); + KEEP(*(.eh_frame_hdr)) + PROVIDE_HIDDEN(__eh_frame_hdr_end = .); + . = ALIGN(8); + } > RAM : rodata + + .eh_frame : ALIGN(8) { + PROVIDE_HIDDEN(__eh_frame_start = .); + KEEP(*(.eh_frame)) + KEEP(*(.eh_frame.*)) + /* GCC/libgcc expects .eh_frame to be terminated by a 0-length entry. + * Without this, __register_frame may walk past the end and fault. + */ + LONG(0) + PROVIDE_HIDDEN(__eh_frame_end = .); + . = ALIGN(8); + } > RAM : rodata + {% endif %} /* Constructor/destructor arrays (used by musl's __libc_start_init) * GNU convention: these come BEFORE .data section @@ -96,23 +117,27 @@ SECTIONS PROVIDE_HIDDEN(fromhost = . - 8); } > RAM : data - /* Heap section - must be PROGBITS not NOLOAD so Spike loads it */ - .heap : ALIGN(4096) { - PROVIDE(__heap_start = .); - . = . + __heap_size; - PROVIDE(__heap_end = .); - } > RAM : data - - /* Stack section - must be PROGBITS not NOLOAD so Spike loads it */ - .stack : ALIGN(16) { - PROVIDE(__stack_bottom = .); - . = . + __stack_size; - PROVIDE(__stack_top = .); - } > RAM : data - - /DISCARD/ : { - *(.eh_frame) - *(.comment) - } + /* Heap/stack reservation without file bloat. + * + * In zkVM/unikernel settings, heap and stack are *reservations* in RAM, not initialized data. + * Encoding them as sections (even NOLOAD/NOBITS) advances the linker location counter and can + * force huge file offsets for non-alloc sections (e.g. .symtab), inflating the ELF on disk. + * + * Instead, compute heap/stack boundaries as linker symbols near the top of RAM, without + * emitting any heap/stack sections at all. + */ + /* Align down to 16 bytes: ALIGN(x - 15, 16) == floor(x/16)*16 */ + PROVIDE(__stack_top = ALIGN((ORIGIN(RAM) + LENGTH(RAM)) - 15, 16)); + PROVIDE(__stack_bottom = __stack_top - __stack_size); + + /* Align heap end to a page boundary below the stack, leaving a 1-page guard gap. */ + PROVIDE(__stack_guard_size = 4096); + PROVIDE(__heap_end = ALIGN((__stack_bottom - __stack_guard_size) - 4095, 4096)); + PROVIDE(__heap_start = ALIGN((__heap_end - __heap_size) - 4095, 4096)); + + /* Safety: ensure the reserved regions do not overlap the loaded image. */ + ASSERT(__heap_start >= __bss_end, "heap overlaps .bss/.data") + ASSERT(__heap_end <= __stack_bottom, "heap overlaps stack") + ASSERT(__heap_end + __stack_guard_size <= __stack_bottom, "heap/stack guard gap violated") }