From f65971664dc3e56405dfb950731c8640566bac1f Mon Sep 17 00:00:00 2001 From: Zou Guangxian Date: Sun, 15 Feb 2026 08:58:48 +0000 Subject: [PATCH] feat: add backtrace infrastructure and binary analysis tooling Add comprehensive backtrace and binary size analysis capabilities: - **New crate: zeroos-backtrace** - Frame pointer and DWARF-based backtraces - **New crate: elf-report** - Binary size analysis and symbol mapping - **New command: analyze_backtrace** - Analyze backtrace output from binaries - **New build script: build-backtrace.sh** - Build binaries with backtrace support Improve xtask tooling: - Add `check_workspace` to enforce workspace consistency rules - Add `massage` to run cargo tasks on specified packages - Add `spike_syscall_instcount` to measure syscall instruction counts - Reorganize xtask commands under `cmds/` module Other improvements: - Update pre-commit hook for better error handling - Add AGENTS.md documentation - Update runtime panic handling to support backtraces - Improve build system and linker configuration --- .ghk/pre-commit | 164 +++++- AGENTS.md | 173 ++++++ CLAUDE.md | 1 + Cargo.lock | 106 ++++ Cargo.toml | 8 + build-backtrace.sh | 66 ++- build-std-smoke.sh | 2 +- crates/elf-report/Cargo.toml | 17 + crates/elf-report/src/analyze.rs | 224 ++++++++ crates/elf-report/src/lib.rs | 9 + crates/elf-report/src/main.rs | 87 +++ crates/elf-report/src/map.rs | 362 ++++++++++++ crates/elf-report/src/render.rs | 422 ++++++++++++++ crates/elf-report/src/symbol.rs | 145 +++++ crates/elf-report/src/types.rs | 44 ++ crates/zeroos-backtrace/Cargo.toml | 10 + crates/zeroos-backtrace/build.rs | 6 + crates/zeroos-backtrace/src/dwarf.rs | 81 +++ crates/zeroos-backtrace/src/frame_pointer.rs | 168 ++++++ crates/zeroos-backtrace/src/lib.rs | 77 +++ crates/zeroos-backtrace/src/noop.rs | 67 +++ crates/zeroos-build/src/cmds/build.rs | 71 ++- crates/zeroos-build/src/cmds/linker.rs | 2 +- .../src/files/generic-linux.json.template | 6 +- .../zeroos-build/src/files/linker.ld.template | 2 +- crates/zeroos-build/src/linker.rs | 11 +- crates/zeroos-build/src/main.rs | 2 +- crates/zeroos-build/src/spec/utils.rs | 16 +- crates/zeroos-macros/src/cfgs.rs | 122 ++++ crates/zeroos-macros/src/lib.rs | 3 + crates/zeroos-runtime-musl/Cargo.toml | 1 + crates/zeroos-runtime-musl/build.rs | 4 + crates/zeroos-runtime-musl/src/lib.rs | 26 +- crates/zeroos-runtime-musl/src/stack.rs | 26 +- crates/zeroos-runtime-nostd/Cargo.toml | 1 + crates/zeroos-runtime-nostd/src/lib.rs | 6 +- crates/zeroos-runtime-nostd/src/panic.rs | 12 +- crates/zeroos/Cargo.toml | 8 +- examples/backtrace/Cargo.toml | 2 - matrix.yaml | 3 +- platforms/spike-platform/src/lib.rs | 8 +- .../spike-platform/src/linker.ld.template | 2 +- release-plz.toml | 5 + xtask/Cargo.toml | 1 + xtask/src/{ => cmds}/act.rs | 0 xtask/src/cmds/analyze_backtrace.rs | 541 ++++++++++++++++++ xtask/src/{ => cmds}/check_workspace.rs | 0 xtask/src/{ => cmds}/massage.rs | 0 xtask/src/cmds/mod.rs | 7 + .../src/{ => cmds}/spike_syscall_instcount.rs | 0 xtask/src/main.rs | 25 +- 51 files changed, 3054 insertions(+), 98 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 crates/elf-report/Cargo.toml create mode 100644 crates/elf-report/src/analyze.rs create mode 100644 crates/elf-report/src/lib.rs create mode 100644 crates/elf-report/src/main.rs create mode 100644 crates/elf-report/src/map.rs create mode 100644 crates/elf-report/src/render.rs create mode 100644 crates/elf-report/src/symbol.rs create mode 100644 crates/elf-report/src/types.rs create mode 100644 crates/zeroos-backtrace/Cargo.toml create mode 100644 crates/zeroos-backtrace/build.rs create mode 100644 crates/zeroos-backtrace/src/dwarf.rs create mode 100644 crates/zeroos-backtrace/src/frame_pointer.rs create mode 100644 crates/zeroos-backtrace/src/lib.rs create mode 100644 crates/zeroos-backtrace/src/noop.rs create mode 100644 crates/zeroos-macros/src/cfgs.rs create mode 100644 crates/zeroos-runtime-musl/build.rs rename xtask/src/{ => cmds}/act.rs (100%) create mode 100644 xtask/src/cmds/analyze_backtrace.rs rename xtask/src/{ => cmds}/check_workspace.rs (100%) rename xtask/src/{ => cmds}/massage.rs (100%) create mode 100644 xtask/src/cmds/mod.rs rename xtask/src/{ => cmds}/spike_syscall_instcount.rs (100%) diff --git a/.ghk/pre-commit b/.ghk/pre-commit index ff96a2d..c11e73a 100755 --- a/.ghk/pre-commit +++ b/.ghk/pre-commit @@ -137,7 +137,16 @@ fn sh(cmd: ShCmd, opts: ShOptions) -> Result { cleanup: Option<&Path>, ) -> Result { opts.apply(&mut c); - let output = c.output()?; + let output = match c.output() { + Ok(o) => o, + Err(e) => { + if let Some(p) = cleanup { + let _ = std::fs::remove_file(p); + } + + return Err(format!("Failed to execute command: {}\nError: {}", desc, e).into()); + } + }; if let Some(p) = cleanup { let _ = std::fs::remove_file(p); } @@ -421,31 +430,107 @@ macro_rules! fmt_task { }; } +// Helper to extract packages from changed files +fn extract_matrix_packages(ctx: &Context, files: &[PathBuf]) -> Vec { + let interesting = filter!(files, ["*.rs", "Cargo.toml"]); + + let manifests: HashSet = interesting + .iter() + .filter_map(|f| find_upwards_bounded(f, &["Cargo.toml"], &ctx.root)) + .map(|m| if m.is_absolute() { m } else { ctx.root.join(m) }) + .collect(); + + let mut packages: Vec = manifests + .iter() + .filter_map(|m| package_name_from_manifest(m)) + .collect(); + packages.sort(); + packages.dedup(); + + packages + .into_iter() + .filter(|p| matrix_has_package(ctx, p)) + .collect() +} + // Rust tasks +define_task!( + RustFixTask, + name = "rust_fix", + depends_on = [], + run = |ctx: &Context, files: &[PathBuf]| { + let interesting = filter!(files, ["*.rs", "Cargo.toml"]); + let filtered = extract_matrix_packages(ctx, files); + + if filtered.is_empty() { + return Ok(interesting); + } + + let mut argv: Vec = vec![ + "run".into(), + "-q".into(), + "-p".into(), + "xtask".into(), + "--".into(), + "matrix".into(), + ]; + for p in &filtered { + argv.push("-p".into()); + argv.push(p.into()); + } + argv.push("--command".into()); + argv.push("fix".into()); + + let mut opts = pipe_opts().clone(); + opts.cwd = Some(ctx.root.clone()); + sh(ShCmd::Argv("cargo".into(), argv), opts)?; + + Ok(interesting) + } +); + define_task!( RustClippyTask, name = "rust_clippy", - depends_on = [], + depends_on = ["rust_fix"], run = |ctx: &Context, files: &[PathBuf]| { let interesting = filter!(files, ["*.rs", "Cargo.toml"]); + let filtered = extract_matrix_packages(ctx, files); - let manifests: HashSet = interesting - .iter() - .filter_map(|f| find_upwards_bounded(f, &["Cargo.toml"], &ctx.root)) - .map(|m| if m.is_absolute() { m } else { ctx.root.join(m) }) - .collect(); + if filtered.is_empty() { + return Ok(interesting); + } - let mut packages: Vec = manifests - .iter() - .filter_map(|m| package_name_from_manifest(m)) - .collect(); - packages.sort(); - packages.dedup(); + let mut argv: Vec = vec![ + "run".into(), + "-q".into(), + "-p".into(), + "xtask".into(), + "--".into(), + "matrix".into(), + ]; + for p in &filtered { + argv.push("-p".into()); + argv.push(p.into()); + } + argv.push("--command".into()); + argv.push("clippy".into()); - let filtered: Vec = packages - .into_iter() - .filter(|p| matrix_has_package(ctx, p)) - .collect(); + let mut opts = pipe_opts().clone(); + opts.cwd = Some(ctx.root.clone()); + sh(ShCmd::Argv("cargo".into(), argv), opts)?; + + Ok(interesting) + } +); + +define_task!( + RustCheckTask, + name = "rust_check", + depends_on = ["rust_clippy"], + run = |ctx: &Context, files: &[PathBuf]| { + let interesting = filter!(files, ["*.rs", "Cargo.toml"]); + let filtered = extract_matrix_packages(ctx, files); if filtered.is_empty() { return Ok(interesting); @@ -464,7 +549,35 @@ define_task!( argv.push(p.into()); } argv.push("--command".into()); - argv.push("fix".into()); + argv.push("check".into()); + + let mut opts = pipe_opts().clone(); + opts.cwd = Some(ctx.root.clone()); + sh(ShCmd::Argv("cargo".into(), argv), opts)?; + + Ok(interesting) + } +); + +define_task!( + RustCheckWorkspace, + name = "rust_check_workspace", + depends_on = [], + run = |ctx: &Context, files: &[PathBuf]| { + let interesting = filter!(files, ["*.rs", "Cargo.toml", "release-plz.toml"]); + + if interesting.is_empty() { + return Ok(interesting); + } + + let argv: Vec = vec![ + "run".into(), + "-q".into(), + "-p".into(), + "xtask".into(), + "--".into(), + "check-workspace".into(), + ]; let mut opts = pipe_opts().clone(); opts.cwd = Some(ctx.root.clone()); @@ -514,6 +627,18 @@ fn main() { }) .init(); + // Expand PATH to include common tool installation locations + if let (Ok(current_path), Ok(home)) = (env::var("PATH"), env::var("HOME")) { + let extra_paths = vec![ + format!("{}/.deno/bin", home), + format!("{}/.cargo/bin", home), + format!("{}/go/bin", home), + format!("{}/.local/bin", home), + ]; + let expanded_path = format!("{}:{}", extra_paths.join(":"), current_path); + env::set_var("PATH", expanded_path); + } + if let Err(e) = run() { log::error!("[pre-commit] error: {e}"); std::process::exit(1); @@ -583,7 +708,10 @@ fn run() -> Result<()> { }; let tasks: Vec> = vec![ + Box::new(RustCheckWorkspace::default()), + Box::new(RustFixTask::default()), Box::new(RustClippyTask::default()), + Box::new(RustCheckTask::default()), Box::new(RustFmtTask::default()), Box::new(ShellFmtTask::default()), Box::new(TomlFmtTask::default()), diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0a0510f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,173 @@ +# AGENTS + +## Scope + +- **Do** keep changes small, targeted, and easy to validate. +- **Do** prefer clear, deterministic behavior over cleverness. +- **Do not** introduce new build systems or heavy dependencies without a strong + reason. + +## Repo norms + +### Style and tone + +Documentation in ZeroOS favors: + +- Short sections with descriptive headings +- Bullet lists for requirements and tradeoffs +- Concrete commands that can be copied and run +- Minimal hype; explain what a feature does and why it exists + +Code favors: + +- Explicit error handling (fail-fast) +- Modularity and compile-time configuration +- Avoiding “magic” behavior and global side effects + +### Commits + +We follow **Conventional Commits** (see `docs/publish-workflow.md`). Use types +like `feat:`, `fix:`, `docs:`, `refactor:`, `test:`. + +**Never** add `Co-Authored-By` or similar AI attribution lines to commit +messages. + +## How to validate changes + +`cargo matrix` and `cargo massage` are workspace-provided cargo aliases (see +`.cargo/config.toml`), not built-in Cargo subcommands. + +Prefer validating in this order: + +1. **Format** + - `cargo matrix fix` + - `cargo matrix fmt` +2. **Lint / check** + - `cargo matrix clippy` + - `cargo matrix check` +3. **Tests** + - `cargo matrix test` + +For a quick “do the reasonable thing” pass: + +- `cargo massage` + +Note: `cargo matrix` and `cargo massage` are workspace aliases defined in +`.cargo/config.toml`. + +If you touch one crate only, it’s fine to run: + +- `cargo test -p ` (when that crate has host tests) +- `cargo matrix check -p ` / `cargo matrix clippy -p ` / + `cargo matrix test -p ` (when you want to match the curated matrix + targets/features) + +If validation fails, narrow the scope (single crate or target), fix, then re-run +the smallest relevant command. + +## Adding or modifying crates + +- Add new crates under `crates/`. +- Add them to the workspace in the root `Cargo.toml`. +- If you want the crate covered by `cargo matrix` (and CI), add it to + `matrix.yaml` with the right target(s) and feature sets. +- Prefer `workspace = true` dependencies where possible. +- If a crate is intended to be published, ensure it’s configured in + `release-plz.toml`. + +Notes: + +- You usually **do not** need to touch `matrix.yaml` for doc-only changes, + formatting-only changes, or changes isolated to a host tool that is already in + the matrix. +- You **should** update `matrix.yaml` when adding a new crate or when changing a + crate’s supported targets/features in a way that should be enforced by CI. + +## Editing guidelines for agents + +### Rust crate structure + +- Keep `main.rs` minimal: argument parsing / logging setup / calling into the + crate. +- If you expect a binary to grow multiple subcommands, it’s fine to start with a + `cli.rs` + `commands/*` layout early to keep `main.rs` as glue. +- Keep `lib.rs` as a small facade: `mod ...;` plus `pub use ...` for the + intended public API. +- Put real logic in focused modules (e.g. `types.rs`, `parse.rs`, `analyze.rs`, + `render.rs`). +- Prefer module-level feature/target gates where possible + (`#[cfg(...)] mod foo;`) to keep boundaries clear. +- Use `cfg_if` when conditional compilation would otherwise create + nested/duplicated `#[cfg]` attributes. +- In `no_std` crates, be strict about dependencies and allocation: keep + guest/runtime crates `no_std` unless there is a strong reason. + +### Architecture and dependencies + +ZeroOS is intentionally layered and mostly `#![no_std]`. Keep dependencies +pointing “downward” and avoid cycles. + +#### High-level layering + +From lowest-level to highest-level: + +- **Foundation**: `crates/zeroos-foundation` (core registries, shared + coordination) +- **Arch / OS / Runtime**: `crates/zeroos-arch-*`, `crates/zeroos-os-*`, + `crates/zeroos-runtime-*` +- **Subsystems**: allocators, VFS core, devices, scheduler, RNG + (`crates/zeroos-allocator-*`, `crates/zeroos-vfs-core`, + `crates/zeroos-device-*`, `crates/zeroos-scheduler-*`, `crates/zeroos-rng`) +- **Facade**: `crates/zeroos` (feature-gated wiring across the layers) +- **Platforms / Examples**: `platforms/*`, `examples/*` (integration glue and + demos) +- **Host tools**: `xtask/`, `crates/cargo-matrix`, `crates/elf-report`, + `platforms/spike-build` (these may use `std`) + +#### Dependency rules of thumb + +- `zeroos-foundation` should stay minimal and must not depend on higher layers + (no devices, no platform code). +- `crates/zeroos-*` guest/runtime crates should remain `no_std` unless there is + a strong reason. +- Devices should depend on VFS interfaces (`zeroos-vfs-core`) and/or foundation + traits — not on platforms/examples. +- Platforms and examples may depend on `zeroos` (facade) and selected features; + avoid pulling platform code into core crates. +- Host tools must not be depended on by guest crates. + +When adding a new crate, be explicit about which layer it lives in and which +crates it is allowed to depend on. + +### Be conservative + +- Avoid reformatting unrelated code. +- Preserve public APIs unless the task explicitly requires a breaking change. +- Keep the patch focused: fewer files, smaller diffs. + +### Be explicit about behavior + +When changing behavior, include one of: + +- a unit test +- a small smoke test command (pick the most relevant one), e.g.: + - `./build-fibonacci.sh` (no-std guest sanity) + - `./build-std-smoke.sh` (std/musl runtime sanity) + - `./build-c-smoke.sh` (C toolchain sanity) + - `./build-backtrace.sh` (backtrace capture + symbolization) +- a doc note in `docs/` if it affects developers/integrators + +### Determinism and security + +- Avoid introducing nondeterministic behavior (time, randomness, + environment-dependent output) unless it is explicitly plumbed as committed + input. +- Prefer memory-safe, bounds-checked parsing. + +### Unsafe Rust policy + +- Keep `unsafe` small, localized, and intentional—especially in guest/runtime + crates. +- Prefer safe abstractions with a narrow `unsafe` core. +- Every `unsafe` block should have a brief comment describing the safety + invariants (what must be true for it to be sound). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c78a8f5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +always include AGENTS.md. diff --git a/Cargo.lock b/Cargo.lock index 3341aa8..7dd1e8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -187,6 +193,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -311,6 +326,18 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elf-report" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "object", + "rustc-demangle", + "serde", + "serde_json", +] + [[package]] name = "embedded-hal" version = "1.0.0" @@ -371,6 +398,16 @@ dependencies = [ "zeroos-debug", ] +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -548,6 +585,16 @@ dependencies = [ name = "mini-template" version = "0.1.0" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -557,6 +604,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "flate2", + "memchr", + "ruzstd", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -735,6 +793,12 @@ dependencies = [ "embedded-hal", ] +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustix" version = "1.1.2" @@ -748,6 +812,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ruzstd" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad02996bfc73da3e301efe90b1837be9ed8f4a462b6ed410aa35d00381de89f" +dependencies = [ + "twox-hash", +] + [[package]] name = "ryu" version = "1.0.20" @@ -852,6 +925,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "smallvec" version = "1.15.1" @@ -902,6 +981,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "std-smoke" version = "0.1.0" @@ -1123,6 +1208,16 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + [[package]] name = "unicode-ident" version = "1.0.22" @@ -1287,6 +1382,7 @@ dependencies = [ "clap", "clap-cargo", "derive_builder", + "elf-report", "log", "serde", "serde_json", @@ -1375,6 +1471,14 @@ dependencies = [ "zeroos-macros", ] +[[package]] +name = "zeroos-backtrace" +version = "0.1.0" +dependencies = [ + "cfg-if", + "zeroos-macros", +] + [[package]] name = "zeroos-build" version = "0.1.0" @@ -1471,6 +1575,7 @@ version = "0.1.0" name = "zeroos-runtime-musl" version = "0.1.0" dependencies = [ + "zeroos-backtrace", "zeroos-debug", "zeroos-foundation", ] @@ -1480,6 +1585,7 @@ name = "zeroos-runtime-nostd" version = "0.1.0" dependencies = [ "cfg-if", + "zeroos-backtrace", "zeroos-foundation", ] diff --git a/Cargo.toml b/Cargo.toml index 114ba32..8dc9534 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,11 @@ members = [ "xtask", "crates/cargo-matrix", + "crates/elf-report", "crates/htif", "crates/mini-template", "crates/zeroos", + "crates/zeroos-backtrace", "crates/zeroos-foundation", "crates/zeroos-debug", "crates/zeroos-macros", @@ -43,8 +45,10 @@ description = "ZeroOS" [workspace.dependencies] # Internal workspace crates cargo-matrix = { path = "crates/cargo-matrix" } +elf-report = { path = "crates/elf-report" } mini-template = { path = "crates/mini-template" } zeroos = { path = "crates/zeroos" } +zeroos-backtrace = { path = "crates/zeroos-backtrace" } foundation = { path = "crates/zeroos-foundation", package = "zeroos-foundation" } debug = { path = "crates/zeroos-debug", package = "zeroos-debug" } zeroos-macros = { path = "crates/zeroos-macros" } @@ -91,6 +95,10 @@ clap = { version = "4.5", features = ["derive", "env"] } anyhow = "1.0" parse-size = "1.0" +# ELF inspection +object = "0.36" +rustc-demangle = "0.1" + # Serialization serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/build-backtrace.sh b/build-backtrace.sh index 48431c4..29f751c 100755 --- a/build-backtrace.sh +++ b/build-backtrace.sh @@ -22,9 +22,8 @@ TARGET_TRIPLE="riscv64imac-unknown-none-elf" OUT_DIR="${ROOT}/target/${TARGET_TRIPLE}/$([ "$PROFILE" = "dev" ] && echo debug || echo "$PROFILE")" BIN="${OUT_DIR}/backtrace" -# Enable frame pointers for accurate stack traces -export RUSTFLAGS="${RUSTFLAGS:-} -Cforce-frame-pointers=yes" -cargo spike build -p backtrace --target "${TARGET_TRIPLE}" -- --quiet --features=with-spike,backtrace --profile "${PROFILE}" +# Use frame pointer walking for no-std mode (lightweight) +cargo spike build -p backtrace --target "${TARGET_TRIPLE}" --backtrace=frame-pointers -- --quiet --features=with-spike --profile "${PROFILE}" echo "Running backtrace example (no-std) - expect panic ..." # The example intentionally panics, so we capture exit code but continue @@ -34,7 +33,25 @@ cargo spike run "${BIN}" --isa RV64IMAC --instructions 10000000 --symbolize-back # Verify panic message and backtrace appears grep -q "intentional panic for backtrace demo" "${OUT_NOSTD}" grep -q "stack backtrace:" "${OUT_NOSTD}" -echo "no-std backtrace test PASSED" +echo "no-std + frame-pointers test PASSED" +echo "" + +# no-std mode with backtrace off +echo "Building backtrace example in no-std mode (off) ..." +cargo spike build -p backtrace --target "${TARGET_TRIPLE}" --backtrace=off -- --quiet --features=with-spike --profile "${PROFILE}" + +echo "Running backtrace example (no-std + off) - expect panic without backtrace ..." +OUT_NOSTD_OFF="$(mktemp)" +trap 'rm -f "${OUT_NOSTD}" "${OUT_STD}" "${OUT_NOSTD_OFF}"' EXIT +cargo spike run "${BIN}" --isa RV64IMAC --instructions 10000000 2>&1 | tee "${OUT_NOSTD_OFF}" || true + +grep -q "intentional panic for backtrace demo" "${OUT_NOSTD_OFF}" +# Verify NO backtrace +if grep -q "stack backtrace:" "${OUT_NOSTD_OFF}"; then + echo "ERROR: backtrace appeared in off mode!" + exit 1 +fi +echo "no-std + off test PASSED (no backtrace as expected)" echo "" # std mode @@ -44,7 +61,7 @@ TARGET_TRIPLE="riscv64imac-zero-linux-musl" OUT_DIR="${ROOT}/target/${TARGET_TRIPLE}/$([ "$PROFILE" = "dev" ] && echo debug || echo "$PROFILE")" BIN="${OUT_DIR}/backtrace" -cargo spike build -p backtrace --target "${TARGET_TRIPLE}" --mode std --backtrace=enable -- --quiet --features=std,with-spike --profile "${PROFILE}" +cargo spike build -p backtrace --target "${TARGET_TRIPLE}" --mode std --backtrace=dwarf -- --quiet --features=std,with-spike --profile "${PROFILE}" echo "Running backtrace example (std) - expect panic ..." # The example intentionally panics, so we capture exit code but continue @@ -54,5 +71,44 @@ cargo spike run "${BIN}" --isa RV64IMAC --instructions 100000000 --symbolize-bac grep -q "intentional panic for backtrace demo" "${OUT_STD}" echo "std backtrace test PASSED" +echo "" +echo "" +echo "Building backtrace example in std mode (frame-pointers) ..." +cargo spike build -p backtrace --target "${TARGET_TRIPLE}" --mode std --backtrace=frame-pointers -- --quiet --features=std,with-spike --profile "${PROFILE}" + +echo "Running backtrace example (std + frame-pointers) - expect panic ..." +OUT_STD_FP="$(mktemp)" +trap 'rm -f "${OUT_NOSTD}" "${OUT_STD}" "${OUT_NOSTD_OFF}" "${OUT_STD_FP}"' EXIT +cargo spike run "${BIN}" --isa RV64IMAC --instructions 100000000 --symbolize-backtrace 2>&1 | tee "${OUT_STD_FP}" || true + +grep -q "intentional panic for backtrace demo" "${OUT_STD_FP}" +# Note: std + frame-pointers doesn't print backtrace with default panic hook +# (Rust's std::backtrace requires DWARF; would need custom panic hook for frame-pointers) +# Just verify binary builds and runs +grep -q "stack backtrace:" "${OUT_STD_FP}" +echo "std + frame-pointers backtrace test PASSED" + +echo "" +echo "Building backtrace example in std mode (off) ..." +cargo spike build -p backtrace --target "${TARGET_TRIPLE}" --mode std --backtrace=off -- --quiet --features=std,with-spike --profile "${PROFILE}" + +echo "Running backtrace example (std + off) - expect panic without backtrace ..." +OUT_STD_OFF="$(mktemp)" +trap 'rm -f "${OUT_NOSTD}" "${OUT_STD}" "${OUT_STD_FP}" "${OUT_STD_OFF}"' EXIT +cargo spike run "${BIN}" --isa RV64IMAC --instructions 100000000 --symbolize-backtrace 2>&1 | tee "${OUT_STD_OFF}" || true + +grep -q "intentional panic for backtrace demo" "${OUT_STD_OFF}" +# Verify NO backtrace appears +if grep -q "stack backtrace:" "${OUT_STD_OFF}"; then + echo "ERROR: backtrace appeared in off mode!" + exit 1 +fi +echo "std + off mode test PASSED (no backtrace as expected)" + echo "" echo "All backtrace tests completed successfully." +echo " - no-std + frame-pointers: ✓" +echo " - no-std + off: ✓" +echo " - std + dwarf: ✓" +echo " - std + frame-pointers: ✓" +echo " - std + off: ✓" diff --git a/build-std-smoke.sh b/build-std-smoke.sh index d5d80bb..182d23c 100755 --- a/build-std-smoke.sh +++ b/build-std-smoke.sh @@ -11,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 --backtrace=enable --memory-size=40MiB --stack-size=4MiB --heap-size=8MiB -- --features=std,backtrace,with-spike --profile "${PROFILE}" +cargo spike build -p std-smoke --target "${TARGET_TRIPLE}" --mode std --backtrace=dwarf --memory-size=40MiB --stack-size=4MiB --heap-size=8MiB -- --features=std,backtrace,with-spike --profile "${PROFILE}" echo "Running on Spike simulator..." OUT="$(mktemp)" diff --git a/crates/elf-report/Cargo.toml b/crates/elf-report/Cargo.toml new file mode 100644 index 0000000..248ba48 --- /dev/null +++ b/crates/elf-report/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "elf-report" +version.workspace = true +edition.workspace = true +license = "MIT OR Apache-2.0" +description = "Portable ELF inspection tool (sections + symbol size summaries)" + +[dependencies] +anyhow.workspace = true +clap.workspace = true +object.workspace = true +rustc-demangle.workspace = true +serde.workspace = true +serde_json.workspace = true + +[features] +default = [] diff --git a/crates/elf-report/src/analyze.rs b/crates/elf-report/src/analyze.rs new file mode 100644 index 0000000..ff70ac3 --- /dev/null +++ b/crates/elf-report/src/analyze.rs @@ -0,0 +1,224 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result}; +use object::{Object, ObjectSection, ObjectSymbol}; +use rustc_demangle::demangle; + +use crate::{ + map, + types::{FileReport, MapSymbol, SectionInfo, SymbolInfo}, +}; + +pub fn normalize_map_args(elfs: &[PathBuf], maps: &[PathBuf]) -> Result>> { + if maps.is_empty() { + return Ok(vec![None; elfs.len()]); + } + if maps.len() == 1 { + return Ok(vec![Some(maps[0].clone()); elfs.len()]); + } + if maps.len() == elfs.len() { + return Ok(maps.iter().cloned().map(Some).collect()); + } + anyhow::bail!( + "Invalid --map usage: got {} map(s) for {} ELF(s). Provide 0 maps, 1 map, or one map per ELF.", + maps.len(), + elfs.len() + ); +} + +pub fn analyze_paths( + paths: &[PathBuf], + maps: &[Option], + top: usize, +) -> Result> { + if paths.is_empty() { + anyhow::bail!("No input ELFs provided"); + } + if maps.len() != paths.len() { + anyhow::bail!("Internal error: map list must match ELF list"); + } + + let mut reports = Vec::with_capacity(paths.len()); + for (i, p) in paths.iter().enumerate() { + let map = maps.get(i).cloned().flatten(); + reports.push(analyze_path(p, map.as_deref(), top)?); + } + Ok(reports) +} + +pub fn analyze_path(path: &Path, map_path: Option<&Path>, top: usize) -> Result { + let bytes = fs::read(path).with_context(|| format!("reading {}", path.display()))?; + let file = + object::File::parse(&*bytes).with_context(|| format!("parsing ELF {}", path.display()))?; + + let file_kind = format!("{:?}", file.kind()); + let arch = format!("{:?}", file.architecture()); + + let mut sections: Vec = file + .sections() + .filter_map(|s| { + let name = s.name().ok()?.to_string(); + Some(SectionInfo { + name, + size: s.size(), + address: s.address(), + }) + }) + .collect(); + sections.sort_by_key(|s| std::cmp::Reverse(s.size)); + + let has_symtab_section = file.sections().any(|s| matches!(s.name(), Ok(".symtab"))); + let has_symbols = file.symbols().next().is_some(); + let mut is_stripped_guess = !(has_symtab_section && has_symbols); + + let mut notes = Vec::new(); + + // Optional map-based symbol attribution. + let map_symbols: Option> = if let Some(mp) = map_path { + match map::parse_map_symbols(mp) { + Ok(syms) => Some(syms), + Err(e) => { + notes.push(format!("Failed to parse map file {}: {e:#}", mp.display())); + None + } + } + } else { + None + }; + + // Prefer ELF symtab symbols when present; otherwise, use map symbols if provided. + let mut map_used = false; + let (top_text_symbols, top_rodata_symbols) = if !is_stripped_guess { + ( + top_symbols_in_section(&file, ".text", top), + top_symbols_in_section(&file, ".rodata", top), + ) + } else if let Some(map_syms) = &map_symbols { + let text = top_symbols_from_map(§ions, map_syms, ".text", top); + let rodata = top_symbols_from_map(§ions, map_syms, ".rodata", top); + if text.is_empty() && rodata.is_empty() { + notes.push( + "ELF appears stripped and map did not yield any symbols in .text/.rodata ranges. Ensure the map corresponds to this exact build and includes symbol addresses/sizes." + .to_string(), + ); + } else { + map_used = true; + is_stripped_guess = false; // for reporting purposes: we *do* have symbol names via map + } + (text, rodata) + } else { + notes.push( + "No usable .symtab symbols detected (binary likely stripped). Provide --map or build an unstripped analysis ELF to get symbol-level attribution." + .to_string(), + ); + (Vec::new(), Vec::new()) + }; + + Ok(FileReport { + path: path.display().to_string(), + file_kind, + arch, + is_stripped_guess, + map_used, + sections, + top_text_symbols, + top_rodata_symbols, + notes, + }) +} + +fn top_symbols_in_section( + file: &object::File<'_>, + section_name: &str, + top: usize, +) -> Vec { + let mut out = Vec::new(); + + for sym in file.symbols() { + let size = sym.size(); + if size == 0 { + continue; + } + + let Some(sec_idx) = sym.section_index() else { + continue; + }; + let Ok(sec) = file.section_by_index(sec_idx) else { + continue; + }; + let Ok(name) = sec.name() else { + continue; + }; + if name != section_name { + continue; + } + + let sym_name = sym.name().unwrap_or(""); + let demangled = demangle(sym_name).to_string(); + + out.push(SymbolInfo { + name: sym_name.to_string(), + demangled, + size, + address: sym.address(), + }); + } + + out.sort_by_key(|s| std::cmp::Reverse(s.size)); + out.truncate(top); + out +} + +fn section_range(sections: &[SectionInfo], name: &str) -> Option<(u64, u64)> { + sections + .iter() + .find(|s| s.name == name) + .map(|s| (s.address, s.address.saturating_add(s.size))) +} + +fn normalize_map_symbol_name(name: &str) -> &str { + // Some maps emit entries like `.text._ZN...` or `.rodata._ZN...`. + name.strip_prefix(".text.") + .or_else(|| name.strip_prefix(".rodata.")) + .unwrap_or(name) +} + +fn top_symbols_from_map( + sections: &[SectionInfo], + map_syms: &[MapSymbol], + section_name: &str, + top: usize, +) -> Vec { + let Some((start, end)) = section_range(sections, section_name) else { + return Vec::new(); + }; + + let mut out = Vec::new(); + for ms in map_syms { + if ms.address < start || ms.address >= end { + continue; + } + // Heuristic: only include plausible symbol names. + if ms.name.starts_with('*') || ms.name.is_empty() { + continue; + } + if map::is_plain_section_label_public(&ms.name) { + continue; + } + let raw = normalize_map_symbol_name(&ms.name); + let demangled = demangle(raw).to_string(); + out.push(SymbolInfo { + name: raw.to_string(), + demangled, + size: ms.size, + address: ms.address, + }); + } + + out.sort_by_key(|s| std::cmp::Reverse(s.size)); + out.truncate(top); + out +} diff --git a/crates/elf-report/src/lib.rs b/crates/elf-report/src/lib.rs new file mode 100644 index 0000000..fa8b610 --- /dev/null +++ b/crates/elf-report/src/lib.rs @@ -0,0 +1,9 @@ +mod analyze; +mod map; +mod render; +mod symbol; +mod types; + +pub use analyze::{analyze_path, analyze_paths, normalize_map_args}; +pub use render::{render_markdown, render_markdown_grouped}; +pub use types::{FileReport, SectionInfo, SymbolGroup, SymbolInfo}; diff --git a/crates/elf-report/src/main.rs b/crates/elf-report/src/main.rs new file mode 100644 index 0000000..46e68e5 --- /dev/null +++ b/crates/elf-report/src/main.rs @@ -0,0 +1,87 @@ +use std::{ + fs, + io::{self, Write}, + path::PathBuf, +}; + +use anyhow::{Context, Result}; +use clap::{Parser, ValueEnum}; + +#[derive(Clone, Copy, Debug, ValueEnum)] +enum OutputFormat { + Md, + Json, +} + +#[derive(Debug, Parser)] +#[command( + about = "Inspect ELF files: section sizes and (when available) largest symbols", + long_about = None +)] +struct Args { + /// One or more ELF paths to inspect. + #[arg(value_name = "ELF")] + paths: Vec, + + /// Optional linker map file(s) for symbol attribution when the ELF is stripped. + /// + /// If multiple ELFs are provided, you may pass either: + /// - one map (applied to all ELFs), or + /// - the same number of maps as ELFs (paired by position). + #[arg(long, value_name = "MAP")] + map: Vec, + + /// Output format. + #[arg(long, value_enum, default_value_t = OutputFormat::Md)] + format: OutputFormat, + + /// Write output to a file instead of stdout. + #[arg(long, value_name = "PATH")] + out: Option, + + /// Number of largest symbols to show per section. + #[arg(long, default_value_t = 50)] + top: usize, + + /// Group symbols by crate/module path depth. + /// + /// Examples: + /// -d 1: Group by crate (std, core, alloc) + /// -d 2: Group by module (std::fmt, core::iter) + /// Without -d: Show individual symbols + #[arg(short = 'd', long)] + depth: Option, +} + +fn main() -> Result<()> { + let args = Args::parse(); + if args.paths.is_empty() { + anyhow::bail!("No input ELFs provided"); + } + + let map_paths = elf_report::normalize_map_args(&args.paths, &args.map)?; + let reports = elf_report::analyze_paths(&args.paths, &map_paths, args.top)?; + + let out = match args.format { + OutputFormat::Json => serde_json::to_string_pretty(&reports)?, + OutputFormat::Md => { + if let Some(depth) = args.depth { + elf_report::render_markdown_grouped(&reports, depth) + } else { + elf_report::render_markdown(&reports) + } + } + }; + + match args.out { + Some(path) => { + fs::write(&path, out).with_context(|| format!("writing {}", path.display()))?; + } + None => { + let mut stdout = io::BufWriter::new(io::stdout().lock()); + stdout.write_all(out.as_bytes())?; + } + } + + Ok(()) +} diff --git a/crates/elf-report/src/map.rs b/crates/elf-report/src/map.rs new file mode 100644 index 0000000..c728ba6 --- /dev/null +++ b/crates/elf-report/src/map.rs @@ -0,0 +1,362 @@ +use std::{fs, path::Path}; + +use anyhow::{Context, Result}; + +use crate::types::MapSymbol; + +fn parse_hex_u64(s: &str) -> Option { + let s = s.trim(); + let s = s + .strip_prefix("0x") + .or_else(|| s.strip_prefix("0X")) + .unwrap_or(s); + u64::from_str_radix(s, 16).ok() +} + +fn looks_like_object_path(s: &str) -> bool { + let s = s.trim(); + if s.is_empty() { + return false; + } + if s.starts_with("*(") || s.starts_with('*') || s.starts_with('(') { + return false; + } + + // Common forms: + // - /abs/path/foo.o + // - relative/foo.o + // - libfoo.a(bar.o) + // - /lib/libc.so.6 + s.contains(".o") || s.contains(".a(") || s.contains(".so") +} + +fn is_plain_section_label(name: &str) -> bool { + // In GNU ld maps, output section headers appear as plain section names + // like `.text`, `.rodata`, `.data`, etc. + // We want to keep `.text.` / `.rodata.` but drop the plain labels. + if !name.starts_with('.') { + return false; + } + let rest = &name[1..]; + if rest.is_empty() { + return true; + } + !rest.contains('.') +} + +pub(crate) fn is_plain_section_label_public(name: &str) -> bool { + is_plain_section_label(name) +} + +/// Best-effort parser for GNU ld / lld map files. +/// +/// Strategy: +/// - Prefer parsing the GNU ld "Linker script and memory map" block structure, where symbol +/// lines often include only an address (no size). In that case, sizes are inferred by taking +/// the delta to the next symbol, and the last symbol is bounded by the containing input section. +/// - Also keep a permissive fallback for map formats that already provide (addr, size, name). +/// +/// Attribution is then done by intersecting address ranges with ELF section ranges. +pub(crate) fn parse_map_symbols(map_path: &Path) -> Result> { + let text = fs::read_to_string(map_path) + .with_context(|| format!("reading map {}", map_path.display()))?; + + Ok(parse_map_symbols_text(&text)) +} + +fn parse_map_symbols_text(text: &str) -> Vec { + #[derive(Debug)] + struct BlockSymbol { + address: u64, + explicit_size: Option, + name: String, + } + + #[derive(Debug)] + struct Block { + start: u64, + end: u64, + _section: String, + symbols: Vec, + } + + fn finish_block(block: Block, out: &mut Vec) { + if block.symbols.is_empty() { + return; + } + + let mut syms = block.symbols; + syms.sort_by_key(|s| s.address); + + for i in 0..syms.len() { + let cur = &syms[i]; + let next_addr = syms.get(i + 1).map(|s| s.address).unwrap_or(block.end); + + let mut size = cur + .explicit_size + .unwrap_or_else(|| next_addr.saturating_sub(cur.address)); + + // Clamp size to the containing input-section block. + if cur.address >= block.end { + continue; + } + let max_len = block.end.saturating_sub(cur.address); + if size > max_len { + size = max_len; + } + if size == 0 { + continue; + } + + out.push(MapSymbol { + address: cur.address, + size, + name: cur.name.clone(), + }); + } + } + + let mut out = Vec::new(); + let mut fallback = Vec::new(); + let mut cur_block: Option = None; + + for line in text.lines() { + let line = line.trim_end(); + if line.is_empty() { + continue; + } + + // Ignore common headers / boilerplate. + if line.starts_with("Merging program properties") + || line.starts_with("Updated property") + || line.starts_with("Removed property") + || line.starts_with("As-needed library") + || line.starts_with("Discarded input sections") + || line.starts_with("Memory Configuration") + || line.starts_with("Linker script and memory map") + || line.starts_with("LOAD ") + || line.starts_with("START GROUP") + || line.starts_with("END GROUP") + { + continue; + } + + let trimmed = line.trim_start(); + let tokens: Vec<&str> = trimmed.split_whitespace().collect(); + if tokens.is_empty() { + continue; + } + + // Detect input-section contribution blocks: + // .text 0xADDR 0xSIZE /path/to/file.o + // .rodata 0xADDR 0xSIZE libfoo.a(bar.o) + if tokens.len() >= 4 && is_plain_section_label(tokens[0]) { + if let (Some(addr), Some(sz)) = (parse_hex_u64(tokens[1]), parse_hex_u64(tokens[2])) { + let rest = tokens[3..].join(" "); + if sz > 0 && looks_like_object_path(&rest) { + if let Some(b) = cur_block.take() { + finish_block(b, &mut out); + } + cur_block = Some(Block { + start: addr, + end: addr.saturating_add(sz), + _section: tokens[0].to_string(), + symbols: Vec::new(), + }); + continue; + } + } + } + + // Output-section header lines like `.text 0x.. 0x..` are block boundaries. + if tokens.len() == 3 && is_plain_section_label(tokens[0]) { + if let (Some(_addr), Some(sz)) = (parse_hex_u64(tokens[1]), parse_hex_u64(tokens[2])) { + if sz > 0 { + if let Some(b) = cur_block.take() { + finish_block(b, &mut out); + } + continue; + } + } + } + + // If we're inside an input-section block, parse symbol lines. + // Common GNU ld form: + // 0xADDR symbol + // Sometimes includes a size: + // 0xADDR 0xSIZE symbol + if let Some(block) = cur_block.as_mut() { + if tokens.len() >= 2 { + if let Some(addr) = parse_hex_u64(tokens[0]) { + if addr < block.start || addr >= block.end { + continue; + } + + let mut explicit_size: Option = None; + let mut name: Option<&str> = None; + + if tokens.len() >= 3 { + if let Some(sz) = parse_hex_u64(tokens[1]) { + explicit_size = Some(sz); + name = tokens + .iter() + .skip(2) + .find(|t| parse_hex_u64(t).is_none()) + .copied(); + } + } + + if name.is_none() { + name = tokens + .iter() + .skip(1) + .find(|t| parse_hex_u64(t).is_none()) + .copied(); + } + + let Some(name) = name else { + continue; + }; + if name == "*fill*" || name.starts_with('*') || name == "PROVIDE" { + continue; + } + if looks_like_object_path(name) { + continue; + } + if name.starts_with('.') { + // Drop local labels like `.L...` which are generally not actionable. + continue; + } + + block.symbols.push(BlockSymbol { + address: addr, + explicit_size, + name: name.to_string(), + }); + + continue; + } + } + } + + // Fallback: extract any line that contains an address + size + name. + // Common shapes: + // 1) 0xADDR 0xSIZE + // 2) 0xADDR 0xSIZE + // 3) 0xADDR 0xSIZE + if tokens.len() >= 3 { + let mut name: Option<&str> = None; + let mut addr: Option = None; + let mut size: Option = None; + + // shape 1 + if let (Some(a), Some(sz)) = (parse_hex_u64(tokens[1]), parse_hex_u64(tokens[2])) { + name = Some(tokens[0]); + addr = Some(a); + size = Some(sz); + } + + // shape 2 + if name.is_none() { + if let (Some(a), Some(sz)) = (parse_hex_u64(tokens[0]), parse_hex_u64(tokens[1])) { + for t in tokens.iter().skip(2) { + if parse_hex_u64(t).is_none() { + name = Some(t); + addr = Some(a); + size = Some(sz); + break; + } + } + } + } + + let (Some(address), Some(size), Some(name)) = (addr, size, name) else { + continue; + }; + if size == 0 { + continue; + } + if name == "*fill*" || name.starts_with('*') || name == "PROVIDE" { + continue; + } + if looks_like_object_path(name) { + continue; + } + if is_plain_section_label(name) { + continue; + } + + fallback.push(MapSymbol { + address, + size, + name: name.to_string(), + }); + } + } + + if let Some(b) = cur_block.take() { + finish_block(b, &mut out); + } + + // Merge fallback symbols, preferring the larger size if duplicates exist. + out.extend(fallback); + out.retain(|s| !(s.name.is_empty() || s.name == "*fill*" || is_plain_section_label(&s.name))); + out.sort_by(|a, b| (a.address, &a.name).cmp(&(b.address, &b.name))); + out.dedup_by(|a, b| { + if a.address == b.address && a.name == b.name { + if b.size > a.size { + a.size = b.size; + } + true + } else { + false + } + }); + + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_gnu_ld_block_symbols_and_infers_sizes() { + let map = r#" +Linker script and memory map + +.text 0x0000000000000640 0x18c + *(.text .stub .text.* .gnu.linkonce.t.*) + .text 0x0000000000000768 0x64 /tmp/cc1j4yF2.o + 0x0000000000000768 foo + 0x0000000000000788 bar + 0x00000000000007b4 main +"#; + + let syms = parse_map_symbols_text(map); + + let foo = syms.iter().find(|s| s.name == "foo").unwrap(); + let bar = syms.iter().find(|s| s.name == "bar").unwrap(); + let main = syms.iter().find(|s| s.name == "main").unwrap(); + + assert_eq!(foo.address, 0x768); + assert_eq!(bar.address, 0x788); + assert_eq!(main.address, 0x7b4); + + // Sizes inferred by deltas and block end (0x768 + 0x64 = 0x7cc). + assert_eq!(foo.size, 0x20); + assert_eq!(bar.size, 0x2c); + assert_eq!(main.size, 0x18); + } + + #[test] + fn drops_plain_section_labels_from_fallback_parser() { + let map = r#" +.text 0x0000000000000640 0x18c +.text._Z3foov 0x0000000000000768 0x20 /tmp/x.o +"#; + let syms = parse_map_symbols_text(map); + assert!(!syms.iter().any(|s| s.name == ".text")); + assert!(syms.iter().any(|s| s.name == ".text._Z3foov")); + } +} diff --git a/crates/elf-report/src/render.rs b/crates/elf-report/src/render.rs new file mode 100644 index 0000000..3d64f36 --- /dev/null +++ b/crates/elf-report/src/render.rs @@ -0,0 +1,422 @@ +use std::collections::HashMap; + +use crate::symbol::extract_path_at_depth; +use crate::types::{FileReport, SymbolGroup, SymbolInfo}; + +fn format_size(bytes: u64) -> String { + if bytes < 1024 { + format!("{} B", bytes) + } else if bytes < 1024 * 1024 { + format!("{:.1} KiB", bytes as f64 / 1024.0) + } else { + format!("{:.2} MiB", bytes as f64 / (1024.0 * 1024.0)) + } +} + +pub fn render_markdown(reports: &[FileReport]) -> String { + let mut s = String::new(); + s.push_str("# ELF size report\n\n"); + s.push_str( + "This report summarizes ELF section sizes and (when symbols exist) the largest .text/.rodata symbols.\n\n", + ); + + for r in reports { + s.push_str(&format!("## {}\n\n", r.path)); + s.push_str(&format!("- kind: `{}`\n", r.file_kind)); + s.push_str(&format!("- arch: `{}`\n", r.arch)); + s.push_str(&format!( + "- stripped (best-effort): `{}`\n\n", + r.is_stripped_guess + )); + + if r.map_used { + s.push_str("- map used: `true` (symbol names attributed via linker map)\n\n"); + } + + if !r.notes.is_empty() { + s.push_str("### Notes\n\n"); + for n in &r.notes { + s.push_str(&format!("- {}\n", n)); + } + s.push('\n'); + } + + s.push_str("### Largest sections\n\n"); + + // Calculate column widths for sections + let sections_to_show: Vec<_> = r.sections.iter().take(30).collect(); + let max_section_name_len = sections_to_show + .iter() + .map(|s| s.name.len() + 2) // +2 for backticks + .max() + .unwrap_or(7) + .max(7); // min width for "section" + let max_size_len = sections_to_show + .iter() + .map(|s| format!("{}", s.size).len()) + .max() + .unwrap_or(12) + .max(12); // min width for "size (bytes)" + let addr_width = 10; // "address" width + + s.push_str(&format!( + "| {:width_size$} | {:>width_addr$} |\n", + "section", + "size (bytes)", + "address", + width_section = max_section_name_len, + width_size = max_size_len, + width_addr = addr_width + )); + s.push_str(&format!( + "|{:-width_size$} | {:>#width_addr$x} |\n", + section_cell, + sec.size, + sec.address, + width_section = max_section_name_len, + width_size = max_size_len, + width_addr = addr_width + )); + } + s.push('\n'); + + if !r.top_text_symbols.is_empty() { + s.push_str("### Top .text symbols\n\n"); + s.push_str("| size (bytes) | address | symbol |\n"); + s.push_str("|---:|---:|---|\n"); + for sym in &r.top_text_symbols { + s.push_str(&format!( + "| {} | 0x{:x} | `{}` |\n", + sym.size, sym.address, sym.demangled + )); + } + s.push('\n'); + } + + if !r.top_rodata_symbols.is_empty() { + s.push_str("### Top .rodata symbols\n\n"); + s.push_str("| size (bytes) | address | symbol |\n"); + s.push_str("|---:|---:|---|\n"); + for sym in &r.top_rodata_symbols { + s.push_str(&format!( + "| {} | 0x{:x} | `{}` |\n", + sym.size, sym.address, sym.demangled + )); + } + s.push('\n'); + } + } + + s +} + +fn group_symbols_by_depth(symbols: &[SymbolInfo], depth: usize) -> Vec { + let mut groups: HashMap> = HashMap::new(); + + for sym in symbols { + let path = + extract_path_at_depth(&sym.demangled, depth).unwrap_or_else(|| "[native]".to_string()); + + groups.entry(path).or_default().push(sym.clone()); + } + + let mut result: Vec = groups + .into_iter() + .map(|(path, symbols)| { + let total_size = symbols.iter().map(|s| s.size).sum(); + SymbolGroup { + path, + total_size, + symbol_count: symbols.len(), + symbols, + } + }) + .collect(); + + result.sort_by_key(|g| std::cmp::Reverse(g.total_size)); + result +} + +pub fn render_markdown_grouped(reports: &[FileReport], depth: usize) -> String { + let mut s = String::new(); + s.push_str(&format!( + "# ELF size report (grouped at depth {})\n\n", + depth + )); + s.push_str("Symbols grouped by crate/module path.\n\n"); + + for r in reports { + s.push_str(&format!("## {}\n\n", r.path)); + s.push_str(&format!("- kind: `{}`\n", r.file_kind)); + s.push_str(&format!("- arch: `{}`\n", r.arch)); + s.push_str(&format!( + "- stripped (best-effort): `{}`\n\n", + r.is_stripped_guess + )); + + if r.map_used { + s.push_str("- map used: `true` (symbol names attributed via linker map)\n\n"); + } + + if !r.notes.is_empty() { + s.push_str("### Notes\n\n"); + for n in &r.notes { + s.push_str(&format!("- {}\n", n)); + } + s.push('\n'); + } + + s.push_str("### Largest sections\n\n"); + + // Calculate column widths for sections + let sections_to_show: Vec<_> = r.sections.iter().take(30).collect(); + let max_section_name_len = sections_to_show + .iter() + .map(|s| s.name.len() + 2) // +2 for backticks + .max() + .unwrap_or(7) + .max(7); // min width for "section" + let max_size_len = sections_to_show + .iter() + .map(|s| format!("{}", s.size).len()) + .max() + .unwrap_or(12) + .max(12); // min width for "size (bytes)" + let addr_width = 10; // "address" width + + s.push_str(&format!( + "| {:width_size$} | {:>width_addr$} |\n", + "section", + "size (bytes)", + "address", + width_section = max_section_name_len, + width_size = max_size_len, + width_addr = addr_width + )); + s.push_str(&format!( + "|{:-width_size$} | {:>#width_addr$x} |\n", + section_cell, + sec.size, + sec.address, + width_section = max_section_name_len, + width_size = max_size_len, + width_addr = addr_width + )); + } + s.push('\n'); + + if !r.top_text_symbols.is_empty() { + s.push_str(&format!("### Top .text groups (depth {})\n\n", depth)); + let groups = group_symbols_by_depth(&r.top_text_symbols, depth); + + // Calculate column widths + let max_path_len = groups + .iter() + .map(|g| g.path.len() + 2) // +2 for backticks + .max() + .unwrap_or(4) + .max(4) + .max(15); // min width for "**TOTAL SHOWN**" + let size_width = 12; // "total size" width + let symbols_width = 9; // "symbols" width + + s.push_str(&format!( + "| {:width_size$} | {:>width_symbols$} |\n", + "path", + "total size", + "symbols", + width_path = max_path_len, + width_size = size_width, + width_symbols = symbols_width + )); + s.push_str(&format!( + "|{:-width_size$} | {:>width_symbols$} |\n", + path_cell, + size_cell, + group.symbol_count, + width_path = max_path_len, + width_size = size_width, + width_symbols = symbols_width + )); + } + + // Add summary rows + let total_shown: u64 = groups.iter().map(|g| g.total_size).sum(); + let total_symbols: usize = groups.iter().map(|g| g.symbol_count).sum(); + let text_section_size = r + .sections + .iter() + .find(|s| s.name == ".text") + .map(|s| s.size) + .unwrap_or(0); + let coverage = if text_section_size > 0 { + (total_shown as f64 / text_section_size as f64) * 100.0 + } else { + 0.0 + }; + + s.push_str(&format!( + "|{:-width_size$} | {:>width_symbols$} |\n", + "**TOTAL SHOWN**", + format_size(total_shown), + total_symbols, + width_path = max_path_len, + width_size = size_width, + width_symbols = symbols_width + )); + s.push_str(&format!( + "| {:width_size$} | {:>width_symbols$} |\n", + "**.text section**", + format_size(text_section_size), + format!("{:.1}%", coverage), + width_path = max_path_len, + width_size = size_width, + width_symbols = symbols_width + )); + + s.push('\n'); + } + + if !r.top_rodata_symbols.is_empty() { + s.push_str(&format!("### Top .rodata groups (depth {})\n\n", depth)); + let groups = group_symbols_by_depth(&r.top_rodata_symbols, depth); + + // Calculate column widths + let max_path_len = groups + .iter() + .map(|g| g.path.len() + 2) // +2 for backticks + .max() + .unwrap_or(4) + .max(4) + .max(17); // min width for "**.rodata section**" + let size_width = 12; // "total size" width + let symbols_width = 9; // "symbols" width + + s.push_str(&format!( + "| {:width_size$} | {:>width_symbols$} |\n", + "path", + "total size", + "symbols", + width_path = max_path_len, + width_size = size_width, + width_symbols = symbols_width + )); + s.push_str(&format!( + "|{:-width_size$} | {:>width_symbols$} |\n", + path_cell, + size_cell, + group.symbol_count, + width_path = max_path_len, + width_size = size_width, + width_symbols = symbols_width + )); + } + + // Add summary rows + let total_shown: u64 = groups.iter().map(|g| g.total_size).sum(); + let total_symbols: usize = groups.iter().map(|g| g.symbol_count).sum(); + let rodata_section_size = r + .sections + .iter() + .find(|s| s.name == ".rodata") + .map(|s| s.size) + .unwrap_or(0); + let coverage = if rodata_section_size > 0 { + (total_shown as f64 / rodata_section_size as f64) * 100.0 + } else { + 0.0 + }; + + s.push_str(&format!( + "|{:-width_size$} | {:>width_symbols$} |\n", + "**TOTAL SHOWN**", + format_size(total_shown), + total_symbols, + width_path = max_path_len, + width_size = size_width, + width_symbols = symbols_width + )); + s.push_str(&format!( + "| {:width_size$} | {:>width_symbols$} |\n", + "**.rodata section**", + format_size(rodata_section_size), + format!("{:.1}%", coverage), + width_path = max_path_len, + width_size = size_width, + width_symbols = symbols_width + )); + + s.push('\n'); + } + } + + s +} diff --git a/crates/elf-report/src/symbol.rs b/crates/elf-report/src/symbol.rs new file mode 100644 index 0000000..3e32343 --- /dev/null +++ b/crates/elf-report/src/symbol.rs @@ -0,0 +1,145 @@ +/// Extract the crate/module path at the specified depth from a demangled symbol. +/// +/// # Examples +/// ```ignore +/// assert_eq!(extract_path_at_depth("std::fmt::write", 1), Some("std".into())); +/// assert_eq!(extract_path_at_depth("std::fmt::write", 2), Some("std::fmt".into())); +/// assert_eq!(extract_path_at_depth(" as Iterator>::next", 1), Some("core".into())); +/// assert_eq!(extract_path_at_depth("main", 1), None); +/// ``` +pub fn extract_path_at_depth(demangled: &str, depth: usize) -> Option { + if depth == 0 { + return None; + } + + let mut working = demangled; + + // Handle trait impls: ::method + if let Some(stripped) = working.strip_prefix('<') { + if let Some(as_pos) = stripped.find(" as ") { + working = &stripped[..as_pos]; + } else if let Some(gt_pos) = stripped.find('>') { + working = &stripped[..gt_pos]; + } + } + + // Remove closure markers + working = working.split("::{{closure}}").next().unwrap_or(working); + + // Remove generics + if let Some(lt_pos) = working.find('<') { + working = &working[..lt_pos]; + } + + // No :: means not a Rust symbol + if !working.contains("::") { + return None; + } + + // Split and take first `depth` components + let parts: Vec<&str> = working.split("::").collect(); + if parts.len() < depth { + Some(parts.join("::")) + } else { + Some(parts[..depth].join("::")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_simple_path() { + assert_eq!( + extract_path_at_depth("std::fmt::write", 1), + Some("std".into()) + ); + assert_eq!( + extract_path_at_depth("std::fmt::write", 2), + Some("std::fmt".into()) + ); + assert_eq!( + extract_path_at_depth("std::fmt::write", 3), + Some("std::fmt::write".into()) + ); + } + + #[test] + fn test_trait_impl() { + let symbol = " as Iterator>::next"; + assert_eq!(extract_path_at_depth(symbol, 1), Some("core".into())); + assert_eq!(extract_path_at_depth(symbol, 2), Some("core::iter".into())); + assert_eq!( + extract_path_at_depth(symbol, 3), + Some("core::iter::Skip".into()) + ); + } + + #[test] + fn test_trait_impl_without_as() { + let symbol = ">::new"; + assert_eq!(extract_path_at_depth(symbol, 1), Some("std".into())); + assert_eq!( + extract_path_at_depth(symbol, 2), + Some("std::collections".into()) + ); + } + + #[test] + fn test_closure() { + let symbol = "std::io::read::{{closure}}"; + assert_eq!(extract_path_at_depth(symbol, 1), Some("std".into())); + assert_eq!(extract_path_at_depth(symbol, 2), Some("std::io".into())); + assert_eq!( + extract_path_at_depth(symbol, 3), + Some("std::io::read".into()) + ); + } + + #[test] + fn test_c_symbols() { + assert_eq!(extract_path_at_depth("main", 1), None); + assert_eq!(extract_path_at_depth("_start", 1), None); + assert_eq!(extract_path_at_depth("__init_cpu_features", 1), None); + } + + #[test] + fn test_nested_generics() { + let symbol = "std::collections::HashMap::insert"; + assert_eq!(extract_path_at_depth(symbol, 1), Some("std".into())); + assert_eq!( + extract_path_at_depth(symbol, 2), + Some("std::collections".into()) + ); + assert_eq!( + extract_path_at_depth(symbol, 3), + Some("std::collections::HashMap".into()) + ); + } + + #[test] + fn test_depth_exceeds_path() { + assert_eq!( + extract_path_at_depth("std::fmt", 10), + Some("std::fmt".into()) + ); + assert_eq!(extract_path_at_depth("core", 5), None); // No :: + } + + #[test] + fn test_depth_zero() { + assert_eq!(extract_path_at_depth("std::fmt::write", 0), None); + } + + #[test] + fn test_complex_trait_impl() { + let symbol = " as core::ops::drop::Drop>::drop"; + assert_eq!(extract_path_at_depth(symbol, 1), Some("alloc".into())); + assert_eq!(extract_path_at_depth(symbol, 2), Some("alloc::vec".into())); + assert_eq!( + extract_path_at_depth(symbol, 3), + Some("alloc::vec::Vec".into()) + ); + } +} diff --git a/crates/elf-report/src/types.rs b/crates/elf-report/src/types.rs new file mode 100644 index 0000000..f880a05 --- /dev/null +++ b/crates/elf-report/src/types.rs @@ -0,0 +1,44 @@ +use serde::Serialize; + +#[derive(Debug, Serialize)] +pub struct FileReport { + pub path: String, + pub file_kind: String, + pub arch: String, + pub is_stripped_guess: bool, + pub map_used: bool, + pub sections: Vec, + pub top_text_symbols: Vec, + pub top_rodata_symbols: Vec, + pub notes: Vec, +} + +#[derive(Debug, Serialize, Clone)] +pub struct SectionInfo { + pub name: String, + pub size: u64, + pub address: u64, +} + +#[derive(Debug, Serialize, Clone)] +pub struct SymbolInfo { + pub name: String, + pub demangled: String, + pub size: u64, + pub address: u64, +} + +#[derive(Debug, Clone)] +pub(crate) struct MapSymbol { + pub(crate) address: u64, + pub(crate) size: u64, + pub(crate) name: String, +} + +#[derive(Debug, Serialize, Clone)] +pub struct SymbolGroup { + pub path: String, + pub total_size: u64, + pub symbol_count: usize, + pub symbols: Vec, +} diff --git a/crates/zeroos-backtrace/Cargo.toml b/crates/zeroos-backtrace/Cargo.toml new file mode 100644 index 0000000..4f97990 --- /dev/null +++ b/crates/zeroos-backtrace/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "zeroos-backtrace" +version.workspace = true +edition = "2024" +license = "MIT OR Apache-2.0" +description = "Unified backtrace abstraction for ZeroOS with multiple unwinding strategies" + +[dependencies] +zeroos-macros = { workspace = true } +cfg-if = { workspace = true } diff --git a/crates/zeroos-backtrace/build.rs b/crates/zeroos-backtrace/build.rs new file mode 100644 index 0000000..e9172ed --- /dev/null +++ b/crates/zeroos-backtrace/build.rs @@ -0,0 +1,6 @@ +fn main() { + // Declare expected cfg values for the zeroos_backtrace configuration + println!( + "cargo::rustc-check-cfg=cfg(zeroos_backtrace, values(\"off\", \"dwarf\", \"frame_pointers\"))" + ); +} diff --git a/crates/zeroos-backtrace/src/dwarf.rs b/crates/zeroos-backtrace/src/dwarf.rs new file mode 100644 index 0000000..781fa97 --- /dev/null +++ b/crates/zeroos-backtrace/src/dwarf.rs @@ -0,0 +1,81 @@ +//! DWARF-based stack unwinding using `.eh_frame` tables. +//! +//! This implementation registers the `.eh_frame` section with libgcc's unwinder, +//! enabling Rust's standard backtrace machinery to work. +//! +//! ## Requirements +//! +//! - Build with `-Cforce-unwind-tables=yes` +//! - Linker script must preserve `.eh_frame` and `.eh_frame_hdr` sections +//! - libgcc must be linked (provides `__register_frame`) +//! - Environment variable `RUST_BACKTRACE=full` must be set +//! +//! ## How It Works +//! +//! 1. Compiler emits `.eh_frame` section containing Call Frame Information (CFI) +//! 2. Linker script preserves these sections (via `KEEP(.eh_frame)`) +//! 3. During runtime init (`.init_array`), we call libgcc's `__register_frame()` +//! 4. This registers the frame tables with libgcc's unwinder +//! 5. Rust's std::backtrace can now walk the stack using registered frames +//! +//! ## Trade-offs +//! +//! - ✓ Excellent accuracy (handles all optimizations) +//! - ✓ Platform-independent (DWARF is universal) +//! - ✗ Larger binary +//! - ✗ Requires libgcc + +use crate::BacktraceCapture; + +unsafe extern "C" { + /// libgcc frame registration API (DWARF2 unwinder). + /// + /// Registers the `.eh_frame` section with libgcc's unwinding machinery. + /// After registration, libgcc can use the frame tables for stack unwinding. + fn __register_frame(begin: *const u8); + + /// Start of `.eh_frame` section (provided by linker script). + static __eh_frame_start: u8; + + /// End of `.eh_frame` section (provided by linker script). + static __eh_frame_end: u8; +} + +/// DWARF-based backtrace implementation. +/// +/// This implementation registers `.eh_frame` tables with libgcc during initialization. +/// Actual backtrace printing is handled by Rust's standard library when +/// `RUST_BACKTRACE=full` is set in the environment. +pub struct DwarfBacktrace; + +impl BacktraceCapture for DwarfBacktrace { + fn init() { + // Register .eh_frame section with libgcc's unwinder + let start = core::ptr::addr_of!(__eh_frame_start); + let end = core::ptr::addr_of!(__eh_frame_end); + + // Only register if .eh_frame section is non-empty + if start != end { + unsafe { + __register_frame(start); + } + } + } + + unsafe fn print_backtrace() { + // In std mode with RUST_BACKTRACE=full set, Rust's standard library + // panic handler automatically prints backtraces using the registered + // .eh_frame tables. We don't need to do anything here. + // + // Note: This function is called from panic handlers, but in std mode + // the default panic hook has already printed the backtrace before + // calling any custom panic handling code. + // + // For nostd mode with DWARF, manually walking .eh_frame is complex + // and not currently implemented. Use frame-pointers mode instead. + } +} + +// NOTE: The .init_array hook (__zeroos_register_eh_frame and __ZEROOS_EH_FRAME_INIT) +// is defined in zeroos-runtime-musl/src/lib.rs, not here. This separation ensures +// that the runtime crate controls initialization timing and avoids duplicate symbols. diff --git a/crates/zeroos-backtrace/src/frame_pointer.rs b/crates/zeroos-backtrace/src/frame_pointer.rs new file mode 100644 index 0000000..8d9e939 --- /dev/null +++ b/crates/zeroos-backtrace/src/frame_pointer.rs @@ -0,0 +1,168 @@ +//! Frame pointer-based stack unwinding. +//! +//! This implementation walks the stack by following the frame pointer chain. +//! It's lightweight and works in no_std environments without external dependencies. +//! +//! ## Requirements +//! +//! - Build with `-Cforce-frame-pointers=yes` +//! - RISC-V architecture (riscv32 or riscv64) +//! +//! ## Trade-offs +//! +//! - ✓ Simple, no external dependencies +//! - ✓ Small overhead (~2-5 KB) +//! - ✗ May miss tail-call optimized frames +//! - ✗ Less accurate with aggressive optimizations + +use crate::BacktraceCapture; +use core::fmt::Write; + +/// Maximum number of stack frames to print +const MAX_FRAMES: usize = 64; + +/// Frame pointer-based backtrace implementation. +pub struct FramePointerBacktrace; + +impl BacktraceCapture for FramePointerBacktrace { + fn init() { + // In std mode, set custom panic hook to use frame pointer walking + // instead of Rust's default DWARF-based std::backtrace + #[cfg(not(target_os = "none"))] + { + extern crate std; + use std::boxed::Box; + use std::panic; + use std::string::String; + use std::sync::Once; + + static INIT: Once = Once::new(); + + INIT.call_once(|| { + panic::set_hook(Box::new(|info| { + // Extract panic message + let msg = if let Some(&s) = info.payload().downcast_ref::<&str>() { + s + } else if let Some(s) = info.payload().downcast_ref::() { + s.as_str() + } else { + "" + }; + + // Print panic info + if let Some(location) = info.location() { + if !msg.is_empty() { + std::eprintln!("\nthread panicked at {}:\n{}", location, msg); + } else { + std::eprintln!("\nthread panicked at {}", location); + } + } else { + std::eprintln!("\nthread panicked"); + } + + // Print backtrace using frame pointer walking + unsafe { + FramePointerBacktrace::print_backtrace(); + } + })); + }); + } + + // no_std mode: No initialization needed (panic handler calls print_backtrace directly) + } + + #[inline(never)] + unsafe fn print_backtrace() { + // Safety: get_frame_pointer returns the current valid frame pointer + unsafe { print_backtrace_from_fp(get_frame_pointer()) }; + } +} + +/// Print a stack backtrace starting from a given frame pointer. +/// +/// # Safety +/// +/// The caller must ensure `fp` is a valid frame pointer from the current stack. +unsafe fn print_backtrace_from_fp(mut fp: *const usize) { + // Use a simple writer that outputs to platform stdout + struct StdoutWriter; + + impl Write for StdoutWriter { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + unsafe extern "C" { + fn __platform_stdout_write(buf: *const u8, len: usize) -> isize; + } + unsafe { + __platform_stdout_write(s.as_ptr(), s.len()); + } + Ok(()) + } + } + + let mut w = StdoutWriter; + + let _ = writeln!(w, "stack backtrace:"); + + let mut frame_num = 0usize; + + while !fp.is_null() && frame_num < MAX_FRAMES { + // Safety: We check fp is not null + let ra = unsafe { read_return_address(fp) }; + + if ra == 0 { + break; + } + + // Format matches Rust stdlib backtrace: " N: 0xADDR - " + let _ = writeln!(w, " {frame_num}: 0x{ra:x} - "); + + // Move to previous frame + fp = unsafe { read_previous_fp(fp) }; + frame_num += 1; + } + + if frame_num == MAX_FRAMES { + let _ = writeln!(w, " ... (truncated)"); + } +} + +/// Get the current frame pointer (s0/fp register on RISC-V). +#[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] +#[inline(always)] +fn get_frame_pointer() -> *const usize { + let fp: usize; + unsafe { + core::arch::asm!("mv {}, s0", out(reg) fp, options(nomem, nostack)); + } + fp as *const usize +} + +#[cfg(not(any(target_arch = "riscv32", target_arch = "riscv64")))] +#[inline(always)] +fn get_frame_pointer() -> *const usize { + core::ptr::null() +} + +/// Read the return address from a frame. +/// +/// On RISC-V with standard calling convention: +/// - fp points to saved fp from caller +/// - fp - wordsize contains return address (ra) +#[inline(always)] +unsafe fn read_return_address(fp: *const usize) -> usize { + let ra_ptr = (fp as usize).wrapping_sub(core::mem::size_of::()) as *const usize; + if ra_ptr.is_null() { + return 0; + } + unsafe { core::ptr::read_volatile(ra_ptr) } +} + +/// Read the previous frame pointer from current frame. +#[inline(always)] +unsafe fn read_previous_fp(fp: *const usize) -> *const usize { + let prev_fp_ptr = (fp as usize).wrapping_sub(2 * core::mem::size_of::()) as *const usize; + if prev_fp_ptr.is_null() { + return core::ptr::null(); + } + unsafe { core::ptr::read_volatile(prev_fp_ptr) as *const usize } +} diff --git a/crates/zeroos-backtrace/src/lib.rs b/crates/zeroos-backtrace/src/lib.rs new file mode 100644 index 0000000..85ca00a --- /dev/null +++ b/crates/zeroos-backtrace/src/lib.rs @@ -0,0 +1,77 @@ +//! Unified backtrace abstraction for ZeroOS. +//! +//! This crate provides a trait-based interface for backtrace capture with multiple +//! implementation strategies, selected at compile time via `cfg(zeroos_backtrace = "...")`. +//! +//! # Backtrace Modes +//! +//! - **off**: No backtrace support (minimal binary size) +//! - **dwarf**: DWARF-based unwinding using `.eh_frame` tables (accurate, ~10-30 KB overhead) +//! - **frame_pointers**: Frame pointer walking (lightweight, ~2-5 KB overhead) +//! +//! # Usage +//! +//! The active implementation is selected at compile time via the build system: +//! +//! ```bash +//! cargo spike build --backtrace=dwarf +//! cargo spike build --backtrace=frame-pointers +//! cargo spike build --backtrace=off +//! ``` +//! +//! In runtime code, use the unified `Backtrace` type: +//! +//! ```rust,ignore +//! use zeroos_backtrace::{Backtrace, BacktraceCapture}; +//! +//! // Initialize (call during runtime startup) +//! Backtrace::init(); +//! +//! // Capture backtrace (call from panic handler) +//! unsafe { +//! Backtrace::print_backtrace(); +//! } +//! ``` + +#![no_std] +#![deny(unsafe_op_in_unsafe_fn)] + +use cfg_if::cfg_if; + +/// Common interface for all backtrace implementations. +pub trait BacktraceCapture { + /// Initialize backtrace support. + /// + /// This is called once during runtime initialization, before main. + /// For DWARF mode, this registers the `.eh_frame` section with libgcc. + /// For frame pointer mode, this is a no-op. + fn init(); + + /// Capture and print the current backtrace to stdout. + /// + /// # Safety + /// + /// This function walks the stack by reading memory. The caller must ensure: + /// - The stack is in a valid state + /// - Frame pointers (if used) are correctly maintained + /// - This is called from a valid execution context (e.g., panic handler) + unsafe fn print_backtrace(); +} + +// Conditional module inclusion and re-exports based on compile-time configuration +cfg_if! { + if #[cfg(zeroos_backtrace = "off")] { + mod noop; + pub use noop::NoopBacktrace as Backtrace; + } else if #[cfg(zeroos_backtrace = "dwarf")] { + mod dwarf; + pub use dwarf::DwarfBacktrace as Backtrace; + } else if #[cfg(zeroos_backtrace = "frame_pointers")] { + mod frame_pointer; + pub use frame_pointer::FramePointerBacktrace as Backtrace; + } else { + // Default to noop when no mode is explicitly set (for cargo check/test without build system) + mod noop; + pub use noop::NoopBacktrace as Backtrace; + } +} diff --git a/crates/zeroos-backtrace/src/noop.rs b/crates/zeroos-backtrace/src/noop.rs new file mode 100644 index 0000000..b63727c --- /dev/null +++ b/crates/zeroos-backtrace/src/noop.rs @@ -0,0 +1,67 @@ +//! No-op backtrace implementation for minimal binary size. +//! +//! This implementation is selected when `--backtrace=off` is specified. +//! It provides empty implementations of all backtrace functions, allowing +//! dead code elimination to remove all backtrace-related code. +//! +//! CRITICAL: Sets a custom panic hook that does NOT use std::backtrace, +//! allowing the linker to eliminate std::backtrace_rs, gimli, and addr2line. + +use crate::BacktraceCapture; + +/// No-op backtrace implementation. +/// +/// This struct provides empty implementations for all backtrace operations. +/// The compiler will optimize away all backtrace-related code when this +/// implementation is selected. +pub struct NoopBacktrace; + +impl BacktraceCapture for NoopBacktrace { + #[inline(always)] + fn init() { + #[cfg(not(target_os = "none"))] + { + extern crate std; + use std::boxed::Box; + use std::panic; + use std::string::String; + use std::sync::Once; + + // Set a custom panic hook that does NOT use backtrace. + // This allows the linker to eliminate std::backtrace_rs and gimli as dead code. + static INIT: Once = Once::new(); + + INIT.call_once(|| { + panic::set_hook(Box::new(|info| { + // Extract panic message + let msg = if let Some(&s) = info.payload().downcast_ref::<&str>() { + s + } else if let Some(s) = info.payload().downcast_ref::() { + s.as_str() + } else { + "" + }; + + // Print panic info WITHOUT backtrace + if let Some(location) = info.location() { + if !msg.is_empty() { + std::eprintln!("\nthread panicked at {}:\n{}", location, msg); + } else { + std::eprintln!("\nthread panicked at {}", location); + } + } else if !msg.is_empty() { + std::eprintln!("\nthread panicked: {}", msg); + } else { + std::eprintln!("\nthread panicked"); + } + // Explicitly do NOT call any backtrace functions here + })); + }); + } + } + + #[inline(always)] + unsafe fn print_backtrace() { + // No backtrace output + } +} diff --git a/crates/zeroos-build/src/cmds/build.rs b/crates/zeroos-build/src/cmds/build.rs index 9ed829f..b20b8e1 100644 --- a/crates/zeroos-build/src/cmds/build.rs +++ b/crates/zeroos-build/src/cmds/build.rs @@ -14,9 +14,18 @@ pub enum StdMode { #[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] pub enum BacktraceMode { + /// No backtrace support (minimal binary size) + Off, + + /// DWARF-based unwinding using `.eh_frame` tables (accurate, ~10-30 KB overhead) + Dwarf, + + /// Frame pointer walking (lightweight, ~2-5 KB overhead) + #[value(name = "frame-pointers")] + FramePointers, + + /// Smart default: dwarf for dev/debug profiles, frame-pointers for release Auto, - Enable, - Disable, } #[derive(clap::Args, Debug, Clone)] @@ -118,11 +127,13 @@ pub fn build_binary_with_rustflags( 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); + let backtrace_mode = resolve_backtrace_mode(args.backtrace, args.mode); + let emit_unwind_tables = matches!(backtrace_mode, BacktraceMode::Dwarf); debug!("target_dir: {}", target_dir.display()); debug!("target: {}", target); debug!("profile: {}", profile); + debug!("backtrace_mode: {:?}", backtrace_mode); let out_dir = target_dir.join(target).join(&profile); let crate_out_dir = out_dir.join("zeroos").join(&args.package); @@ -133,7 +144,7 @@ pub fn build_binary_with_rustflags( .with_memory(memory_origin, memory_size) .with_stack_size(stack_size) .with_heap_size(heap_size) - .with_backtrace(backtrace_enabled); + .with_emit_unwind_tables(emit_unwind_tables); let config = if let Some(template) = linker_template { config.with_template(template) @@ -153,9 +164,7 @@ pub fn build_binary_with_rustflags( write_target_spec( target_spec_path, target, - TargetRenderOptions { - backtrace: backtrace_enabled, - }, + TargetRenderOptions { emit_unwind_tables }, ) .ok(); Some(crate_out_dir.clone()) @@ -199,11 +208,27 @@ pub fn build_binary_with_rustflags( } } - // 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()); + // Set cfg flag for conditional compilation based on backtrace mode + match backtrace_mode { + BacktraceMode::Off => { + rustflags_parts.push("--cfg".to_string()); + rustflags_parts.push("zeroos_backtrace=\"off\"".to_string()); + } + BacktraceMode::Dwarf => { + rustflags_parts.push("--cfg".to_string()); + rustflags_parts.push("zeroos_backtrace=\"dwarf\"".to_string()); + // Force emission of .eh_frame tables for DWARF unwinding + rustflags_parts.push("-C".to_string()); + rustflags_parts.push("force-unwind-tables=yes".to_string()); + } + BacktraceMode::FramePointers => { + rustflags_parts.push("--cfg".to_string()); + rustflags_parts.push("zeroos_backtrace=\"frame_pointers\"".to_string()); + // Force frame pointer maintenance for stack walking + rustflags_parts.push("-C".to_string()); + rustflags_parts.push("force-frame-pointers=yes".to_string()); + } + BacktraceMode::Auto => unreachable!("Auto mode should be resolved earlier"), } for arg in &link_args { @@ -355,15 +380,17 @@ 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") - } +/// Resolve backtrace mode based on std mode if Auto is selected. +/// +/// Default behavior when Auto is selected: +/// - Std mode: Dwarf (requires unwind tables, works with std panic handler) +/// - NoStd mode: FramePointers (lightweight, no unwind tables needed) +fn resolve_backtrace_mode(mode: BacktraceMode, std_mode: StdMode) -> BacktraceMode { + match mode { + BacktraceMode::Auto => match std_mode { + StdMode::Std => BacktraceMode::Dwarf, + StdMode::NoStd => BacktraceMode::FramePointers, + }, + other => other, } } diff --git a/crates/zeroos-build/src/cmds/linker.rs b/crates/zeroos-build/src/cmds/linker.rs index 30b0f2c..8f782e1 100644 --- a/crates/zeroos-build/src/cmds/linker.rs +++ b/crates/zeroos-build/src/cmds/linker.rs @@ -51,7 +51,7 @@ pub fn generate_linker_script(args: &GenerateLinkerArgs) -> Result RAM : rodata - {% if backtrace %} + {% if EMIT_UNWIND_TABLES %} .eh_frame_hdr : ALIGN(4) { PROVIDE_HIDDEN(__eh_frame_hdr_start = .); KEEP(*(.eh_frame_hdr)) diff --git a/crates/zeroos-build/src/linker.rs b/crates/zeroos-build/src/linker.rs index dcb5e50..67f4955 100644 --- a/crates/zeroos-build/src/linker.rs +++ b/crates/zeroos-build/src/linker.rs @@ -13,7 +13,8 @@ pub struct LinkerConfig { pub stack_size: usize, - pub backtrace: bool, + /// Whether to emit DWARF unwind tables (.eh_frame sections) + pub emit_unwind_tables: bool, template: Option, } @@ -31,7 +32,7 @@ impl LinkerConfig { memory_size: DEFAULT_MEMORY_SIZE, heap_size: None, stack_size: DEFAULT_STACK_SIZE, - backtrace: false, + emit_unwind_tables: false, template: None, } } @@ -57,8 +58,8 @@ impl LinkerConfig { self } - pub fn with_backtrace(mut self, backtrace: bool) -> Self { - self.backtrace = backtrace; + pub fn with_emit_unwind_tables(mut self, backtrace: bool) -> Self { + self.emit_unwind_tables = backtrace; self } @@ -86,7 +87,7 @@ impl LinkerConfig { .or(self.template.as_deref()) .unwrap_or(LINKER_SCRIPT_TEMPLATE); let ctx = ztpl::Context::new() - .with_bool("backtrace", self.backtrace) + .with_bool("EMIT_UNWIND_TABLES", self.emit_unwind_tables) .with_str("MEMORY_ORIGIN", origin) .with_str("MEMORY_SIZE", mem_size) .with_str("HEAP_SIZE", heap_size) diff --git a/crates/zeroos-build/src/main.rs b/crates/zeroos-build/src/main.rs index 6e25f7d..a12c7b1 100644 --- a/crates/zeroos-build/src/main.rs +++ b/crates/zeroos-build/src/main.rs @@ -360,7 +360,7 @@ fn generate_target_command(cli_args: ZeroosGenerateTargetArgs) -> Result<()> { let json_content = generate_target_spec( &cli_args.base, TargetRenderOptions { - backtrace: cli_args.backtrace, + emit_unwind_tables: cli_args.backtrace, }, ) .map_err(|e| anyhow::anyhow!("{}", e))?; diff --git a/crates/zeroos-build/src/spec/utils.rs b/crates/zeroos-build/src/spec/utils.rs index e0cbc36..89dac11 100644 --- a/crates/zeroos-build/src/spec/utils.rs +++ b/crates/zeroos-build/src/spec/utils.rs @@ -6,12 +6,15 @@ use mini_template as ztpl; #[derive(Debug, Clone, Copy)] pub struct TargetRenderOptions { - pub backtrace: bool, + /// Whether to emit DWARF unwind tables (.eh_frame sections) + pub emit_unwind_tables: bool, } impl Default for TargetRenderOptions { fn default() -> Self { - Self { backtrace: true } + Self { + emit_unwind_tables: true, + } } } @@ -65,7 +68,14 @@ impl TargetConfig { .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" }); + .with_str( + "EMIT_UNWIND_TABLES", + if opts.emit_unwind_tables { + "true" + } else { + "false" + }, + ); ztpl::render(template, &ctx).map_err(|e| e.to_string()) } diff --git a/crates/zeroos-macros/src/cfgs.rs b/crates/zeroos-macros/src/cfgs.rs new file mode 100644 index 0000000..4dd4563 --- /dev/null +++ b/crates/zeroos-macros/src/cfgs.rs @@ -0,0 +1,122 @@ +/// Require exactly one cfg value to be set for a given cfg key. +/// +/// This macro generates compile-time checks to ensure that exactly one of the +/// provided cfg values is active for a given configuration key. +/// +/// # Example +/// +/// ```rust,ignore +/// use zeroos_macros::require_exactly_one_cfg; +/// +/// // Ensure exactly one target OS is configured +/// require_exactly_one_cfg!(target_os: "linux", "windows", "macos"); +/// ``` +/// +/// This will: +/// 1. Check that at least one value is set (compile error if none) +/// 2. Check that at most one value is set (compile error if multiple) +/// +/// Works with any cfg key-value pairs, both built-in (e.g., `target_os`, `target_arch`) +/// and custom cfgs defined in your project. +/// +/// # Refactoring Example +/// +/// **Before (15 lines of repetitive code):** +/// ```rust,ignore +/// // Ensure exactly one target OS is configured +/// #[cfg(not(any( +/// target_os = "linux", +/// target_os = "windows", +/// target_os = "macos" +/// )))] +/// compile_error!("No target OS selected!"); +/// +/// // Prevent multiple OS targets from being active simultaneously +/// #[cfg(all(target_os = "linux", target_os = "windows"))] +/// compile_error!("Multiple target OS selected: linux and windows"); +/// +/// #[cfg(all(target_os = "linux", target_os = "macos"))] +/// compile_error!("Multiple target OS selected: linux and macos"); +/// +/// #[cfg(all(target_os = "windows", target_os = "macos"))] +/// compile_error!("Multiple target OS selected: windows and macos"); +/// ``` +/// +/// **After (2 lines with DRY macro):** +/// ```rust,ignore +/// use zeroos_macros::require_exactly_one_cfg; +/// require_exactly_one_cfg!(target_os: "linux", "windows", "macos"); +/// ``` +/// +/// **Benefits:** +/// - DRY: No repetitive compile_error statements +/// - Maintainable: Add new values easily +/// - Clear: Intent is obvious +/// - Reusable: Works for any cfg key-value validation +#[macro_export] +macro_rules! require_exactly_one_cfg { + ($cfg_key:ident: $($value:literal),+ $(,)?) => { + // Check that at least one is set + #[cfg(not(any( + $($cfg_key = $value),+ + )))] + compile_error!(concat!( + "No ", + stringify!($cfg_key), + " mode selected! Expected one of: ", + $($value, ", "),+ + )); + + // Check that at most one is set (generate all pairwise combinations) + $crate::__require_exactly_one_cfg_pairs!($cfg_key: $($value),+); + }; +} + +/// Internal helper macro to generate pairwise conflict checks. +/// +/// For each pair of values, generates a compile_error if both are set. +#[doc(hidden)] +#[macro_export] +macro_rules! __require_exactly_one_cfg_pairs { + // Base case: single value (no pairs to check) + ($cfg_key:ident: $single:literal) => {}; + + // Recursive case: check first value against all others, then recurse + ($cfg_key:ident: $first:literal, $($rest:literal),+) => { + $( + #[cfg(all($cfg_key = $first, $cfg_key = $rest))] + compile_error!(concat!( + "Multiple ", + stringify!($cfg_key), + " modes selected: ", + $first, + " and ", + $rest + )); + )+ + + // Recurse with remaining values + $crate::__require_exactly_one_cfg_pairs!($cfg_key: $($rest),+); + }; +} + +/// Require at most one cfg value to be set for a given cfg key. +/// +/// Similar to `require_exactly_one_cfg`, but allows zero values to be set. +/// +/// # Example +/// +/// ```rust,ignore +/// use zeroos_macros::require_at_most_one_cfg; +/// +/// require_at_most_one_cfg!( +/// build_mode: "debug", "release", "profile" +/// ); +/// ``` +#[macro_export] +macro_rules! require_at_most_one_cfg { + ($cfg_key:ident: $($value:literal),+ $(,)?) => { + // Only check for conflicts (allow none to be set) + $crate::__require_exactly_one_cfg_pairs!($cfg_key: $($value),+); + }; +} diff --git a/crates/zeroos-macros/src/lib.rs b/crates/zeroos-macros/src/lib.rs index a66a708..eb149fa 100644 --- a/crates/zeroos-macros/src/lib.rs +++ b/crates/zeroos-macros/src/lib.rs @@ -5,3 +5,6 @@ mod asm_block; #[macro_use] mod features; + +#[macro_use] +mod cfgs; diff --git a/crates/zeroos-runtime-musl/Cargo.toml b/crates/zeroos-runtime-musl/Cargo.toml index 534cdc0..73642a8 100644 --- a/crates/zeroos-runtime-musl/Cargo.toml +++ b/crates/zeroos-runtime-musl/Cargo.toml @@ -12,6 +12,7 @@ crate-type = ["rlib"] [dependencies] debug.workspace = true foundation.workspace = true +zeroos-backtrace = { workspace = true } [features] default = [] diff --git a/crates/zeroos-runtime-musl/build.rs b/crates/zeroos-runtime-musl/build.rs new file mode 100644 index 0000000..6461523 --- /dev/null +++ b/crates/zeroos-runtime-musl/build.rs @@ -0,0 +1,4 @@ +fn main() { + // Declare the custom cfg for backtrace mode + println!("cargo:rustc-check-cfg=cfg(zeroos_backtrace, values(\"off\", \"dwarf\", \"frame-pointers\"))"); +} diff --git a/crates/zeroos-runtime-musl/src/lib.rs b/crates/zeroos-runtime-musl/src/lib.rs index 5986586..7a00358 100644 --- a/crates/zeroos-runtime-musl/src/lib.rs +++ b/crates/zeroos-runtime-musl/src/lib.rs @@ -1,11 +1,33 @@ #![no_std] -#[cfg(feature = "backtrace")] -mod eh_frame_register; mod lock_override; mod stack; pub use stack::build_musl_stack; +// Re-export unified backtrace interface for public API +pub use zeroos_backtrace::{Backtrace, BacktraceCapture}; + +// Backtrace initialization hook (runs from .init_array before main) +// Each implementation handles its own initialization: +// - DWARF: Registers .eh_frame with libgcc +// - Frame-pointers (std): Sets custom panic hook +// - Frame-pointers (no_std): No-op +// - Off: No-op +mod backtrace { + use zeroos_backtrace::BacktraceCapture; + + #[unsafe(no_mangle)] + extern "C" fn __zeroos_init_backtrace() { + // Polymorphic call - each implementation handles its own setup + zeroos_backtrace::Backtrace::init(); + } + + // Place initialization function in .init_array + #[used] + #[unsafe(link_section = ".init_array")] + static __ZEROOS_BACKTRACE_INIT: extern "C" fn() = __zeroos_init_backtrace; +} + #[cfg(target_arch = "riscv64")] pub mod riscv64; diff --git a/crates/zeroos-runtime-musl/src/stack.rs b/crates/zeroos-runtime-musl/src/stack.rs index cd5d274..69c9ab1 100644 --- a/crates/zeroos-runtime-musl/src/stack.rs +++ b/crates/zeroos-runtime-musl/src/stack.rs @@ -75,7 +75,7 @@ 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")] + #[cfg(zeroos_backtrace = "dwarf")] fn push_bytes_aligned(&mut self, bytes: &[u8], align: usize) -> usize { debug_assert!(align.is_power_of_two()); let len = bytes.len(); @@ -140,9 +140,23 @@ pub unsafe fn build_musl_stack( ) -> usize { let mut ds = DownwardStack::::new(stack_top, stack_bottom); - // Optional environment variables for musl's `__libc_start_main`: - // it computes envp = argv + argc + 1. - #[cfg(feature = "backtrace")] + // Pre-populate RUST_BACKTRACE environment variable for DWARF mode. + // + // Why this is necessary: + // 1. Rust's std panic handler checks RUST_BACKTRACE to decide whether to print backtraces + // 2. Guest programs cannot access host environment variables (isolation) + // 3. We pre-populate it on the stack before program execution starts + // + // Why "full" instead of "1": + // - "1" (short mode) requires detecting __rust_begin/end_short_backtrace markers + // - Marker detection requires symbol resolution to work in the guest + // - In unikernels, all frames are relevant (no meaningful user/stdlib distinction) + // - "full" mode prints all frames unconditionally (more reliable for debugging) + // + // This only happens in DWARF mode because: + // - Frame-pointers mode uses custom backtrace walker (not std::backtrace) + // - Off mode has no backtrace capability + #[cfg(zeroos_backtrace = "dwarf")] let rust_backtrace_ptr = ds.push_bytes_aligned(b"RUST_BACKTRACE=full\0", core::mem::align_of::()); @@ -199,8 +213,8 @@ pub unsafe fn build_musl_stack( // envp terminator (always present) ds.push(0); - // envp[0] (optional) - #[cfg(feature = "backtrace")] + // envp[0] (optional, only in DWARF mode) + #[cfg(zeroos_backtrace = "dwarf")] ds.push(rust_backtrace_ptr); // argv terminator diff --git a/crates/zeroos-runtime-nostd/Cargo.toml b/crates/zeroos-runtime-nostd/Cargo.toml index 81e31c6..b50287e 100644 --- a/crates/zeroos-runtime-nostd/Cargo.toml +++ b/crates/zeroos-runtime-nostd/Cargo.toml @@ -16,3 +16,4 @@ memory = ["foundation/memory"] [dependencies] foundation = { workspace = true } cfg-if = { workspace = true } +zeroos-backtrace = { workspace = true } diff --git a/crates/zeroos-runtime-nostd/src/lib.rs b/crates/zeroos-runtime-nostd/src/lib.rs index 62622e7..3e43f6f 100644 --- a/crates/zeroos-runtime-nostd/src/lib.rs +++ b/crates/zeroos-runtime-nostd/src/lib.rs @@ -24,6 +24,6 @@ cfg_if! { #[cfg(feature = "panic")] pub mod panic; -// Stack backtrace via frame pointer walking -#[cfg(feature = "backtrace")] -pub mod backtrace; +// Concrete implementation (NoopBacktrace, DwarfBacktrace, or FramePointerBacktrace) +// selected at compile time via cfg(zeroos_backtrace = "off"|"dwarf"|"frame_pointers") +pub use zeroos_backtrace::{Backtrace, BacktraceCapture}; diff --git a/crates/zeroos-runtime-nostd/src/panic.rs b/crates/zeroos-runtime-nostd/src/panic.rs index 76f5a09..8047880 100644 --- a/crates/zeroos-runtime-nostd/src/panic.rs +++ b/crates/zeroos-runtime-nostd/src/panic.rs @@ -15,9 +15,6 @@ use core::panic::PanicInfo; use crate::io::StdoutWriter; -#[cfg(feature = "backtrace")] -use crate::backtrace; - /// Standard signal number for abort (SIGABRT) const SIGABRT: i32 = 6; @@ -70,9 +67,12 @@ fn panic(info: &PanicInfo) -> ! { let _ = writeln!(w, "PANIC: panicked: {}", info.message()); } - // Print stack backtrace if feature enabled - #[cfg(feature = "backtrace")] - backtrace::print_backtrace(); + // Print stack backtrace (polymorphic: no-op if zeroos_backtrace="off") + // Safety: Called from panic handler with valid stack state + unsafe { + use crate::BacktraceCapture; + crate::Backtrace::print_backtrace(); + } // Call platform-specific abort handler with SIGABRT // This will exit with code 128 + 6 = 134 diff --git a/crates/zeroos/Cargo.toml b/crates/zeroos/Cargo.toml index 06bb579..2e4e541 100644 --- a/crates/zeroos/Cargo.toml +++ b/crates/zeroos/Cargo.toml @@ -35,9 +35,6 @@ runtime-musl = ["dep:runtime-musl"] runtime-gnu = ["dep:runtime-gnu"] panic = ["runtime-nostd?/panic"] -# Backtraces -backtrace = ["runtime-musl?/backtrace", "runtime-nostd?/backtrace"] - # Capabilities ## Memory memory = ["foundation/memory", "runtime-nostd?/memory", "os-linux?/memory"] @@ -61,6 +58,11 @@ random = ["foundation/random", "os-linux?/random"] rng-lcg = ["random", "dep:rng", "rng/lcg"] rng-chacha = ["random", "dep:rng", "rng/chacha"] +## Backtrace (controlled via cfg, not features) +# Note: Actual backtrace mode is set via cfg(zeroos_backtrace) by the build system +# This feature exists for compatibility but doesn't enable additional dependencies +backtrace = [] + [dependencies] debug = { workspace = true } zeroos-macros.workspace = true diff --git a/examples/backtrace/Cargo.toml b/examples/backtrace/Cargo.toml index a5dbb16..ea9727d 100644 --- a/examples/backtrace/Cargo.toml +++ b/examples/backtrace/Cargo.toml @@ -13,14 +13,12 @@ cfg-if.workspace = true default = [] debug = ["platform/debug"] -backtrace = ["platform/backtrace"] std = [ "platform/std", "platform/vfs-device-console", "platform/memory", "platform/bounds-checks", - "platform/backtrace", ] with-spike = ["platform/with-spike"] diff --git a/matrix.yaml b/matrix.yaml index 2bc7086..a04a831 100644 --- a/matrix.yaml +++ b/matrix.yaml @@ -38,6 +38,7 @@ entries: - xtask - cargo-matrix - mini-template + - elf-report - zeroos-build - spike-build target: @@ -46,6 +47,7 @@ entries: - package: - zeroos-debug - zeroos-macros + - zeroos-backtrace target: - *host_targets - *guest_targets @@ -233,7 +235,6 @@ entries: cargo spike build --package {package} --target "{target}" -- {features_flag} --quiet features: - with-spike - - backtrace - package: backtrace target: diff --git a/platforms/spike-platform/src/lib.rs b/platforms/spike-platform/src/lib.rs index efee788..0d8c5a4 100644 --- a/platforms/spike-platform/src/lib.rs +++ b/platforms/spike-platform/src/lib.rs @@ -42,8 +42,12 @@ cfg_if::cfg_if! { fn panic(info: &core::panic::PanicInfo) -> ! { eprintln!("PANIC: {}", info); - #[cfg(feature = "backtrace")] - zeroos::runtime_nostd::backtrace::print_backtrace(); + // Print backtrace (polymorphic: no-op if mode is "off") + // Safety: Called from panic handler with valid stack + unsafe { + use zeroos::runtime_nostd::BacktraceCapture; + zeroos::runtime_nostd::Backtrace::print_backtrace(); + } // Exit with SIGABRT (128 + 6 = 134) per Linux convention __platform_abort(6) diff --git a/platforms/spike-platform/src/linker.ld.template b/platforms/spike-platform/src/linker.ld.template index e259eeb..fd4cf29 100644 --- a/platforms/spike-platform/src/linker.ld.template +++ b/platforms/spike-platform/src/linker.ld.template @@ -35,7 +35,7 @@ SECTIONS . = ALIGN(8); } > RAM : rodata - {% if backtrace %} + {% if EMIT_UNWIND_TABLES %} .eh_frame_hdr : ALIGN(4) { PROVIDE_HIDDEN(__eh_frame_hdr_start = .); KEEP(*(.eh_frame_hdr)) diff --git a/release-plz.toml b/release-plz.toml index 04973f7..c28c631 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -60,6 +60,11 @@ name = "zeroos-runtime-nostd" version_group = "zeroos" release = false +[[package]] +name = "zeroos-backtrace" +version_group = "zeroos" +release = false + [[package]] name = "zeroos-build" version_group = "zeroos" diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index e58a172..c528c26 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -17,3 +17,4 @@ anyhow.workspace = true walkdir.workspace = true toml.workspace = true cargo_toml.workspace = true +elf-report.workspace = true diff --git a/xtask/src/act.rs b/xtask/src/cmds/act.rs similarity index 100% rename from xtask/src/act.rs rename to xtask/src/cmds/act.rs diff --git a/xtask/src/cmds/analyze_backtrace.rs b/xtask/src/cmds/analyze_backtrace.rs new file mode 100644 index 0000000..f35e0c9 --- /dev/null +++ b/xtask/src/cmds/analyze_backtrace.rs @@ -0,0 +1,541 @@ +//! Comprehensive binary size analysis for all backtrace configurations. +//! +//! Builds and analyzes ALL combinations: +//! - Runtime: no-std, std +//! - Backtrace: off, frame-pointers, dwarf (std only) +//! - Debug: 0, 1, 2 +//! - Strip: false, true +//! +//! Total: 30 builds (5 mode combos × 3 debug × 2 strip) + +use anyhow::Result; +use std::collections::HashMap; +use std::path::PathBuf; +use std::process::Command; + +#[derive(Debug, clap::Args)] +pub struct AnalyzeBacktraceArgs { + /// LTO mode: false, true, thin, fat + #[arg(long, default_value = "fat")] + lto: String, + + /// Number of top symbols to show in detailed analysis + #[arg(long, default_value = "50")] + top: usize, +} + +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +struct BuildConfig { + runtime: String, + backtrace: String, + debug: u8, + strip: bool, +} + +pub fn run(args: AnalyzeBacktraceArgs) -> Result<()> { + println!("====================================================================="); + println!("Comprehensive Backtrace Size Analysis"); + println!("====================================================================="); + println!(); + println!("Build matrix:"); + println!(" - Runtimes: no-std, std"); + println!(" - Backtrace: off, frame-pointers, dwarf (std only)"); + println!(" - Debug levels: 0, 1, 2"); + println!(" - Strip: false, true"); + println!(" - Total builds: 30"); + println!(); + println!("Settings:"); + println!(" - LTO: {}", args.lto); + println!(" - Opt-level: z (size)"); + println!(); + + let modes = vec![ + ("no-std", "off"), + ("no-std", "frame-pointers"), + ("std", "off"), + ("std", "frame-pointers"), + ("std", "dwarf"), + ]; + + let debug_levels = [0, 1, 2]; + let strip_values = [false, true]; + + let total = modes.len() * debug_levels.len() * strip_values.len(); + let mut results: HashMap = HashMap::new(); + let mut count = 0; + + for (runtime, backtrace) in &modes { + for &debug in &debug_levels { + for &strip in &strip_values { + count += 1; + let strip_str = if strip { "strip" } else { "no-strip" }; + print!( + "[{:2}/{}] {:8} + {:15} debug={} {}... ", + count, total, runtime, backtrace, debug, strip_str + ); + std::io::Write::flush(&mut std::io::stdout()).ok(); + + match build_binary(runtime, backtrace, debug, strip, &args.lto) { + Ok(size) => { + println!("{:>10}", bytefmt::format(size)); + results.insert( + BuildConfig { + runtime: runtime.to_string(), + backtrace: backtrace.to_string(), + debug, + strip, + }, + size, + ); + } + Err(e) => { + println!("FAILED: {}", e); + } + } + } + } + println!(); + } + + print_results_table(&results, &modes)?; + print_insights(&results, &modes)?; + print_detailed_elf_analysis(&args.lto, args.top)?; + + Ok(()) +} + +fn build_binary(runtime: &str, backtrace: &str, debug: u8, strip: bool, lto: &str) -> Result { + std::env::set_var("CARGO_PROFILE_RELEASE_DEBUG", debug.to_string()); + std::env::set_var("CARGO_PROFILE_RELEASE_STRIP", strip.to_string()); + std::env::set_var("CARGO_PROFILE_RELEASE_LTO", lto); + std::env::set_var("CARGO_PROFILE_RELEASE_OPT_LEVEL", "z"); + + let target = if runtime == "no-std" { + "riscv64imac-unknown-none-elf" + } else { + "riscv64imac-zero-linux-musl" + }; + + let mut cmd = Command::new("cargo"); + cmd.arg("spike") + .arg("build") + .arg("-p") + .arg("backtrace") + .arg("--target") + .arg(target); + + if runtime == "std" { + cmd.arg("--mode").arg("std"); + } + + cmd.arg("--backtrace") + .arg(backtrace) + .arg("--") + .arg("--quiet"); + + if runtime == "std" { + cmd.arg("--features").arg("std,with-spike"); + } else { + cmd.arg("--features").arg("with-spike"); + } + + cmd.arg("--profile").arg("release"); + + // Suppress warnings + cmd.stderr(std::process::Stdio::null()); + + let output = cmd.output()?; + if !output.status.success() { + anyhow::bail!("cargo spike build failed"); + } + + let bin_path = PathBuf::from(format!("target/{}/release/backtrace", target)); + Ok(std::fs::metadata(&bin_path)?.len()) +} + +fn print_results_table(results: &HashMap, modes: &[(&str, &str)]) -> Result<()> { + println!(); + println!("====================================================================="); + println!("RESULTS: strip=false (with symbols/debug)"); + println!("====================================================================="); + println!(); + println!( + "{:<25} {:>12} {:>12} {:>12}", + "Mode", "debug=0", "debug=1", "debug=2" + ); + println!("{:-<25} {:-<12} {:-<12} {:-<12}", "", "", "", ""); + + for (runtime, backtrace) in modes { + let mode_name = format!("{} + {}", runtime, backtrace); + let sizes: Vec = [0, 1, 2] + .iter() + .map(|&d| { + let config = BuildConfig { + runtime: runtime.to_string(), + backtrace: backtrace.to_string(), + debug: d, + strip: false, + }; + results + .get(&config) + .map(|s| format!("{:>10}", bytefmt::format(*s))) + .unwrap_or_else(|| "N/A".to_string()) + }) + .collect(); + + println!( + "{:<25} {:>12} {:>12} {:>12}", + mode_name, sizes[0], sizes[1], sizes[2] + ); + } + + println!(); + println!("====================================================================="); + println!("RESULTS: strip=true (production minimal)"); + println!("====================================================================="); + println!(); + println!( + "{:<25} {:>12} {:>12} {:>12}", + "Mode", "debug=0", "debug=1", "debug=2" + ); + println!("{:-<25} {:-<12} {:-<12} {:-<12}", "", "", "", ""); + + for (runtime, backtrace) in modes { + let mode_name = format!("{} + {}", runtime, backtrace); + let sizes: Vec = [0, 1, 2] + .iter() + .map(|&d| { + let config = BuildConfig { + runtime: runtime.to_string(), + backtrace: backtrace.to_string(), + debug: d, + strip: true, + }; + results + .get(&config) + .map(|s| format!("{:>10}", bytefmt::format(*s))) + .unwrap_or_else(|| "N/A".to_string()) + }) + .collect(); + + println!( + "{:<25} {:>12} {:>12} {:>12}", + mode_name, sizes[0], sizes[1], sizes[2] + ); + } + + Ok(()) +} + +fn print_detailed_elf_analysis(lto: &str, top: usize) -> Result<()> { + println!(); + println!("====================================================================="); + println!("DETAILED ELF ANALYSIS (std + off + debug=0 + strip=false)"); + println!("====================================================================="); + println!(); + println!("Building unstripped binary for symbol analysis..."); + println!("Note: Using strip=false to preserve symbols for detailed breakdown."); + println!(); + + // Build with debug=0 but strip=false to preserve symbols for analysis + std::env::set_var("CARGO_PROFILE_RELEASE_DEBUG", "0"); + std::env::set_var("CARGO_PROFILE_RELEASE_STRIP", "false"); + std::env::set_var("CARGO_PROFILE_RELEASE_LTO", lto); + std::env::set_var("CARGO_PROFILE_RELEASE_OPT_LEVEL", "z"); + + let target = "riscv64imac-zero-linux-musl"; + + let mut cmd = Command::new("cargo"); + cmd.arg("spike") + .arg("build") + .arg("-p") + .arg("backtrace") + .arg("--target") + .arg(target) + .arg("--mode") + .arg("std") + .arg("--backtrace") + .arg("off") + .arg("--") + .arg("--quiet") + .arg("--features") + .arg("std,with-spike") + .arg("--profile") + .arg("release"); + + // Suppress warnings + cmd.stderr(std::process::Stdio::null()); + + let output = cmd.output()?; + if !output.status.success() { + println!("⚠ Failed to build binary for detailed analysis"); + return Ok(()); + } + + let bin_path = PathBuf::from(format!("target/{}/release/backtrace", target)); + + // Analyze with elf-report library + println!( + "Running elf-report analysis with module-level grouping (depth=2, top={})...", + top + ); + println!(); + + let reports = match elf_report::analyze_paths(std::slice::from_ref(&bin_path), &[None], top) { + Ok(r) => r, + Err(e) => { + println!("⚠ elf-report analysis failed: {}", e); + return Ok(()); + } + }; + + let output = elf_report::render_markdown_grouped(&reports, 2); + println!("{}", output); + + println!(); + println!("NOTE: Production stripped size (strip=true) is shown in the main results table."); + println!(); + + // Show stripped binary sections for comparison + println!("====================================================================="); + println!("STRIPPED BINARY SECTIONS (std + off + debug=0 + strip=true)"); + println!("====================================================================="); + println!(); + println!("Building stripped binary to show actual production sections..."); + + // Build stripped version + std::env::set_var("CARGO_PROFILE_RELEASE_DEBUG", "0"); + std::env::set_var("CARGO_PROFILE_RELEASE_STRIP", "true"); + std::env::set_var("CARGO_PROFILE_RELEASE_LTO", lto); + std::env::set_var("CARGO_PROFILE_RELEASE_OPT_LEVEL", "z"); + + let mut cmd = Command::new("cargo"); + cmd.arg("spike") + .arg("build") + .arg("-p") + .arg("backtrace") + .arg("--target") + .arg(target) + .arg("--mode") + .arg("std") + .arg("--backtrace") + .arg("off") + .arg("--") + .arg("--quiet") + .arg("--features") + .arg("std,with-spike") + .arg("--profile") + .arg("release"); + + cmd.stderr(std::process::Stdio::null()); + + let output = cmd.output()?; + if !output.status.success() { + println!("⚠ Failed to build stripped binary"); + return Ok(()); + } + + let stripped_bin_path = PathBuf::from(format!("target/{}/release/backtrace", target)); + + // Analyze stripped binary with elf-report library + println!(); + let stripped_reports = match elf_report::analyze_paths(&[stripped_bin_path], &[None], 0) { + Ok(r) => r, + Err(e) => { + println!("⚠ elf-report analysis failed: {}", e); + return Ok(()); + } + }; + + // Show just the sections table + if let Some(report) = stripped_reports.first() { + println!("### Largest sections\n"); + + let sections_to_show: Vec<_> = report.sections.iter().take(30).collect(); + let max_section_name_len = sections_to_show + .iter() + .map(|s| s.name.len() + 2) + .max() + .unwrap_or(7) + .max(7); + let max_size_len = sections_to_show + .iter() + .map(|s| format!("{}", s.size).len()) + .max() + .unwrap_or(12) + .max(12); + let addr_width = 10; + + println!( + "| {:width_size$} | {:>width_addr$} |", + "section", + "size (bytes)", + "address", + width_section = max_section_name_len, + width_size = max_size_len, + width_addr = addr_width + ); + println!( + "|{:-width_size$} | {:>#width_addr$x} |", + section_cell, + sec.size, + sec.address, + width_section = max_section_name_len, + width_size = max_size_len, + width_addr = addr_width + ); + } + println!(); + } + + println!(); + println!("EXPLANATION:"); + println!(" - The grouped symbols shown above are only the TOP symbols (default: 50)"); + println!(" - The .text section contains ALL code, including many small functions"); + println!( + " - Size breakdown: .text (code) + .rodata (constants) + .data + .eh_frame + etc = total" + ); + + Ok(()) +} + +fn print_insights(results: &HashMap, _modes: &[(&str, &str)]) -> Result<()> { + println!(); + println!("====================================================================="); + println!("KEY INSIGHTS"); + println!("====================================================================="); + println!(); + + // Absolute minimum + let min_config = BuildConfig { + runtime: "no-std".to_string(), + backtrace: "off".to_string(), + debug: 0, + strip: true, + }; + if let Some(&min_size) = results.get(&min_config) { + println!( + "✓ Absolute minimum (no-std + off + debug=0 + strip): {}", + bytefmt::format(min_size) + ); + } + + // Strip impact + let nostd_off_d0_nostrip = results.get(&BuildConfig { + runtime: "no-std".to_string(), + backtrace: "off".to_string(), + debug: 0, + strip: false, + }); + let nostd_off_d0_strip = results.get(&BuildConfig { + runtime: "no-std".to_string(), + backtrace: "off".to_string(), + debug: 0, + strip: true, + }); + if let (Some(&nostrip), Some(&strip)) = (nostd_off_d0_nostrip, nostd_off_d0_strip) { + let saved = nostrip.saturating_sub(strip); + println!( + "✓ Strip impact (debug=0): {} → {} (saves {})", + bytefmt::format(nostrip), + bytefmt::format(strip), + bytefmt::format(saved) + ); + println!(" (Debug sections from dependencies removed)"); + } + + // Debug level impact + let nostd_off_strip_d0 = results.get(&BuildConfig { + runtime: "no-std".to_string(), + backtrace: "off".to_string(), + debug: 0, + strip: true, + }); + let nostd_off_strip_d1 = results.get(&BuildConfig { + runtime: "no-std".to_string(), + backtrace: "off".to_string(), + debug: 1, + strip: true, + }); + if let (Some(&d0), Some(&d1)) = (nostd_off_strip_d0, nostd_off_strip_d1) { + let cost = d1.saturating_sub(d0); + println!("✓ Debug=1 cost (line tables): +{}", bytefmt::format(cost)); + } + + // Backtrace overhead (no-std, minimal) + let nostd_off = results.get(&BuildConfig { + runtime: "no-std".to_string(), + backtrace: "off".to_string(), + debug: 0, + strip: true, + }); + let nostd_fp = results.get(&BuildConfig { + runtime: "no-std".to_string(), + backtrace: "frame-pointers".to_string(), + debug: 0, + strip: true, + }); + if let (Some(&off), Some(&fp)) = (nostd_off, nostd_fp) { + let overhead = fp.saturating_sub(off); + println!( + "✓ Frame-pointer overhead (no-std, production): +{}", + bytefmt::format(overhead) + ); + } + + // DWARF overhead (std, minimal) + let std_off = results.get(&BuildConfig { + runtime: "std".to_string(), + backtrace: "off".to_string(), + debug: 0, + strip: true, + }); + let std_dwarf = results.get(&BuildConfig { + runtime: "std".to_string(), + backtrace: "dwarf".to_string(), + debug: 0, + strip: true, + }); + if let (Some(&off), Some(&dwarf)) = (std_off, std_dwarf) { + let overhead = (dwarf as i64 - off as i64).unsigned_abs(); + println!( + "✓ DWARF overhead (std, production): +{}", + bytefmt::format(overhead) + ); + } + + println!(); + println!("Analysis complete!"); + Ok(()) +} + +mod bytefmt { + pub fn format(bytes: u64) -> String { + const UNITS: &[&str] = &["B", "KiB", "MiB", "GiB"]; + let mut size = bytes as f64; + let mut unit_idx = 0; + + while size >= 1024.0 && unit_idx < UNITS.len() - 1 { + size /= 1024.0; + unit_idx += 1; + } + + if unit_idx == 0 { + format!("{} {}", bytes, UNITS[0]) + } else { + format!("{:.1} {}", size, UNITS[unit_idx]) + } + } +} diff --git a/xtask/src/check_workspace.rs b/xtask/src/cmds/check_workspace.rs similarity index 100% rename from xtask/src/check_workspace.rs rename to xtask/src/cmds/check_workspace.rs diff --git a/xtask/src/massage.rs b/xtask/src/cmds/massage.rs similarity index 100% rename from xtask/src/massage.rs rename to xtask/src/cmds/massage.rs diff --git a/xtask/src/cmds/mod.rs b/xtask/src/cmds/mod.rs new file mode 100644 index 0000000..cb791ad --- /dev/null +++ b/xtask/src/cmds/mod.rs @@ -0,0 +1,7 @@ +//! Subcommand implementations for xtask + +pub mod act; +pub mod analyze_backtrace; +pub mod check_workspace; +pub mod massage; +pub mod spike_syscall_instcount; diff --git a/xtask/src/spike_syscall_instcount.rs b/xtask/src/cmds/spike_syscall_instcount.rs similarity index 100% rename from xtask/src/spike_syscall_instcount.rs rename to xtask/src/cmds/spike_syscall_instcount.rs diff --git a/xtask/src/main.rs b/xtask/src/main.rs index fc93398..e961f35 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,9 +1,6 @@ -mod act; -mod check_workspace; +mod cmds; mod findup; -mod massage; mod sh; -mod spike_syscall_instcount; use clap::{Parser, Subcommand}; @@ -20,26 +17,30 @@ struct Cli { #[derive(Subcommand)] enum Command { /// Run the 'massage' task - Massage(massage::MassageArgs), + Massage(cmds::massage::MassageArgs), /// Run a curated matrix of cargo commands (targets/features) from config Matrix(cargo_matrix::MatrixArgs), /// Run GitHub Actions locally via `act` (forwards all args to the `act` CLI) - Act(act::ActArgs), + Act(cmds::act::ActArgs), /// Measure syscall instruction-count "cost" using Spike commit logs. #[command(name = "spike-syscall-instcount")] - SpikeSyscallInstCount(spike_syscall_instcount::SpikeSyscallInstCountArgs), + SpikeSyscallInstCount(cmds::spike_syscall_instcount::SpikeSyscallInstCountArgs), /// Check workspace consistency (versions, dependencies) #[command(name = "check-workspace")] - CheckWorkspace(check_workspace::CheckWorkspaceArgs), + CheckWorkspace(cmds::check_workspace::CheckWorkspaceArgs), + /// Analyze binary sizes for different backtrace modes + #[command(name = "analyze-backtrace")] + AnalyzeBacktrace(cmds::analyze_backtrace::AnalyzeBacktraceArgs), } fn run(cli: Cli) -> Result<(), Box> { match cli.command { - Command::Massage(args) => massage::run(args), + Command::Massage(args) => cmds::massage::run(args), Command::Matrix(args) => cargo_matrix::run(args).map_err(|e| e.into()), - Command::Act(args) => act::run(args), - Command::SpikeSyscallInstCount(args) => spike_syscall_instcount::run(args), - Command::CheckWorkspace(args) => check_workspace::run(args).map_err(|e| e.into()), + Command::Act(args) => cmds::act::run(args), + Command::SpikeSyscallInstCount(args) => cmds::spike_syscall_instcount::run(args), + Command::CheckWorkspace(args) => cmds::check_workspace::run(args).map_err(|e| e.into()), + Command::AnalyzeBacktrace(args) => cmds::analyze_backtrace::run(args).map_err(|e| e.into()), } }