From d912612dc757116e3d5f059bc61ad7ac6d2be069 Mon Sep 17 00:00:00 2001 From: Niu Zhihong Date: Tue, 22 Sep 2026 15:23:30 +0800 Subject: [PATCH 1/8] =?UTF-8?q?feat(ostool):=20=E5=A2=9E=E5=8A=A0=E5=8F=AF?= =?UTF-8?q?=E9=80=89=20ELF=20=E8=B0=83=E8=AF=95=E4=BA=A7=E7=89=A9=E4=B8=8E?= =?UTF-8?q?=E5=85=83=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增默认关闭的 artifacts.analysis 配置,复用 Rust 工具链 LLVM 和现有 artifact registry 生成反汇编、ELF 信息与符号列表。通过 object 解析架构、 入口、PT_LOAD 与可选 __executable_start,显式请求失败时返回具体错误。 分离原始 ELF 和运行时副本,避免 .elf 输入原地覆盖;添加 BIN 时保留 符号来源与调试产物登记,新构建则清理旧登记。完整 BuildConfig 接入 构建与运行入口,保留 Cargo 选择、hooks 和 Custom runner 配置读取顺序。 同步中英文 README 与公开 API 编译用例。 验证:devbox 原生格式化、workspace clippy/build/test 全部通过; ostool all-targets/all-features clippy -D warnings 通过。新增实际 LLVM 产物、ELF metadata、状态更新和 CLI hook 回归测试。 x86_64 CI 命令已尝试,本地因缺少交叉 sysroot/libudev 与 C 编译器阻塞。 Co-authored-by: OpenAI Codex Signed-off-by: Niu Zhihong --- README.en.md | 53 +++ README.md | 28 ++ ostool/src/artifact/analysis.rs | 491 ++++++++++++++++++++++ ostool/src/artifact/elf_metadata.rs | 343 +++++++++++++++ ostool/src/artifact/llvm_tools.rs | 25 +- ostool/src/artifact/mod.rs | 2 + ostool/src/artifact/runtime.rs | 16 +- ostool/src/artifact/state.rs | 24 +- ostool/src/board/config.rs | 1 + ostool/src/board/mod.rs | 4 +- ostool/src/build/analysis_tests.rs | 359 ++++++++++++++++ ostool/src/build/config.rs | 68 ++- ostool/src/build/mod.rs | 126 +++++- ostool/src/invocation.rs | 15 +- ostool/src/lib.rs | 2 +- ostool/src/main.rs | 34 +- ostool/src/run/qemu.rs | 1 + ostool/src/run/uboot.rs | 2 + ostool/tests/analysis_cli.rs | 68 +++ ostool/tests/ui/pass_module_level_apis.rs | 4 + 20 files changed, 1601 insertions(+), 65 deletions(-) create mode 100644 ostool/src/artifact/analysis.rs create mode 100644 ostool/src/artifact/elf_metadata.rs create mode 100644 ostool/src/build/analysis_tests.rs create mode 100644 ostool/tests/analysis_cli.rs diff --git a/README.en.md b/README.en.md index 03e53a9a..930c52ea 100644 --- a/README.en.md +++ b/README.en.md @@ -232,6 +232,59 @@ Command-line `--package`/`--bin`/`--test` overrides are applied to the final Car selection before `${package}` variable expansion and someboot `build-info.toml` argument injection. +#### ELF Analysis Artifacts + +Enable the analysis outputs you need at the root of `.build.toml`. All three +switches default to `false`; when disabled, ostool invokes no analysis tools and +registers no analysis files. + +```toml +[artifacts.analysis] +disassembly = true +elf_info = true +symbols = true +``` + +Analysis runs after the existing post-build hooks and always reads the same +preserved original ELF. For Cargo builds, this is Cargo's selected executable, so +it honors `package`, `bin`, `test`, and `profile`; for custom builds, it is +`elf_path`. Cargo may already have copied or converted a runtime artifact, but +analysis still uses this original source, so all three outputs describe the +original input rather than a runtime copy. + +Files are written beside that original ELF with stable suffixes appended to its +full source filename: `.disassembly`, `.elf-info`, and +`.symbols`. They are independent of `--bin-dir`. Each build regenerates the +requested files and refreshes this build's artifact registry; disabling a switch +later does not delete an older corresponding file from disk. + +`elf-info` reports the architecture and entry address parsed from the ELF object, every `PT_LOAD` +entry (including physical and virtual addresses), an optional +`__executable_start`, and detailed `llvm-readobj` output. If the ELF has no +needed symbol, the output says so explicitly and the build still succeeds. + +This feature uses LLVM tools from the existing Rust toolchain. If they are +missing, install them with `rustup component add llvm-tools`; no `cargo-binutils` +wrapper is required. A missing tool, nonzero tool exit, or malformed ELF fails +the build before the runner starts. + +In the public Rust API, +`build::run_with_config(invocation, &BuildConfig, config_path, &CargoRunnerKind)` +accepts the complete configuration and reads this root configuration for both +Cargo and Custom builds. `build::prepare_with_config` completes the build, runtime +ELF/BIN preparation, and analysis before callers load their runner configuration +and run it. Existing `cargo_build` and `cargo_run` remain +Cargo-only helper APIs and use the default analysis configuration. When +constructing a `BuildConfig` literal directly, add `artifacts: Default::default()` +or use `..Default::default()`: + +```rust +let config = BuildConfig { + system, + artifacts: Default::default(), +}; +``` + #### Custom Build System Example ```toml diff --git a/README.md b/README.md index 4cd58589..ef2c6e1e 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,34 @@ to_bin = false 命令行 `--package`/`--bin`/`--test` 会先覆盖 `.build.toml` 中的 Cargo 包/目标选择,再用于 `${package}` 变量展开和 someboot `build-info.toml` 自动参数注入。 +#### ELF 分析产物 + +在 `.build.toml` 根级启用需要的分析产物。三个开关均默认为 `false`;未启用时不会调用分析工具,也不会登记分析文件。 + +```toml +[artifacts.analysis] +disassembly = true +elf_info = true +symbols = true +``` + +分析在既有的构建后钩子完成后执行,并始终读取同一个保留的原始 ELF:Cargo 构建使用 Cargo 解析出的已选可执行文件(因此会遵循 `package`、`bin`、`test` 和 `profile`),自定义构建使用 `elf_path`。Cargo 的运行时产物可能已完成复制或转换,但分析仍使用该原始源文件,因此三类分析产物描述的是原始输入,而不是运行时副本。 + +文件与该原始 ELF 放在同一目录,且在完整源文件名后追加稳定后缀:`.disassembly`、`.elf-info` 和 `.symbols`。它们不受 `--bin-dir` 影响。每次构建都会重新生成当前请求的文件并刷新本次产物登记;后来关闭某一开关不会删除磁盘上已有的对应文件。 + +`elf-info` 给出从 ELF 对象解析出的架构、入口地址、各 `PT_LOAD` 段(包含物理和虚拟地址),以及存在时的 `__executable_start`,并附加 `llvm-readobj` 的详细输出。若 ELF 没有需要的符号,会在输出中明确说明而不是使构建失败。 + +这些功能使用现有 Rust 工具链中的 LLVM 工具;若缺少它们,运行 `rustup component add llvm-tools` 安装。无需安装 `cargo-binutils` 包装器。工具缺失、工具返回非零,或输入不是可解析的 ELF 时,构建会在启动 runner 前失败。 + +公开 Rust API 中,`build::run_with_config(invocation, &BuildConfig, config_path, &CargoRunnerKind)` 接受完整配置,并为 Cargo 和 Custom 构建读取此根级配置。`build::prepare_with_config` 可先完成构建、运行时 ELF/BIN 与分析,再由调用方加载 runner 配置并运行。既有的 `cargo_build` 与 `cargo_run` 仍是 Cargo 专用辅助 API,并使用默认的分析配置。直接构造 `BuildConfig` 字面量时,请显式添加 `artifacts: Default::default()`,或使用 `..Default::default()`: + +```rust +let config = BuildConfig { + system, + artifacts: Default::default(), +}; +``` + #### 自定义构建系统示例 ```toml diff --git a/ostool/src/artifact/analysis.rs b/ostool/src/artifact/analysis.rs new file mode 100644 index 00000000..0500ae60 --- /dev/null +++ b/ostool/src/artifact/analysis.rs @@ -0,0 +1,491 @@ +//! Human-readable ELF analysis artifact generation. + +use std::{ + ffi::OsString, + fs, + path::{Path, PathBuf}, + process::Output, +}; + +use anyhow::{Context, anyhow, bail}; + +use crate::{ + artifact::{ + elf_metadata::ElfMetadata, + llvm_tools, + state::{DebugArtifactKind, DebugArtifactRegistry}, + }, + build::config::AnalysisConfig, + process::ProcessContext, +}; + +/// Generates every requested analysis artifact beside `source_elf`. +/// +/// Outputs are staged in sibling paths and registered only after all succeed. +/// A failure removes both temporary paths and any stale requested analysis outputs. +pub(crate) fn generate( + context: &ProcessContext, + config: &AnalysisConfig, + source_elf: &Path, +) -> anyhow::Result { + generate_with_tool_resolver(context, config, source_elf, llvm_tools::llvm_tool) +} + +fn generate_with_tool_resolver( + context: &ProcessContext, + config: &AnalysisConfig, + source_elf: &Path, + mut resolve_tool: F, +) -> anyhow::Result +where + F: FnMut(&str) -> anyhow::Result, +{ + if !config.is_enabled() { + return Ok(DebugArtifactRegistry::default()); + } + + let requested = requested_artifacts(config, source_elf)?; + let temporary = requested + .iter() + .map(|artifact| artifact.temporary_path()) + .collect::>(); + + let result = generate_requested_artifacts(context, source_elf, &requested, &mut resolve_tool) + .and_then(|contents| publish_artifacts(&requested, &temporary, contents)); + + match result { + Ok(registry) => Ok(registry), + Err(error) => { + remove_paths( + temporary + .iter() + .chain(requested.iter().map(|artifact| &artifact.path)), + ); + Err(error) + } + } +} + +#[derive(Clone)] +struct RequestedArtifact { + kind: DebugArtifactKind, + path: PathBuf, +} + +impl RequestedArtifact { + fn temporary_path(&self) -> PathBuf { + let mut name = OsString::from("."); + name.push( + self.path + .file_name() + .expect("analysis artifact has a file name"), + ); + name.push(format!(".{}.tmp", std::process::id())); + self.path.with_file_name(name) + } +} + +fn requested_artifacts( + config: &AnalysisConfig, + source_elf: &Path, +) -> anyhow::Result> { + if source_elf.file_name().is_none() { + bail!("invalid ELF file path: {}", source_elf.display()); + } + + let mut artifacts = Vec::new(); + if config.disassembly { + artifacts.push(RequestedArtifact { + kind: DebugArtifactKind::Disassembly, + path: output_path(source_elf, "disassembly"), + }); + } + if config.elf_info { + artifacts.push(RequestedArtifact { + kind: DebugArtifactKind::ElfInfo, + path: output_path(source_elf, "elf-info"), + }); + } + if config.symbols { + artifacts.push(RequestedArtifact { + kind: DebugArtifactKind::Symbols, + path: output_path(source_elf, "symbols"), + }); + } + Ok(artifacts) +} + +fn generate_requested_artifacts( + context: &ProcessContext, + source_elf: &Path, + requested: &[RequestedArtifact], + resolve_tool: &mut F, +) -> anyhow::Result>> +where + F: FnMut(&str) -> anyhow::Result, +{ + let metadata = elf_metadata(source_elf)?; + + let mut contents = Vec::with_capacity(requested.len()); + for artifact in requested { + let (tool_name, args) = match artifact.kind { + DebugArtifactKind::Disassembly => ("llvm-objdump", vec!["--disassemble"]), + DebugArtifactKind::ElfInfo => ("llvm-readobj", vec!["--all"]), + DebugArtifactKind::Symbols => ( + "llvm-nm", + vec!["--demangle", "--print-size", "--numeric-sort"], + ), + }; + let tool = resolve_tool(tool_name).with_context(|| { + format!( + "failed to resolve {tool_name} for ELF {}", + source_elf.display() + ) + })?; + let stdout = run_tool(context, &tool, tool_name, &args, source_elf)?; + + if artifact.kind == DebugArtifactKind::ElfInfo { + let mut elf_info = metadata_summary(&metadata); + elf_info.extend_from_slice(b"\n\nllvm-readobj output:\n"); + elf_info.extend_from_slice(&stdout); + contents.push(elf_info); + } else { + contents.push(stdout); + } + } + Ok(contents) +} + +fn elf_metadata(source_elf: &Path) -> anyhow::Result { + let bytes = fs::read(source_elf) + .with_context(|| format!("failed to read ELF file: {}", source_elf.display()))?; + ElfMetadata::parse(&bytes) + .with_context(|| format!("failed to parse ELF file: {}", source_elf.display())) +} + +fn run_tool( + context: &ProcessContext, + program: &Path, + tool_name: &str, + args: &[&str], + source_elf: &Path, +) -> anyhow::Result> { + let mut command = crate::process::command(program, context).into_std(); + command.args(args).arg(source_elf); + let output = command.output().with_context(|| { + format!( + "failed to execute {tool_name} for ELF {}", + source_elf.display() + ) + })?; + successful_stdout(tool_name, source_elf, output) +} + +fn successful_stdout( + tool_name: &str, + source_elf: &Path, + output: Output, +) -> anyhow::Result> { + if output.status.success() { + return Ok(output.stdout); + } + + let stderr = String::from_utf8_lossy(&output.stderr); + Err(anyhow!( + "{tool_name} failed for ELF {} with status {}: {}", + source_elf.display(), + output.status, + stderr.trim() + )) +} + +fn publish_artifacts( + requested: &[RequestedArtifact], + temporary: &[PathBuf], + contents: Vec>, +) -> anyhow::Result { + for (path, content) in temporary.iter().zip(contents) { + fs::write(path, content) + .with_context(|| format!("failed to write analysis artifact: {}", path.display()))?; + } + + let mut registry = DebugArtifactRegistry::default(); + for (artifact, temporary) in requested.iter().zip(temporary) { + if artifact.path.exists() { + fs::remove_file(&artifact.path).with_context(|| { + format!( + "failed to replace analysis artifact: {}", + artifact.path.display() + ) + })?; + } + fs::rename(temporary, &artifact.path).with_context(|| { + format!( + "failed to publish analysis artifact: {}", + artifact.path.display() + ) + })?; + registry.register(artifact.kind, artifact.path.clone()); + } + Ok(registry) +} + +fn remove_paths<'a>(paths: impl Iterator) { + for path in paths { + if let Err(error) = fs::remove_file(path) + && error.kind() != std::io::ErrorKind::NotFound + { + log::warn!( + "failed to remove analysis artifact {}: {error}", + path.display() + ); + } + } +} + +fn metadata_summary(metadata: &ElfMetadata) -> Vec { + let mut summary = format!( + "ELF metadata\narchitecture: {:?}\nentry: 0x{:x}\nload segments:\n", + metadata.arch, metadata.entry + ); + if metadata.load_segments.is_empty() { + summary.push_str(" absent\n"); + } else { + for segment in &metadata.load_segments { + summary.push_str(&format!( + " virtual_address: 0x{:x}\n physical_address: 0x{:x}\n file_offset: 0x{:x}\n file_size: 0x{:x}\n memory_size: 0x{:x}\n alignment: 0x{:x}\n flags: 0x{:x}\n", + segment.virtual_address, + segment.physical_address, + segment.file_offset, + segment.file_size, + segment.memory_size, + segment.alignment, + segment.flags, + )); + } + } + match metadata.executable_start { + Some(address) => summary.push_str(&format!("executable_start: 0x{address:x}\n")), + None => summary.push_str("executable_start: absent\n"), + } + summary.into_bytes() +} + +fn output_path(source_elf: &Path, suffix: &str) -> PathBuf { + let mut name = source_elf.file_name().unwrap_or_default().to_os_string(); + name.push("."); + name.push(suffix); + source_elf.with_file_name(name) +} + +#[cfg(test)] +mod tests { + use std::{ + fs, + path::{Path, PathBuf}, + }; + + use crate::{ + artifact::{analysis::generate_with_tool_resolver, state::DebugArtifactKind}, + build::config::AnalysisConfig, + process::ProcessContext, + project::{resolve_project_layout, variables::VariableScope}, + }; + use anyhow::anyhow; + + fn process_context(root: &std::path::Path) -> ProcessContext { + std::fs::write( + root.join("Cargo.toml"), + "[package]\nname = \"sample\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", + ) + .unwrap(); + std::fs::create_dir_all(root.join("src")).unwrap(); + std::fs::write(root.join("src/lib.rs"), "").unwrap(); + + let layout = resolve_project_layout(Some(root.to_path_buf())).unwrap(); + let scope = VariableScope::for_package(&layout, root.to_path_buf()); + ProcessContext::new(root.to_path_buf(), root.to_path_buf(), scope, None) + } + + fn write_elf(path: &Path) { + // Minimal ELF64 with no sections or program headers. + let mut elf = [0u8; 64]; + elf[..7].copy_from_slice(b"\x7fELF\x02\x01\x01"); + elf[16] = 2; // ET_EXEC + elf[18] = 62; // EM_X86_64 + elf[20] = 1; + elf[52] = 64; + fs::write(path, elf).unwrap(); + } + + #[test] + fn disabled_analysis_does_not_read_the_elf_or_lookup_tools() { + let temp = tempfile::tempdir().unwrap(); + let context = process_context(temp.path()); + let missing_source = temp.path().join("missing kernel.elf"); + + let registry = generate_with_tool_resolver( + &context, + &AnalysisConfig::default(), + &missing_source, + |_| panic!("disabled analysis must not resolve any tools"), + ) + .unwrap(); + + assert!(registry.is_empty()); + } + + #[test] + fn artifact_output_paths_append_full_source_filename() { + let source = PathBuf::from("output with spaces/kernel.elf"); + + assert_eq!( + super::output_path(&source, "disassembly"), + PathBuf::from("output with spaces/kernel.elf.disassembly") + ); + assert_eq!( + super::output_path(&source, "elf-info"), + PathBuf::from("output with spaces/kernel.elf.elf-info") + ); + } + + #[cfg(unix)] + #[test] + fn artifact_names_preserve_non_utf8_source_names() { + use std::{ffi::OsString, os::unix::ffi::OsStringExt}; + let source = PathBuf::from(OsString::from_vec(b"kernel-\xff.elf".to_vec())); + let expected = PathBuf::from(OsString::from_vec(b"kernel-\xff.elf.symbols".to_vec())); + assert_eq!(super::output_path(&source, "symbols"), expected); + } + + #[cfg(unix)] + fn make_executable(path: &Path) { + use std::os::unix::fs::PermissionsExt; + + let mut permissions = fs::metadata(path).unwrap().permissions(); + permissions.set_mode(0o755); + fs::set_permissions(path, permissions).unwrap(); + } + + #[cfg(unix)] + fn fake_tool(root: &Path, name: &str, body: &str) -> PathBuf { + let path = root.join(name); + fs::write(&path, format!("#!/bin/sh\n{body}\n")).unwrap(); + make_executable(&path); + path + } + + #[cfg(unix)] + #[test] + fn missing_tool_clears_stale_requested_output() { + let temp = tempfile::tempdir().unwrap(); + let context = process_context(temp.path()); + let source = temp.path().join("kernel with spaces.elf"); + write_elf(&source); + let stale = super::output_path(&source, "symbols"); + fs::write(&stale, "stale").unwrap(); + let config = AnalysisConfig { + symbols: true, + ..Default::default() + }; + + let error = generate_with_tool_resolver(&context, &config, &source, |_| { + Err(anyhow!("llvm-tools unavailable")) + }) + .unwrap_err(); + + assert!(error.to_string().contains("failed to resolve llvm-nm")); + assert!(!stale.exists()); + } + + #[cfg(unix)] + #[test] + fn tool_failure_reports_source_status_and_stderr_without_partial_outputs() { + let temp = tempfile::tempdir().unwrap(); + let context = process_context(temp.path()); + let source = temp.path().join("kernel with spaces.elf"); + write_elf(&source); + let disassembly = super::output_path(&source, "disassembly"); + let symbols = super::output_path(&source, "symbols"); + fs::write(&disassembly, "stale disassembly").unwrap(); + fs::write(&symbols, "stale symbols").unwrap(); + let objdump = fake_tool(temp.path(), "objdump", "printf disassembly"); + let nm = fake_tool(temp.path(), "nm", "printf 'broken symbols' >&2\nexit 7"); + let config = AnalysisConfig { + disassembly: true, + symbols: true, + ..Default::default() + }; + + let error = generate_with_tool_resolver(&context, &config, &source, |tool| match tool { + "llvm-objdump" => Ok(objdump.clone()), + "llvm-nm" => Ok(nm.clone()), + unexpected => Err(anyhow!("unexpected tool: {unexpected}")), + }) + .unwrap_err(); + let message = format!("{error:#}"); + + assert!(message.contains("llvm-nm")); + assert!(message.contains(&source.display().to_string())); + assert!(message.contains("exit status: 7")); + assert!(message.contains("broken symbols")); + assert!(!disassembly.exists()); + assert!(!symbols.exists()); + } + + #[cfg(unix)] + #[test] + fn successful_generation_overwrites_output_and_registers_it() { + let temp = tempfile::tempdir().unwrap(); + let context = process_context(temp.path()); + let source = temp.path().join("kernel with spaces.elf"); + write_elf(&source); + let output = super::output_path(&source, "disassembly"); + fs::write(&output, "stale").unwrap(); + let objdump = fake_tool(temp.path(), "objdump", "printf 'fresh disassembly'"); + let config = AnalysisConfig { + disassembly: true, + ..Default::default() + }; + + let registry = + generate_with_tool_resolver(&context, &config, &source, |_| Ok(objdump.clone())) + .unwrap(); + + assert_eq!(fs::read_to_string(&output).unwrap(), "fresh disassembly"); + assert_eq!( + registry.get(DebugArtifactKind::Disassembly), + Some(output.as_path()) + ); + } + + #[cfg(unix)] + #[test] + fn invalid_elf_fails_before_any_tool_is_resolved() { + let temp = tempfile::tempdir().unwrap(); + let context = process_context(temp.path()); + let source = temp.path().join("invalid.elf"); + fs::write(&source, "not an ELF").unwrap(); + for config in [ + AnalysisConfig { + disassembly: true, + ..Default::default() + }, + AnalysisConfig { + elf_info: true, + ..Default::default() + }, + AnalysisConfig { + symbols: true, + ..Default::default() + }, + ] { + let error = generate_with_tool_resolver(&context, &config, &source, |_| { + panic!("invalid ELF must be rejected before tool lookup") + }) + .unwrap_err(); + assert!(format!("{error:#}").contains("failed to parse ELF file")); + } + assert!(!super::output_path(&source, "elf-info").exists()); + } +} diff --git a/ostool/src/artifact/elf_metadata.rs b/ostool/src/artifact/elf_metadata.rs new file mode 100644 index 00000000..528a04e1 --- /dev/null +++ b/ostool/src/artifact/elf_metadata.rs @@ -0,0 +1,343 @@ +use anyhow::{Context, bail}; +use object::{ + Object, ObjectSegment, ObjectSymbol, + read::elf::{ElfFile, FileHeader, ProgramHeader}, +}; + +pub(crate) struct ElfMetadata { + pub(crate) arch: object::Architecture, + pub(crate) entry: u64, + pub(crate) load_segments: Vec, + pub(crate) executable_start: Option, +} + +pub(crate) struct LoadSegment { + pub(crate) virtual_address: u64, + pub(crate) physical_address: u64, + pub(crate) file_offset: u64, + pub(crate) file_size: u64, + pub(crate) memory_size: u64, + pub(crate) alignment: u64, + pub(crate) flags: u32, +} + +impl ElfMetadata { + pub(crate) fn parse(data: &[u8]) -> anyhow::Result { + let file = object::File::parse(data).context("failed to parse ELF")?; + + match file { + object::File::Elf32(file) => Self::from_elf(&file), + object::File::Elf64(file) => Self::from_elf(&file), + _ => bail!("expected an ELF file"), + } + } + + fn from_elf(file: &ElfFile<'_, Elf>) -> anyhow::Result + where + Elf: FileHeader, + { + let endian = file.endian(); + let load_segments = file + .segments() + .map(|segment| { + let header = segment.elf_program_header(); + LoadSegment { + virtual_address: segment.address(), + physical_address: header.p_paddr(endian).into(), + file_offset: header.p_offset(endian).into(), + file_size: header.p_filesz(endian).into(), + memory_size: header.p_memsz(endian).into(), + alignment: header.p_align(endian).into(), + flags: header.p_flags(endian), + } + }) + .collect(); + + let mut executable_start = None; + for symbol in file.symbols() { + if symbol.name()? == "__executable_start" && symbol.is_definition() { + executable_start = Some(symbol.address()); + break; + } + } + + Ok(Self { + arch: file.architecture(), + entry: file.entry(), + load_segments, + executable_start, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use object::Architecture; + + #[derive(Clone, Copy)] + enum Endian { + Little, + Big, + } + + fn write_u16(data: &mut [u8], offset: usize, value: u16, endian: Endian) { + let bytes = match endian { + Endian::Little => value.to_le_bytes(), + Endian::Big => value.to_be_bytes(), + }; + data[offset..offset + bytes.len()].copy_from_slice(&bytes); + } + + fn write_u32(data: &mut [u8], offset: usize, value: u32, endian: Endian) { + let bytes = match endian { + Endian::Little => value.to_le_bytes(), + Endian::Big => value.to_be_bytes(), + }; + data[offset..offset + bytes.len()].copy_from_slice(&bytes); + } + + fn write_u64(data: &mut [u8], offset: usize, value: u64, endian: Endian) { + let bytes = match endian { + Endian::Little => value.to_le_bytes(), + Endian::Big => value.to_be_bytes(), + }; + data[offset..offset + bytes.len()].copy_from_slice(&bytes); + } + + fn elf64_with_symbol(symbol_defined: bool) -> Vec { + const PROGRAM_HEADER_OFFSET: usize = 64; + const STRING_TABLE_OFFSET: usize = 0x180; + const SYMBOL_TABLE_OFFSET: usize = 0x1c0; + const SECTION_HEADER_OFFSET: usize = 0x200; + const STRING_TABLE: &[u8] = b"\0__executable_start\0"; + + let mut data = vec![0; 0x300]; + data[..4].copy_from_slice(b"\x7fELF"); + data[4] = 2; + data[5] = 1; + data[6] = 1; + + write_u16(&mut data, 16, 2, Endian::Little); + write_u16(&mut data, 18, 183, Endian::Little); + write_u32(&mut data, 20, 1, Endian::Little); + write_u64(&mut data, 24, 0x400080, Endian::Little); + write_u64(&mut data, 32, PROGRAM_HEADER_OFFSET as u64, Endian::Little); + write_u64(&mut data, 40, SECTION_HEADER_OFFSET as u64, Endian::Little); + write_u16(&mut data, 52, 64, Endian::Little); + write_u16(&mut data, 54, 56, Endian::Little); + write_u16(&mut data, 56, 3, Endian::Little); + write_u16(&mut data, 58, 64, Endian::Little); + write_u16(&mut data, 60, 3, Endian::Little); + write_u16(&mut data, 62, 1, Endian::Little); + + write_u32(&mut data, PROGRAM_HEADER_OFFSET, 1, Endian::Little); + write_u32(&mut data, PROGRAM_HEADER_OFFSET + 4, 5, Endian::Little); + write_u64(&mut data, PROGRAM_HEADER_OFFSET + 8, 0x100, Endian::Little); + write_u64( + &mut data, + PROGRAM_HEADER_OFFSET + 16, + 0x400000, + Endian::Little, + ); + write_u64( + &mut data, + PROGRAM_HEADER_OFFSET + 24, + 0x500000, + Endian::Little, + ); + write_u64(&mut data, PROGRAM_HEADER_OFFSET + 32, 0x30, Endian::Little); + write_u64(&mut data, PROGRAM_HEADER_OFFSET + 40, 0x40, Endian::Little); + write_u64( + &mut data, + PROGRAM_HEADER_OFFSET + 48, + 0x1000, + Endian::Little, + ); + + let second_program_header = PROGRAM_HEADER_OFFSET + 56; + write_u32(&mut data, second_program_header, 1, Endian::Little); + write_u32(&mut data, second_program_header + 4, 6, Endian::Little); + write_u64(&mut data, second_program_header + 8, 0x200, Endian::Little); + write_u64( + &mut data, + second_program_header + 16, + 0x401000, + Endian::Little, + ); + write_u64( + &mut data, + second_program_header + 24, + 0x501000, + Endian::Little, + ); + write_u64(&mut data, second_program_header + 32, 0x20, Endian::Little); + write_u64(&mut data, second_program_header + 40, 0x30, Endian::Little); + write_u64( + &mut data, + second_program_header + 48, + 0x1000, + Endian::Little, + ); + + let note_program_header = second_program_header + 56; + write_u32(&mut data, note_program_header, 4, Endian::Little); + write_u32(&mut data, note_program_header + 4, 4, Endian::Little); + write_u64(&mut data, note_program_header + 8, 0x280, Endian::Little); + write_u64( + &mut data, + note_program_header + 16, + 0x402000, + Endian::Little, + ); + write_u64( + &mut data, + note_program_header + 24, + 0x502000, + Endian::Little, + ); + write_u64(&mut data, note_program_header + 32, 0x10, Endian::Little); + write_u64(&mut data, note_program_header + 40, 0x10, Endian::Little); + write_u64(&mut data, note_program_header + 48, 4, Endian::Little); + + data[STRING_TABLE_OFFSET..STRING_TABLE_OFFSET + STRING_TABLE.len()] + .copy_from_slice(STRING_TABLE); + + let symbol = SYMBOL_TABLE_OFFSET + 24; + write_u32(&mut data, symbol, 1, Endian::Little); + data[symbol + 4] = 0x10; + write_u16( + &mut data, + symbol + 6, + u16::from(symbol_defined), + Endian::Little, + ); + write_u64(&mut data, symbol + 8, 0x400080, Endian::Little); + + let string_table_section = SECTION_HEADER_OFFSET + 64; + write_u32(&mut data, string_table_section + 4, 3, Endian::Little); + write_u64( + &mut data, + string_table_section + 24, + STRING_TABLE_OFFSET as u64, + Endian::Little, + ); + write_u64( + &mut data, + string_table_section + 32, + STRING_TABLE.len() as u64, + Endian::Little, + ); + write_u64(&mut data, string_table_section + 48, 1, Endian::Little); + + let symbol_table_section = SECTION_HEADER_OFFSET + 128; + write_u32(&mut data, symbol_table_section + 4, 2, Endian::Little); + write_u64( + &mut data, + symbol_table_section + 24, + SYMBOL_TABLE_OFFSET as u64, + Endian::Little, + ); + write_u64(&mut data, symbol_table_section + 32, 48, Endian::Little); + write_u32(&mut data, symbol_table_section + 40, 1, Endian::Little); + write_u64(&mut data, symbol_table_section + 48, 8, Endian::Little); + write_u64(&mut data, symbol_table_section + 56, 24, Endian::Little); + + data + } + + fn elf32_big_endian_without_symbols() -> Vec { + const PROGRAM_HEADER_OFFSET: usize = 52; + + let mut data = vec![0; 0xa0]; + data[..4].copy_from_slice(b"\x7fELF"); + data[4] = 1; + data[5] = 2; + data[6] = 1; + + write_u16(&mut data, 16, 2, Endian::Big); + write_u16(&mut data, 18, 40, Endian::Big); + write_u32(&mut data, 20, 1, Endian::Big); + write_u32(&mut data, 24, 0x8004, Endian::Big); + write_u32(&mut data, 28, PROGRAM_HEADER_OFFSET as u32, Endian::Big); + write_u16(&mut data, 40, 52, Endian::Big); + write_u16(&mut data, 42, 32, Endian::Big); + write_u16(&mut data, 44, 1, Endian::Big); + + write_u32(&mut data, PROGRAM_HEADER_OFFSET, 1, Endian::Big); + write_u32(&mut data, PROGRAM_HEADER_OFFSET + 4, 0x80, Endian::Big); + write_u32(&mut data, PROGRAM_HEADER_OFFSET + 8, 0x8000, Endian::Big); + write_u32(&mut data, PROGRAM_HEADER_OFFSET + 12, 0x9000, Endian::Big); + write_u32(&mut data, PROGRAM_HEADER_OFFSET + 16, 0x10, Endian::Big); + write_u32(&mut data, PROGRAM_HEADER_OFFSET + 20, 0x20, Endian::Big); + write_u32(&mut data, PROGRAM_HEADER_OFFSET + 24, 6, Endian::Big); + write_u32(&mut data, PROGRAM_HEADER_OFFSET + 28, 0x1000, Endian::Big); + + data + } + + #[test] + fn parses_elf64_load_segments_and_executable_start_symbol() { + let metadata = ElfMetadata::parse(&elf64_with_symbol(true)).unwrap(); + + assert_eq!(metadata.arch, Architecture::Aarch64); + assert_eq!(metadata.entry, 0x400080); + assert_eq!(metadata.executable_start, Some(0x400080)); + assert_eq!(metadata.load_segments.len(), 2); + + let first = &metadata.load_segments[0]; + assert_eq!(first.virtual_address, 0x400000); + assert_eq!(first.physical_address, 0x500000); + assert_eq!(first.file_offset, 0x100); + assert_eq!(first.file_size, 0x30); + assert_eq!(first.memory_size, 0x40); + assert_eq!(first.alignment, 0x1000); + assert_eq!(first.flags, 5); + + let second = &metadata.load_segments[1]; + assert_eq!(second.virtual_address, 0x401000); + assert_eq!(second.physical_address, 0x501000); + assert_eq!(second.file_offset, 0x200); + assert_eq!(second.file_size, 0x20); + assert_eq!(second.memory_size, 0x30); + assert_eq!(second.alignment, 0x1000); + assert_eq!(second.flags, 6); + } + + #[test] + fn parses_big_endian_elf32_without_executable_start_symbol() { + let metadata = ElfMetadata::parse(&elf32_big_endian_without_symbols()).unwrap(); + + assert_eq!(metadata.arch, Architecture::Arm); + assert_eq!(metadata.entry, 0x8004); + assert_eq!(metadata.executable_start, None); + assert_eq!(metadata.load_segments.len(), 1); + + let segment = &metadata.load_segments[0]; + assert_eq!(segment.virtual_address, 0x8000); + assert_eq!(segment.physical_address, 0x9000); + assert_eq!(segment.file_offset, 0x80); + assert_eq!(segment.file_size, 0x10); + assert_eq!(segment.memory_size, 0x20); + assert_eq!(segment.alignment, 0x1000); + assert_eq!(segment.flags, 6); + } + + #[test] + fn treats_an_undefined_executable_start_symbol_as_absent() { + let metadata = ElfMetadata::parse(&elf64_with_symbol(false)).unwrap(); + + assert_eq!(metadata.executable_start, None); + } + + #[test] + fn rejects_non_elf_input() { + assert!(ElfMetadata::parse(b"not an ELF").is_err()); + } + + #[test] + fn rejects_truncated_elf_header() { + assert!(ElfMetadata::parse(b"\x7fELF\x02\x01\x01").is_err()); + } +} diff --git a/ostool/src/artifact/llvm_tools.rs b/ostool/src/artifact/llvm_tools.rs index d073987f..b4716b55 100644 --- a/ostool/src/artifact/llvm_tools.rs +++ b/ostool/src/artifact/llvm_tools.rs @@ -58,27 +58,32 @@ fn rustlib_tool(rustc: &OsStr, tool: &str) -> anyhow::Result { Ok(path) } -fn llvm_objcopy_with_rustc(rustc: &OsStr) -> anyhow::Result { - let path = rustlib_tool(rustc, "llvm-objcopy")?; +fn llvm_tool_with_rustc(rustc: &OsStr, tool: &str) -> anyhow::Result { + let path = rustlib_tool(rustc, tool)?; if !path.exists() { bail!( - "could not find toolchain llvm-objcopy at {}; install the Rust llvm-tools component", - path.display() + "could not find toolchain {tool} at {}; install the Rust llvm-tools component", + path.display(), ); } Ok(path) } -pub(crate) fn llvm_objcopy() -> anyhow::Result { +/// Locates an LLVM utility shipped by the active Rust toolchain. +pub(crate) fn llvm_tool(tool: &str) -> anyhow::Result { let rustc = rustc_program(); - llvm_objcopy_with_rustc(&rustc) + llvm_tool_with_rustc(&rustc, tool) +} + +pub(crate) fn llvm_objcopy() -> anyhow::Result { + llvm_tool("llvm-objcopy") } #[cfg(test)] mod tests { use std::{fs, path::Path}; - use super::llvm_objcopy_with_rustc; + use super::llvm_tool_with_rustc; #[cfg(unix)] fn make_executable(path: &Path) { @@ -91,7 +96,7 @@ mod tests { #[cfg(unix)] #[test] - fn missing_llvm_objcopy_error_mentions_llvm_tools_component() { + fn missing_llvm_tool_error_mentions_tool_and_llvm_tools_component() { let temp = tempfile::tempdir().unwrap(); let sysroot = temp.path().join("sysroot"); let fake_rustc = temp.path().join("fake-rustc"); @@ -112,10 +117,10 @@ mod tests { .unwrap(); make_executable(&fake_rustc); - let err = llvm_objcopy_with_rustc(fake_rustc.as_os_str()).unwrap_err(); + let err = llvm_tool_with_rustc(fake_rustc.as_os_str(), "llvm-readobj").unwrap_err(); let message = err.to_string(); - assert!(message.contains("could not find toolchain llvm-objcopy")); + assert!(message.contains("could not find toolchain llvm-readobj")); assert!(message.contains("install the Rust llvm-tools component")); } } diff --git a/ostool/src/artifact/mod.rs b/ostool/src/artifact/mod.rs index 75f27061..27bc0608 100644 --- a/ostool/src/artifact/mod.rs +++ b/ostool/src/artifact/mod.rs @@ -1,5 +1,7 @@ //! Runtime artifact preparation and state tracking. +pub(crate) mod analysis; +pub(crate) mod elf_metadata; pub(crate) mod llvm_tools; pub(crate) mod runtime; pub(crate) mod state; diff --git a/ostool/src/artifact/runtime.rs b/ostool/src/artifact/runtime.rs index 1e427641..0d0cd335 100644 --- a/ostool/src/artifact/runtime.rs +++ b/ostool/src/artifact/runtime.rs @@ -33,6 +33,7 @@ pub(crate) struct RuntimeArtifactOptions { /// Runtime artifacts prepared from a single input ELF. pub(crate) struct PreparedRuntimeArtifacts { + source_elf: PathBuf, elf: PathBuf, bin: Option, source_artifact_dir: PathBuf, @@ -42,6 +43,11 @@ pub(crate) struct PreparedRuntimeArtifacts { } impl PreparedRuntimeArtifacts { + /// Returns the original input, before runtime copying or stripping. + pub(crate) fn source_elf(&self) -> &Path { + &self.source_elf + } + /// Returns the runtime ELF path. pub(crate) fn elf(&self) -> &Path { &self.elf @@ -93,10 +99,11 @@ pub(crate) fn prepare_runtime_artifacts( let runtime_elf = if options.strip_elf { strip_runtime_elf(context, &input_elf, arch)? } else { - input_elf + input_elf.clone() }; let mut prepared = PreparedRuntimeArtifacts { + source_elf: input_elf, elf: runtime_elf.clone(), bin: None, source_artifact_dir: input_dir, @@ -126,7 +133,7 @@ fn strip_runtime_elf( elf_path: &Path, arch: Architecture, ) -> anyhow::Result { - let stripped_elf_path = elf_path.with_file_name( + let mut stripped_elf_path = elf_path.with_file_name( elf_path .file_stem() .ok_or_else(|| anyhow!("invalid ELF file path: {}", elf_path.display()))? @@ -134,6 +141,11 @@ fn strip_runtime_elf( .to_string() + ".elf", ); + if stripped_elf_path == elf_path { + let mut name = elf_path.as_os_str().to_os_string(); + name.push(".runtime.elf"); + stripped_elf_path = PathBuf::from(name); + } println!( "{}", format!( diff --git a/ostool/src/artifact/state.rs b/ostool/src/artifact/state.rs index 517b4cda..3f4a6991 100644 --- a/ostool/src/artifact/state.rs +++ b/ostool/src/artifact/state.rs @@ -12,19 +12,18 @@ use crate::artifact::runtime::PreparedRuntimeArtifacts; pub struct OutputArtifacts { cargo: Option, runtime: RuntimeArtifactState, - #[allow(dead_code)] debug: DebugArtifactRegistry, } #[derive(Clone, Debug)] struct CargoArtifactState { - #[allow(dead_code)] elf: PathBuf, artifact_dir: PathBuf, } #[derive(Default, Clone, Debug)] struct RuntimeArtifactState { + source_elf: Option, elf: Option, bin: Option, artifact_dir: Option, @@ -118,10 +117,12 @@ impl OutputArtifacts { /// Replaces artifact state from a prepared runtime artifact set. pub(crate) fn apply_prepared_runtime_artifacts(&mut self, prepared: &PreparedRuntimeArtifacts) { + self.debug = DebugArtifactRegistry::default(); + self.runtime.source_elf = Some(prepared.source_elf().to_path_buf()); self.cargo = prepared .cargo_source_artifact_dir() .map(|artifact_dir| CargoArtifactState { - elf: prepared.elf().to_path_buf(), + elf: prepared.source_elf().to_path_buf(), artifact_dir: artifact_dir.to_path_buf(), }); self.runtime.elf = Some(prepared.elf().to_path_buf()); @@ -130,6 +131,22 @@ impl OutputArtifacts { self.runtime.source_artifact_dir = prepared.cargo_artifact_dir().map(PathBuf::from); } + /// Returns the selected source ELF, preserving symbols lost in runtime derivatives. + pub(crate) fn analysis_source_elf(&self) -> Option<&Path> { + self.cargo_source_elf() + .or(self.runtime.source_elf.as_deref()) + } + + pub(crate) fn replace_debug_artifacts(&mut self, debug: DebugArtifactRegistry) { + self.debug = debug; + } + + /// Adding a BIN for a runner does not select a new source ELF. + pub(crate) fn apply_runtime_bin(&mut self, prepared: &PreparedRuntimeArtifacts) { + self.runtime.bin = prepared.bin().map(PathBuf::from); + self.runtime.artifact_dir = prepared.runtime_artifact_dir().map(PathBuf::from); + } + #[allow(dead_code)] pub(crate) fn debug_artifacts(&self) -> &DebugArtifactRegistry { &self.debug @@ -140,7 +157,6 @@ impl OutputArtifacts { self.debug.register(kind, path); } - #[cfg(test)] pub(crate) fn cargo_source_elf(&self) -> Option<&Path> { self.cargo.as_ref().map(|cargo| cargo.elf.as_path()) } diff --git a/ostool/src/board/config.rs b/ostool/src/board/config.rs index cc06c0a8..b6230c66 100644 --- a/ostool/src/board/config.rs +++ b/ostool/src/board/config.rs @@ -524,6 +524,7 @@ dtb_file = "${package}/board.dtb" crate::build::activate_build_config( &mut invocation, &BuildConfig { + artifacts: Default::default(), system: BuildSystem::Cargo(Box::new(Cargo { env: HashMap::new(), target: "aarch64-unknown-none".into(), diff --git a/ostool/src/board/mod.rs b/ostool/src/board/mod.rs index c3c7663a..1437e409 100644 --- a/ostool/src/board/mod.rs +++ b/ostool/src/board/mod.rs @@ -401,8 +401,7 @@ pub async fn run_board( board_config: &BoardRunConfig, options: RunBoardOptions, ) -> anyhow::Result<()> { - crate::build::prepare_runtime_artifacts(invocation, build_config, build_config_path, false) - .await?; + crate::build::prepare_with_config(invocation, build_config, build_config_path, false).await?; run_prepared_board(invocation, board_config, options).await } @@ -419,6 +418,7 @@ pub async fn cargo_run_board( run_board( invocation, &BuildConfig { + artifacts: Default::default(), system: BuildSystem::Cargo(Box::new(cargo.clone())), }, build_config_path, diff --git a/ostool/src/build/analysis_tests.rs b/ostool/src/build/analysis_tests.rs new file mode 100644 index 00000000..3726e4ea --- /dev/null +++ b/ostool/src/build/analysis_tests.rs @@ -0,0 +1,359 @@ +use std::{ + collections::{BTreeSet, HashMap}, + fs, + path::{Path, PathBuf}, + process::Command, +}; + +use crate::{ + artifact::state::DebugArtifactKind, + build::config::{ + AnalysisConfig, ArtifactConfig, BuildConfig, BuildSystem, Cargo, CargoBuildProfile, Custom, + }, + invocation::{Invocation, InvocationOptions}, +}; + +use super::{ + CargoQemuRunnerArgs, CargoRunnerKind, RuntimeArtifactInput, build_with_config, + prepare_runtime_artifact, run_with_config, +}; + +struct Fixture { + _temp: tempfile::TempDir, + root: PathBuf, +} + +impl Fixture { + fn new() -> Self { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("project with spaces"); + fs::create_dir_all(root.join("src")).unwrap(); + fs::write( + root.join("Cargo.toml"), + "[package]\nname = \"analysis-fixture\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", + ) + .unwrap(); + fs::write( + root.join("src/main.rs"), + "fn main() { println!(\"fixture\"); }\n", + ) + .unwrap(); + Self { _temp: temp, root } + } + + fn invocation(&self) -> Invocation { + Invocation::new(InvocationOptions::new( + Some(self.root.clone()), + None, + None, + false, + )) + .unwrap() + } + + fn copy_current_executable(&self, name: &str) -> PathBuf { + let path = self.root.join(name); + fs::copy(std::env::current_exe().unwrap(), &path).unwrap(); + path + } +} + +fn native_target() -> String { + let output = Command::new("rustc").arg("-vV").output().unwrap(); + assert!(output.status.success()); + String::from_utf8(output.stdout) + .unwrap() + .lines() + .find_map(|line| line.strip_prefix("host: ")) + .expect("rustc -vV reports a host target") + .to_owned() +} + +fn quoted(path: &Path) -> String { + format!("'{}'", path.display()) +} + +fn analysis_config(disassembly: bool, elf_info: bool, symbols: bool) -> ArtifactConfig { + ArtifactConfig { + analysis: AnalysisConfig { + disassembly, + elf_info, + symbols, + }, + } +} + +fn custom_build(elf_path: &Path, build_cmd: String, artifacts: ArtifactConfig) -> BuildConfig { + BuildConfig { + artifacts, + system: BuildSystem::Custom(Custom { + build_cmd, + elf_path: elf_path.display().to_string(), + to_bin: false, + }), + } +} + +fn llvm_nm(path: &Path) -> Vec { + let output = Command::new(crate::artifact::llvm_tools::llvm_tool("llvm-nm").unwrap()) + .args(["--demangle", "--print-size", "--numeric-sort"]) + .arg(path) + .output() + .unwrap(); + assert!(output.status.success()); + output.stdout +} + +fn symbol_lines(output: &[u8]) -> BTreeSet<&[u8]> { + output.split(|byte| *byte == b'\n').collect() +} + +#[tokio::test] +async fn cargo_build_emits_each_requested_analysis_artifact_after_hooks() { + let fixture = Fixture::new(); + let pre_marker = fixture.root.join("pre build marker"); + let post_marker = fixture.root.join("post build marker"); + let mut invocation = fixture.invocation(); + let config = BuildConfig { + artifacts: analysis_config(true, true, true), + system: BuildSystem::Cargo(Box::new(Cargo { + env: HashMap::from([("RUSTC_BOOTSTRAP".into(), "1".into())]), + target: native_target(), + package: "analysis-fixture".into(), + bin: Some("analysis-fixture".into()), + profile: Some(CargoBuildProfile::Debug), + disable_someboot_build_config: true, + pre_build_cmds: vec![format!("touch {}", quoted(&pre_marker))], + post_build_cmds: vec![format!( + "printf '%s' \"$KERNEL_ELF\" > {}", + quoted(&post_marker) + )], + ..Default::default() + })), + }; + + build_with_config(&mut invocation, &config, None) + .await + .unwrap(); + + let source = invocation + .runtime_artifacts() + .analysis_source_elf() + .unwrap() + .to_path_buf(); + assert!(pre_marker.exists()); + assert_eq!( + fs::read_to_string(&post_marker).unwrap(), + source.display().to_string() + ); + for (kind, suffix) in [ + (DebugArtifactKind::Disassembly, "disassembly"), + (DebugArtifactKind::ElfInfo, "elf-info"), + (DebugArtifactKind::Symbols, "symbols"), + ] { + let path = invocation + .runtime_artifacts() + .debug_artifacts() + .get(kind) + .unwrap(); + assert_eq!( + path, + source + .with_file_name(format!("analysis-fixture.{suffix}")) + .as_path() + ); + assert!(!fs::read(path).unwrap().is_empty()); + } +} + +#[tokio::test] +async fn custom_build_analysis_keeps_build_only_runtime_state_empty() { + let fixture = Fixture::new(); + let source = fixture.copy_current_executable("custom source with spaces"); + let marker = fixture.root.join("custom build marker"); + let mut invocation = fixture.invocation(); + let config = custom_build( + &source, + format!("touch {}", quoted(&marker)), + analysis_config(false, false, true), + ); + + build_with_config(&mut invocation, &config, None) + .await + .unwrap(); + + assert!(marker.exists()); + assert!(invocation.runtime_artifacts().elf().is_none()); + assert!(invocation.runtime_artifacts().bin().is_none()); + assert_eq!( + invocation + .runtime_artifacts() + .debug_artifacts() + .get(DebugArtifactKind::Symbols), + Some( + source + .with_file_name("custom source with spaces.symbols") + .as_path() + ) + ); +} + +#[test] +fn prepared_runtime_copy_keeps_source_and_debug_registry_when_adding_bin() { + let fixture = Fixture::new(); + let source = fixture.copy_current_executable("original image.elf"); + let original_bytes = fs::read(&source).unwrap(); + let cargo_artifact_dir = fixture.root.join("cargo artifacts"); + fs::create_dir_all(&cargo_artifact_dir).unwrap(); + let mut invocation = fixture.invocation(); + + prepare_runtime_artifact( + &mut invocation, + RuntimeArtifactInput::new(&source, false) + .with_cargo_artifact_dir(&cargo_artifact_dir) + .strip_elf(true), + ) + .unwrap(); + let source = source.canonicalize().unwrap(); + let runtime_elf = source.with_file_name("original image.elf.runtime.elf"); + let stripped_runtime = source.with_file_name("runtime symbols removed.elf"); + let status = Command::new(crate::artifact::llvm_tools::llvm_objcopy().unwrap()) + .arg("--strip-all") + .arg(&runtime_elf) + .arg(&stripped_runtime) + .status() + .unwrap(); + assert!(status.success()); + fs::rename(&stripped_runtime, &runtime_elf).unwrap(); + let config = BuildConfig { + artifacts: analysis_config(false, false, true), + system: BuildSystem::Cargo(Box::default()), + }; + super::generate_build_analysis(&mut invocation, &config).unwrap(); + let symbols = source.with_file_name("original image.elf.symbols"); + + let bin = invocation.ensure_runtime_bin().unwrap(); + + assert_ne!(runtime_elf, source); + assert!( + fs::read(&source).unwrap() == original_bytes, + "runtime preparation must preserve the source bytes" + ); + assert!(runtime_elf.exists()); + assert_eq!( + invocation.runtime_artifacts().analysis_source_elf(), + Some(source.as_path()) + ); + assert_eq!( + invocation.runtime_artifacts().elf(), + Some(runtime_elf.as_path()) + ); + assert!(bin.exists()); + let source_symbols = llvm_nm(&source); + let runtime_symbols = llvm_nm(&runtime_elf); + assert_ne!( + symbol_lines(&source_symbols), + symbol_lines(&runtime_symbols) + ); + assert!( + symbol_lines(&fs::read(&symbols).unwrap()) == symbol_lines(&source_symbols), + "analysis must use the original ELF symbols" + ); + assert_ne!( + symbol_lines(&fs::read(&symbols).unwrap()), + symbol_lines(&runtime_symbols) + ); + assert_eq!( + invocation + .runtime_artifacts() + .debug_artifacts() + .get(DebugArtifactKind::Symbols), + Some(symbols.as_path()) + ); +} + +#[tokio::test] +async fn disabled_analysis_clears_debug_artifacts_from_previous_build() { + let fixture = Fixture::new(); + let source = fixture.copy_current_executable("analysis source"); + let mut invocation = fixture.invocation(); + let requested = custom_build(&source, "true".into(), analysis_config(false, false, true)); + let disabled = custom_build(&source, "true".into(), ArtifactConfig::default()); + + build_with_config(&mut invocation, &requested, None) + .await + .unwrap(); + assert!( + invocation + .runtime_artifacts() + .debug_artifacts() + .get(DebugArtifactKind::Symbols) + .is_some() + ); + + build_with_config(&mut invocation, &disabled, None) + .await + .unwrap(); + + assert!(invocation.runtime_artifacts().debug_artifacts().is_empty()); +} + +#[tokio::test] +async fn failed_analysis_clears_debug_artifacts_from_previous_build() { + let fixture = Fixture::new(); + let source = fixture.copy_current_executable("analysis source"); + let mut invocation = fixture.invocation(); + let requested = custom_build(&source, "true".into(), analysis_config(false, false, true)); + let missing = fixture.root.join("missing source"); + let failing = custom_build(&missing, "true".into(), analysis_config(false, false, true)); + + build_with_config(&mut invocation, &requested, None) + .await + .unwrap(); + assert!( + invocation + .runtime_artifacts() + .debug_artifacts() + .get(DebugArtifactKind::Symbols) + .is_some() + ); + + assert!( + build_with_config(&mut invocation, &failing, None) + .await + .is_err() + ); + + assert!(invocation.runtime_artifacts().debug_artifacts().is_empty()); +} + +#[tokio::test] +async fn invalid_runtime_elf_stops_before_qemu_config_is_generated() { + let fixture = Fixture::new(); + let invalid_elf = fixture.root.join("not an elf"); + fs::write(&invalid_elf, "not an ELF").unwrap(); + let mut invocation = fixture.invocation(); + let config = custom_build( + &invalid_elf, + "true".into(), + analysis_config(false, false, true), + ); + + let error = run_with_config( + &mut invocation, + &config, + None, + &CargoRunnerKind::new_qemu(CargoQemuRunnerArgs::default()), + ) + .await + .unwrap_err(); + + assert!(error.to_string().contains("failed to parse ELF file")); + assert!(fs::read_dir(&fixture.root).unwrap().all(|entry| { + !entry + .unwrap() + .file_name() + .to_string_lossy() + .starts_with(".qemu") + })); +} diff --git a/ostool/src/build/config.rs b/ostool/src/build/config.rs index 15648feb..a75390fd 100644 --- a/ostool/src/build/config.rs +++ b/ostool/src/build/config.rs @@ -35,16 +35,48 @@ fn is_false(value: &bool) -> bool { pub struct BuildConfig { /// The build system configuration. pub system: BuildSystem, + /// Optional derived artifacts generated beside the built ELF. + #[serde(default)] + pub artifacts: ArtifactConfig, } impl Default for BuildConfig { fn default() -> Self { Self { system: BuildSystem::Cargo(Box::default()), + artifacts: ArtifactConfig::default(), } } } +/// Derived artifacts emitted for a completed build. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct ArtifactConfig { + /// ELF analysis artifacts. + #[serde(default)] + pub analysis: AnalysisConfig, +} + +/// Selects human-readable analysis artifacts to generate from an ELF. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct AnalysisConfig { + /// Generate a disassembly with `llvm-objdump`. + #[serde(default)] + pub disassembly: bool, + /// Generate ELF metadata and `llvm-readobj` output. + #[serde(default)] + pub elf_info: bool, + /// Generate a symbol listing with `llvm-nm`. + #[serde(default)] + pub symbols: bool, +} + +impl AnalysisConfig { + pub(crate) fn is_enabled(&self) -> bool { + self.disassembly || self.elf_info || self.symbols + } +} + /// Specifies the build system to use. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub enum BuildSystem { @@ -168,7 +200,41 @@ pub enum LogLevel { #[cfg(test)] mod tests { - use super::{Cargo, Custom}; + use super::{ArtifactConfig, Cargo, Custom}; + + #[test] + fn artifact_analysis_defaults_to_disabled() { + let artifacts: ArtifactConfig = toml::from_str("").unwrap(); + + assert!(!artifacts.analysis.disassembly); + assert!(!artifacts.analysis.elf_info); + assert!(!artifacts.analysis.symbols); + assert!(!artifacts.analysis.is_enabled()); + } + + #[test] + fn legacy_build_configs_and_analysis_flags_round_trip() { + let legacy = "[system.Custom]\nbuild_cmd = \"make\"\nelf_path = \"kernel.elf\"\n"; + let config: super::BuildConfig = toml::from_str(legacy).unwrap(); + assert!(!config.artifacts.analysis.is_enabled()); + let enabled = format!( + "{legacy}\n[artifacts.analysis]\ndisassembly = true\nelf_info = true\nsymbols = true\n" + ); + let config: super::BuildConfig = toml::from_str(&enabled).unwrap(); + assert!(config.artifacts.analysis.disassembly); + assert!(config.artifacts.analysis.elf_info); + assert!(config.artifacts.analysis.symbols); + let rendered = toml::to_string(&config).unwrap(); + assert_eq!( + toml::from_str::(&rendered).unwrap(), + config + ); + let partial: super::BuildConfig = + toml::from_str(&format!("{legacy}\n[artifacts.analysis]\nsymbols = true\n")).unwrap(); + assert!(!partial.artifacts.analysis.disassembly); + assert!(!partial.artifacts.analysis.elf_info); + assert!(partial.artifacts.analysis.symbols); + } #[test] fn cargo_config_defaults_someboot_injection_to_enabled_when_field_is_absent() { diff --git a/ostool/src/build/mod.rs b/ostool/src/build/mod.rs index 4349075e..d6b77a21 100644 --- a/ostool/src/build/mod.rs +++ b/ostool/src/build/mod.rs @@ -19,7 +19,7 @@ use std::path::{Path, PathBuf}; -use anyhow::bail; +use anyhow::{Context, bail}; use crate::{ artifact::runtime::{ @@ -37,6 +37,8 @@ use crate::{ }, }; +#[cfg(test)] +mod analysis_tests; mod artifact_selector; pub(crate) mod config_hooks; pub(crate) mod config_loader; @@ -305,6 +307,7 @@ pub async fn build_with_config( config: &config::BuildConfig, config_path: Option<&Path>, ) -> anyhow::Result<()> { + invocation.clear_debug_artifacts(); activate_build_config(invocation, config, config_path)?; match &config.system { config::BuildSystem::Custom(custom) => build_custom(invocation, custom)?, @@ -312,7 +315,7 @@ pub async fn build_with_config( cargo_build(invocation, cargo, config_path).await?; } } - Ok(()) + generate_build_analysis(invocation, config) } /// Runs the custom build command from a build configuration. @@ -331,6 +334,7 @@ pub fn prepare_runtime_artifact( invocation: &mut Invocation, input: RuntimeArtifactInput, ) -> anyhow::Result<()> { + invocation.clear_debug_artifacts(); let process_context = invocation.process_context()?; let prepared = prepare_runtime_artifact_outputs( &process_context, @@ -355,9 +359,11 @@ pub async fn cargo_build( config: &Cargo, config_path: Option<&Path>, ) -> anyhow::Result { + invocation.clear_debug_artifacts(); activate_build_config( invocation, &BuildConfig { + artifacts: Default::default(), system: BuildSystem::Cargo(Box::new(config.clone())), }, config_path, @@ -371,21 +377,54 @@ pub async fn cargo_build( } /// Builds or imports the configured artifact and prepares the runtime outputs. -pub(crate) async fn prepare_runtime_artifacts( +pub async fn prepare_with_config( invocation: &mut Invocation, config: &config::BuildConfig, config_path: Option<&Path>, debug: bool, ) -> anyhow::Result<()> { + invocation.clear_debug_artifacts(); activate_build_config(invocation, config, config_path)?; match &config.system { config::BuildSystem::Custom(custom) => { - prepare_custom_runtime_artifacts(invocation, custom).await + prepare_custom_runtime_artifacts(invocation, custom).await?; } config::BuildSystem::Cargo(cargo) => { - prepare_cargo_runtime_artifacts(invocation, cargo, debug).await + prepare_cargo_runtime_artifacts(invocation, cargo, debug).await?; } } + generate_build_analysis(invocation, config) +} + +fn generate_build_analysis( + invocation: &mut Invocation, + config: &BuildConfig, +) -> anyhow::Result<()> { + if !config.artifacts.analysis.is_enabled() { + return Ok(()); + } + let source = match &config.system { + BuildSystem::Cargo(_) => invocation + .runtime_artifacts() + .analysis_source_elf() + .context("Cargo build did not select an ELF for analysis")? + .to_path_buf(), + BuildSystem::Custom(custom) => { + let path = PathBuf::from(&custom.elf_path); + if path.is_absolute() { + path + } else { + invocation.manifest_dir().join(path) + } + } + }; + let artifacts = crate::artifact::analysis::generate( + &invocation.process_context()?, + &config.artifacts.analysis, + &source, + )?; + invocation.replace_debug_artifacts(artifacts); + Ok(()) } async fn prepare_custom_runtime_artifacts( @@ -393,9 +432,13 @@ async fn prepare_custom_runtime_artifacts( config: &Custom, ) -> anyhow::Result<()> { build_custom(invocation, config)?; - invocation - .prepare_elf_artifact(config.elf_path.clone().into(), config.to_bin) - .await + let path = PathBuf::from(&config.elf_path); + let path = if path.is_absolute() { + path + } else { + invocation.manifest_dir().join(path) + }; + invocation.prepare_elf_artifact(path, config.to_bin).await } async fn prepare_cargo_runtime_artifacts( @@ -423,29 +466,50 @@ pub async fn cargo_run( config_path: Option<&Path>, runner: &CargoRunnerKind, ) -> anyhow::Result<()> { - activate_build_config( + run_with_config( invocation, &BuildConfig { system: BuildSystem::Cargo(Box::new(config.clone())), + artifacts: Default::default(), }, config_path, - )?; + runner, + ) + .await +} - let debug = matches!(runner, CargoRunnerKind::Qemu(args) if args.debug); - let input = cargo_build_input(invocation, config, debug)?; - let outcome = CargoBuildPipeline::build(input, config) - .skip_objcopy(true) - .resolve_artifact_from_json(true) - .execute() - .await?; - apply_cargo_build_outcome(invocation, config, &outcome, true, debug)?; - run_cargo_post_build_cmds(invocation, config)?; +/// Builds and runs a complete build configuration, including optional analysis. +/// +/// Cargo-only callers may continue to use [`cargo_run`]. Analysis failures are +/// returned before starting the runner. +pub async fn run_with_config( + invocation: &mut Invocation, + config: &BuildConfig, + config_path: Option<&Path>, + runner: &CargoRunnerKind, +) -> anyhow::Result<()> { + let debug = match &config.system { + BuildSystem::Cargo(_) => matches!(runner, CargoRunnerKind::Qemu(args) if args.debug), + BuildSystem::Custom(_) => invocation.options().debug(), + }; + prepare_with_config(invocation, config, config_path, debug).await?; match runner { CargoRunnerKind::Qemu(args) => { let qemu = match &args.qemu { Some(config) => config.clone(), - None => crate::run::qemu::ensure_config_for_cargo(invocation, config).await?, + None => match &config.system { + BuildSystem::Cargo(cargo) => { + crate::run::qemu::ensure_config_for_cargo(invocation, cargo).await? + } + BuildSystem::Custom(_) => { + crate::run::qemu::ensure_config_in_dir( + invocation, + invocation.workspace_dir(), + ) + .await? + } + }, }; crate::run::qemu::run_qemu_with_debug( invocation, @@ -460,7 +524,18 @@ pub async fn cargo_run( CargoRunnerKind::Uboot(args) => { let uboot = match &args.uboot { Some(config) => config.clone(), - None => crate::run::uboot::ensure_config_for_cargo(invocation, config).await?, + None => match &config.system { + BuildSystem::Cargo(cargo) => { + crate::run::uboot::ensure_config_for_cargo(invocation, cargo).await? + } + BuildSystem::Custom(_) => { + crate::run::uboot::ensure_config_in_dir( + invocation, + invocation.workspace_dir(), + ) + .await? + } + }, }; crate::run::uboot::run_uboot(invocation, &uboot).await?; } @@ -644,14 +719,15 @@ mod tests { prepare_runtime_artifact( &mut invocation, RuntimeArtifactInput::new(&elf_path, false) - .with_cargo_artifact_dir(cargo_artifact_dir.clone()), + .with_cargo_artifact_dir(cargo_artifact_dir.clone()) + .strip_elf(true), ) .unwrap(); let expected_elf = elf_path.canonicalize().unwrap(); assert_eq!( invocation.runtime_artifacts().elf(), - Some(expected_elf.as_path()) + Some(elf_path.with_extension("elf").as_path()) ); assert!(invocation.runtime_artifacts().bin().is_none()); assert_eq!( @@ -688,6 +764,7 @@ mod tests { )) .unwrap(); let config = BuildConfig { + artifacts: Default::default(), system: BuildSystem::Custom(Custom { build_cmd: format!("printf built > {}", marker.display()), elf_path: "target/kernel.elf".into(), @@ -719,6 +796,7 @@ mod tests { let layout = resolve_project_layout(Some(temp.path().to_path_buf())).unwrap(); let config_path = temp.path().join(".build.toml"); let config = BuildConfig { + artifacts: Default::default(), system: BuildSystem::Cargo(Box::new(Cargo { package: "placeholder".into(), ..Default::default() @@ -758,6 +836,7 @@ mod tests { )) .unwrap(); let config = BuildConfig { + artifacts: Default::default(), system: BuildSystem::Cargo(Box::new(Cargo { package: "kernel".into(), ..Default::default() @@ -787,6 +866,7 @@ mod tests { fs::write(temp.path().join("src/main.rs"), "fn main() {}\n").unwrap(); let layout = resolve_project_layout(Some(temp.path().to_path_buf())).unwrap(); let config = BuildConfig { + artifacts: Default::default(), system: BuildSystem::Custom(Custom { build_cmd: "make".into(), elf_path: "target/kernel.elf".into(), diff --git a/ostool/src/invocation.rs b/ostool/src/invocation.rs index 4773a816..71855e37 100644 --- a/ostool/src/invocation.rs +++ b/ostool/src/invocation.rs @@ -8,7 +8,7 @@ use object::Architecture; use crate::{ artifact::{ runtime::{PreparedRuntimeArtifacts, RuntimeArtifactOptions, prepare_runtime_artifacts}, - state::OutputArtifacts, + state::{DebugArtifactRegistry, OutputArtifacts}, }, process::ProcessContext, project::{ProjectLayout, resolve_project_layout, variables::VariableScope}, @@ -164,6 +164,16 @@ impl Invocation { self.state.apply_prepared_runtime_artifacts(&prepared); } + pub(crate) fn clear_debug_artifacts(&mut self) { + self.state + .artifacts + .replace_debug_artifacts(DebugArtifactRegistry::default()); + } + + pub(crate) fn replace_debug_artifacts(&mut self, artifacts: DebugArtifactRegistry) { + self.state.artifacts.replace_debug_artifacts(artifacts); + } + pub(crate) fn ensure_runtime_bin(&mut self) -> anyhow::Result { if let Some(bin) = self.runtime_artifacts().bin() { debug!("BIN file already exists: {bin:?}"); @@ -194,7 +204,7 @@ impl Invocation { .bin() .ok_or_else(|| anyhow!("bin not exist after conversion"))? .to_path_buf(); - self.apply_prepared_runtime_artifacts(prepared); + self.state.artifacts.apply_runtime_bin(&prepared); Ok(bin_path) } @@ -205,6 +215,7 @@ impl Invocation { path: PathBuf, to_bin: bool, ) -> anyhow::Result<()> { + self.clear_debug_artifacts(); let process_context = self.process_context()?; let prepared = prepare_runtime_artifacts( &process_context, diff --git a/ostool/src/lib.rs b/ostool/src/lib.rs index 04ecd44f..28b760e6 100644 --- a/ostool/src/lib.rs +++ b/ostool/src/lib.rs @@ -43,7 +43,7 @@ pub mod auth; /// Build system configuration and Cargo integration. /// /// Provides functionality for configuring and executing Cargo builds -/// with custom options and target specifications. +/// with custom options, target specifications, and optional ELF analysis artifacts. pub mod build; /// ostool-server board client and remote terminal integration. diff --git a/ostool/src/main.rs b/ostool/src/main.rs index e0748c65..5cd93b80 100644 --- a/ostool/src/main.rs +++ b/ostool/src/main.rs @@ -378,27 +378,22 @@ async fn try_main() -> Result<()> { debug, dtb_dump, }); - build::cargo_run( + build::run_with_config( &mut invocation, - config, + &loaded_build_config.config, Some(loaded_build_config.path.as_path()), &kind, ) .await?; } - build::config::BuildSystem::Custom(custom_cfg) => { - build::build_with_config( + build::config::BuildSystem::Custom(_) => { + build::prepare_with_config( &mut invocation, &loaded_build_config.config, Some(loaded_build_config.path.as_path()), + false, ) .await?; - invocation - .prepare_elf_artifact( - custom_cfg.elf_path.clone().into(), - custom_cfg.to_bin, - ) - .await?; let qemu_config = load_qemu_config(&mut invocation, qemu.qemu_config.as_deref()).await?; ostool::run::qemu::run_qemu( @@ -442,27 +437,22 @@ async fn try_main() -> Result<()> { let kind = CargoRunnerKind::new_uboot(CargoUbootRunnerArgs { uboot: uboot_config, }); - build::cargo_run( + build::run_with_config( &mut invocation, - config, + &loaded_build_config.config, Some(loaded_build_config.path.as_path()), &kind, ) .await?; } - build::config::BuildSystem::Custom(custom_cfg) => { - build::build_with_config( + build::config::BuildSystem::Custom(_) => { + build::prepare_with_config( &mut invocation, &loaded_build_config.config, Some(loaded_build_config.path.as_path()), + false, ) .await?; - invocation - .prepare_elf_artifact( - custom_cfg.elf_path.clone().into(), - custom_cfg.to_bin, - ) - .await?; let uboot_config = load_uboot_config(&mut invocation, uboot.uboot_config.as_deref()) .await?; @@ -971,6 +961,7 @@ mod tests { fn apply_cargo_selector_overrides_cargo_build_config() { let (_temp, mut invocation) = test_invocation(); let mut build_config = build::config::BuildConfig { + artifacts: Default::default(), system: build::config::BuildSystem::Cargo(Box::new(build::config::Cargo { package: "default-package".into(), bin: None, @@ -1004,6 +995,7 @@ mod tests { fn apply_cargo_selector_rejects_custom_build_config() { let (_temp, mut invocation) = test_invocation(); let mut build_config = build::config::BuildConfig { + artifacts: Default::default(), system: build::config::BuildSystem::Custom(build::config::Custom { build_cmd: "make".into(), elf_path: "target/kernel.elf".into(), @@ -1033,6 +1025,7 @@ mod tests { fn apply_cargo_selector_overrides_test_target() { let (_temp, mut invocation) = test_invocation(); let mut build_config = build::config::BuildConfig { + artifacts: Default::default(), system: build::config::BuildSystem::Cargo(Box::new(build::config::Cargo { package: "default-package".into(), bin: Some("old-bin".into()), @@ -1102,6 +1095,7 @@ dtb_file = "${package}/board.dtb" let mut invocation = Invocation::new(InvocationOptions::new(Some(app_dir), None, None, false)).unwrap(); let mut build_config = build::config::BuildConfig { + artifacts: Default::default(), system: build::config::BuildSystem::Cargo(Box::new(build::config::Cargo { package: "app".into(), target: "aarch64-unknown-none".into(), diff --git a/ostool/src/run/qemu.rs b/ostool/src/run/qemu.rs index 357b8a42..6f5826b2 100644 --- a/ostool/src/run/qemu.rs +++ b/ostool/src/run/qemu.rs @@ -1584,6 +1584,7 @@ shell_check_steps = [ crate::build::activate_build_config( &mut invocation, &BuildConfig { + artifacts: Default::default(), system: BuildSystem::Cargo(Box::new(Cargo { env: HashMap::new(), target: "aarch64-unknown-none".into(), diff --git a/ostool/src/run/uboot.rs b/ostool/src/run/uboot.rs index 2f17a605..b16d6947 100644 --- a/ostool/src/run/uboot.rs +++ b/ostool/src/run/uboot.rs @@ -2542,6 +2542,7 @@ board_power_off_cmd = "power-off-board" crate::build::activate_build_config( &mut invocation, &BuildConfig { + artifacts: Default::default(), system: BuildSystem::Cargo(Box::new(Cargo { env: HashMap::new(), target: "aarch64-unknown-none".into(), @@ -2756,6 +2757,7 @@ baud_rate = "115200" crate::build::activate_build_config( &mut invocation, &BuildConfig { + artifacts: Default::default(), system: BuildSystem::Cargo(Box::new(Cargo { env: HashMap::new(), target: "aarch64-unknown-none".into(), diff --git a/ostool/tests/analysis_cli.rs b/ostool/tests/analysis_cli.rs new file mode 100644 index 00000000..235359c1 --- /dev/null +++ b/ostool/tests/analysis_cli.rs @@ -0,0 +1,68 @@ +//! Custom build hooks may generate the explicitly requested runner configuration. +#![cfg(unix)] + +use std::{fs, process::Command}; + +fn custom_build_generates_runner_config(runner: &str, field: &str) { + let temp = tempfile::tempdir().unwrap(); + fs::create_dir(temp.path().join("src")).unwrap(); + fs::write( + temp.path().join("Cargo.toml"), + "[package]\nname = \"hook-fixture\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", + ) + .unwrap(); + fs::write(temp.path().join("src/main.rs"), "fn main() {}\n").unwrap(); + fs::copy(std::env::current_exe().unwrap(), temp.path().join("kernel")).unwrap(); + // Deliberately invalid field type: config parsing must fail after the hook, + // without starting QEMU or accessing any serial device. + let config = ostool::build::config::BuildConfig { + system: ostool::build::config::BuildSystem::Custom(ostool::build::config::Custom { + build_cmd: format!("printf '{field} = 42\\n' > generated.toml"), + elf_path: "kernel".into(), + to_bin: false, + }), + artifacts: ostool::build::config::ArtifactConfig { + analysis: ostool::build::config::AnalysisConfig { + elf_info: true, + ..Default::default() + }, + }, + }; + fs::write( + temp.path().join(".build.toml"), + toml::to_string(&config).unwrap(), + ) + .unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_ostool")) + .current_dir(temp.path()) + .args([ + "run", + runner, + &format!("--{runner}-config"), + "generated.toml", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + assert_eq!( + fs::read_to_string(temp.path().join("generated.toml")).unwrap(), + format!("{field} = 42\n") + ); + assert!(temp.path().join("kernel.elf-info").exists()); + let message = format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(message.contains("invalid type"), "{message}"); +} + +#[test] +fn custom_qemu_config_is_read_after_build_hook() { + custom_build_generates_runner_config("qemu", "args"); +} + +#[test] +fn custom_uboot_config_is_read_after_build_hook() { + custom_build_generates_runner_config("uboot", "dtb_file"); +} diff --git a/ostool/tests/ui/pass_module_level_apis.rs b/ostool/tests/ui/pass_module_level_apis.rs index a7149073..b2a44917 100644 --- a/ostool/tests/ui/pass_module_level_apis.rs +++ b/ostool/tests/ui/pass_module_level_apis.rs @@ -37,9 +37,11 @@ fn main() { ..Cargo::default() }; let cargo_build = BuildConfig { + artifacts: Default::default(), system: BuildSystem::Cargo(Box::new(cargo.clone())), }; let custom_build = BuildConfig { + artifacts: Default::default(), system: BuildSystem::Custom(Custom { build_cmd: "true".into(), elf_path: "target/kernel.elf".into(), @@ -88,6 +90,8 @@ fn main() { .with_cargo_artifact_dir("target/aarch64/debug") .strip_elf(false), ); + let _ = build::prepare_with_config(&mut invocation, &custom_build, None, false).await; + let _ = build::run_with_config(&mut invocation, &cargo_build, None, &qemu_runner).await; let _ = build::cargo_run(&mut invocation, &cargo, None, &qemu_runner).await; let _ = build::cargo_run(&mut invocation, &cargo, None, &uboot_runner).await; From 08eff023964eaedafe38f5d590e3d2de409b13de Mon Sep 17 00:00:00 2001 From: Niu Zhihong Date: Tue, 22 Sep 2026 15:54:52 +0800 Subject: [PATCH 2/8] =?UTF-8?q?refactor(ostool):=20=E7=B2=BE=E7=AE=80=20EL?= =?UTF-8?q?F=20=E5=88=86=E6=9E=90=E5=AE=9E=E7=8E=B0=E4=B8=8E=E5=BF=85?= =?UTF-8?q?=E8=A6=81=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复用 tempfile 管理分析产物的临时文件和替换,移除手工暂存路径及重复的 runtime 来源状态。将完整配置运行入口收窄为 cargo_run_with_config, Custom CLI 继续在构建完成后读取 runner 配置,并同步中英文说明。 删除重复成功/路径/默认配置断言和 ELF32 大端专项夹具,合并状态更新与 runner hooks 测试;保留 ELF 元数据、错误处理、strip 来源和构建兼容覆盖。 Co-authored-by: OpenAI Codex Signed-off-by: Niu Zhihong --- README.en.md | 14 +- README.md | 2 +- ostool/Cargo.toml | 2 +- ostool/src/artifact/analysis.rs | 319 ++++++---------------- ostool/src/artifact/elf_metadata.rs | 300 +++++++------------- ostool/src/artifact/state.rs | 8 - ostool/src/build/analysis_tests.rs | 86 ++---- ostool/src/build/config.rs | 12 +- ostool/src/build/mod.rs | 44 +-- ostool/src/main.rs | 4 +- ostool/tests/analysis_cli.rs | 11 +- ostool/tests/ui/pass_module_level_apis.rs | 2 +- 12 files changed, 239 insertions(+), 565 deletions(-) diff --git a/README.en.md b/README.en.md index 930c52ea..8f11fd2c 100644 --- a/README.en.md +++ b/README.en.md @@ -268,15 +268,11 @@ missing, install them with `rustup component add llvm-tools`; no `cargo-binutils wrapper is required. A missing tool, nonzero tool exit, or malformed ELF fails the build before the runner starts. -In the public Rust API, -`build::run_with_config(invocation, &BuildConfig, config_path, &CargoRunnerKind)` -accepts the complete configuration and reads this root configuration for both -Cargo and Custom builds. `build::prepare_with_config` completes the build, runtime -ELF/BIN preparation, and analysis before callers load their runner configuration -and run it. Existing `cargo_build` and `cargo_run` remain -Cargo-only helper APIs and use the default analysis configuration. When -constructing a `BuildConfig` literal directly, add `artifacts: Default::default()` -or use `..Default::default()`: +For library callers, `build::cargo_run_with_config` accepts the complete +`BuildConfig` for Cargo builds. `build::prepare_with_config` prepares either +build system's runtime and analysis outputs before callers load and run their +runner configuration. Existing `cargo_build` and `cargo_run` use default analysis +settings. Add `artifacts: Default::default()` to direct `BuildConfig` literals: ```rust let config = BuildConfig { diff --git a/README.md b/README.md index ef2c6e1e..e41d2cdf 100644 --- a/README.md +++ b/README.md @@ -248,7 +248,7 @@ symbols = true 这些功能使用现有 Rust 工具链中的 LLVM 工具;若缺少它们,运行 `rustup component add llvm-tools` 安装。无需安装 `cargo-binutils` 包装器。工具缺失、工具返回非零,或输入不是可解析的 ELF 时,构建会在启动 runner 前失败。 -公开 Rust API 中,`build::run_with_config(invocation, &BuildConfig, config_path, &CargoRunnerKind)` 接受完整配置,并为 Cargo 和 Custom 构建读取此根级配置。`build::prepare_with_config` 可先完成构建、运行时 ELF/BIN 与分析,再由调用方加载 runner 配置并运行。既有的 `cargo_build` 与 `cargo_run` 仍是 Cargo 专用辅助 API,并使用默认的分析配置。直接构造 `BuildConfig` 字面量时,请显式添加 `artifacts: Default::default()`,或使用 `..Default::default()`: +库调用方可通过 `build::cargo_run_with_config` 为 Cargo 构建传入完整 `BuildConfig`。`build::prepare_with_config` 为两种构建系统准备运行时与分析产物,随后由调用方加载 runner 配置并运行。既有 `cargo_build` 与 `cargo_run` 使用默认分析配置。直接构造 `BuildConfig` 时添加 `artifacts: Default::default()`: ```rust let config = BuildConfig { diff --git a/ostool/Cargo.toml b/ostool/Cargo.toml index 709387a5..b2749f05 100644 --- a/ostool/Cargo.toml +++ b/ostool/Cargo.toml @@ -53,6 +53,7 @@ schemars = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } serialport = "4.6" +tempfile = "3" tftpd = "0.5" tokio = { workspace = true, features = ["full"] } tokio-util = { version = "0.7", features = ["compat"] } @@ -72,7 +73,6 @@ tokio-tungstenite = { version = "0.28", features = ["rustls-tls-native-roots"] } tokio-serial = "5.4" [dev-dependencies] -tempfile = "3" trybuild = "1" [target.'cfg(unix)'.dependencies] diff --git a/ostool/src/artifact/analysis.rs b/ostool/src/artifact/analysis.rs index 0500ae60..0fdfab63 100644 --- a/ostool/src/artifact/analysis.rs +++ b/ostool/src/artifact/analysis.rs @@ -1,10 +1,9 @@ //! Human-readable ELF analysis artifact generation. use std::{ - ffi::OsString, fs, + io::Write, path::{Path, PathBuf}, - process::Output, }; use anyhow::{Context, anyhow, bail}; @@ -44,116 +43,90 @@ where return Ok(DebugArtifactRegistry::default()); } - let requested = requested_artifacts(config, source_elf)?; - let temporary = requested - .iter() - .map(|artifact| artifact.temporary_path()) - .collect::>(); - - let result = generate_requested_artifacts(context, source_elf, &requested, &mut resolve_tool) - .and_then(|contents| publish_artifacts(&requested, &temporary, contents)); - - match result { - Ok(registry) => Ok(registry), - Err(error) => { - remove_paths( - temporary - .iter() - .chain(requested.iter().map(|artifact| &artifact.path)), - ); - Err(error) - } - } -} - -#[derive(Clone)] -struct RequestedArtifact { - kind: DebugArtifactKind, - path: PathBuf, -} - -impl RequestedArtifact { - fn temporary_path(&self) -> PathBuf { - let mut name = OsString::from("."); - name.push( - self.path - .file_name() - .expect("analysis artifact has a file name"), - ); - name.push(format!(".{}.tmp", std::process::id())); - self.path.with_file_name(name) - } -} - -fn requested_artifacts( - config: &AnalysisConfig, - source_elf: &Path, -) -> anyhow::Result> { if source_elf.file_name().is_none() { bail!("invalid ELF file path: {}", source_elf.display()); } + let requested: Vec<_> = [ + ( + config.disassembly, + DebugArtifactKind::Disassembly, + "disassembly", + "llvm-objdump", + &["--disassemble"][..], + ), + ( + config.elf_info, + DebugArtifactKind::ElfInfo, + "elf-info", + "llvm-readobj", + &["--all"][..], + ), + ( + config.symbols, + DebugArtifactKind::Symbols, + "symbols", + "llvm-nm", + &["--demangle", "--print-size", "--numeric-sort"][..], + ), + ] + .into_iter() + .filter(|(enabled, ..)| *enabled) + .map(|(_, kind, suffix, tool, args)| (kind, output_path(source_elf, suffix), tool, args)) + .collect(); + + let result = (|| { + let metadata = elf_metadata(source_elf)?; + let mut staged = Vec::new(); + for (kind, path, tool_name, args) in &requested { + let tool = resolve_tool(tool_name).with_context(|| { + format!( + "failed to resolve {tool_name} for ELF {}", + source_elf.display() + ) + })?; + let mut contents = run_tool(context, &tool, tool_name, args, source_elf)?; + let mut temporary = tempfile::NamedTempFile::new_in(path.parent().unwrap()) + .with_context(|| { + format!("failed to stage analysis artifact: {}", path.display()) + })?; + if *kind == DebugArtifactKind::ElfInfo { + let mut summary = metadata_summary(&metadata); + summary.extend_from_slice(b"\n\nllvm-readobj output:\n"); + summary.extend_from_slice(&contents); + contents = summary; + } + temporary.write_all(&contents).with_context(|| { + format!("failed to write analysis artifact: {}", path.display()) + })?; + staged.push((kind, path, temporary)); + } - let mut artifacts = Vec::new(); - if config.disassembly { - artifacts.push(RequestedArtifact { - kind: DebugArtifactKind::Disassembly, - path: output_path(source_elf, "disassembly"), - }); - } - if config.elf_info { - artifacts.push(RequestedArtifact { - kind: DebugArtifactKind::ElfInfo, - path: output_path(source_elf, "elf-info"), - }); - } - if config.symbols { - artifacts.push(RequestedArtifact { - kind: DebugArtifactKind::Symbols, - path: output_path(source_elf, "symbols"), - }); - } - Ok(artifacts) -} - -fn generate_requested_artifacts( - context: &ProcessContext, - source_elf: &Path, - requested: &[RequestedArtifact], - resolve_tool: &mut F, -) -> anyhow::Result>> -where - F: FnMut(&str) -> anyhow::Result, -{ - let metadata = elf_metadata(source_elf)?; - - let mut contents = Vec::with_capacity(requested.len()); - for artifact in requested { - let (tool_name, args) = match artifact.kind { - DebugArtifactKind::Disassembly => ("llvm-objdump", vec!["--disassemble"]), - DebugArtifactKind::ElfInfo => ("llvm-readobj", vec!["--all"]), - DebugArtifactKind::Symbols => ( - "llvm-nm", - vec!["--demangle", "--print-size", "--numeric-sort"], - ), - }; - let tool = resolve_tool(tool_name).with_context(|| { - format!( - "failed to resolve {tool_name} for ELF {}", - source_elf.display() - ) - })?; - let stdout = run_tool(context, &tool, tool_name, &args, source_elf)?; - - if artifact.kind == DebugArtifactKind::ElfInfo { - let mut elf_info = metadata_summary(&metadata); - elf_info.extend_from_slice(b"\n\nllvm-readobj output:\n"); - elf_info.extend_from_slice(&stdout); - contents.push(elf_info); - } else { - contents.push(stdout); + let mut registry = DebugArtifactRegistry::default(); + for (kind, path, temporary) in staged { + temporary + .persist(path) + .map_err(|error| error.error) + .with_context(|| { + format!("failed to publish analysis artifact: {}", path.display()) + })?; + registry.register(*kind, path.clone()); + } + Ok(registry) + })(); + if result.is_err() { + // Tempfiles clean themselves up; discard stale or partially published outputs too. + for (_, path, _, _) in &requested { + if let Err(error) = fs::remove_file(path) + && error.kind() != std::io::ErrorKind::NotFound + { + log::warn!( + "failed to remove analysis artifact {}: {error}", + path.display() + ); + } } } - Ok(contents) + result } fn elf_metadata(source_elf: &Path) -> anyhow::Result { @@ -178,14 +151,6 @@ fn run_tool( source_elf.display() ) })?; - successful_stdout(tool_name, source_elf, output) -} - -fn successful_stdout( - tool_name: &str, - source_elf: &Path, - output: Output, -) -> anyhow::Result> { if output.status.success() { return Ok(output.stdout); } @@ -199,50 +164,6 @@ fn successful_stdout( )) } -fn publish_artifacts( - requested: &[RequestedArtifact], - temporary: &[PathBuf], - contents: Vec>, -) -> anyhow::Result { - for (path, content) in temporary.iter().zip(contents) { - fs::write(path, content) - .with_context(|| format!("failed to write analysis artifact: {}", path.display()))?; - } - - let mut registry = DebugArtifactRegistry::default(); - for (artifact, temporary) in requested.iter().zip(temporary) { - if artifact.path.exists() { - fs::remove_file(&artifact.path).with_context(|| { - format!( - "failed to replace analysis artifact: {}", - artifact.path.display() - ) - })?; - } - fs::rename(temporary, &artifact.path).with_context(|| { - format!( - "failed to publish analysis artifact: {}", - artifact.path.display() - ) - })?; - registry.register(artifact.kind, artifact.path.clone()); - } - Ok(registry) -} - -fn remove_paths<'a>(paths: impl Iterator) { - for path in paths { - if let Err(error) = fs::remove_file(path) - && error.kind() != std::io::ErrorKind::NotFound - { - log::warn!( - "failed to remove analysis artifact {}: {error}", - path.display() - ); - } - } -} - fn metadata_summary(metadata: &ElfMetadata) -> Vec { let mut summary = format!( "ELF metadata\narchitecture: {:?}\nentry: 0x{:x}\nload segments:\n", @@ -286,7 +207,7 @@ mod tests { }; use crate::{ - artifact::{analysis::generate_with_tool_resolver, state::DebugArtifactKind}, + artifact::analysis::generate_with_tool_resolver, build::config::AnalysisConfig, process::ProcessContext, project::{resolve_project_layout, variables::VariableScope}, @@ -335,29 +256,6 @@ mod tests { assert!(registry.is_empty()); } - #[test] - fn artifact_output_paths_append_full_source_filename() { - let source = PathBuf::from("output with spaces/kernel.elf"); - - assert_eq!( - super::output_path(&source, "disassembly"), - PathBuf::from("output with spaces/kernel.elf.disassembly") - ); - assert_eq!( - super::output_path(&source, "elf-info"), - PathBuf::from("output with spaces/kernel.elf.elf-info") - ); - } - - #[cfg(unix)] - #[test] - fn artifact_names_preserve_non_utf8_source_names() { - use std::{ffi::OsString, os::unix::ffi::OsStringExt}; - let source = PathBuf::from(OsString::from_vec(b"kernel-\xff.elf".to_vec())); - let expected = PathBuf::from(OsString::from_vec(b"kernel-\xff.elf.symbols".to_vec())); - assert_eq!(super::output_path(&source, "symbols"), expected); - } - #[cfg(unix)] fn make_executable(path: &Path) { use std::os::unix::fs::PermissionsExt; @@ -433,32 +331,6 @@ mod tests { assert!(!symbols.exists()); } - #[cfg(unix)] - #[test] - fn successful_generation_overwrites_output_and_registers_it() { - let temp = tempfile::tempdir().unwrap(); - let context = process_context(temp.path()); - let source = temp.path().join("kernel with spaces.elf"); - write_elf(&source); - let output = super::output_path(&source, "disassembly"); - fs::write(&output, "stale").unwrap(); - let objdump = fake_tool(temp.path(), "objdump", "printf 'fresh disassembly'"); - let config = AnalysisConfig { - disassembly: true, - ..Default::default() - }; - - let registry = - generate_with_tool_resolver(&context, &config, &source, |_| Ok(objdump.clone())) - .unwrap(); - - assert_eq!(fs::read_to_string(&output).unwrap(), "fresh disassembly"); - assert_eq!( - registry.get(DebugArtifactKind::Disassembly), - Some(output.as_path()) - ); - } - #[cfg(unix)] #[test] fn invalid_elf_fails_before_any_tool_is_resolved() { @@ -466,26 +338,15 @@ mod tests { let context = process_context(temp.path()); let source = temp.path().join("invalid.elf"); fs::write(&source, "not an ELF").unwrap(); - for config in [ - AnalysisConfig { - disassembly: true, - ..Default::default() - }, - AnalysisConfig { - elf_info: true, - ..Default::default() - }, - AnalysisConfig { - symbols: true, - ..Default::default() - }, - ] { - let error = generate_with_tool_resolver(&context, &config, &source, |_| { - panic!("invalid ELF must be rejected before tool lookup") - }) - .unwrap_err(); - assert!(format!("{error:#}").contains("failed to parse ELF file")); - } - assert!(!super::output_path(&source, "elf-info").exists()); + let config = AnalysisConfig { + symbols: true, + ..Default::default() + }; + let error = generate_with_tool_resolver(&context, &config, &source, |_| { + panic!("invalid ELF must be rejected before tool lookup") + }) + .unwrap_err(); + assert!(format!("{error:#}").contains("failed to parse ELF file")); + assert!(!super::output_path(&source, "symbols").exists()); } } diff --git a/ostool/src/artifact/elf_metadata.rs b/ostool/src/artifact/elf_metadata.rs index 528a04e1..83db651c 100644 --- a/ostool/src/artifact/elf_metadata.rs +++ b/ostool/src/artifact/elf_metadata.rs @@ -76,37 +76,22 @@ mod tests { use object::Architecture; - #[derive(Clone, Copy)] - enum Endian { - Little, - Big, - } - - fn write_u16(data: &mut [u8], offset: usize, value: u16, endian: Endian) { - let bytes = match endian { - Endian::Little => value.to_le_bytes(), - Endian::Big => value.to_be_bytes(), - }; + fn write_u16(data: &mut [u8], offset: usize, value: u16) { + let bytes = value.to_le_bytes(); data[offset..offset + bytes.len()].copy_from_slice(&bytes); } - fn write_u32(data: &mut [u8], offset: usize, value: u32, endian: Endian) { - let bytes = match endian { - Endian::Little => value.to_le_bytes(), - Endian::Big => value.to_be_bytes(), - }; + fn write_u32(data: &mut [u8], offset: usize, value: u32) { + let bytes = value.to_le_bytes(); data[offset..offset + bytes.len()].copy_from_slice(&bytes); } - fn write_u64(data: &mut [u8], offset: usize, value: u64, endian: Endian) { - let bytes = match endian { - Endian::Little => value.to_le_bytes(), - Endian::Big => value.to_be_bytes(), - }; + fn write_u64(data: &mut [u8], offset: usize, value: u64) { + let bytes = value.to_le_bytes(); data[offset..offset + bytes.len()].copy_from_slice(&bytes); } - fn elf64_with_symbol(symbol_defined: bool) -> Vec { + fn elf64(symbol_defined: Option) -> Vec { const PROGRAM_HEADER_OFFSET: usize = 64; const STRING_TABLE_OFFSET: usize = 0x180; const SYMBOL_TABLE_OFFSET: usize = 0x1c0; @@ -119,167 +104,97 @@ mod tests { data[5] = 1; data[6] = 1; - write_u16(&mut data, 16, 2, Endian::Little); - write_u16(&mut data, 18, 183, Endian::Little); - write_u32(&mut data, 20, 1, Endian::Little); - write_u64(&mut data, 24, 0x400080, Endian::Little); - write_u64(&mut data, 32, PROGRAM_HEADER_OFFSET as u64, Endian::Little); - write_u64(&mut data, 40, SECTION_HEADER_OFFSET as u64, Endian::Little); - write_u16(&mut data, 52, 64, Endian::Little); - write_u16(&mut data, 54, 56, Endian::Little); - write_u16(&mut data, 56, 3, Endian::Little); - write_u16(&mut data, 58, 64, Endian::Little); - write_u16(&mut data, 60, 3, Endian::Little); - write_u16(&mut data, 62, 1, Endian::Little); - - write_u32(&mut data, PROGRAM_HEADER_OFFSET, 1, Endian::Little); - write_u32(&mut data, PROGRAM_HEADER_OFFSET + 4, 5, Endian::Little); - write_u64(&mut data, PROGRAM_HEADER_OFFSET + 8, 0x100, Endian::Little); - write_u64( - &mut data, - PROGRAM_HEADER_OFFSET + 16, - 0x400000, - Endian::Little, - ); - write_u64( - &mut data, - PROGRAM_HEADER_OFFSET + 24, - 0x500000, - Endian::Little, - ); - write_u64(&mut data, PROGRAM_HEADER_OFFSET + 32, 0x30, Endian::Little); - write_u64(&mut data, PROGRAM_HEADER_OFFSET + 40, 0x40, Endian::Little); + write_u16(&mut data, 16, 2); + write_u16(&mut data, 18, 183); + write_u32(&mut data, 20, 1); + write_u64(&mut data, 24, 0x400080); + write_u64(&mut data, 32, PROGRAM_HEADER_OFFSET as u64); write_u64( &mut data, - PROGRAM_HEADER_OFFSET + 48, - 0x1000, - Endian::Little, + 40, + symbol_defined + .map(|_| SECTION_HEADER_OFFSET as u64) + .unwrap_or(0), ); + write_u16(&mut data, 52, 64); + write_u16(&mut data, 54, 56); + write_u16(&mut data, 56, 3); + write_u16(&mut data, 58, 64); + write_u16(&mut data, 60, u16::from(symbol_defined.is_some()) * 3); + write_u16(&mut data, 62, u16::from(symbol_defined.is_some())); + + write_u32(&mut data, PROGRAM_HEADER_OFFSET, 1); + write_u32(&mut data, PROGRAM_HEADER_OFFSET + 4, 5); + write_u64(&mut data, PROGRAM_HEADER_OFFSET + 8, 0x100); + write_u64(&mut data, PROGRAM_HEADER_OFFSET + 16, 0x400000); + write_u64(&mut data, PROGRAM_HEADER_OFFSET + 24, 0x500000); + write_u64(&mut data, PROGRAM_HEADER_OFFSET + 32, 0x30); + write_u64(&mut data, PROGRAM_HEADER_OFFSET + 40, 0x40); + write_u64(&mut data, PROGRAM_HEADER_OFFSET + 48, 0x1000); let second_program_header = PROGRAM_HEADER_OFFSET + 56; - write_u32(&mut data, second_program_header, 1, Endian::Little); - write_u32(&mut data, second_program_header + 4, 6, Endian::Little); - write_u64(&mut data, second_program_header + 8, 0x200, Endian::Little); - write_u64( - &mut data, - second_program_header + 16, - 0x401000, - Endian::Little, - ); - write_u64( - &mut data, - second_program_header + 24, - 0x501000, - Endian::Little, - ); - write_u64(&mut data, second_program_header + 32, 0x20, Endian::Little); - write_u64(&mut data, second_program_header + 40, 0x30, Endian::Little); - write_u64( - &mut data, - second_program_header + 48, - 0x1000, - Endian::Little, - ); + write_u32(&mut data, second_program_header, 1); + write_u32(&mut data, second_program_header + 4, 6); + write_u64(&mut data, second_program_header + 8, 0x200); + write_u64(&mut data, second_program_header + 16, 0x401000); + write_u64(&mut data, second_program_header + 24, 0x501000); + write_u64(&mut data, second_program_header + 32, 0x20); + write_u64(&mut data, second_program_header + 40, 0x30); + write_u64(&mut data, second_program_header + 48, 0x1000); let note_program_header = second_program_header + 56; - write_u32(&mut data, note_program_header, 4, Endian::Little); - write_u32(&mut data, note_program_header + 4, 4, Endian::Little); - write_u64(&mut data, note_program_header + 8, 0x280, Endian::Little); - write_u64( - &mut data, - note_program_header + 16, - 0x402000, - Endian::Little, - ); - write_u64( - &mut data, - note_program_header + 24, - 0x502000, - Endian::Little, - ); - write_u64(&mut data, note_program_header + 32, 0x10, Endian::Little); - write_u64(&mut data, note_program_header + 40, 0x10, Endian::Little); - write_u64(&mut data, note_program_header + 48, 4, Endian::Little); - - data[STRING_TABLE_OFFSET..STRING_TABLE_OFFSET + STRING_TABLE.len()] - .copy_from_slice(STRING_TABLE); - - let symbol = SYMBOL_TABLE_OFFSET + 24; - write_u32(&mut data, symbol, 1, Endian::Little); - data[symbol + 4] = 0x10; - write_u16( - &mut data, - symbol + 6, - u16::from(symbol_defined), - Endian::Little, - ); - write_u64(&mut data, symbol + 8, 0x400080, Endian::Little); - - let string_table_section = SECTION_HEADER_OFFSET + 64; - write_u32(&mut data, string_table_section + 4, 3, Endian::Little); - write_u64( - &mut data, - string_table_section + 24, - STRING_TABLE_OFFSET as u64, - Endian::Little, - ); - write_u64( - &mut data, - string_table_section + 32, - STRING_TABLE.len() as u64, - Endian::Little, - ); - write_u64(&mut data, string_table_section + 48, 1, Endian::Little); - - let symbol_table_section = SECTION_HEADER_OFFSET + 128; - write_u32(&mut data, symbol_table_section + 4, 2, Endian::Little); - write_u64( - &mut data, - symbol_table_section + 24, - SYMBOL_TABLE_OFFSET as u64, - Endian::Little, - ); - write_u64(&mut data, symbol_table_section + 32, 48, Endian::Little); - write_u32(&mut data, symbol_table_section + 40, 1, Endian::Little); - write_u64(&mut data, symbol_table_section + 48, 8, Endian::Little); - write_u64(&mut data, symbol_table_section + 56, 24, Endian::Little); - - data - } - - fn elf32_big_endian_without_symbols() -> Vec { - const PROGRAM_HEADER_OFFSET: usize = 52; - - let mut data = vec![0; 0xa0]; - data[..4].copy_from_slice(b"\x7fELF"); - data[4] = 1; - data[5] = 2; - data[6] = 1; - - write_u16(&mut data, 16, 2, Endian::Big); - write_u16(&mut data, 18, 40, Endian::Big); - write_u32(&mut data, 20, 1, Endian::Big); - write_u32(&mut data, 24, 0x8004, Endian::Big); - write_u32(&mut data, 28, PROGRAM_HEADER_OFFSET as u32, Endian::Big); - write_u16(&mut data, 40, 52, Endian::Big); - write_u16(&mut data, 42, 32, Endian::Big); - write_u16(&mut data, 44, 1, Endian::Big); - - write_u32(&mut data, PROGRAM_HEADER_OFFSET, 1, Endian::Big); - write_u32(&mut data, PROGRAM_HEADER_OFFSET + 4, 0x80, Endian::Big); - write_u32(&mut data, PROGRAM_HEADER_OFFSET + 8, 0x8000, Endian::Big); - write_u32(&mut data, PROGRAM_HEADER_OFFSET + 12, 0x9000, Endian::Big); - write_u32(&mut data, PROGRAM_HEADER_OFFSET + 16, 0x10, Endian::Big); - write_u32(&mut data, PROGRAM_HEADER_OFFSET + 20, 0x20, Endian::Big); - write_u32(&mut data, PROGRAM_HEADER_OFFSET + 24, 6, Endian::Big); - write_u32(&mut data, PROGRAM_HEADER_OFFSET + 28, 0x1000, Endian::Big); + write_u32(&mut data, note_program_header, 4); + write_u32(&mut data, note_program_header + 4, 4); + write_u64(&mut data, note_program_header + 8, 0x280); + write_u64(&mut data, note_program_header + 16, 0x402000); + write_u64(&mut data, note_program_header + 24, 0x502000); + write_u64(&mut data, note_program_header + 32, 0x10); + write_u64(&mut data, note_program_header + 40, 0x10); + write_u64(&mut data, note_program_header + 48, 4); + + if let Some(symbol_defined) = symbol_defined { + data[STRING_TABLE_OFFSET..STRING_TABLE_OFFSET + STRING_TABLE.len()] + .copy_from_slice(STRING_TABLE); + + let symbol = SYMBOL_TABLE_OFFSET + 24; + write_u32(&mut data, symbol, 1); + data[symbol + 4] = 0x10; + write_u16(&mut data, symbol + 6, u16::from(symbol_defined)); + write_u64(&mut data, symbol + 8, 0x400080); + + let string_table_section = SECTION_HEADER_OFFSET + 64; + write_u32(&mut data, string_table_section + 4, 3); + write_u64( + &mut data, + string_table_section + 24, + STRING_TABLE_OFFSET as u64, + ); + write_u64( + &mut data, + string_table_section + 32, + STRING_TABLE.len() as u64, + ); + write_u64(&mut data, string_table_section + 48, 1); + + let symbol_table_section = SECTION_HEADER_OFFSET + 128; + write_u32(&mut data, symbol_table_section + 4, 2); + write_u64( + &mut data, + symbol_table_section + 24, + SYMBOL_TABLE_OFFSET as u64, + ); + write_u64(&mut data, symbol_table_section + 32, 48); + write_u32(&mut data, symbol_table_section + 40, 1); + write_u64(&mut data, symbol_table_section + 48, 8); + write_u64(&mut data, symbol_table_section + 56, 24); + } data } #[test] fn parses_elf64_load_segments_and_executable_start_symbol() { - let metadata = ElfMetadata::parse(&elf64_with_symbol(true)).unwrap(); + let metadata = ElfMetadata::parse(&elf64(Some(true))).unwrap(); assert_eq!(metadata.arch, Architecture::Aarch64); assert_eq!(metadata.entry, 0x400080); @@ -306,38 +221,23 @@ mod tests { } #[test] - fn parses_big_endian_elf32_without_executable_start_symbol() { - let metadata = ElfMetadata::parse(&elf32_big_endian_without_symbols()).unwrap(); - - assert_eq!(metadata.arch, Architecture::Arm); - assert_eq!(metadata.entry, 0x8004); - assert_eq!(metadata.executable_start, None); - assert_eq!(metadata.load_segments.len(), 1); - - let segment = &metadata.load_segments[0]; - assert_eq!(segment.virtual_address, 0x8000); - assert_eq!(segment.physical_address, 0x9000); - assert_eq!(segment.file_offset, 0x80); - assert_eq!(segment.file_size, 0x10); - assert_eq!(segment.memory_size, 0x20); - assert_eq!(segment.alignment, 0x1000); - assert_eq!(segment.flags, 6); - } - - #[test] - fn treats_an_undefined_executable_start_symbol_as_absent() { - let metadata = ElfMetadata::parse(&elf64_with_symbol(false)).unwrap(); - - assert_eq!(metadata.executable_start, None); - } - - #[test] - fn rejects_non_elf_input() { - assert!(ElfMetadata::parse(b"not an ELF").is_err()); + fn treats_missing_or_undefined_executable_start_symbol_as_absent() { + assert_eq!( + ElfMetadata::parse(&elf64(None)).unwrap().executable_start, + None + ); + assert_eq!( + ElfMetadata::parse(&elf64(Some(false))) + .unwrap() + .executable_start, + None + ); } #[test] - fn rejects_truncated_elf_header() { - assert!(ElfMetadata::parse(b"\x7fELF\x02\x01\x01").is_err()); + fn rejects_invalid_elf_input() { + for data in [b"not an ELF".as_slice(), b"\x7fELF\x02\x01\x01"] { + assert!(ElfMetadata::parse(data).is_err()); + } } } diff --git a/ostool/src/artifact/state.rs b/ostool/src/artifact/state.rs index 3f4a6991..9ba8a027 100644 --- a/ostool/src/artifact/state.rs +++ b/ostool/src/artifact/state.rs @@ -23,7 +23,6 @@ struct CargoArtifactState { #[derive(Default, Clone, Debug)] struct RuntimeArtifactState { - source_elf: Option, elf: Option, bin: Option, artifact_dir: Option, @@ -118,7 +117,6 @@ impl OutputArtifacts { /// Replaces artifact state from a prepared runtime artifact set. pub(crate) fn apply_prepared_runtime_artifacts(&mut self, prepared: &PreparedRuntimeArtifacts) { self.debug = DebugArtifactRegistry::default(); - self.runtime.source_elf = Some(prepared.source_elf().to_path_buf()); self.cargo = prepared .cargo_source_artifact_dir() .map(|artifact_dir| CargoArtifactState { @@ -131,12 +129,6 @@ impl OutputArtifacts { self.runtime.source_artifact_dir = prepared.cargo_artifact_dir().map(PathBuf::from); } - /// Returns the selected source ELF, preserving symbols lost in runtime derivatives. - pub(crate) fn analysis_source_elf(&self) -> Option<&Path> { - self.cargo_source_elf() - .or(self.runtime.source_elf.as_deref()) - } - pub(crate) fn replace_debug_artifacts(&mut self, debug: DebugArtifactRegistry) { self.debug = debug; } diff --git a/ostool/src/build/analysis_tests.rs b/ostool/src/build/analysis_tests.rs index 3726e4ea..db3d02a7 100644 --- a/ostool/src/build/analysis_tests.rs +++ b/ostool/src/build/analysis_tests.rs @@ -1,5 +1,5 @@ use std::{ - collections::{BTreeSet, HashMap}, + collections::HashMap, fs, path::{Path, PathBuf}, process::Command, @@ -13,10 +13,7 @@ use crate::{ invocation::{Invocation, InvocationOptions}, }; -use super::{ - CargoQemuRunnerArgs, CargoRunnerKind, RuntimeArtifactInput, build_with_config, - prepare_runtime_artifact, run_with_config, -}; +use super::{RuntimeArtifactInput, build_with_config, prepare_runtime_artifact}; struct Fixture { _temp: tempfile::TempDir, @@ -104,10 +101,6 @@ fn llvm_nm(path: &Path) -> Vec { output.stdout } -fn symbol_lines(output: &[u8]) -> BTreeSet<&[u8]> { - output.split(|byte| *byte == b'\n').collect() -} - #[tokio::test] async fn cargo_build_emits_each_requested_analysis_artifact_after_hooks() { let fixture = Fixture::new(); @@ -138,7 +131,7 @@ async fn cargo_build_emits_each_requested_analysis_artifact_after_hooks() { let source = invocation .runtime_artifacts() - .analysis_source_elf() + .cargo_source_elf() .unwrap() .to_path_buf(); assert!(pre_marker.exists()); @@ -241,7 +234,7 @@ fn prepared_runtime_copy_keeps_source_and_debug_registry_when_adding_bin() { ); assert!(runtime_elf.exists()); assert_eq!( - invocation.runtime_artifacts().analysis_source_elf(), + invocation.runtime_artifacts().cargo_source_elf(), Some(source.as_path()) ); assert_eq!( @@ -251,18 +244,12 @@ fn prepared_runtime_copy_keeps_source_and_debug_registry_when_adding_bin() { assert!(bin.exists()); let source_symbols = llvm_nm(&source); let runtime_symbols = llvm_nm(&runtime_elf); - assert_ne!( - symbol_lines(&source_symbols), - symbol_lines(&runtime_symbols) - ); + assert_ne!(source_symbols, runtime_symbols); assert!( - symbol_lines(&fs::read(&symbols).unwrap()) == symbol_lines(&source_symbols), + fs::read(&symbols).unwrap() == source_symbols, "analysis must use the original ELF symbols" ); - assert_ne!( - symbol_lines(&fs::read(&symbols).unwrap()), - symbol_lines(&runtime_symbols) - ); + assert_ne!(fs::read(&symbols).unwrap(), runtime_symbols); assert_eq!( invocation .runtime_artifacts() @@ -273,7 +260,7 @@ fn prepared_runtime_copy_keeps_source_and_debug_registry_when_adding_bin() { } #[tokio::test] -async fn disabled_analysis_clears_debug_artifacts_from_previous_build() { +async fn rebuild_clears_debug_artifacts_when_analysis_is_disabled_or_fails() { let fixture = Fixture::new(); let source = fixture.copy_current_executable("analysis source"); let mut invocation = fixture.invocation(); @@ -283,12 +270,24 @@ async fn disabled_analysis_clears_debug_artifacts_from_previous_build() { build_with_config(&mut invocation, &requested, None) .await .unwrap(); - assert!( + let symbols = invocation + .runtime_artifacts() + .debug_artifacts() + .get(DebugArtifactKind::Symbols) + .unwrap() + .to_path_buf(); + fs::write(&symbols, "stale symbols").unwrap(); + + build_with_config(&mut invocation, &requested, None) + .await + .unwrap(); + assert_ne!(fs::read(&symbols).unwrap(), b"stale symbols"); + assert_eq!( invocation .runtime_artifacts() .debug_artifacts() - .get(DebugArtifactKind::Symbols) - .is_some() + .get(DebugArtifactKind::Symbols), + Some(symbols.as_path()) ); build_with_config(&mut invocation, &disabled, None) @@ -296,14 +295,6 @@ async fn disabled_analysis_clears_debug_artifacts_from_previous_build() { .unwrap(); assert!(invocation.runtime_artifacts().debug_artifacts().is_empty()); -} - -#[tokio::test] -async fn failed_analysis_clears_debug_artifacts_from_previous_build() { - let fixture = Fixture::new(); - let source = fixture.copy_current_executable("analysis source"); - let mut invocation = fixture.invocation(); - let requested = custom_build(&source, "true".into(), analysis_config(false, false, true)); let missing = fixture.root.join("missing source"); let failing = custom_build(&missing, "true".into(), analysis_config(false, false, true)); @@ -326,34 +317,3 @@ async fn failed_analysis_clears_debug_artifacts_from_previous_build() { assert!(invocation.runtime_artifacts().debug_artifacts().is_empty()); } - -#[tokio::test] -async fn invalid_runtime_elf_stops_before_qemu_config_is_generated() { - let fixture = Fixture::new(); - let invalid_elf = fixture.root.join("not an elf"); - fs::write(&invalid_elf, "not an ELF").unwrap(); - let mut invocation = fixture.invocation(); - let config = custom_build( - &invalid_elf, - "true".into(), - analysis_config(false, false, true), - ); - - let error = run_with_config( - &mut invocation, - &config, - None, - &CargoRunnerKind::new_qemu(CargoQemuRunnerArgs::default()), - ) - .await - .unwrap_err(); - - assert!(error.to_string().contains("failed to parse ELF file")); - assert!(fs::read_dir(&fixture.root).unwrap().all(|entry| { - !entry - .unwrap() - .file_name() - .to_string_lossy() - .starts_with(".qemu") - })); -} diff --git a/ostool/src/build/config.rs b/ostool/src/build/config.rs index a75390fd..d07d6a80 100644 --- a/ostool/src/build/config.rs +++ b/ostool/src/build/config.rs @@ -200,17 +200,7 @@ pub enum LogLevel { #[cfg(test)] mod tests { - use super::{ArtifactConfig, Cargo, Custom}; - - #[test] - fn artifact_analysis_defaults_to_disabled() { - let artifacts: ArtifactConfig = toml::from_str("").unwrap(); - - assert!(!artifacts.analysis.disassembly); - assert!(!artifacts.analysis.elf_info); - assert!(!artifacts.analysis.symbols); - assert!(!artifacts.analysis.is_enabled()); - } + use super::{Cargo, Custom}; #[test] fn legacy_build_configs_and_analysis_flags_round_trip() { diff --git a/ostool/src/build/mod.rs b/ostool/src/build/mod.rs index d6b77a21..fc01f149 100644 --- a/ostool/src/build/mod.rs +++ b/ostool/src/build/mod.rs @@ -406,7 +406,7 @@ fn generate_build_analysis( let source = match &config.system { BuildSystem::Cargo(_) => invocation .runtime_artifacts() - .analysis_source_elf() + .cargo_source_elf() .context("Cargo build did not select an ELF for analysis")? .to_path_buf(), BuildSystem::Custom(custom) => { @@ -466,7 +466,7 @@ pub async fn cargo_run( config_path: Option<&Path>, runner: &CargoRunnerKind, ) -> anyhow::Result<()> { - run_with_config( + cargo_run_with_config( invocation, &BuildConfig { system: BuildSystem::Cargo(Box::new(config.clone())), @@ -478,38 +478,27 @@ pub async fn cargo_run( .await } -/// Builds and runs a complete build configuration, including optional analysis. +/// Builds and runs Cargo with the complete configuration, including optional analysis. /// -/// Cargo-only callers may continue to use [`cargo_run`]. Analysis failures are -/// returned before starting the runner. -pub async fn run_with_config( +/// Rejects custom build configurations. Analysis failures are returned before +/// starting the runner; [`cargo_run`] uses the default analysis configuration. +pub async fn cargo_run_with_config( invocation: &mut Invocation, config: &BuildConfig, config_path: Option<&Path>, runner: &CargoRunnerKind, ) -> anyhow::Result<()> { - let debug = match &config.system { - BuildSystem::Cargo(_) => matches!(runner, CargoRunnerKind::Qemu(args) if args.debug), - BuildSystem::Custom(_) => invocation.options().debug(), + let BuildSystem::Cargo(cargo) = &config.system else { + anyhow::bail!("cargo_run_with_config requires a Cargo build configuration"); }; + let debug = matches!(runner, CargoRunnerKind::Qemu(args) if args.debug); prepare_with_config(invocation, config, config_path, debug).await?; match runner { CargoRunnerKind::Qemu(args) => { let qemu = match &args.qemu { Some(config) => config.clone(), - None => match &config.system { - BuildSystem::Cargo(cargo) => { - crate::run::qemu::ensure_config_for_cargo(invocation, cargo).await? - } - BuildSystem::Custom(_) => { - crate::run::qemu::ensure_config_in_dir( - invocation, - invocation.workspace_dir(), - ) - .await? - } - }, + None => crate::run::qemu::ensure_config_for_cargo(invocation, cargo).await?, }; crate::run::qemu::run_qemu_with_debug( invocation, @@ -524,18 +513,7 @@ pub async fn run_with_config( CargoRunnerKind::Uboot(args) => { let uboot = match &args.uboot { Some(config) => config.clone(), - None => match &config.system { - BuildSystem::Cargo(cargo) => { - crate::run::uboot::ensure_config_for_cargo(invocation, cargo).await? - } - BuildSystem::Custom(_) => { - crate::run::uboot::ensure_config_in_dir( - invocation, - invocation.workspace_dir(), - ) - .await? - } - }, + None => crate::run::uboot::ensure_config_for_cargo(invocation, cargo).await?, }; crate::run::uboot::run_uboot(invocation, &uboot).await?; } diff --git a/ostool/src/main.rs b/ostool/src/main.rs index 5cd93b80..d6dc2c60 100644 --- a/ostool/src/main.rs +++ b/ostool/src/main.rs @@ -378,7 +378,7 @@ async fn try_main() -> Result<()> { debug, dtb_dump, }); - build::run_with_config( + build::cargo_run_with_config( &mut invocation, &loaded_build_config.config, Some(loaded_build_config.path.as_path()), @@ -437,7 +437,7 @@ async fn try_main() -> Result<()> { let kind = CargoRunnerKind::new_uboot(CargoUbootRunnerArgs { uboot: uboot_config, }); - build::run_with_config( + build::cargo_run_with_config( &mut invocation, &loaded_build_config.config, Some(loaded_build_config.path.as_path()), diff --git a/ostool/tests/analysis_cli.rs b/ostool/tests/analysis_cli.rs index 235359c1..5f16def1 100644 --- a/ostool/tests/analysis_cli.rs +++ b/ostool/tests/analysis_cli.rs @@ -58,11 +58,8 @@ fn custom_build_generates_runner_config(runner: &str, field: &str) { } #[test] -fn custom_qemu_config_is_read_after_build_hook() { - custom_build_generates_runner_config("qemu", "args"); -} - -#[test] -fn custom_uboot_config_is_read_after_build_hook() { - custom_build_generates_runner_config("uboot", "dtb_file"); +fn custom_runner_configs_are_read_after_build_hooks() { + for (runner, field) in [("qemu", "args"), ("uboot", "dtb_file")] { + custom_build_generates_runner_config(runner, field); + } } diff --git a/ostool/tests/ui/pass_module_level_apis.rs b/ostool/tests/ui/pass_module_level_apis.rs index b2a44917..706af276 100644 --- a/ostool/tests/ui/pass_module_level_apis.rs +++ b/ostool/tests/ui/pass_module_level_apis.rs @@ -91,7 +91,7 @@ fn main() { .strip_elf(false), ); let _ = build::prepare_with_config(&mut invocation, &custom_build, None, false).await; - let _ = build::run_with_config(&mut invocation, &cargo_build, None, &qemu_runner).await; + let _ = build::cargo_run_with_config(&mut invocation, &cargo_build, None, &qemu_runner).await; let _ = build::cargo_run(&mut invocation, &cargo, None, &qemu_runner).await; let _ = build::cargo_run(&mut invocation, &cargo, None, &uboot_runner).await; From c3eb392ae7ed576acab71740bb949a53cf00138c Mon Sep 17 00:00:00 2001 From: Niu Zhihong Date: Tue, 22 Sep 2026 16:17:45 +0800 Subject: [PATCH 3/8] =?UTF-8?q?test(ostool):=20=E5=88=A0=E9=99=A4=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E9=AA=8C=E8=AF=81=E5=B9=B6=E7=B2=BE=E7=AE=80=20ELF=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=A4=B9=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除 metadata 层重复的非法 ELF 验证,保留分析入口的失败回归。 将 Custom build-only 状态断言并入重建测试,删除配置通用序列化 往返及可由其他检查推出的断言。简化无符号 ELF 与 PT_NOTE 夹具, 保留多段地址区分、符号定义状态和原始 ELF 来源验证。 Co-authored-by: OpenAI Codex Signed-off-by: Niu Zhihong --- ostool/src/artifact/elf_metadata.rs | 37 +++++------------------------ ostool/src/build/analysis_tests.rs | 36 ++-------------------------- ostool/src/build/config.rs | 7 +----- 3 files changed, 9 insertions(+), 71 deletions(-) diff --git a/ostool/src/artifact/elf_metadata.rs b/ostool/src/artifact/elf_metadata.rs index 83db651c..d742a1cf 100644 --- a/ostool/src/artifact/elf_metadata.rs +++ b/ostool/src/artifact/elf_metadata.rs @@ -109,19 +109,9 @@ mod tests { write_u32(&mut data, 20, 1); write_u64(&mut data, 24, 0x400080); write_u64(&mut data, 32, PROGRAM_HEADER_OFFSET as u64); - write_u64( - &mut data, - 40, - symbol_defined - .map(|_| SECTION_HEADER_OFFSET as u64) - .unwrap_or(0), - ); write_u16(&mut data, 52, 64); write_u16(&mut data, 54, 56); write_u16(&mut data, 56, 3); - write_u16(&mut data, 58, 64); - write_u16(&mut data, 60, u16::from(symbol_defined.is_some()) * 3); - write_u16(&mut data, 62, u16::from(symbol_defined.is_some())); write_u32(&mut data, PROGRAM_HEADER_OFFSET, 1); write_u32(&mut data, PROGRAM_HEADER_OFFSET + 4, 5); @@ -142,17 +132,14 @@ mod tests { write_u64(&mut data, second_program_header + 40, 0x30); write_u64(&mut data, second_program_header + 48, 0x1000); - let note_program_header = second_program_header + 56; - write_u32(&mut data, note_program_header, 4); - write_u32(&mut data, note_program_header + 4, 4); - write_u64(&mut data, note_program_header + 8, 0x280); - write_u64(&mut data, note_program_header + 16, 0x402000); - write_u64(&mut data, note_program_header + 24, 0x502000); - write_u64(&mut data, note_program_header + 32, 0x10); - write_u64(&mut data, note_program_header + 40, 0x10); - write_u64(&mut data, note_program_header + 48, 4); + // An empty PT_NOTE must not appear among the load segments. + write_u32(&mut data, PROGRAM_HEADER_OFFSET + 112, 4); if let Some(symbol_defined) = symbol_defined { + write_u64(&mut data, 40, SECTION_HEADER_OFFSET as u64); + write_u16(&mut data, 58, 64); + write_u16(&mut data, 60, 3); + write_u16(&mut data, 62, 1); data[STRING_TABLE_OFFSET..STRING_TABLE_OFFSET + STRING_TABLE.len()] .copy_from_slice(STRING_TABLE); @@ -213,11 +200,6 @@ mod tests { let second = &metadata.load_segments[1]; assert_eq!(second.virtual_address, 0x401000); assert_eq!(second.physical_address, 0x501000); - assert_eq!(second.file_offset, 0x200); - assert_eq!(second.file_size, 0x20); - assert_eq!(second.memory_size, 0x30); - assert_eq!(second.alignment, 0x1000); - assert_eq!(second.flags, 6); } #[test] @@ -233,11 +215,4 @@ mod tests { None ); } - - #[test] - fn rejects_invalid_elf_input() { - for data in [b"not an ELF".as_slice(), b"\x7fELF\x02\x01\x01"] { - assert!(ElfMetadata::parse(data).is_err()); - } - } } diff --git a/ostool/src/build/analysis_tests.rs b/ostool/src/build/analysis_tests.rs index db3d02a7..6d74a033 100644 --- a/ostool/src/build/analysis_tests.rs +++ b/ostool/src/build/analysis_tests.rs @@ -159,38 +159,6 @@ async fn cargo_build_emits_each_requested_analysis_artifact_after_hooks() { } } -#[tokio::test] -async fn custom_build_analysis_keeps_build_only_runtime_state_empty() { - let fixture = Fixture::new(); - let source = fixture.copy_current_executable("custom source with spaces"); - let marker = fixture.root.join("custom build marker"); - let mut invocation = fixture.invocation(); - let config = custom_build( - &source, - format!("touch {}", quoted(&marker)), - analysis_config(false, false, true), - ); - - build_with_config(&mut invocation, &config, None) - .await - .unwrap(); - - assert!(marker.exists()); - assert!(invocation.runtime_artifacts().elf().is_none()); - assert!(invocation.runtime_artifacts().bin().is_none()); - assert_eq!( - invocation - .runtime_artifacts() - .debug_artifacts() - .get(DebugArtifactKind::Symbols), - Some( - source - .with_file_name("custom source with spaces.symbols") - .as_path() - ) - ); -} - #[test] fn prepared_runtime_copy_keeps_source_and_debug_registry_when_adding_bin() { let fixture = Fixture::new(); @@ -227,7 +195,6 @@ fn prepared_runtime_copy_keeps_source_and_debug_registry_when_adding_bin() { let bin = invocation.ensure_runtime_bin().unwrap(); - assert_ne!(runtime_elf, source); assert!( fs::read(&source).unwrap() == original_bytes, "runtime preparation must preserve the source bytes" @@ -249,7 +216,6 @@ fn prepared_runtime_copy_keeps_source_and_debug_registry_when_adding_bin() { fs::read(&symbols).unwrap() == source_symbols, "analysis must use the original ELF symbols" ); - assert_ne!(fs::read(&symbols).unwrap(), runtime_symbols); assert_eq!( invocation .runtime_artifacts() @@ -270,6 +236,8 @@ async fn rebuild_clears_debug_artifacts_when_analysis_is_disabled_or_fails() { build_with_config(&mut invocation, &requested, None) .await .unwrap(); + assert!(invocation.runtime_artifacts().elf().is_none()); + assert!(invocation.runtime_artifacts().bin().is_none()); let symbols = invocation .runtime_artifacts() .debug_artifacts() diff --git a/ostool/src/build/config.rs b/ostool/src/build/config.rs index d07d6a80..3b0e34fe 100644 --- a/ostool/src/build/config.rs +++ b/ostool/src/build/config.rs @@ -203,7 +203,7 @@ mod tests { use super::{Cargo, Custom}; #[test] - fn legacy_build_configs_and_analysis_flags_round_trip() { + fn legacy_build_configs_and_analysis_flags_parse_with_defaults() { let legacy = "[system.Custom]\nbuild_cmd = \"make\"\nelf_path = \"kernel.elf\"\n"; let config: super::BuildConfig = toml::from_str(legacy).unwrap(); assert!(!config.artifacts.analysis.is_enabled()); @@ -214,11 +214,6 @@ mod tests { assert!(config.artifacts.analysis.disassembly); assert!(config.artifacts.analysis.elf_info); assert!(config.artifacts.analysis.symbols); - let rendered = toml::to_string(&config).unwrap(); - assert_eq!( - toml::from_str::(&rendered).unwrap(), - config - ); let partial: super::BuildConfig = toml::from_str(&format!("{legacy}\n[artifacts.analysis]\nsymbols = true\n")).unwrap(); assert!(!partial.artifacts.analysis.disassembly); From 4157eb53da2669914ed72886aa9bddd9ac0e4c97 Mon Sep 17 00:00:00 2001 From: Niu Zhihong Date: Tue, 22 Sep 2026 16:43:46 +0800 Subject: [PATCH 4/8] =?UTF-8?q?test(ostool):=20=E7=B2=BE=E7=AE=80=E5=88=86?= =?UTF-8?q?=E6=9E=90=E6=B5=8B=E8=AF=95=E5=B9=B6=E5=A4=8D=E7=94=A8=E6=97=A2?= =?UTF-8?q?=E6=9C=89=E5=9B=9E=E5=BD=92=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除独立构建与 CLI 分析验收文件、手工 ELF 测试夹具和假工具测试。 完整场景保留为本地验收,仓库仅新增短配置解析测试,并在已有 runtime/build 用例中验证源 ELF 不被覆写及旧分析登记失效。 移除只用于测试注入的 resolver 包装,直接调用既有 LLVM 定位函数。 产物生成、metadata 解析及错误处理行为保持一致。 Co-authored-by: OpenAI Codex Signed-off-by: Niu Zhihong --- ostool/src/artifact/analysis.rs | 166 +--------------- ostool/src/artifact/elf_metadata.rs | 147 -------------- ostool/src/artifact/runtime.rs | 8 +- ostool/src/build/analysis_tests.rs | 287 ---------------------------- ostool/src/build/mod.rs | 9 +- ostool/tests/analysis_cli.rs | 65 ------- 6 files changed, 14 insertions(+), 668 deletions(-) delete mode 100644 ostool/src/build/analysis_tests.rs delete mode 100644 ostool/tests/analysis_cli.rs diff --git a/ostool/src/artifact/analysis.rs b/ostool/src/artifact/analysis.rs index 0fdfab63..231aacdf 100644 --- a/ostool/src/artifact/analysis.rs +++ b/ostool/src/artifact/analysis.rs @@ -27,18 +27,6 @@ pub(crate) fn generate( config: &AnalysisConfig, source_elf: &Path, ) -> anyhow::Result { - generate_with_tool_resolver(context, config, source_elf, llvm_tools::llvm_tool) -} - -fn generate_with_tool_resolver( - context: &ProcessContext, - config: &AnalysisConfig, - source_elf: &Path, - mut resolve_tool: F, -) -> anyhow::Result -where - F: FnMut(&str) -> anyhow::Result, -{ if !config.is_enabled() { return Ok(DebugArtifactRegistry::default()); } @@ -78,7 +66,7 @@ where let metadata = elf_metadata(source_elf)?; let mut staged = Vec::new(); for (kind, path, tool_name, args) in &requested { - let tool = resolve_tool(tool_name).with_context(|| { + let tool = llvm_tools::llvm_tool(tool_name).with_context(|| { format!( "failed to resolve {tool_name} for ELF {}", source_elf.display() @@ -198,155 +186,3 @@ fn output_path(source_elf: &Path, suffix: &str) -> PathBuf { name.push(suffix); source_elf.with_file_name(name) } - -#[cfg(test)] -mod tests { - use std::{ - fs, - path::{Path, PathBuf}, - }; - - use crate::{ - artifact::analysis::generate_with_tool_resolver, - build::config::AnalysisConfig, - process::ProcessContext, - project::{resolve_project_layout, variables::VariableScope}, - }; - use anyhow::anyhow; - - fn process_context(root: &std::path::Path) -> ProcessContext { - std::fs::write( - root.join("Cargo.toml"), - "[package]\nname = \"sample\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", - ) - .unwrap(); - std::fs::create_dir_all(root.join("src")).unwrap(); - std::fs::write(root.join("src/lib.rs"), "").unwrap(); - - let layout = resolve_project_layout(Some(root.to_path_buf())).unwrap(); - let scope = VariableScope::for_package(&layout, root.to_path_buf()); - ProcessContext::new(root.to_path_buf(), root.to_path_buf(), scope, None) - } - - fn write_elf(path: &Path) { - // Minimal ELF64 with no sections or program headers. - let mut elf = [0u8; 64]; - elf[..7].copy_from_slice(b"\x7fELF\x02\x01\x01"); - elf[16] = 2; // ET_EXEC - elf[18] = 62; // EM_X86_64 - elf[20] = 1; - elf[52] = 64; - fs::write(path, elf).unwrap(); - } - - #[test] - fn disabled_analysis_does_not_read_the_elf_or_lookup_tools() { - let temp = tempfile::tempdir().unwrap(); - let context = process_context(temp.path()); - let missing_source = temp.path().join("missing kernel.elf"); - - let registry = generate_with_tool_resolver( - &context, - &AnalysisConfig::default(), - &missing_source, - |_| panic!("disabled analysis must not resolve any tools"), - ) - .unwrap(); - - assert!(registry.is_empty()); - } - - #[cfg(unix)] - fn make_executable(path: &Path) { - use std::os::unix::fs::PermissionsExt; - - let mut permissions = fs::metadata(path).unwrap().permissions(); - permissions.set_mode(0o755); - fs::set_permissions(path, permissions).unwrap(); - } - - #[cfg(unix)] - fn fake_tool(root: &Path, name: &str, body: &str) -> PathBuf { - let path = root.join(name); - fs::write(&path, format!("#!/bin/sh\n{body}\n")).unwrap(); - make_executable(&path); - path - } - - #[cfg(unix)] - #[test] - fn missing_tool_clears_stale_requested_output() { - let temp = tempfile::tempdir().unwrap(); - let context = process_context(temp.path()); - let source = temp.path().join("kernel with spaces.elf"); - write_elf(&source); - let stale = super::output_path(&source, "symbols"); - fs::write(&stale, "stale").unwrap(); - let config = AnalysisConfig { - symbols: true, - ..Default::default() - }; - - let error = generate_with_tool_resolver(&context, &config, &source, |_| { - Err(anyhow!("llvm-tools unavailable")) - }) - .unwrap_err(); - - assert!(error.to_string().contains("failed to resolve llvm-nm")); - assert!(!stale.exists()); - } - - #[cfg(unix)] - #[test] - fn tool_failure_reports_source_status_and_stderr_without_partial_outputs() { - let temp = tempfile::tempdir().unwrap(); - let context = process_context(temp.path()); - let source = temp.path().join("kernel with spaces.elf"); - write_elf(&source); - let disassembly = super::output_path(&source, "disassembly"); - let symbols = super::output_path(&source, "symbols"); - fs::write(&disassembly, "stale disassembly").unwrap(); - fs::write(&symbols, "stale symbols").unwrap(); - let objdump = fake_tool(temp.path(), "objdump", "printf disassembly"); - let nm = fake_tool(temp.path(), "nm", "printf 'broken symbols' >&2\nexit 7"); - let config = AnalysisConfig { - disassembly: true, - symbols: true, - ..Default::default() - }; - - let error = generate_with_tool_resolver(&context, &config, &source, |tool| match tool { - "llvm-objdump" => Ok(objdump.clone()), - "llvm-nm" => Ok(nm.clone()), - unexpected => Err(anyhow!("unexpected tool: {unexpected}")), - }) - .unwrap_err(); - let message = format!("{error:#}"); - - assert!(message.contains("llvm-nm")); - assert!(message.contains(&source.display().to_string())); - assert!(message.contains("exit status: 7")); - assert!(message.contains("broken symbols")); - assert!(!disassembly.exists()); - assert!(!symbols.exists()); - } - - #[cfg(unix)] - #[test] - fn invalid_elf_fails_before_any_tool_is_resolved() { - let temp = tempfile::tempdir().unwrap(); - let context = process_context(temp.path()); - let source = temp.path().join("invalid.elf"); - fs::write(&source, "not an ELF").unwrap(); - let config = AnalysisConfig { - symbols: true, - ..Default::default() - }; - let error = generate_with_tool_resolver(&context, &config, &source, |_| { - panic!("invalid ELF must be rejected before tool lookup") - }) - .unwrap_err(); - assert!(format!("{error:#}").contains("failed to parse ELF file")); - assert!(!super::output_path(&source, "symbols").exists()); - } -} diff --git a/ostool/src/artifact/elf_metadata.rs b/ostool/src/artifact/elf_metadata.rs index d742a1cf..a07c77c3 100644 --- a/ostool/src/artifact/elf_metadata.rs +++ b/ostool/src/artifact/elf_metadata.rs @@ -69,150 +69,3 @@ impl ElfMetadata { }) } } - -#[cfg(test)] -mod tests { - use super::*; - - use object::Architecture; - - fn write_u16(data: &mut [u8], offset: usize, value: u16) { - let bytes = value.to_le_bytes(); - data[offset..offset + bytes.len()].copy_from_slice(&bytes); - } - - fn write_u32(data: &mut [u8], offset: usize, value: u32) { - let bytes = value.to_le_bytes(); - data[offset..offset + bytes.len()].copy_from_slice(&bytes); - } - - fn write_u64(data: &mut [u8], offset: usize, value: u64) { - let bytes = value.to_le_bytes(); - data[offset..offset + bytes.len()].copy_from_slice(&bytes); - } - - fn elf64(symbol_defined: Option) -> Vec { - const PROGRAM_HEADER_OFFSET: usize = 64; - const STRING_TABLE_OFFSET: usize = 0x180; - const SYMBOL_TABLE_OFFSET: usize = 0x1c0; - const SECTION_HEADER_OFFSET: usize = 0x200; - const STRING_TABLE: &[u8] = b"\0__executable_start\0"; - - let mut data = vec![0; 0x300]; - data[..4].copy_from_slice(b"\x7fELF"); - data[4] = 2; - data[5] = 1; - data[6] = 1; - - write_u16(&mut data, 16, 2); - write_u16(&mut data, 18, 183); - write_u32(&mut data, 20, 1); - write_u64(&mut data, 24, 0x400080); - write_u64(&mut data, 32, PROGRAM_HEADER_OFFSET as u64); - write_u16(&mut data, 52, 64); - write_u16(&mut data, 54, 56); - write_u16(&mut data, 56, 3); - - write_u32(&mut data, PROGRAM_HEADER_OFFSET, 1); - write_u32(&mut data, PROGRAM_HEADER_OFFSET + 4, 5); - write_u64(&mut data, PROGRAM_HEADER_OFFSET + 8, 0x100); - write_u64(&mut data, PROGRAM_HEADER_OFFSET + 16, 0x400000); - write_u64(&mut data, PROGRAM_HEADER_OFFSET + 24, 0x500000); - write_u64(&mut data, PROGRAM_HEADER_OFFSET + 32, 0x30); - write_u64(&mut data, PROGRAM_HEADER_OFFSET + 40, 0x40); - write_u64(&mut data, PROGRAM_HEADER_OFFSET + 48, 0x1000); - - let second_program_header = PROGRAM_HEADER_OFFSET + 56; - write_u32(&mut data, second_program_header, 1); - write_u32(&mut data, second_program_header + 4, 6); - write_u64(&mut data, second_program_header + 8, 0x200); - write_u64(&mut data, second_program_header + 16, 0x401000); - write_u64(&mut data, second_program_header + 24, 0x501000); - write_u64(&mut data, second_program_header + 32, 0x20); - write_u64(&mut data, second_program_header + 40, 0x30); - write_u64(&mut data, second_program_header + 48, 0x1000); - - // An empty PT_NOTE must not appear among the load segments. - write_u32(&mut data, PROGRAM_HEADER_OFFSET + 112, 4); - - if let Some(symbol_defined) = symbol_defined { - write_u64(&mut data, 40, SECTION_HEADER_OFFSET as u64); - write_u16(&mut data, 58, 64); - write_u16(&mut data, 60, 3); - write_u16(&mut data, 62, 1); - data[STRING_TABLE_OFFSET..STRING_TABLE_OFFSET + STRING_TABLE.len()] - .copy_from_slice(STRING_TABLE); - - let symbol = SYMBOL_TABLE_OFFSET + 24; - write_u32(&mut data, symbol, 1); - data[symbol + 4] = 0x10; - write_u16(&mut data, symbol + 6, u16::from(symbol_defined)); - write_u64(&mut data, symbol + 8, 0x400080); - - let string_table_section = SECTION_HEADER_OFFSET + 64; - write_u32(&mut data, string_table_section + 4, 3); - write_u64( - &mut data, - string_table_section + 24, - STRING_TABLE_OFFSET as u64, - ); - write_u64( - &mut data, - string_table_section + 32, - STRING_TABLE.len() as u64, - ); - write_u64(&mut data, string_table_section + 48, 1); - - let symbol_table_section = SECTION_HEADER_OFFSET + 128; - write_u32(&mut data, symbol_table_section + 4, 2); - write_u64( - &mut data, - symbol_table_section + 24, - SYMBOL_TABLE_OFFSET as u64, - ); - write_u64(&mut data, symbol_table_section + 32, 48); - write_u32(&mut data, symbol_table_section + 40, 1); - write_u64(&mut data, symbol_table_section + 48, 8); - write_u64(&mut data, symbol_table_section + 56, 24); - } - - data - } - - #[test] - fn parses_elf64_load_segments_and_executable_start_symbol() { - let metadata = ElfMetadata::parse(&elf64(Some(true))).unwrap(); - - assert_eq!(metadata.arch, Architecture::Aarch64); - assert_eq!(metadata.entry, 0x400080); - assert_eq!(metadata.executable_start, Some(0x400080)); - assert_eq!(metadata.load_segments.len(), 2); - - let first = &metadata.load_segments[0]; - assert_eq!(first.virtual_address, 0x400000); - assert_eq!(first.physical_address, 0x500000); - assert_eq!(first.file_offset, 0x100); - assert_eq!(first.file_size, 0x30); - assert_eq!(first.memory_size, 0x40); - assert_eq!(first.alignment, 0x1000); - assert_eq!(first.flags, 5); - - let second = &metadata.load_segments[1]; - assert_eq!(second.virtual_address, 0x401000); - assert_eq!(second.physical_address, 0x501000); - } - - #[test] - fn treats_missing_or_undefined_executable_start_symbol_as_absent() { - assert_eq!( - ElfMetadata::parse(&elf64(None)).unwrap().executable_start, - None - ); - assert_eq!( - ElfMetadata::parse(&elf64(Some(false))) - .unwrap() - .executable_start, - None - ); - } -} diff --git a/ostool/src/artifact/runtime.rs b/ostool/src/artifact/runtime.rs index 0d0cd335..a66f57da 100644 --- a/ostool/src/artifact/runtime.rs +++ b/ostool/src/artifact/runtime.rs @@ -291,7 +291,9 @@ mod tests { fn prepares_stripped_elf_without_mutating_tool_state() { let temp = tempfile::tempdir().unwrap(); let context = process_context(temp.path()); - let input = copy_current_exe(temp.path()); + let input = temp.path().join("sample.elf"); + fs::copy(std::env::current_exe().unwrap(), &input).unwrap(); + let source_bytes = fs::read(&input).unwrap(); let prepared = prepare_runtime_artifacts( &context, @@ -306,7 +308,9 @@ mod tests { ) .unwrap(); - let expected_elf = input.with_file_name("sample.elf"); + let expected_elf = input.with_file_name("sample.elf.runtime.elf"); + assert_eq!(prepared.source_elf(), input.canonicalize().unwrap()); + assert!(fs::read(&input).unwrap() == source_bytes); assert_eq!(prepared.elf(), expected_elf); assert!(prepared.bin().is_none()); assert_eq!( diff --git a/ostool/src/build/analysis_tests.rs b/ostool/src/build/analysis_tests.rs deleted file mode 100644 index 6d74a033..00000000 --- a/ostool/src/build/analysis_tests.rs +++ /dev/null @@ -1,287 +0,0 @@ -use std::{ - collections::HashMap, - fs, - path::{Path, PathBuf}, - process::Command, -}; - -use crate::{ - artifact::state::DebugArtifactKind, - build::config::{ - AnalysisConfig, ArtifactConfig, BuildConfig, BuildSystem, Cargo, CargoBuildProfile, Custom, - }, - invocation::{Invocation, InvocationOptions}, -}; - -use super::{RuntimeArtifactInput, build_with_config, prepare_runtime_artifact}; - -struct Fixture { - _temp: tempfile::TempDir, - root: PathBuf, -} - -impl Fixture { - fn new() -> Self { - let temp = tempfile::tempdir().unwrap(); - let root = temp.path().join("project with spaces"); - fs::create_dir_all(root.join("src")).unwrap(); - fs::write( - root.join("Cargo.toml"), - "[package]\nname = \"analysis-fixture\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", - ) - .unwrap(); - fs::write( - root.join("src/main.rs"), - "fn main() { println!(\"fixture\"); }\n", - ) - .unwrap(); - Self { _temp: temp, root } - } - - fn invocation(&self) -> Invocation { - Invocation::new(InvocationOptions::new( - Some(self.root.clone()), - None, - None, - false, - )) - .unwrap() - } - - fn copy_current_executable(&self, name: &str) -> PathBuf { - let path = self.root.join(name); - fs::copy(std::env::current_exe().unwrap(), &path).unwrap(); - path - } -} - -fn native_target() -> String { - let output = Command::new("rustc").arg("-vV").output().unwrap(); - assert!(output.status.success()); - String::from_utf8(output.stdout) - .unwrap() - .lines() - .find_map(|line| line.strip_prefix("host: ")) - .expect("rustc -vV reports a host target") - .to_owned() -} - -fn quoted(path: &Path) -> String { - format!("'{}'", path.display()) -} - -fn analysis_config(disassembly: bool, elf_info: bool, symbols: bool) -> ArtifactConfig { - ArtifactConfig { - analysis: AnalysisConfig { - disassembly, - elf_info, - symbols, - }, - } -} - -fn custom_build(elf_path: &Path, build_cmd: String, artifacts: ArtifactConfig) -> BuildConfig { - BuildConfig { - artifacts, - system: BuildSystem::Custom(Custom { - build_cmd, - elf_path: elf_path.display().to_string(), - to_bin: false, - }), - } -} - -fn llvm_nm(path: &Path) -> Vec { - let output = Command::new(crate::artifact::llvm_tools::llvm_tool("llvm-nm").unwrap()) - .args(["--demangle", "--print-size", "--numeric-sort"]) - .arg(path) - .output() - .unwrap(); - assert!(output.status.success()); - output.stdout -} - -#[tokio::test] -async fn cargo_build_emits_each_requested_analysis_artifact_after_hooks() { - let fixture = Fixture::new(); - let pre_marker = fixture.root.join("pre build marker"); - let post_marker = fixture.root.join("post build marker"); - let mut invocation = fixture.invocation(); - let config = BuildConfig { - artifacts: analysis_config(true, true, true), - system: BuildSystem::Cargo(Box::new(Cargo { - env: HashMap::from([("RUSTC_BOOTSTRAP".into(), "1".into())]), - target: native_target(), - package: "analysis-fixture".into(), - bin: Some("analysis-fixture".into()), - profile: Some(CargoBuildProfile::Debug), - disable_someboot_build_config: true, - pre_build_cmds: vec![format!("touch {}", quoted(&pre_marker))], - post_build_cmds: vec![format!( - "printf '%s' \"$KERNEL_ELF\" > {}", - quoted(&post_marker) - )], - ..Default::default() - })), - }; - - build_with_config(&mut invocation, &config, None) - .await - .unwrap(); - - let source = invocation - .runtime_artifacts() - .cargo_source_elf() - .unwrap() - .to_path_buf(); - assert!(pre_marker.exists()); - assert_eq!( - fs::read_to_string(&post_marker).unwrap(), - source.display().to_string() - ); - for (kind, suffix) in [ - (DebugArtifactKind::Disassembly, "disassembly"), - (DebugArtifactKind::ElfInfo, "elf-info"), - (DebugArtifactKind::Symbols, "symbols"), - ] { - let path = invocation - .runtime_artifacts() - .debug_artifacts() - .get(kind) - .unwrap(); - assert_eq!( - path, - source - .with_file_name(format!("analysis-fixture.{suffix}")) - .as_path() - ); - assert!(!fs::read(path).unwrap().is_empty()); - } -} - -#[test] -fn prepared_runtime_copy_keeps_source_and_debug_registry_when_adding_bin() { - let fixture = Fixture::new(); - let source = fixture.copy_current_executable("original image.elf"); - let original_bytes = fs::read(&source).unwrap(); - let cargo_artifact_dir = fixture.root.join("cargo artifacts"); - fs::create_dir_all(&cargo_artifact_dir).unwrap(); - let mut invocation = fixture.invocation(); - - prepare_runtime_artifact( - &mut invocation, - RuntimeArtifactInput::new(&source, false) - .with_cargo_artifact_dir(&cargo_artifact_dir) - .strip_elf(true), - ) - .unwrap(); - let source = source.canonicalize().unwrap(); - let runtime_elf = source.with_file_name("original image.elf.runtime.elf"); - let stripped_runtime = source.with_file_name("runtime symbols removed.elf"); - let status = Command::new(crate::artifact::llvm_tools::llvm_objcopy().unwrap()) - .arg("--strip-all") - .arg(&runtime_elf) - .arg(&stripped_runtime) - .status() - .unwrap(); - assert!(status.success()); - fs::rename(&stripped_runtime, &runtime_elf).unwrap(); - let config = BuildConfig { - artifacts: analysis_config(false, false, true), - system: BuildSystem::Cargo(Box::default()), - }; - super::generate_build_analysis(&mut invocation, &config).unwrap(); - let symbols = source.with_file_name("original image.elf.symbols"); - - let bin = invocation.ensure_runtime_bin().unwrap(); - - assert!( - fs::read(&source).unwrap() == original_bytes, - "runtime preparation must preserve the source bytes" - ); - assert!(runtime_elf.exists()); - assert_eq!( - invocation.runtime_artifacts().cargo_source_elf(), - Some(source.as_path()) - ); - assert_eq!( - invocation.runtime_artifacts().elf(), - Some(runtime_elf.as_path()) - ); - assert!(bin.exists()); - let source_symbols = llvm_nm(&source); - let runtime_symbols = llvm_nm(&runtime_elf); - assert_ne!(source_symbols, runtime_symbols); - assert!( - fs::read(&symbols).unwrap() == source_symbols, - "analysis must use the original ELF symbols" - ); - assert_eq!( - invocation - .runtime_artifacts() - .debug_artifacts() - .get(DebugArtifactKind::Symbols), - Some(symbols.as_path()) - ); -} - -#[tokio::test] -async fn rebuild_clears_debug_artifacts_when_analysis_is_disabled_or_fails() { - let fixture = Fixture::new(); - let source = fixture.copy_current_executable("analysis source"); - let mut invocation = fixture.invocation(); - let requested = custom_build(&source, "true".into(), analysis_config(false, false, true)); - let disabled = custom_build(&source, "true".into(), ArtifactConfig::default()); - - build_with_config(&mut invocation, &requested, None) - .await - .unwrap(); - assert!(invocation.runtime_artifacts().elf().is_none()); - assert!(invocation.runtime_artifacts().bin().is_none()); - let symbols = invocation - .runtime_artifacts() - .debug_artifacts() - .get(DebugArtifactKind::Symbols) - .unwrap() - .to_path_buf(); - fs::write(&symbols, "stale symbols").unwrap(); - - build_with_config(&mut invocation, &requested, None) - .await - .unwrap(); - assert_ne!(fs::read(&symbols).unwrap(), b"stale symbols"); - assert_eq!( - invocation - .runtime_artifacts() - .debug_artifacts() - .get(DebugArtifactKind::Symbols), - Some(symbols.as_path()) - ); - - build_with_config(&mut invocation, &disabled, None) - .await - .unwrap(); - - assert!(invocation.runtime_artifacts().debug_artifacts().is_empty()); - let missing = fixture.root.join("missing source"); - let failing = custom_build(&missing, "true".into(), analysis_config(false, false, true)); - - build_with_config(&mut invocation, &requested, None) - .await - .unwrap(); - assert!( - invocation - .runtime_artifacts() - .debug_artifacts() - .get(DebugArtifactKind::Symbols) - .is_some() - ); - - assert!( - build_with_config(&mut invocation, &failing, None) - .await - .is_err() - ); - - assert!(invocation.runtime_artifacts().debug_artifacts().is_empty()); -} diff --git a/ostool/src/build/mod.rs b/ostool/src/build/mod.rs index fc01f149..a1038046 100644 --- a/ostool/src/build/mod.rs +++ b/ostool/src/build/mod.rs @@ -37,8 +37,6 @@ use crate::{ }, }; -#[cfg(test)] -mod analysis_tests; mod artifact_selector; pub(crate) mod config_hooks; pub(crate) mod config_loader; @@ -741,6 +739,12 @@ mod tests { false, )) .unwrap(); + let mut stale = crate::artifact::state::DebugArtifactRegistry::default(); + stale.register( + crate::artifact::state::DebugArtifactKind::Symbols, + temp.path().join("old.symbols"), + ); + invocation.replace_debug_artifacts(stale); let config = BuildConfig { artifacts: Default::default(), system: BuildSystem::Custom(Custom { @@ -758,6 +762,7 @@ mod tests { assert!(invocation.runtime_artifacts().elf().is_none()); assert!(invocation.runtime_artifacts().bin().is_none()); assert!(invocation.runtime_arch().is_none()); + assert!(invocation.runtime_artifacts().debug_artifacts().is_empty()); } #[test] diff --git a/ostool/tests/analysis_cli.rs b/ostool/tests/analysis_cli.rs deleted file mode 100644 index 5f16def1..00000000 --- a/ostool/tests/analysis_cli.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Custom build hooks may generate the explicitly requested runner configuration. -#![cfg(unix)] - -use std::{fs, process::Command}; - -fn custom_build_generates_runner_config(runner: &str, field: &str) { - let temp = tempfile::tempdir().unwrap(); - fs::create_dir(temp.path().join("src")).unwrap(); - fs::write( - temp.path().join("Cargo.toml"), - "[package]\nname = \"hook-fixture\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", - ) - .unwrap(); - fs::write(temp.path().join("src/main.rs"), "fn main() {}\n").unwrap(); - fs::copy(std::env::current_exe().unwrap(), temp.path().join("kernel")).unwrap(); - // Deliberately invalid field type: config parsing must fail after the hook, - // without starting QEMU or accessing any serial device. - let config = ostool::build::config::BuildConfig { - system: ostool::build::config::BuildSystem::Custom(ostool::build::config::Custom { - build_cmd: format!("printf '{field} = 42\\n' > generated.toml"), - elf_path: "kernel".into(), - to_bin: false, - }), - artifacts: ostool::build::config::ArtifactConfig { - analysis: ostool::build::config::AnalysisConfig { - elf_info: true, - ..Default::default() - }, - }, - }; - fs::write( - temp.path().join(".build.toml"), - toml::to_string(&config).unwrap(), - ) - .unwrap(); - let output = Command::new(env!("CARGO_BIN_EXE_ostool")) - .current_dir(temp.path()) - .args([ - "run", - runner, - &format!("--{runner}-config"), - "generated.toml", - ]) - .output() - .unwrap(); - assert!(!output.status.success()); - assert_eq!( - fs::read_to_string(temp.path().join("generated.toml")).unwrap(), - format!("{field} = 42\n") - ); - assert!(temp.path().join("kernel.elf-info").exists()); - let message = format!( - "{}{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - assert!(message.contains("invalid type"), "{message}"); -} - -#[test] -fn custom_runner_configs_are_read_after_build_hooks() { - for (runner, field) in [("qemu", "args"), ("uboot", "dtb_file")] { - custom_build_generates_runner_config(runner, field); - } -} From b53a19b6aedabbd4678e8d795d10fd41d1bc2a22 Mon Sep 17 00:00:00 2001 From: Niu Zhihong Date: Tue, 22 Sep 2026 17:21:55 +0800 Subject: [PATCH 5/8] =?UTF-8?q?feat(ostool):=20=E5=A2=9E=E5=8A=A0=E5=86=85?= =?UTF-8?q?=E9=83=A8=E5=8F=AF=E9=85=8D=E7=BD=AE=20ELF=20=E4=B8=8E=20BIN=20?= =?UTF-8?q?FIT=20=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 crate 内部 FIT format、OS、load/entry、FDT 与输出配置,继续复用 fitimage builder 与既有 runtime BIN 转换。原始 ELF 提供符号和入口, runtime ELF 提供实际 payload;拒绝来源布局不一致和无效加载数据。 显式地址优先,auto load 优先采用 __executable_start;缺失符号时, ELF 要求 PT_LOAD 的物理地址减文件偏移具有共同基址,BIN 按实际 file-backed allocated section 的 LMA 推导。合法零地址保留,歧义和 溢出明确报错。小幅补充既有 ELF metadata 的 section 信息,不新增解析器。 默认 U-Boot Linux FIT、LoongArch header 及 unsupported architecture 行为保持兼容。新配置输出原子替换以保护输入硬链接,空 BIN 明确失败。 用户入口留待后续接入,不增加 CLI、公开 Rust API 或配置文件字段。 新增真实 FIT 属性和提取字节检查、地址/来源/错误路径及配置往返测试。 devbox 原生 fmt、workspace clippy/build/test(630 项)以及 ostool all-targets/all-features Clippy -D warnings 通过,FIT 聚焦 26 项通过。 x86_64 CI 三条命令已尝试,受容器交叉 C 编译器和 libudev sysroot 缺失阻塞。 Co-authored-by: OpenAI Codex Signed-off-by: Niu Zhihong --- ostool/src/artifact/elf_metadata.rs | 36 +- ostool/src/boot/fit.rs | 190 ++++++- ostool/src/boot/fit_config.rs | 392 ++++++++++++++ ostool/src/boot/fit_tests.rs | 763 ++++++++++++++++++++++++++++ ostool/src/boot/mod.rs | 3 + 5 files changed, 1375 insertions(+), 9 deletions(-) create mode 100644 ostool/src/boot/fit_config.rs create mode 100644 ostool/src/boot/fit_tests.rs diff --git a/ostool/src/artifact/elf_metadata.rs b/ostool/src/artifact/elf_metadata.rs index a07c77c3..6479a728 100644 --- a/ostool/src/artifact/elf_metadata.rs +++ b/ostool/src/artifact/elf_metadata.rs @@ -1,16 +1,19 @@ use anyhow::{Context, bail}; use object::{ - Object, ObjectSegment, ObjectSymbol, - read::elf::{ElfFile, FileHeader, ProgramHeader}, + Object, ObjectSection, ObjectSegment, ObjectSymbol, + read::elf::{ElfFile, FileHeader, ProgramHeader, SectionHeader}, }; pub(crate) struct ElfMetadata { pub(crate) arch: object::Architecture, pub(crate) entry: u64, pub(crate) load_segments: Vec, + /// Allocated sections with file bytes that can appear in a raw binary image. + pub(crate) load_sections: Vec, pub(crate) executable_start: Option, } +#[derive(Debug, PartialEq, Eq)] pub(crate) struct LoadSegment { pub(crate) virtual_address: u64, pub(crate) physical_address: u64, @@ -21,6 +24,14 @@ pub(crate) struct LoadSegment { pub(crate) flags: u32, } +/// A file-backed allocated section, represented in both ELF address spaces. +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct LoadSection { + pub(crate) virtual_address: u64, + pub(crate) file_offset: u64, + pub(crate) size: u64, +} + impl ElfMetadata { pub(crate) fn parse(data: &[u8]) -> anyhow::Result { let file = object::File::parse(data).context("failed to parse ELF")?; @@ -53,6 +64,26 @@ impl ElfMetadata { }) .collect(); + let load_sections = file + .sections() + .filter_map(|section| { + let header = section.elf_section_header(); + let flags: u64 = header.sh_flags(endian).into(); + if flags & u64::from(object::elf::SHF_ALLOC) == 0 + || header.sh_type(endian) == object::elf::SHT_NOBITS + { + return None; + } + + let (file_offset, size) = section.file_range()?; + (size != 0).then_some(LoadSection { + virtual_address: section.address(), + file_offset, + size, + }) + }) + .collect(); + let mut executable_start = None; for symbol in file.symbols() { if symbol.name()? == "__executable_start" && symbol.is_definition() { @@ -65,6 +96,7 @@ impl ElfMetadata { arch: file.architecture(), entry: file.entry(), load_segments, + load_sections, executable_start, }) } diff --git a/ostool/src/boot/fit.rs b/ostool/src/boot/fit.rs index cee64888..f60ff1e4 100644 --- a/ostool/src/boot/fit.rs +++ b/ostool/src/boot/fit.rs @@ -1,5 +1,9 @@ //! U-Boot FIT image generation. +#[cfg(test)] +#[path = "fit_tests.rs"] +mod configured_tests; + use std::path::{Path, PathBuf}; use anyhow::Context; @@ -9,7 +13,15 @@ use log::{info, warn}; use object::Architecture; use tokio::fs; -use crate::utils::PathResultExt; +use crate::{ + artifact::{ + elf_metadata::ElfMetadata, + runtime::{PreparedRuntimeArtifacts, RuntimeArtifactOptions, prepare_runtime_artifacts}, + }, + boot::fit_config::{FitConfig, FitFormat, FitOs}, + process::ProcessContext, + utils::PathResultExt, +}; const KERNEL_COMPONENT_NAME: &str = "kernel"; const FDT_COMPONENT_NAME: &str = "fdt"; @@ -50,6 +62,150 @@ impl GeneratedFitImage { } } +/// Builds a configured payload from one prepared ELF selection. Paths in the +/// configuration are already resolved by the caller; this does not run a guest. +/// Symbols come from the original ELF, even when the runtime copy is stripped. +#[allow(dead_code)] // Consumed by the boot-prepare entry point in the next change. +pub(crate) async fn generate_configured_fit( + context: &ProcessContext, + prepared: &PreparedRuntimeArtifacts, + config: &FitConfig, +) -> anyhow::Result { + let source_data = fs::read(prepared.source_elf()) + .await + .with_path("failed to read source ELF", prepared.source_elf())?; + let metadata = ElfMetadata::parse(&source_data) + .with_context(|| format!("invalid source ELF: {}", prepared.source_elf().display()))?; + validate_elf_load_data(&metadata, source_data.len())?; + fit_arch_name(metadata.arch)?; + + let runtime_data = fs::read(prepared.elf()) + .await + .with_path("failed to read runtime ELF", prepared.elf())?; + let runtime_metadata = ElfMetadata::parse(&runtime_data) + .with_context(|| format!("invalid runtime ELF: {}", prepared.elf().display()))?; + validate_elf_load_data(&runtime_metadata, runtime_data.len())?; + if metadata.arch != runtime_metadata.arch + || metadata.entry != runtime_metadata.entry + || metadata.load_segments != runtime_metadata.load_segments + || metadata.load_sections != runtime_metadata.load_sections + { + anyhow::bail!("source and runtime ELF load layouts differ; prepare the selected ELF again"); + } + let (load, entry) = config.resolve_addresses(&metadata)?; + let output = match &config.output { + Some(path) => path.clone(), + None => default_output_path(prepared.elf())?, + }; + // An output alias must not destroy an input needed by subsequent preparation. + if let Ok(destination) = output.canonicalize() { + for input in [ + Some(prepared.source_elf()), + Some(prepared.elf()), + config.fdt.as_ref().map(|fdt| fdt.path.as_path()), + ] + .into_iter() + .flatten() + { + if input.canonicalize().ok().as_ref() == Some(&destination) { + anyhow::bail!("FIT output would overwrite an input: {}", output.display()); + } + } + } + let kernel_data = match config.format { + FitFormat::Elf => runtime_data, + FitFormat::Bin => { + // Derive from this runtime ELF, never reuse an unrelated/stale BIN. + let temporary = tempfile::tempdir().context("failed to stage FIT BIN payload")?; + let binary = prepare_runtime_artifacts( + context, + RuntimeArtifactOptions { + elf_path: prepared.elf().to_path_buf(), + to_bin: true, + bin_dir: Some(temporary.path().to_path_buf()), + debug: false, + cargo_artifact_dir: None, + strip_elf: false, + }, + ) + .with_context(|| { + format!( + "failed to derive FIT BIN payload with llvm-objcopy from {}", + prepared.elf().display() + ) + })?; + fs::read(binary.bin().context("BIN conversion produced no path")?) + .await + .context("failed to read derived FIT BIN payload")? + } + }; + if kernel_data.is_empty() { + anyhow::bail!("cannot build FIT with an empty kernel payload"); + } + // Replace the output directory entry only after success. In particular, a + // hard link to an input must not truncate that input's shared inode. + let output_dir = output + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .unwrap_or(Path::new(".")); + let temporary = tempfile::NamedTempFile::new_in(output_dir) + .with_path("failed to stage FIT output", &output)?; + write_fit_image( + FitInput { + kernel_path: prepared.elf().to_path_buf(), + dtb_path: config.fdt.as_ref().map(|fdt| fdt.path.clone()), + arch: metadata.arch, + kernel_load_addr: load, + kernel_entry_addr: entry, + fdt_load_addr: config.fdt.as_ref().and_then(|fdt| fdt.load), + output_path: Some(temporary.path().to_path_buf()), + }, + kernel_data, + entry, + config.os, + ) + .await?; + temporary + .persist(&output) + .map_err(|error| error.error) + .with_path(errors::FIT_SAVE_ERROR, &output)?; + Ok(GeneratedFitImage { path: output }) +} + +// Metadata parsing reads descriptions, not the bytes they describe. Explicit +// addresses bypass inference, but cannot make truncated load data valid. +fn validate_elf_load_data(metadata: &ElfMetadata, file_size: usize) -> anyhow::Result<()> { + let in_file = |offset: u64, size: u64| { + offset + .checked_add(size) + .is_some_and(|end| end <= file_size as u64) + }; + for segment in &metadata.load_segments { + if segment.file_size > segment.memory_size + || !in_file(segment.file_offset, segment.file_size) + || segment + .virtual_address + .checked_add(segment.memory_size) + .is_none() + || segment + .physical_address + .checked_add(segment.memory_size) + .is_none() + { + anyhow::bail!("invalid ELF PT_LOAD data range"); + } + } + for section in &metadata.load_sections { + if !in_file(section.file_offset, section.size) + || section.virtual_address.checked_add(section.size).is_none() + { + anyhow::bail!("invalid ELF allocated section data range"); + } + } + Ok(()) +} + +/// Existing U-Boot path: Linux payload with its historical header handling. pub(crate) async fn generate_fit_image(input: FitInput) -> anyhow::Result { info!("Making FIT image..."); @@ -73,6 +229,15 @@ pub(crate) async fn generate_fit_image(input: FitInput) -> anyhow::Result, + kernel_entry_addr: u64, + os: FitOs, +) -> anyhow::Result { let arch_name = fit_arch_name(input.arch)?; let dtb_data = if let Some(dtb_path) = &input.dtb_path { let data = fs::read(dtb_path) @@ -89,8 +254,9 @@ pub(crate) async fn generate_fit_image(input: FitInput) -> anyhow::Result anyhow::Result { Ok(Path::new(output_dir).join(FIT_IMAGE_NAME)) } -fn build_default_fit_config( +fn build_fit_config( arch_name: &'static str, + os: FitOs, kernel_data: Vec, dtb_data: Option>, kernel_load_addr: u64, @@ -183,7 +350,7 @@ fn build_default_fit_config( .with_description("This kernel") .with_type("kernel") .with_arch(arch_name) - .with_os("linux") + .with_os(os.as_str()) .with_compression(false) .with_load_address(kernel_load_addr) .with_entry_point(kernel_entry_addr), @@ -219,7 +386,7 @@ fn build_default_fit_config( #[cfg(test)] mod tests { use super::{ - FitInput, build_default_fit_config, default_output_path, fit_arch_name, + FitInput, FitOs, build_fit_config, default_output_path, fit_arch_name, resolve_kernel_entry_addr, }; use object::Architecture; @@ -272,7 +439,15 @@ mod tests { #[test] fn default_fit_config_keeps_linux_kernel_defaults_without_dtb() { - let config = build_default_fit_config("arm64", vec![1, 2, 3], None, 0x80000, 0x80000, None); + let config = build_fit_config( + "arm64", + FitOs::Linux, + vec![1, 2, 3], + None, + 0x80000, + 0x80000, + None, + ); let kernel = config.kernel.as_ref().unwrap(); assert_eq!( @@ -296,8 +471,9 @@ mod tests { #[test] fn default_fit_config_adds_optional_fdt_component() { - let config = build_default_fit_config( + let config = build_fit_config( "riscv", + FitOs::Linux, vec![1, 2, 3], Some(vec![4, 5, 6]), 0x8020_0000, diff --git a/ostool/src/boot/fit_config.rs b/ostool/src/boot/fit_config.rs new file mode 100644 index 00000000..6a3b9b60 --- /dev/null +++ b/ostool/src/boot/fit_config.rs @@ -0,0 +1,392 @@ +//! Typed internal configuration and address resolution for FIT images. + +use std::path::PathBuf; + +use anyhow::{Result, anyhow, bail}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::artifact::elf_metadata::{ElfMetadata, LoadSection, LoadSegment}; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub(crate) enum FitFormat { + Elf, + #[default] + Bin, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub(crate) enum FitOs { + #[default] + Linux, + Elf, +} + +impl FitOs { + pub(crate) fn as_str(self) -> &'static str { + match self { + Self::Linux => "linux", + Self::Elf => "elf", + } + } +} + +/// An address supplied directly or derived from ELF metadata. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub(crate) enum FitAddress { + #[default] + Auto, + Explicit(u64), +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub(crate) struct FitFdt { + pub(crate) path: PathBuf, + pub(crate) load: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(default, deny_unknown_fields)] +pub(crate) struct FitConfig { + pub(crate) format: FitFormat, + pub(crate) os: FitOs, + pub(crate) load: FitAddress, + pub(crate) entry: FitAddress, + pub(crate) fdt: Option, + pub(crate) output: Option, +} + +impl Default for FitConfig { + fn default() -> Self { + Self { + format: FitFormat::Bin, + os: FitOs::Linux, + load: FitAddress::Auto, + entry: FitAddress::Auto, + fdt: None, + output: None, + } + } +} + +impl FitConfig { + /// Resolves FIT addresses from the original, unstripped ELF metadata. + pub(crate) fn resolve_addresses(&self, metadata: &ElfMetadata) -> Result<(u64, u64)> { + let load = match self.load { + FitAddress::Explicit(address) => address, + FitAddress::Auto => match metadata.executable_start { + // The linker-provided origin describes the selected project's image base. + Some(address) => address, + None => match self.format { + FitFormat::Elf => elf_load_base(metadata)?, + FitFormat::Bin => bin_load_base(metadata)?, + }, + }, + }; + let entry = match self.entry { + FitAddress::Explicit(address) => address, + FitAddress::Auto => metadata.entry, + }; + Ok((load, entry)) + } +} + +fn elf_load_base(metadata: &ElfMetadata) -> Result { + let mut bases = metadata + .load_segments + .iter() + .filter(|segment| segment.file_size != 0) + .map(elf_container_base); + let first = bases.next().ok_or_else(|| { + anyhow!("cannot derive ELF FIT load address: no file-backed PT_LOAD segment") + })??; + for base in bases { + let base = base?; + if base != first { + bail!( + "cannot derive ELF FIT load address: PT_LOAD segments have different file bases; set load explicitly" + ); + } + } + Ok(first) +} + +fn elf_container_base(segment: &LoadSegment) -> Result { + valid_segment_range(segment)?; + segment.physical_address.checked_sub(segment.file_offset).ok_or_else(|| { + anyhow!( + "cannot derive ELF FIT load address: PT_LOAD physical address {:#x} precedes file offset {:#x}; set load explicitly", + segment.physical_address, + segment.file_offset + ) + }) +} + +fn bin_load_base(metadata: &ElfMetadata) -> Result { + metadata + .load_sections + .iter() + .map(|section| section_lma(section, &metadata.load_segments)) + .try_fold(None::, |minimum, address| { + let address = address?; + Ok::<_, anyhow::Error>(Some(minimum.map_or(address, |current| current.min(address)))) + })? + .ok_or_else(|| { + anyhow!( + "cannot derive BIN FIT load address: no file-backed allocated section; set load explicitly" + ) + }) +} + +fn section_lma(section: &LoadSection, segments: &[LoadSegment]) -> Result { + let section_file_end = checked_end(section.file_offset, section.size, "section file range")?; + let section_virtual_end = checked_end( + section.virtual_address, + section.size, + "section virtual range", + )?; + let mut candidates = Vec::new(); + + for segment in segments.iter().filter(|segment| segment.file_size != 0) { + valid_segment_range(segment)?; + let segment_file_end = + checked_end(segment.file_offset, segment.file_size, "PT_LOAD file range")?; + let segment_virtual_end = checked_end( + segment.virtual_address, + segment.file_size, + "PT_LOAD virtual range", + )?; + if section.file_offset < segment.file_offset + || section_file_end > segment_file_end + || section.virtual_address < segment.virtual_address + || section_virtual_end > segment_virtual_end + { + continue; + } + + let file_delta = section.file_offset - segment.file_offset; + let virtual_delta = section.virtual_address - segment.virtual_address; + if file_delta != virtual_delta { + continue; + } + candidates.push( + segment + .physical_address + .checked_add(file_delta) + .ok_or_else(|| { + anyhow!("cannot derive BIN FIT load address: PT_LOAD LMA overflow") + })?, + ); + } + + let first = candidates.first().copied().ok_or_else(|| { + anyhow!( + "cannot derive BIN FIT load address: an allocated file-backed section has no matching PT_LOAD segment; set load explicitly" + ) + })?; + if candidates.iter().any(|candidate| *candidate != first) { + bail!( + "cannot derive BIN FIT load address: an allocated file-backed section has ambiguous PT_LOAD LMA mappings; set load explicitly" + ); + } + Ok(first) +} + +fn valid_segment_range(segment: &LoadSegment) -> Result<()> { + checked_end(segment.file_offset, segment.file_size, "PT_LOAD file range")?; + checked_end( + segment.virtual_address, + segment.file_size, + "PT_LOAD virtual range", + )?; + checked_end( + segment.physical_address, + segment.file_size, + "PT_LOAD physical range", + )?; + Ok(()) +} + +fn checked_end(start: u64, size: u64, kind: &str) -> Result { + start + .checked_add(size) + .ok_or_else(|| anyhow!("cannot derive FIT load address: {kind} overflows")) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::{FitAddress, FitConfig, FitFdt, FitFormat, FitOs}; + use crate::artifact::elf_metadata::{ElfMetadata, LoadSection, LoadSegment}; + use object::Architecture; + + fn segment(vaddr: u64, paddr: u64, offset: u64, file_size: u64) -> LoadSegment { + LoadSegment { + virtual_address: vaddr, + physical_address: paddr, + file_offset: offset, + file_size, + memory_size: file_size, + alignment: 1, + flags: 0, + } + } + + fn section(vaddr: u64, offset: u64, size: u64) -> LoadSection { + LoadSection { + virtual_address: vaddr, + file_offset: offset, + size, + } + } + + fn metadata( + entry: u64, + executable_start: Option, + load_segments: Vec, + load_sections: Vec, + ) -> ElfMetadata { + ElfMetadata { + arch: Architecture::Riscv64, + entry, + load_segments, + load_sections, + executable_start, + } + } + + #[test] + fn resolves_symbol_and_entry_zero_and_honors_explicit_addresses() { + let metadata = metadata(0, Some(0), Vec::new(), Vec::new()); + assert_eq!( + FitConfig::default().resolve_addresses(&metadata).unwrap(), + (0, 0) + ); + + let config = FitConfig { + load: FitAddress::Explicit(0x1000), + entry: FitAddress::Explicit(0x2000), + ..FitConfig::default() + }; + assert_eq!( + config.resolve_addresses(&metadata).unwrap(), + (0x1000, 0x2000) + ); + } + + #[test] + fn elf_auto_load_requires_one_common_container_base() { + let metadata = metadata( + 0x8010, + None, + vec![ + segment(0x8000, 0x8000, 0, 0x1000), + segment(0x9000, 0x9000, 0x1000, 0x200), + ], + Vec::new(), + ); + let config = FitConfig { + format: FitFormat::Elf, + ..FitConfig::default() + }; + + assert_eq!( + config.resolve_addresses(&metadata).unwrap(), + (0x8000, 0x8010) + ); + } + + #[test] + fn elf_auto_load_rejects_different_or_overflowing_segment_ranges() { + let config = FitConfig { + format: FitFormat::Elf, + ..FitConfig::default() + }; + let different = metadata( + 0, + None, + vec![segment(0x8000, 0x8000, 0, 4), segment(0x9000, 0x9000, 4, 4)], + Vec::new(), + ); + assert!(config.resolve_addresses(&different).is_err()); + + let overflowing = metadata( + 0, + None, + vec![segment(u64::MAX - 1, u64::MAX - 1, 0, 2)], + Vec::new(), + ); + assert!(config.resolve_addresses(&overflowing).is_err()); + } + + #[test] + fn bin_auto_load_starts_at_first_output_section_not_pt_load_header() { + let metadata = metadata( + 0, + None, + vec![segment(0, 0x8000_0000, 0, 0x200)], + vec![section(0x100, 0x100, 0x20)], + ); + + assert_eq!( + FitConfig::default().resolve_addresses(&metadata).unwrap(), + (0x8000_0100, 0) + ); + } + + #[test] + fn bin_auto_load_keeps_zero_and_rejects_bss_only_or_ambiguous_mappings() { + let zero = metadata(0, None, vec![segment(0, 0, 0, 4)], vec![section(0, 0, 4)]); + assert_eq!( + FitConfig::default().resolve_addresses(&zero).unwrap(), + (0, 0) + ); + + let bss_only = metadata(0, None, vec![segment(0, 0, 0, 4)], Vec::new()); + assert!(FitConfig::default().resolve_addresses(&bss_only).is_err()); + + let ambiguous = metadata( + 0, + None, + vec![segment(0, 0x1000, 0, 4), segment(0, 0x2000, 0, 4)], + vec![section(0, 0, 4)], + ); + assert!(FitConfig::default().resolve_addresses(&ambiguous).is_err()); + } + + #[test] + fn config_round_trips_json_and_toml_with_explicit_zero_and_high_addresses() { + let config = FitConfig { + format: FitFormat::Elf, + os: FitOs::Elf, + load: FitAddress::Explicit(0), + entry: FitAddress::Explicit(0x9000_0000_8000_0000), + fdt: Some(FitFdt { + path: PathBuf::from("board.dtb"), + load: Some(0), + }), + output: Some(PathBuf::from("boot.fit")), + }; + + let json = serde_json::to_string(&config).unwrap(); + assert_eq!(serde_json::from_str::(&json).unwrap(), config); + + let toml = toml::to_string(&config).unwrap(); + assert_eq!(toml::from_str::(&toml).unwrap(), config); + } + + #[test] + fn omitted_addresses_default_to_auto_and_fdt_load_remains_optional() { + let config: FitConfig = toml::from_str("[fdt]\npath = 'board.dtb'").unwrap(); + assert_eq!(config.load, FitAddress::Auto); + assert_eq!(config.entry, FitAddress::Auto); + assert_eq!(config.fdt.as_ref().unwrap().load, None); + let encoded = toml::to_string(&config).unwrap(); + assert_eq!(toml::from_str::(&encoded).unwrap(), config); + } +} diff --git a/ostool/src/boot/fit_tests.rs b/ostool/src/boot/fit_tests.rs new file mode 100644 index 00000000..3f1d6099 --- /dev/null +++ b/ostool/src/boot/fit_tests.rs @@ -0,0 +1,763 @@ +//! End-to-end regression tests for configured FIT images. +//! +//! These tests deliberately inspect FIT images with U-Boot's `dumpimage` and +//! extract each embedded component. Checking the FDT header alone would not +//! prove that the requested payload, addresses, or optional FDT made it into +//! the image. + +use std::{ffi::OsString, fs, path::Path, process::Command}; + +use super::{FitInput, generate_configured_fit, generate_fit_image}; +use crate::{ + artifact::{ + elf_metadata::ElfMetadata, + llvm_tools::llvm_objcopy, + runtime::{PreparedRuntimeArtifacts, RuntimeArtifactOptions, prepare_runtime_artifacts}, + }, + boot::fit_config::{FitAddress, FitConfig, FitFdt, FitFormat, FitOs}, + process::ProcessContext, + project::{resolve_project_layout, variables::VariableScope}, +}; + +const ELF_LOAD: u64 = 0x400_000; +const ELF_ENTRY: u64 = ELF_LOAD + 4; + +fn process_context(root: &Path) -> ProcessContext { + fs::write( + root.join("Cargo.toml"), + "[package]\nname = \"fit-fixture\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", + ) + .unwrap(); + fs::create_dir_all(root.join("src")).unwrap(); + fs::write(root.join("src/lib.rs"), "").unwrap(); + + let layout = resolve_project_layout(Some(root.to_path_buf())).unwrap(); + let scope = VariableScope::for_package(&layout, root.to_path_buf()); + ProcessContext::new(root.to_path_buf(), root.to_path_buf(), scope, None) +} + +fn prepare_fixture( + root: &Path, + architecture: u16, + strip_elf: bool, +) -> (ProcessContext, PreparedRuntimeArtifacts) { + let context = process_context(root); + let source = root.join("kernel.elf"); + fs::write(&source, minimal_elf(architecture)).unwrap(); + let prepared = prepare_runtime_artifacts( + &context, + RuntimeArtifactOptions { + elf_path: source, + to_bin: false, + bin_dir: None, + debug: false, + cargo_artifact_dir: None, + strip_elf, + }, + ) + .unwrap(); + (context, prepared) +} + +/// Builds the smallest useful little-endian ELF64 fixture in memory. +/// +/// The file contains one executable PT_LOAD segment, a `.text` section, and +/// a defined `__executable_start` symbol. This lets the configured FIT path +/// exercise the same metadata reader as production without compiling an +/// external target or relying on host architecture. +fn minimal_elf(machine: u16) -> Vec { + const PROGRAM_OFFSET: usize = 0x100; + const SHSTRTAB_OFFSET: usize = 0x120; + const STRTAB_OFFSET: usize = 0x150; + const SYMTAB_OFFSET: usize = 0x170; + const SECTION_HEADERS_OFFSET: usize = 0x200; + const SECTION_HEADER_SIZE: usize = 64; + + let mut elf = vec![0_u8; SECTION_HEADERS_OFFSET + SECTION_HEADER_SIZE * 5]; + elf[..4].copy_from_slice(b"\x7fELF"); + elf[4] = 2; // ELFCLASS64 + elf[5] = 1; // ELFDATA2LSB + elf[6] = 1; // EV_CURRENT + put_u16(&mut elf, 16, 2); // ET_EXEC + put_u16(&mut elf, 18, machine); + put_u32(&mut elf, 20, 1); + put_u64(&mut elf, 24, ELF_ENTRY); + put_u64(&mut elf, 32, 64); // e_phoff + put_u64(&mut elf, 40, SECTION_HEADERS_OFFSET as u64); + put_u16(&mut elf, 52, 64); // e_ehsize + put_u16(&mut elf, 54, 56); // e_phentsize + put_u16(&mut elf, 56, 1); // e_phnum + put_u16(&mut elf, 58, SECTION_HEADER_SIZE as u16); + put_u16(&mut elf, 60, 5); // e_shnum + put_u16(&mut elf, 62, 1); // e_shstrndx + + // The single PT_LOAD makes `llvm-objcopy -O binary` deterministic. + put_u32(&mut elf, 64, 1); // PT_LOAD + put_u32(&mut elf, 68, 5); // PF_R | PF_X + put_u64(&mut elf, 72, PROGRAM_OFFSET as u64); + put_u64(&mut elf, 80, ELF_LOAD); + put_u64(&mut elf, 88, ELF_LOAD); + put_u64(&mut elf, 96, 8); + put_u64(&mut elf, 104, 8); + put_u64(&mut elf, 112, 0x100); + elf[PROGRAM_OFFSET..PROGRAM_OFFSET + 8].copy_from_slice(b"KERNEL\0!"); + + let section_names = b"\0.shstrtab\0.strtab\0.symtab\0.text\0"; + elf[SHSTRTAB_OFFSET..SHSTRTAB_OFFSET + section_names.len()].copy_from_slice(section_names); + let symbol_names = b"\0__executable_start\0"; + elf[STRTAB_OFFSET..STRTAB_OFFSET + symbol_names.len()].copy_from_slice(symbol_names); + + // ELF64 symbol table: the first null entry plus __executable_start. + let symbol = SYMTAB_OFFSET + 24; + put_u32(&mut elf, symbol, 1); + elf[symbol + 4] = 0x10; // STB_GLOBAL | STT_NOTYPE + put_u16(&mut elf, symbol + 6, 4); // .text section index + put_u64(&mut elf, symbol + 8, ELF_LOAD); + + // Section 1: .shstrtab; Section 2: .strtab; Section 3: .symtab; + // Section 4: .text. The production parser only needs the normal ELF + // layout here; the contents are intentionally tiny. + section_header( + &mut elf, + SECTION_HEADERS_OFFSET + SECTION_HEADER_SIZE, + 1, + 3, + 0, + 0, + SHSTRTAB_OFFSET as u64, + section_names.len() as u64, + 0, + 0, + 1, + 0, + ); + section_header( + &mut elf, + SECTION_HEADERS_OFFSET + SECTION_HEADER_SIZE * 2, + 11, + 3, + 0, + 0, + STRTAB_OFFSET as u64, + symbol_names.len() as u64, + 0, + 0, + 1, + 0, + ); + section_header( + &mut elf, + SECTION_HEADERS_OFFSET + SECTION_HEADER_SIZE * 3, + 19, + 2, + 0, + 0, + SYMTAB_OFFSET as u64, + 48, + 2, + 1, + 8, + 24, + ); + section_header( + &mut elf, + SECTION_HEADERS_OFFSET + SECTION_HEADER_SIZE * 4, + 27, + 1, + 0x6, + ELF_LOAD, + PROGRAM_OFFSET as u64, + 8, + 0, + 0, + 4, + 0, + ); + + elf +} + +fn minimal_elf_without_executable_start(machine: u16) -> Vec { + let mut elf = minimal_elf(machine); + // The ELF and first PT_LOAD both start at file offset zero. Its physical + // base differs from .text by 0x100, so ELF containers and raw BINs have + // intentionally different correct auto-load values. + put_u64(&mut elf, 72, 0); + put_u64(&mut elf, 80, ELF_LOAD - 0x100); + put_u64(&mut elf, 88, ELF_LOAD - 0x100); + put_u64(&mut elf, 96, 0x108); + put_u64(&mut elf, 104, 0x108); + // Keep a named but undefined symbol to model a stripped source ELF. + put_u16(&mut elf, 0x170 + 24 + 6, 0); + elf +} + +#[allow(clippy::too_many_arguments)] +fn section_header( + bytes: &mut [u8], + offset: usize, + name: u32, + kind: u32, + flags: u64, + address: u64, + file_offset: u64, + size: u64, + link: u32, + info: u32, + alignment: u64, + entry_size: u64, +) { + put_u32(bytes, offset, name); + put_u32(bytes, offset + 4, kind); + put_u64(bytes, offset + 8, flags); + put_u64(bytes, offset + 16, address); + put_u64(bytes, offset + 24, file_offset); + put_u64(bytes, offset + 32, size); + put_u32(bytes, offset + 40, link); + put_u32(bytes, offset + 44, info); + put_u64(bytes, offset + 48, alignment); + put_u64(bytes, offset + 56, entry_size); +} + +fn put_u16(bytes: &mut [u8], offset: usize, value: u16) { + bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes()); +} + +fn put_u32(bytes: &mut [u8], offset: usize, value: u32) { + bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); +} + +fn put_u64(bytes: &mut [u8], offset: usize, value: u64) { + bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); +} + +fn configured(format: FitFormat) -> FitConfig { + FitConfig { + format, + os: FitOs::Linux, + load: FitAddress::Auto, + entry: FitAddress::Auto, + fdt: None, + output: None, + } +} + +fn run_dumpimage(args: &[OsString]) -> std::process::Output { + Command::new("dumpimage") + .args(args) + .output() + .unwrap_or_else(|err| { + panic!( + "failed to execute dumpimage; install u-boot-tools (the CI FIT inspector): {err}" + ) + }) +} + +fn inspect_fit(path: &Path) -> String { + let output = run_dumpimage(&[OsString::from("-l"), path.as_os_str().to_os_string()]); + assert!( + output.status.success(), + "dumpimage -l failed with status {}\nstdout:\n{}\nstderr:\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ) +} + +fn fit_property(path: &Path, node: &str, property: &str) -> String { + let output = Command::new("fdtget") + .args(["-t", "s"]) + .arg(path) + .arg(node) + .arg(property) + .output() + .unwrap_or_else(|err| { + panic!("failed to execute fdtget; install u-boot-tools (the CI FIT inspector): {err}") + }); + assert!( + output.status.success(), + "fdtget {node} {property} failed with status {}\nstdout:\n{}\nstderr:\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + String::from_utf8(output.stdout).unwrap().trim().to_owned() +} + +fn extract_fit_component(path: &Path, position: u32, destination: &Path) -> Vec { + let output = run_dumpimage(&[ + OsString::from("-T"), + OsString::from("flat_dt"), + OsString::from("-p"), + OsString::from(position.to_string()), + OsString::from("-o"), + destination.as_os_str().to_os_string(), + path.as_os_str().to_os_string(), + ]); + assert!( + output.status.success(), + "dumpimage extraction for component {position} failed with status {}\nstdout:\n{}\nstderr:\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + fs::read(destination).unwrap() +} + +fn assert_linux_arm64_header(listing: &str, load: u64, entry: u64) { + let listing = listing.to_ascii_lowercase(); + assert_dumpimage_field(&listing, "os", &["linux"]); + assert_dumpimage_field(&listing, "architecture", &["aarch64", "arm64"]); + assert_dumpimage_address(&listing, "load address", load); + assert_dumpimage_address(&listing, "entry point", entry); +} + +fn assert_dumpimage_field(listing: &str, label: &str, expected: &[&str]) { + let value = listing + .lines() + .find_map(|line| line.trim().strip_prefix(label)) + .and_then(|line| line.strip_prefix(':')) + .map(str::trim) + .unwrap_or_else(|| panic!("dumpimage listing has no {label}:\n{listing}")); + assert!( + expected.contains(&value), + "unexpected dumpimage {label} `{value}`; expected one of {expected:?}\n{listing}" + ); +} + +fn assert_dumpimage_address(listing: &str, label: &str, expected: u64) { + let address = listing + .lines() + .find_map(|line| line.trim().strip_prefix(label)) + .and_then(|line| line.strip_prefix(':')) + .map(str::trim) + .unwrap_or_else(|| panic!("dumpimage listing has no {label}:\n{listing}")); + assert_eq!(address, format!("0x{expected:08x}"), "{listing}"); +} + +fn image_listing(listing: &str, position: u32) -> &str { + let marker = format!("Image {position} ("); + let start = listing + .find(&marker) + .unwrap_or_else(|| panic!("dumpimage listing has no {marker}:\n{listing}")); + let remainder = &listing[start..]; + let next_marker = format!("\n Image {} (", position + 1); + &remainder[..remainder.find(&next_marker).unwrap_or(remainder.len())] +} + +fn expected_bin(context: &ProcessContext, source: &Path, output: &Path) -> Vec { + let objcopy = llvm_objcopy().unwrap(); + let result = crate::process::command(objcopy, context) + .arg("--strip-all") + .arg("-O") + .arg("binary") + .arg(source) + .arg(output) + .run(); + result.unwrap(); + fs::read(output).unwrap() +} + +fn strip_elf(context: &ProcessContext, source: &Path, output: &Path) { + let objcopy = llvm_objcopy().unwrap(); + crate::process::command(objcopy, context) + .arg("--strip-all") + .arg(source) + .arg(output) + .run() + .unwrap(); +} + +#[tokio::test] +async fn configured_elf_uses_runtime_payload_source_metadata_and_fdt() { + let temp = tempfile::Builder::new() + .prefix("FIT input with spaces ") + .tempdir() + .unwrap(); + let (context, prepared) = prepare_fixture(temp.path(), 183, true); // EM_AARCH64 + let source = prepared.source_elf().to_path_buf(); + let runtime = prepared.elf().to_path_buf(); + assert_ne!( + source, runtime, + "the fixture must exercise distinct sources" + ); + + // A stripped runtime ELF has no __executable_start. It remains a valid + // ELF payload, while auto-address resolution must use source_elf. + let stripped = temp.path().join("actually-stripped.elf"); + strip_elf(&context, &source, &stripped); + fs::copy(&stripped, &runtime).unwrap(); + let source_metadata = ElfMetadata::parse(&fs::read(&source).unwrap()).unwrap(); + let runtime_metadata = ElfMetadata::parse(&fs::read(&runtime).unwrap()).unwrap(); + assert_eq!(source_metadata.executable_start, Some(ELF_LOAD)); + assert_eq!(runtime_metadata.executable_start, None); + let runtime_payload = fs::read(&runtime).unwrap(); + let fdt_path = temp.path().join("board.dtb"); + let fdt_payload = b"test dtb payload"; + fs::write(&fdt_path, fdt_payload).unwrap(); + let output = temp.path().join("configured.fit"); + + let image = generate_configured_fit( + &context, + &prepared, + &FitConfig { + os: FitOs::Elf, + fdt: Some(FitFdt { + path: fdt_path, + load: Some(0x4_8000_0000), + }), + output: Some(output.clone()), + ..configured(FitFormat::Elf) + }, + ) + .await + .unwrap(); + + assert_eq!(image.path(), output); + let listing = inspect_fit(image.path()); + assert_dumpimage_field( + &listing.to_ascii_lowercase(), + "architecture", + &["aarch64", "arm64"], + ); + assert_dumpimage_address(&listing.to_ascii_lowercase(), "load address", ELF_LOAD); + assert_dumpimage_address(&listing.to_ascii_lowercase(), "entry point", ELF_ENTRY); + assert_eq!(fit_property(image.path(), "/images/kernel", "os"), "elf"); + assert_eq!( + extract_fit_component(image.path(), 0, &temp.path().join("kernel.out")), + runtime_payload + ); + assert_eq!( + extract_fit_component(image.path(), 1, &temp.path().join("fdt.out")), + fdt_payload + ); + assert_dumpimage_address( + &image_listing(&listing, 1).to_ascii_lowercase(), + "load address", + 0x4_8000_0000, + ); +} + +#[tokio::test] +async fn configured_bin_derives_payload_and_uses_runtime_directory_default_output() { + let temp = tempfile::tempdir().unwrap(); + let (context, prepared) = prepare_fixture(temp.path(), 183, false); // EM_AARCH64 + let expected = expected_bin(&context, prepared.elf(), &temp.path().join("expected.bin")); + + let image = generate_configured_fit(&context, &prepared, &configured(FitFormat::Bin)) + .await + .unwrap(); + + assert_eq!(image.path(), temp.path().join("image.fit")); + let listing = inspect_fit(image.path()); + assert_linux_arm64_header(&listing, ELF_LOAD, ELF_ENTRY); + assert_eq!( + extract_fit_component(image.path(), 0, &temp.path().join("kernel.bin.out")), + expected + ); +} + +#[tokio::test] +async fn configured_explicit_zero_addresses_override_auto_resolution() { + let temp = tempfile::tempdir().unwrap(); + let (context, prepared) = prepare_fixture(temp.path(), 183, false); // EM_AARCH64 + let output = temp.path().join("zero.fit"); + let image = generate_configured_fit( + &context, + &prepared, + &FitConfig { + load: FitAddress::Explicit(0), + entry: FitAddress::Explicit(0), + output: Some(output), + ..configured(FitFormat::Elf) + }, + ) + .await + .unwrap(); + + let listing = inspect_fit(image.path()).to_ascii_lowercase(); + assert_dumpimage_address(&listing, "load address", 0); + assert_dumpimage_address(&listing, "entry point", 0); +} + +#[tokio::test] +async fn configured_auto_load_distinguishes_elf_container_from_bin_section_base() { + let temp = tempfile::tempdir().unwrap(); + let (context, prepared) = prepare_fixture(temp.path(), 183, false); // EM_AARCH64 + fs::write( + prepared.source_elf(), + minimal_elf_without_executable_start(183), + ) + .unwrap(); + let metadata = ElfMetadata::parse(&fs::read(prepared.source_elf()).unwrap()).unwrap(); + assert_eq!(metadata.executable_start, None); + + let elf_image = generate_configured_fit( + &context, + &prepared, + &FitConfig { + output: Some(temp.path().join("fallback-elf.fit")), + ..configured(FitFormat::Elf) + }, + ) + .await + .unwrap(); + let elf_listing = inspect_fit(elf_image.path()).to_ascii_lowercase(); + assert_dumpimage_address(&elf_listing, "load address", ELF_LOAD - 0x100); + assert_dumpimage_address(&elf_listing, "entry point", ELF_ENTRY); + + let bin_image = generate_configured_fit( + &context, + &prepared, + &FitConfig { + output: Some(temp.path().join("fallback-bin.fit")), + ..configured(FitFormat::Bin) + }, + ) + .await + .unwrap(); + let bin_listing = inspect_fit(bin_image.path()).to_ascii_lowercase(); + assert_dumpimage_address(&bin_listing, "load address", ELF_LOAD); + assert_dumpimage_address(&bin_listing, "entry point", ELF_ENTRY); + assert_eq!( + extract_fit_component(bin_image.path(), 0, &temp.path().join("fallback-bin.out")), + b"KERNEL\0!" + ); +} + +#[tokio::test] +async fn configured_fit_rejects_missing_fdt_and_unsupported_architecture() { + let temp = tempfile::tempdir().unwrap(); + let (context, prepared) = prepare_fixture(temp.path(), 183, false); // EM_AARCH64 + let missing_fdt = temp.path().join("missing.dtb"); + let err = generate_configured_fit( + &context, + &prepared, + &FitConfig { + fdt: Some(FitFdt { + path: missing_fdt, + load: None, + }), + ..configured(FitFormat::Elf) + }, + ) + .await + .unwrap_err(); + assert!(err.to_string().contains("DTB"), "{err:#}"); + + let unsupported_root = tempfile::tempdir().unwrap(); + let (unsupported_context, unsupported) = prepare_fixture(unsupported_root.path(), 62, false); // EM_X86_64 + let err = generate_configured_fit( + &unsupported_context, + &unsupported, + &configured(FitFormat::Elf), + ) + .await + .unwrap_err(); + assert!( + err.to_string() + .contains("unsupported architecture for FIT image generation"), + "{err:#}" + ); +} + +#[tokio::test] +async fn configured_fit_reports_missing_raw_elf_and_unwritable_output() { + let temp = tempfile::tempdir().unwrap(); + let (context, prepared) = prepare_fixture(temp.path(), 183, true); // EM_AARCH64 + let mut runtime = fs::read(prepared.elf()).unwrap(); + put_u64(&mut runtime, 24, 0); // A different ELF must not borrow this source's symbols. + fs::write(prepared.elf(), runtime).unwrap(); + let err = generate_configured_fit(&context, &prepared, &configured(FitFormat::Elf)) + .await + .unwrap_err(); + assert!(err.to_string().contains("load layouts differ"), "{err:#}"); + fs::write(prepared.source_elf(), b"not an ELF anymore").unwrap(); + let err = generate_configured_fit(&context, &prepared, &configured(FitFormat::Elf)) + .await + .unwrap_err(); + assert!(err.to_string().contains("ELF"), "{err:#}"); + + fs::remove_file(prepared.source_elf()).unwrap(); + let err = generate_configured_fit(&context, &prepared, &configured(FitFormat::Elf)) + .await + .unwrap_err(); + assert!(err.to_string().contains("source ELF"), "{err:#}"); + + let alias_root = tempfile::tempdir().unwrap(); + let (alias_context, alias_prepared) = prepare_fixture(alias_root.path(), 183, false); + let err = generate_configured_fit( + &alias_context, + &alias_prepared, + &FitConfig { + output: Some(alias_prepared.source_elf().to_path_buf()), + ..configured(FitFormat::Elf) + }, + ) + .await + .unwrap_err(); + assert!(err.to_string().contains("overwrite"), "{err:#}"); + + let missing_runtime_root = tempfile::tempdir().unwrap(); + let (missing_runtime_context, missing_runtime) = + prepare_fixture(missing_runtime_root.path(), 183, true); // EM_AARCH64 + fs::remove_file(missing_runtime.elf()).unwrap(); + let err = generate_configured_fit( + &missing_runtime_context, + &missing_runtime, + &configured(FitFormat::Elf), + ) + .await + .unwrap_err(); + assert!(err.to_string().contains("ELF"), "{err:#}"); + + let output_root = tempfile::tempdir().unwrap(); + let (output_context, output_prepared) = prepare_fixture(output_root.path(), 183, false); + let output = output_root.path().join("absent-parent/image.fit"); + let err = generate_configured_fit( + &output_context, + &output_prepared, + &FitConfig { + output: Some(output), + ..configured(FitFormat::Elf) + }, + ) + .await + .unwrap_err(); + assert!(err.to_string().contains("FIT"), "{err:#}"); +} + +#[tokio::test] +async fn configured_elf_rejects_invalid_load_ranges_before_explicit_addresses() { + let temp = tempfile::tempdir().unwrap(); + let (context, prepared) = prepare_fixture(temp.path(), 183, false); + for (offset, value) in [(72, 0x10_0000), (104, 1), (0x200 + 64 * 4 + 24, 0x10_0000)] { + let mut elf = minimal_elf(183); + put_u64(&mut elf, offset, value); + fs::write(prepared.elf(), elf).unwrap(); + let result = generate_configured_fit( + &context, + &prepared, + &FitConfig { + load: FitAddress::Explicit(0), + entry: FitAddress::Explicit(0), + ..configured(FitFormat::Elf) + }, + ) + .await; + assert!( + result.is_err(), + "invalid load range at field {offset:#x} accepted" + ); + } +} + +#[tokio::test] +async fn configured_bin_rejects_empty_payload_even_with_explicit_addresses() { + let temp = tempfile::tempdir().unwrap(); + let (context, prepared) = prepare_fixture(temp.path(), 183, false); + let mut elf = minimal_elf(183); + put_u64(&mut elf, 40, 0); // No section table or file-backed load bytes. + put_u16(&mut elf, 60, 0); + put_u16(&mut elf, 62, 0); + put_u64(&mut elf, 96, 0); + fs::write(prepared.elf(), elf).unwrap(); + let output = temp.path().join("empty.fit"); + let result = generate_configured_fit( + &context, + &prepared, + &FitConfig { + load: FitAddress::Explicit(ELF_LOAD), + entry: FitAddress::Explicit(ELF_ENTRY), + output: Some(output.clone()), + ..configured(FitFormat::Bin) + }, + ) + .await; + assert!(result.unwrap_err().to_string().contains("empty")); + assert!(!output.exists()); +} + +#[tokio::test] +async fn configured_output_replaces_hard_link_without_changing_source() { + let temp = tempfile::tempdir().unwrap(); + let (context, prepared) = prepare_fixture(temp.path(), 183, false); + let source = fs::read(prepared.source_elf()).unwrap(); + let output = temp.path().join("aliased.fit"); + fs::hard_link(prepared.source_elf(), &output).unwrap(); + let image = generate_configured_fit( + &context, + &prepared, + &FitConfig { + output: Some(output), + ..configured(FitFormat::Elf) + }, + ) + .await + .unwrap(); + assert_eq!(fs::read(prepared.source_elf()).unwrap(), source); + assert_linux_arm64_header(&inspect_fit(image.path()), ELF_LOAD, ELF_ENTRY); +} + +#[tokio::test] +async fn configured_bin_surfaces_tool_launch_failure() { + let temp = tempfile::tempdir().unwrap(); + let (context, prepared) = prepare_fixture(temp.path(), 183, false); + // Use the real toolchain tool, but a vanished command working directory. + // The ELF is valid, so failure must come from executing the conversion. + let context = ProcessContext::new( + temp.path().join("missing-workdir"), + temp.path().to_path_buf(), + context.variables().clone(), + None, + ); + let err = generate_configured_fit(&context, &prepared, &configured(FitFormat::Bin)) + .await + .unwrap_err(); + assert!(err.to_string().contains("llvm-objcopy"), "{err:#}"); + assert!(!temp.path().join("image.fit").exists()); +} + +#[tokio::test] +async fn legacy_linux_loongarch_fit_keeps_header_address_rule() { + let temp = tempfile::tempdir().unwrap(); + let kernel_path = temp.path().join("Image"); + let output_path = temp.path().join("legacy.fit"); + let mut kernel = vec![0_u8; 0x40_0000]; + kernel[..2].copy_from_slice(b"MZ"); + kernel[8..16].copy_from_slice(&0x5c_8b40_u64.to_le_bytes()); + kernel[24..32].copy_from_slice(&0x20_0000_u64.to_le_bytes()); + fs::write(&kernel_path, &kernel).unwrap(); + + let image = generate_fit_image(FitInput { + kernel_path, + dtb_path: None, + arch: object::Architecture::LoongArch64, + kernel_load_addr: 0x9000_0000_9800_0000, + kernel_entry_addr: 0x9000_0000_9800_0000, + fdt_load_addr: None, + output_path: Some(output_path), + }) + .await + .unwrap(); + + let listing = inspect_fit(image.path()).to_ascii_lowercase(); + assert_dumpimage_field(&listing, "os", &["linux"]); + // Older U-Boot inspectors label LoongArch as unknown; inspect its stored + // property directly while still checking the full FIT with dumpimage. + assert_eq!( + fit_property(image.path(), "/images/kernel", "arch"), + "loongarch" + ); + assert_dumpimage_address(&listing, "load address", 0x9000_0000_9800_0000); + assert_dumpimage_address(&listing, "entry point", 0x9000_0000_983c_8b40); + assert_eq!( + extract_fit_component(image.path(), 0, &temp.path().join("legacy-kernel.out")), + kernel + ); +} diff --git a/ostool/src/boot/mod.rs b/ostool/src/boot/mod.rs index 39c6c823..f0fa7f4d 100644 --- a/ostool/src/boot/mod.rs +++ b/ostool/src/boot/mod.rs @@ -2,3 +2,6 @@ pub(crate) mod artifacts; pub(crate) mod fit; +// The configuration entry point is internal until boot preparation consumes it. +#[allow(dead_code)] +pub(crate) mod fit_config; From 45ba85e262f512ebdbfcf164941d72c402ff5a6f Mon Sep 17 00:00:00 2001 From: Niu Zhihong Date: Tue, 22 Sep 2026 17:37:13 +0800 Subject: [PATCH 6/8] =?UTF-8?q?test(ostool):=20=E7=B2=BE=E7=AE=80=20FIT=20?= =?UTF-8?q?=E5=9B=9E=E5=BD=92=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 保留 ELF/BIN 自动加载地址区别和配置往返两项短测试, 移除独立 FIT 集成测试文件、手写 ELF 和外部工具夹具。 生成逻辑保持不变,完整场景保留为本地验收。 devbox 格式检查、cargo test -p ostool 和严格 Clippy 通过。 Co-authored-by: OpenAI Codex Signed-off-by: Niu Zhihong --- ostool/src/boot/fit.rs | 4 - ostool/src/boot/fit_config.rs | 168 ++------ ostool/src/boot/fit_tests.rs | 763 ---------------------------------- 3 files changed, 24 insertions(+), 911 deletions(-) delete mode 100644 ostool/src/boot/fit_tests.rs diff --git a/ostool/src/boot/fit.rs b/ostool/src/boot/fit.rs index f60ff1e4..96b93416 100644 --- a/ostool/src/boot/fit.rs +++ b/ostool/src/boot/fit.rs @@ -1,9 +1,5 @@ //! U-Boot FIT image generation. -#[cfg(test)] -#[path = "fit_tests.rs"] -mod configured_tests; - use std::path::{Path, PathBuf}; use anyhow::Context; diff --git a/ostool/src/boot/fit_config.rs b/ostool/src/boot/fit_config.rs index 6a3b9b60..872af4d6 100644 --- a/ostool/src/boot/fit_config.rs +++ b/ostool/src/boot/fit_config.rs @@ -219,144 +219,34 @@ fn checked_end(start: u64, size: u64, kind: &str) -> Result { #[cfg(test)] mod tests { - use std::path::PathBuf; - - use super::{FitAddress, FitConfig, FitFdt, FitFormat, FitOs}; - use crate::artifact::elf_metadata::{ElfMetadata, LoadSection, LoadSegment}; + use super::*; use object::Architecture; - fn segment(vaddr: u64, paddr: u64, offset: u64, file_size: u64) -> LoadSegment { - LoadSegment { - virtual_address: vaddr, - physical_address: paddr, - file_offset: offset, - file_size, - memory_size: file_size, - alignment: 1, - flags: 0, - } - } - - fn section(vaddr: u64, offset: u64, size: u64) -> LoadSection { - LoadSection { - virtual_address: vaddr, - file_offset: offset, - size, - } - } - - fn metadata( - entry: u64, - executable_start: Option, - load_segments: Vec, - load_sections: Vec, - ) -> ElfMetadata { - ElfMetadata { - arch: Architecture::Riscv64, - entry, - load_segments, - load_sections, - executable_start, - } - } - - #[test] - fn resolves_symbol_and_entry_zero_and_honors_explicit_addresses() { - let metadata = metadata(0, Some(0), Vec::new(), Vec::new()); - assert_eq!( - FitConfig::default().resolve_addresses(&metadata).unwrap(), - (0, 0) - ); - - let config = FitConfig { - load: FitAddress::Explicit(0x1000), - entry: FitAddress::Explicit(0x2000), - ..FitConfig::default() - }; - assert_eq!( - config.resolve_addresses(&metadata).unwrap(), - (0x1000, 0x2000) - ); - } - - #[test] - fn elf_auto_load_requires_one_common_container_base() { - let metadata = metadata( - 0x8010, - None, - vec![ - segment(0x8000, 0x8000, 0, 0x1000), - segment(0x9000, 0x9000, 0x1000, 0x200), - ], - Vec::new(), - ); - let config = FitConfig { - format: FitFormat::Elf, - ..FitConfig::default() - }; - - assert_eq!( - config.resolve_addresses(&metadata).unwrap(), - (0x8000, 0x8010) - ); - } - #[test] - fn elf_auto_load_rejects_different_or_overflowing_segment_ranges() { - let config = FitConfig { - format: FitFormat::Elf, - ..FitConfig::default() + fn auto_load_distinguishes_elf_container_and_bin_payload() { + let metadata = ElfMetadata { + arch: Architecture::Riscv64, + entry: 0x8000_0100, + executable_start: None, + load_segments: vec![LoadSegment { + virtual_address: 0, + physical_address: 0x8000_0000, + file_offset: 0x100, + file_size: 0x200, + memory_size: 0x200, + alignment: 1, + flags: 0, + }], + load_sections: vec![LoadSection { + virtual_address: 0x100, + file_offset: 0x200, + size: 0x20, + }], }; - let different = metadata( - 0, - None, - vec![segment(0x8000, 0x8000, 0, 4), segment(0x9000, 0x9000, 4, 4)], - Vec::new(), - ); - assert!(config.resolve_addresses(&different).is_err()); - - let overflowing = metadata( - 0, - None, - vec![segment(u64::MAX - 1, u64::MAX - 1, 0, 2)], - Vec::new(), - ); - assert!(config.resolve_addresses(&overflowing).is_err()); - } - - #[test] - fn bin_auto_load_starts_at_first_output_section_not_pt_load_header() { - let metadata = metadata( - 0, - None, - vec![segment(0, 0x8000_0000, 0, 0x200)], - vec![section(0x100, 0x100, 0x20)], - ); - - assert_eq!( - FitConfig::default().resolve_addresses(&metadata).unwrap(), - (0x8000_0100, 0) - ); - } - - #[test] - fn bin_auto_load_keeps_zero_and_rejects_bss_only_or_ambiguous_mappings() { - let zero = metadata(0, None, vec![segment(0, 0, 0, 4)], vec![section(0, 0, 4)]); - assert_eq!( - FitConfig::default().resolve_addresses(&zero).unwrap(), - (0, 0) - ); - - let bss_only = metadata(0, None, vec![segment(0, 0, 0, 4)], Vec::new()); - assert!(FitConfig::default().resolve_addresses(&bss_only).is_err()); - - let ambiguous = metadata( - 0, - None, - vec![segment(0, 0x1000, 0, 4), segment(0, 0x2000, 0, 4)], - vec![section(0, 0, 4)], - ); - assert!(FitConfig::default().resolve_addresses(&ambiguous).is_err()); + let mut config = FitConfig::default(); + assert_eq!(config.resolve_addresses(&metadata).unwrap().0, 0x8000_0100); + config.format = FitFormat::Elf; + assert_eq!(config.resolve_addresses(&metadata).unwrap().0, 0x7fff_ff00); } #[test] @@ -379,14 +269,4 @@ mod tests { let toml = toml::to_string(&config).unwrap(); assert_eq!(toml::from_str::(&toml).unwrap(), config); } - - #[test] - fn omitted_addresses_default_to_auto_and_fdt_load_remains_optional() { - let config: FitConfig = toml::from_str("[fdt]\npath = 'board.dtb'").unwrap(); - assert_eq!(config.load, FitAddress::Auto); - assert_eq!(config.entry, FitAddress::Auto); - assert_eq!(config.fdt.as_ref().unwrap().load, None); - let encoded = toml::to_string(&config).unwrap(); - assert_eq!(toml::from_str::(&encoded).unwrap(), config); - } } diff --git a/ostool/src/boot/fit_tests.rs b/ostool/src/boot/fit_tests.rs deleted file mode 100644 index 3f1d6099..00000000 --- a/ostool/src/boot/fit_tests.rs +++ /dev/null @@ -1,763 +0,0 @@ -//! End-to-end regression tests for configured FIT images. -//! -//! These tests deliberately inspect FIT images with U-Boot's `dumpimage` and -//! extract each embedded component. Checking the FDT header alone would not -//! prove that the requested payload, addresses, or optional FDT made it into -//! the image. - -use std::{ffi::OsString, fs, path::Path, process::Command}; - -use super::{FitInput, generate_configured_fit, generate_fit_image}; -use crate::{ - artifact::{ - elf_metadata::ElfMetadata, - llvm_tools::llvm_objcopy, - runtime::{PreparedRuntimeArtifacts, RuntimeArtifactOptions, prepare_runtime_artifacts}, - }, - boot::fit_config::{FitAddress, FitConfig, FitFdt, FitFormat, FitOs}, - process::ProcessContext, - project::{resolve_project_layout, variables::VariableScope}, -}; - -const ELF_LOAD: u64 = 0x400_000; -const ELF_ENTRY: u64 = ELF_LOAD + 4; - -fn process_context(root: &Path) -> ProcessContext { - fs::write( - root.join("Cargo.toml"), - "[package]\nname = \"fit-fixture\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", - ) - .unwrap(); - fs::create_dir_all(root.join("src")).unwrap(); - fs::write(root.join("src/lib.rs"), "").unwrap(); - - let layout = resolve_project_layout(Some(root.to_path_buf())).unwrap(); - let scope = VariableScope::for_package(&layout, root.to_path_buf()); - ProcessContext::new(root.to_path_buf(), root.to_path_buf(), scope, None) -} - -fn prepare_fixture( - root: &Path, - architecture: u16, - strip_elf: bool, -) -> (ProcessContext, PreparedRuntimeArtifacts) { - let context = process_context(root); - let source = root.join("kernel.elf"); - fs::write(&source, minimal_elf(architecture)).unwrap(); - let prepared = prepare_runtime_artifacts( - &context, - RuntimeArtifactOptions { - elf_path: source, - to_bin: false, - bin_dir: None, - debug: false, - cargo_artifact_dir: None, - strip_elf, - }, - ) - .unwrap(); - (context, prepared) -} - -/// Builds the smallest useful little-endian ELF64 fixture in memory. -/// -/// The file contains one executable PT_LOAD segment, a `.text` section, and -/// a defined `__executable_start` symbol. This lets the configured FIT path -/// exercise the same metadata reader as production without compiling an -/// external target or relying on host architecture. -fn minimal_elf(machine: u16) -> Vec { - const PROGRAM_OFFSET: usize = 0x100; - const SHSTRTAB_OFFSET: usize = 0x120; - const STRTAB_OFFSET: usize = 0x150; - const SYMTAB_OFFSET: usize = 0x170; - const SECTION_HEADERS_OFFSET: usize = 0x200; - const SECTION_HEADER_SIZE: usize = 64; - - let mut elf = vec![0_u8; SECTION_HEADERS_OFFSET + SECTION_HEADER_SIZE * 5]; - elf[..4].copy_from_slice(b"\x7fELF"); - elf[4] = 2; // ELFCLASS64 - elf[5] = 1; // ELFDATA2LSB - elf[6] = 1; // EV_CURRENT - put_u16(&mut elf, 16, 2); // ET_EXEC - put_u16(&mut elf, 18, machine); - put_u32(&mut elf, 20, 1); - put_u64(&mut elf, 24, ELF_ENTRY); - put_u64(&mut elf, 32, 64); // e_phoff - put_u64(&mut elf, 40, SECTION_HEADERS_OFFSET as u64); - put_u16(&mut elf, 52, 64); // e_ehsize - put_u16(&mut elf, 54, 56); // e_phentsize - put_u16(&mut elf, 56, 1); // e_phnum - put_u16(&mut elf, 58, SECTION_HEADER_SIZE as u16); - put_u16(&mut elf, 60, 5); // e_shnum - put_u16(&mut elf, 62, 1); // e_shstrndx - - // The single PT_LOAD makes `llvm-objcopy -O binary` deterministic. - put_u32(&mut elf, 64, 1); // PT_LOAD - put_u32(&mut elf, 68, 5); // PF_R | PF_X - put_u64(&mut elf, 72, PROGRAM_OFFSET as u64); - put_u64(&mut elf, 80, ELF_LOAD); - put_u64(&mut elf, 88, ELF_LOAD); - put_u64(&mut elf, 96, 8); - put_u64(&mut elf, 104, 8); - put_u64(&mut elf, 112, 0x100); - elf[PROGRAM_OFFSET..PROGRAM_OFFSET + 8].copy_from_slice(b"KERNEL\0!"); - - let section_names = b"\0.shstrtab\0.strtab\0.symtab\0.text\0"; - elf[SHSTRTAB_OFFSET..SHSTRTAB_OFFSET + section_names.len()].copy_from_slice(section_names); - let symbol_names = b"\0__executable_start\0"; - elf[STRTAB_OFFSET..STRTAB_OFFSET + symbol_names.len()].copy_from_slice(symbol_names); - - // ELF64 symbol table: the first null entry plus __executable_start. - let symbol = SYMTAB_OFFSET + 24; - put_u32(&mut elf, symbol, 1); - elf[symbol + 4] = 0x10; // STB_GLOBAL | STT_NOTYPE - put_u16(&mut elf, symbol + 6, 4); // .text section index - put_u64(&mut elf, symbol + 8, ELF_LOAD); - - // Section 1: .shstrtab; Section 2: .strtab; Section 3: .symtab; - // Section 4: .text. The production parser only needs the normal ELF - // layout here; the contents are intentionally tiny. - section_header( - &mut elf, - SECTION_HEADERS_OFFSET + SECTION_HEADER_SIZE, - 1, - 3, - 0, - 0, - SHSTRTAB_OFFSET as u64, - section_names.len() as u64, - 0, - 0, - 1, - 0, - ); - section_header( - &mut elf, - SECTION_HEADERS_OFFSET + SECTION_HEADER_SIZE * 2, - 11, - 3, - 0, - 0, - STRTAB_OFFSET as u64, - symbol_names.len() as u64, - 0, - 0, - 1, - 0, - ); - section_header( - &mut elf, - SECTION_HEADERS_OFFSET + SECTION_HEADER_SIZE * 3, - 19, - 2, - 0, - 0, - SYMTAB_OFFSET as u64, - 48, - 2, - 1, - 8, - 24, - ); - section_header( - &mut elf, - SECTION_HEADERS_OFFSET + SECTION_HEADER_SIZE * 4, - 27, - 1, - 0x6, - ELF_LOAD, - PROGRAM_OFFSET as u64, - 8, - 0, - 0, - 4, - 0, - ); - - elf -} - -fn minimal_elf_without_executable_start(machine: u16) -> Vec { - let mut elf = minimal_elf(machine); - // The ELF and first PT_LOAD both start at file offset zero. Its physical - // base differs from .text by 0x100, so ELF containers and raw BINs have - // intentionally different correct auto-load values. - put_u64(&mut elf, 72, 0); - put_u64(&mut elf, 80, ELF_LOAD - 0x100); - put_u64(&mut elf, 88, ELF_LOAD - 0x100); - put_u64(&mut elf, 96, 0x108); - put_u64(&mut elf, 104, 0x108); - // Keep a named but undefined symbol to model a stripped source ELF. - put_u16(&mut elf, 0x170 + 24 + 6, 0); - elf -} - -#[allow(clippy::too_many_arguments)] -fn section_header( - bytes: &mut [u8], - offset: usize, - name: u32, - kind: u32, - flags: u64, - address: u64, - file_offset: u64, - size: u64, - link: u32, - info: u32, - alignment: u64, - entry_size: u64, -) { - put_u32(bytes, offset, name); - put_u32(bytes, offset + 4, kind); - put_u64(bytes, offset + 8, flags); - put_u64(bytes, offset + 16, address); - put_u64(bytes, offset + 24, file_offset); - put_u64(bytes, offset + 32, size); - put_u32(bytes, offset + 40, link); - put_u32(bytes, offset + 44, info); - put_u64(bytes, offset + 48, alignment); - put_u64(bytes, offset + 56, entry_size); -} - -fn put_u16(bytes: &mut [u8], offset: usize, value: u16) { - bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes()); -} - -fn put_u32(bytes: &mut [u8], offset: usize, value: u32) { - bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); -} - -fn put_u64(bytes: &mut [u8], offset: usize, value: u64) { - bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); -} - -fn configured(format: FitFormat) -> FitConfig { - FitConfig { - format, - os: FitOs::Linux, - load: FitAddress::Auto, - entry: FitAddress::Auto, - fdt: None, - output: None, - } -} - -fn run_dumpimage(args: &[OsString]) -> std::process::Output { - Command::new("dumpimage") - .args(args) - .output() - .unwrap_or_else(|err| { - panic!( - "failed to execute dumpimage; install u-boot-tools (the CI FIT inspector): {err}" - ) - }) -} - -fn inspect_fit(path: &Path) -> String { - let output = run_dumpimage(&[OsString::from("-l"), path.as_os_str().to_os_string()]); - assert!( - output.status.success(), - "dumpimage -l failed with status {}\nstdout:\n{}\nstderr:\n{}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); - format!( - "{}{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ) -} - -fn fit_property(path: &Path, node: &str, property: &str) -> String { - let output = Command::new("fdtget") - .args(["-t", "s"]) - .arg(path) - .arg(node) - .arg(property) - .output() - .unwrap_or_else(|err| { - panic!("failed to execute fdtget; install u-boot-tools (the CI FIT inspector): {err}") - }); - assert!( - output.status.success(), - "fdtget {node} {property} failed with status {}\nstdout:\n{}\nstderr:\n{}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); - String::from_utf8(output.stdout).unwrap().trim().to_owned() -} - -fn extract_fit_component(path: &Path, position: u32, destination: &Path) -> Vec { - let output = run_dumpimage(&[ - OsString::from("-T"), - OsString::from("flat_dt"), - OsString::from("-p"), - OsString::from(position.to_string()), - OsString::from("-o"), - destination.as_os_str().to_os_string(), - path.as_os_str().to_os_string(), - ]); - assert!( - output.status.success(), - "dumpimage extraction for component {position} failed with status {}\nstdout:\n{}\nstderr:\n{}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); - fs::read(destination).unwrap() -} - -fn assert_linux_arm64_header(listing: &str, load: u64, entry: u64) { - let listing = listing.to_ascii_lowercase(); - assert_dumpimage_field(&listing, "os", &["linux"]); - assert_dumpimage_field(&listing, "architecture", &["aarch64", "arm64"]); - assert_dumpimage_address(&listing, "load address", load); - assert_dumpimage_address(&listing, "entry point", entry); -} - -fn assert_dumpimage_field(listing: &str, label: &str, expected: &[&str]) { - let value = listing - .lines() - .find_map(|line| line.trim().strip_prefix(label)) - .and_then(|line| line.strip_prefix(':')) - .map(str::trim) - .unwrap_or_else(|| panic!("dumpimage listing has no {label}:\n{listing}")); - assert!( - expected.contains(&value), - "unexpected dumpimage {label} `{value}`; expected one of {expected:?}\n{listing}" - ); -} - -fn assert_dumpimage_address(listing: &str, label: &str, expected: u64) { - let address = listing - .lines() - .find_map(|line| line.trim().strip_prefix(label)) - .and_then(|line| line.strip_prefix(':')) - .map(str::trim) - .unwrap_or_else(|| panic!("dumpimage listing has no {label}:\n{listing}")); - assert_eq!(address, format!("0x{expected:08x}"), "{listing}"); -} - -fn image_listing(listing: &str, position: u32) -> &str { - let marker = format!("Image {position} ("); - let start = listing - .find(&marker) - .unwrap_or_else(|| panic!("dumpimage listing has no {marker}:\n{listing}")); - let remainder = &listing[start..]; - let next_marker = format!("\n Image {} (", position + 1); - &remainder[..remainder.find(&next_marker).unwrap_or(remainder.len())] -} - -fn expected_bin(context: &ProcessContext, source: &Path, output: &Path) -> Vec { - let objcopy = llvm_objcopy().unwrap(); - let result = crate::process::command(objcopy, context) - .arg("--strip-all") - .arg("-O") - .arg("binary") - .arg(source) - .arg(output) - .run(); - result.unwrap(); - fs::read(output).unwrap() -} - -fn strip_elf(context: &ProcessContext, source: &Path, output: &Path) { - let objcopy = llvm_objcopy().unwrap(); - crate::process::command(objcopy, context) - .arg("--strip-all") - .arg(source) - .arg(output) - .run() - .unwrap(); -} - -#[tokio::test] -async fn configured_elf_uses_runtime_payload_source_metadata_and_fdt() { - let temp = tempfile::Builder::new() - .prefix("FIT input with spaces ") - .tempdir() - .unwrap(); - let (context, prepared) = prepare_fixture(temp.path(), 183, true); // EM_AARCH64 - let source = prepared.source_elf().to_path_buf(); - let runtime = prepared.elf().to_path_buf(); - assert_ne!( - source, runtime, - "the fixture must exercise distinct sources" - ); - - // A stripped runtime ELF has no __executable_start. It remains a valid - // ELF payload, while auto-address resolution must use source_elf. - let stripped = temp.path().join("actually-stripped.elf"); - strip_elf(&context, &source, &stripped); - fs::copy(&stripped, &runtime).unwrap(); - let source_metadata = ElfMetadata::parse(&fs::read(&source).unwrap()).unwrap(); - let runtime_metadata = ElfMetadata::parse(&fs::read(&runtime).unwrap()).unwrap(); - assert_eq!(source_metadata.executable_start, Some(ELF_LOAD)); - assert_eq!(runtime_metadata.executable_start, None); - let runtime_payload = fs::read(&runtime).unwrap(); - let fdt_path = temp.path().join("board.dtb"); - let fdt_payload = b"test dtb payload"; - fs::write(&fdt_path, fdt_payload).unwrap(); - let output = temp.path().join("configured.fit"); - - let image = generate_configured_fit( - &context, - &prepared, - &FitConfig { - os: FitOs::Elf, - fdt: Some(FitFdt { - path: fdt_path, - load: Some(0x4_8000_0000), - }), - output: Some(output.clone()), - ..configured(FitFormat::Elf) - }, - ) - .await - .unwrap(); - - assert_eq!(image.path(), output); - let listing = inspect_fit(image.path()); - assert_dumpimage_field( - &listing.to_ascii_lowercase(), - "architecture", - &["aarch64", "arm64"], - ); - assert_dumpimage_address(&listing.to_ascii_lowercase(), "load address", ELF_LOAD); - assert_dumpimage_address(&listing.to_ascii_lowercase(), "entry point", ELF_ENTRY); - assert_eq!(fit_property(image.path(), "/images/kernel", "os"), "elf"); - assert_eq!( - extract_fit_component(image.path(), 0, &temp.path().join("kernel.out")), - runtime_payload - ); - assert_eq!( - extract_fit_component(image.path(), 1, &temp.path().join("fdt.out")), - fdt_payload - ); - assert_dumpimage_address( - &image_listing(&listing, 1).to_ascii_lowercase(), - "load address", - 0x4_8000_0000, - ); -} - -#[tokio::test] -async fn configured_bin_derives_payload_and_uses_runtime_directory_default_output() { - let temp = tempfile::tempdir().unwrap(); - let (context, prepared) = prepare_fixture(temp.path(), 183, false); // EM_AARCH64 - let expected = expected_bin(&context, prepared.elf(), &temp.path().join("expected.bin")); - - let image = generate_configured_fit(&context, &prepared, &configured(FitFormat::Bin)) - .await - .unwrap(); - - assert_eq!(image.path(), temp.path().join("image.fit")); - let listing = inspect_fit(image.path()); - assert_linux_arm64_header(&listing, ELF_LOAD, ELF_ENTRY); - assert_eq!( - extract_fit_component(image.path(), 0, &temp.path().join("kernel.bin.out")), - expected - ); -} - -#[tokio::test] -async fn configured_explicit_zero_addresses_override_auto_resolution() { - let temp = tempfile::tempdir().unwrap(); - let (context, prepared) = prepare_fixture(temp.path(), 183, false); // EM_AARCH64 - let output = temp.path().join("zero.fit"); - let image = generate_configured_fit( - &context, - &prepared, - &FitConfig { - load: FitAddress::Explicit(0), - entry: FitAddress::Explicit(0), - output: Some(output), - ..configured(FitFormat::Elf) - }, - ) - .await - .unwrap(); - - let listing = inspect_fit(image.path()).to_ascii_lowercase(); - assert_dumpimage_address(&listing, "load address", 0); - assert_dumpimage_address(&listing, "entry point", 0); -} - -#[tokio::test] -async fn configured_auto_load_distinguishes_elf_container_from_bin_section_base() { - let temp = tempfile::tempdir().unwrap(); - let (context, prepared) = prepare_fixture(temp.path(), 183, false); // EM_AARCH64 - fs::write( - prepared.source_elf(), - minimal_elf_without_executable_start(183), - ) - .unwrap(); - let metadata = ElfMetadata::parse(&fs::read(prepared.source_elf()).unwrap()).unwrap(); - assert_eq!(metadata.executable_start, None); - - let elf_image = generate_configured_fit( - &context, - &prepared, - &FitConfig { - output: Some(temp.path().join("fallback-elf.fit")), - ..configured(FitFormat::Elf) - }, - ) - .await - .unwrap(); - let elf_listing = inspect_fit(elf_image.path()).to_ascii_lowercase(); - assert_dumpimage_address(&elf_listing, "load address", ELF_LOAD - 0x100); - assert_dumpimage_address(&elf_listing, "entry point", ELF_ENTRY); - - let bin_image = generate_configured_fit( - &context, - &prepared, - &FitConfig { - output: Some(temp.path().join("fallback-bin.fit")), - ..configured(FitFormat::Bin) - }, - ) - .await - .unwrap(); - let bin_listing = inspect_fit(bin_image.path()).to_ascii_lowercase(); - assert_dumpimage_address(&bin_listing, "load address", ELF_LOAD); - assert_dumpimage_address(&bin_listing, "entry point", ELF_ENTRY); - assert_eq!( - extract_fit_component(bin_image.path(), 0, &temp.path().join("fallback-bin.out")), - b"KERNEL\0!" - ); -} - -#[tokio::test] -async fn configured_fit_rejects_missing_fdt_and_unsupported_architecture() { - let temp = tempfile::tempdir().unwrap(); - let (context, prepared) = prepare_fixture(temp.path(), 183, false); // EM_AARCH64 - let missing_fdt = temp.path().join("missing.dtb"); - let err = generate_configured_fit( - &context, - &prepared, - &FitConfig { - fdt: Some(FitFdt { - path: missing_fdt, - load: None, - }), - ..configured(FitFormat::Elf) - }, - ) - .await - .unwrap_err(); - assert!(err.to_string().contains("DTB"), "{err:#}"); - - let unsupported_root = tempfile::tempdir().unwrap(); - let (unsupported_context, unsupported) = prepare_fixture(unsupported_root.path(), 62, false); // EM_X86_64 - let err = generate_configured_fit( - &unsupported_context, - &unsupported, - &configured(FitFormat::Elf), - ) - .await - .unwrap_err(); - assert!( - err.to_string() - .contains("unsupported architecture for FIT image generation"), - "{err:#}" - ); -} - -#[tokio::test] -async fn configured_fit_reports_missing_raw_elf_and_unwritable_output() { - let temp = tempfile::tempdir().unwrap(); - let (context, prepared) = prepare_fixture(temp.path(), 183, true); // EM_AARCH64 - let mut runtime = fs::read(prepared.elf()).unwrap(); - put_u64(&mut runtime, 24, 0); // A different ELF must not borrow this source's symbols. - fs::write(prepared.elf(), runtime).unwrap(); - let err = generate_configured_fit(&context, &prepared, &configured(FitFormat::Elf)) - .await - .unwrap_err(); - assert!(err.to_string().contains("load layouts differ"), "{err:#}"); - fs::write(prepared.source_elf(), b"not an ELF anymore").unwrap(); - let err = generate_configured_fit(&context, &prepared, &configured(FitFormat::Elf)) - .await - .unwrap_err(); - assert!(err.to_string().contains("ELF"), "{err:#}"); - - fs::remove_file(prepared.source_elf()).unwrap(); - let err = generate_configured_fit(&context, &prepared, &configured(FitFormat::Elf)) - .await - .unwrap_err(); - assert!(err.to_string().contains("source ELF"), "{err:#}"); - - let alias_root = tempfile::tempdir().unwrap(); - let (alias_context, alias_prepared) = prepare_fixture(alias_root.path(), 183, false); - let err = generate_configured_fit( - &alias_context, - &alias_prepared, - &FitConfig { - output: Some(alias_prepared.source_elf().to_path_buf()), - ..configured(FitFormat::Elf) - }, - ) - .await - .unwrap_err(); - assert!(err.to_string().contains("overwrite"), "{err:#}"); - - let missing_runtime_root = tempfile::tempdir().unwrap(); - let (missing_runtime_context, missing_runtime) = - prepare_fixture(missing_runtime_root.path(), 183, true); // EM_AARCH64 - fs::remove_file(missing_runtime.elf()).unwrap(); - let err = generate_configured_fit( - &missing_runtime_context, - &missing_runtime, - &configured(FitFormat::Elf), - ) - .await - .unwrap_err(); - assert!(err.to_string().contains("ELF"), "{err:#}"); - - let output_root = tempfile::tempdir().unwrap(); - let (output_context, output_prepared) = prepare_fixture(output_root.path(), 183, false); - let output = output_root.path().join("absent-parent/image.fit"); - let err = generate_configured_fit( - &output_context, - &output_prepared, - &FitConfig { - output: Some(output), - ..configured(FitFormat::Elf) - }, - ) - .await - .unwrap_err(); - assert!(err.to_string().contains("FIT"), "{err:#}"); -} - -#[tokio::test] -async fn configured_elf_rejects_invalid_load_ranges_before_explicit_addresses() { - let temp = tempfile::tempdir().unwrap(); - let (context, prepared) = prepare_fixture(temp.path(), 183, false); - for (offset, value) in [(72, 0x10_0000), (104, 1), (0x200 + 64 * 4 + 24, 0x10_0000)] { - let mut elf = minimal_elf(183); - put_u64(&mut elf, offset, value); - fs::write(prepared.elf(), elf).unwrap(); - let result = generate_configured_fit( - &context, - &prepared, - &FitConfig { - load: FitAddress::Explicit(0), - entry: FitAddress::Explicit(0), - ..configured(FitFormat::Elf) - }, - ) - .await; - assert!( - result.is_err(), - "invalid load range at field {offset:#x} accepted" - ); - } -} - -#[tokio::test] -async fn configured_bin_rejects_empty_payload_even_with_explicit_addresses() { - let temp = tempfile::tempdir().unwrap(); - let (context, prepared) = prepare_fixture(temp.path(), 183, false); - let mut elf = minimal_elf(183); - put_u64(&mut elf, 40, 0); // No section table or file-backed load bytes. - put_u16(&mut elf, 60, 0); - put_u16(&mut elf, 62, 0); - put_u64(&mut elf, 96, 0); - fs::write(prepared.elf(), elf).unwrap(); - let output = temp.path().join("empty.fit"); - let result = generate_configured_fit( - &context, - &prepared, - &FitConfig { - load: FitAddress::Explicit(ELF_LOAD), - entry: FitAddress::Explicit(ELF_ENTRY), - output: Some(output.clone()), - ..configured(FitFormat::Bin) - }, - ) - .await; - assert!(result.unwrap_err().to_string().contains("empty")); - assert!(!output.exists()); -} - -#[tokio::test] -async fn configured_output_replaces_hard_link_without_changing_source() { - let temp = tempfile::tempdir().unwrap(); - let (context, prepared) = prepare_fixture(temp.path(), 183, false); - let source = fs::read(prepared.source_elf()).unwrap(); - let output = temp.path().join("aliased.fit"); - fs::hard_link(prepared.source_elf(), &output).unwrap(); - let image = generate_configured_fit( - &context, - &prepared, - &FitConfig { - output: Some(output), - ..configured(FitFormat::Elf) - }, - ) - .await - .unwrap(); - assert_eq!(fs::read(prepared.source_elf()).unwrap(), source); - assert_linux_arm64_header(&inspect_fit(image.path()), ELF_LOAD, ELF_ENTRY); -} - -#[tokio::test] -async fn configured_bin_surfaces_tool_launch_failure() { - let temp = tempfile::tempdir().unwrap(); - let (context, prepared) = prepare_fixture(temp.path(), 183, false); - // Use the real toolchain tool, but a vanished command working directory. - // The ELF is valid, so failure must come from executing the conversion. - let context = ProcessContext::new( - temp.path().join("missing-workdir"), - temp.path().to_path_buf(), - context.variables().clone(), - None, - ); - let err = generate_configured_fit(&context, &prepared, &configured(FitFormat::Bin)) - .await - .unwrap_err(); - assert!(err.to_string().contains("llvm-objcopy"), "{err:#}"); - assert!(!temp.path().join("image.fit").exists()); -} - -#[tokio::test] -async fn legacy_linux_loongarch_fit_keeps_header_address_rule() { - let temp = tempfile::tempdir().unwrap(); - let kernel_path = temp.path().join("Image"); - let output_path = temp.path().join("legacy.fit"); - let mut kernel = vec![0_u8; 0x40_0000]; - kernel[..2].copy_from_slice(b"MZ"); - kernel[8..16].copy_from_slice(&0x5c_8b40_u64.to_le_bytes()); - kernel[24..32].copy_from_slice(&0x20_0000_u64.to_le_bytes()); - fs::write(&kernel_path, &kernel).unwrap(); - - let image = generate_fit_image(FitInput { - kernel_path, - dtb_path: None, - arch: object::Architecture::LoongArch64, - kernel_load_addr: 0x9000_0000_9800_0000, - kernel_entry_addr: 0x9000_0000_9800_0000, - fdt_load_addr: None, - output_path: Some(output_path), - }) - .await - .unwrap(); - - let listing = inspect_fit(image.path()).to_ascii_lowercase(); - assert_dumpimage_field(&listing, "os", &["linux"]); - // Older U-Boot inspectors label LoongArch as unknown; inspect its stored - // property directly while still checking the full FIT with dumpimage. - assert_eq!( - fit_property(image.path(), "/images/kernel", "arch"), - "loongarch" - ); - assert_dumpimage_address(&listing, "load address", 0x9000_0000_9800_0000); - assert_dumpimage_address(&listing, "entry point", 0x9000_0000_983c_8b40); - assert_eq!( - extract_fit_component(image.path(), 0, &temp.path().join("legacy-kernel.out")), - kernel - ); -} From 8076e5d40c1e32c148c5c2b6d4daad73896f683d Mon Sep 17 00:00:00 2001 From: Niu Zhihong Date: Fri, 9 Oct 2026 14:59:44 +0800 Subject: [PATCH 7/8] =?UTF-8?q?fix(ostool):=20=E4=BF=AE=E6=AD=A3=20FIT=20?= =?UTF-8?q?=E5=9C=B0=E5=9D=80=E8=A7=A3=E6=9E=90=E4=B8=8E=20ELF=20=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ELF OS 仅接受 ELF payload,并要求显式提供安全暂存地址;自动入口使用文件头地址,拒绝不一致的显式入口。LoongArch Linux BIN 自动入口复用已有镜像头换算。 BIN 自动地址核对实际 payload LMA;符号冲突、入口映射不明或加载位置变化时要求显式覆盖。保留合法零地址和现有默认生成流程,不增加用户配置入口。 验证:ARM64 devbox 格式、Clippy、构建及 ostool 测试通过。真实 ELF/FIT 本地验收检查 payload 完整字节、OS、架构和地址。未运行 x86_64 CI matrix 或硬件启动。 Co-authored-by: OpenAI Codex Signed-off-by: Niu Zhihong --- ostool/src/boot/fit.rs | 14 +++- ostool/src/boot/fit_config.rs | 132 ++++++++++++++++++++++++++++------ 2 files changed, 122 insertions(+), 24 deletions(-) diff --git a/ostool/src/boot/fit.rs b/ostool/src/boot/fit.rs index 71dec608..348141b3 100644 --- a/ostool/src/boot/fit.rs +++ b/ostool/src/boot/fit.rs @@ -15,7 +15,7 @@ use crate::{ elf_metadata::ElfMetadata, runtime::{PreparedRuntimeArtifacts, RuntimeArtifactOptions, prepare_runtime_artifacts}, }, - boot::fit_config::{FitConfig, FitFormat, FitOs}, + boot::fit_config::{FitAddress, FitConfig, FitFormat, FitOs}, process::ProcessContext, utils::PathResultExt, }; @@ -91,7 +91,7 @@ pub(crate) async fn generate_configured_fit( { anyhow::bail!("source and runtime ELF load layouts differ; prepare the selected ELF again"); } - let (load, entry) = config.resolve_addresses(&metadata)?; + let load = config.resolve_load(&metadata)?; let output = match &config.output { Some(path) => path.clone(), None => default_output_path(prepared.elf())?, @@ -141,6 +141,16 @@ pub(crate) async fn generate_configured_fit( if kernel_data.is_empty() { anyhow::bail!("cannot build FIT with an empty kernel payload"); } + let entry = if config.entry == FitAddress::Auto + && config.format == FitFormat::Bin + && config.os == FitOs::Linux + && metadata.arch == Architecture::LoongArch64 + && kernel_data.starts_with(LOONGARCH_IMAGE_MAGIC) + { + resolve_kernel_entry_addr(metadata.arch, &kernel_data, load, metadata.entry)? + } else { + config.resolve_entry(&metadata, load)? + }; // Replace the output directory entry only after success. In particular, a // hard link to an input must not truncate that input's shared inode. let output_dir = output diff --git a/ostool/src/boot/fit_config.rs b/ostool/src/boot/fit_config.rs index 872af4d6..e8642e14 100644 --- a/ostool/src/boot/fit_config.rs +++ b/ostool/src/boot/fit_config.rs @@ -74,25 +74,86 @@ impl Default for FitConfig { } impl FitConfig { - /// Resolves FIT addresses from the original, unstripped ELF metadata. - pub(crate) fn resolve_addresses(&self, metadata: &ElfMetadata) -> Result<(u64, u64)> { - let load = match self.load { - FitAddress::Explicit(address) => address, + /// Resolves the payload load address from the original, unstripped ELF. + pub(crate) fn resolve_load(&self, metadata: &ElfMetadata) -> Result { + if self.os == FitOs::Elf && self.format != FitFormat::Elf { + bail!("FIT OS elf requires format elf and U-Boot CONFIG_BOOTM_ELF"); + } + match self.load { + FitAddress::Explicit(address) => Ok(address), + // An ELF container needs staging memory, independent of its runtime + // sections. Metadata cannot establish a safe region on the board. + FitAddress::Auto if self.os == FitOs::Elf => { + bail!("FIT OS elf requires an explicit load address in safe staging memory") + } + FitAddress::Auto if self.format == FitFormat::Bin => { + let base = bin_load_base(metadata)?; + if metadata + .executable_start + .is_some_and(|address| address != base) + { + bail!( + "cannot derive BIN FIT load address: __executable_start differs from the first payload LMA; set load explicitly" + ); + } + Ok(base) + } FitAddress::Auto => match metadata.executable_start { - // The linker-provided origin describes the selected project's image base. - Some(address) => address, - None => match self.format { - FitFormat::Elf => elf_load_base(metadata)?, - FitFormat::Bin => bin_load_base(metadata)?, - }, + Some(address) => Ok(address), + None => elf_load_base(metadata), }, - }; - let entry = match self.entry { - FitAddress::Explicit(address) => address, - FitAddress::Auto => metadata.entry, - }; - Ok((load, entry)) + } + } + + /// FIT OS elf passes the ELF container address to bootelf, which reads e_entry + /// itself. Other OS handlers consume an execution address. + pub(crate) fn resolve_entry(&self, metadata: &ElfMetadata, load: u64) -> Result { + if self.os == FitOs::Elf { + if let FitAddress::Explicit(address) = self.entry + && address != load + { + bail!("FIT OS elf entry must equal the ELF payload load address; use entry auto"); + } + return Ok(load); + } + match self.entry { + FitAddress::Explicit(address) => Ok(address), + FitAddress::Auto if self.format == FitFormat::Bin => bin_entry(metadata, load), + FitAddress::Auto => Ok(metadata.entry), + } + } +} + +fn bin_entry(metadata: &ElfMetadata, load: u64) -> Result { + // Raw bytes carry no relocation or address-mapping contract. Only preserve + // e_entry when it still addresses its original bytes without either. + if load != bin_load_base(metadata)? { + bail!("cannot derive BIN FIT entry for a relocated payload; set entry explicitly"); + } + let mut found = false; + for section in &metadata.load_sections { + if metadata.entry >= section.virtual_address + && metadata.entry + < checked_end( + section.virtual_address, + section.size, + "section virtual range", + )? + { + if section_lma(section, &metadata.load_segments)? != section.virtual_address { + bail!( + "cannot derive BIN FIT entry: ELF entry VMA differs from its LMA; set entry explicitly" + ); + } + found = true; + } } + if !found { + bail!( + "cannot derive BIN FIT entry: ELF entry is outside the file-backed payload; set entry explicitly" + ); + } + Ok(metadata.entry) } fn elf_load_base(metadata: &ElfMetadata) -> Result { @@ -223,13 +284,13 @@ mod tests { use object::Architecture; #[test] - fn auto_load_distinguishes_elf_container_and_bin_payload() { - let metadata = ElfMetadata { + fn resolves_payload_addresses_and_rejects_ambiguous_auto_values() { + let mut metadata = ElfMetadata { arch: Architecture::Riscv64, entry: 0x8000_0100, executable_start: None, load_segments: vec![LoadSegment { - virtual_address: 0, + virtual_address: 0x8000_0000, physical_address: 0x8000_0000, file_offset: 0x100, file_size: 0x200, @@ -238,15 +299,42 @@ mod tests { flags: 0, }], load_sections: vec![LoadSection { - virtual_address: 0x100, + virtual_address: 0x8000_0100, file_offset: 0x200, size: 0x20, }], }; let mut config = FitConfig::default(); - assert_eq!(config.resolve_addresses(&metadata).unwrap().0, 0x8000_0100); + let load = config.resolve_load(&metadata).unwrap(); + assert_eq!(load, 0x8000_0100); + assert_eq!( + config.resolve_entry(&metadata, load).unwrap(), + metadata.entry + ); + metadata.executable_start = Some(0x9000_0100); + assert!(config.resolve_load(&metadata).is_err()); + metadata.load_segments[0].virtual_address += 0x1000_0000; + metadata.load_sections[0].virtual_address += 0x1000_0000; + metadata.entry += 0x1000_0000; + assert!(config.resolve_entry(&metadata, load).is_err()); + config.entry = FitAddress::Explicit(0); + config.load = FitAddress::Explicit(0); + assert_eq!(config.resolve_load(&metadata).unwrap(), 0); + assert_eq!(config.resolve_entry(&metadata, 0).unwrap(), 0); + config.load = FitAddress::Auto; + metadata.executable_start = None; config.format = FitFormat::Elf; - assert_eq!(config.resolve_addresses(&metadata).unwrap().0, 0x7fff_ff00); + let load = config.resolve_load(&metadata).unwrap(); + assert_eq!(load, 0x7fff_ff00); + config.os = FitOs::Elf; + assert!(config.resolve_load(&metadata).is_err()); + config.load = FitAddress::Explicit(load); + config.entry = FitAddress::Auto; + assert_eq!(config.resolve_entry(&metadata, load).unwrap(), load); + config.entry = FitAddress::Explicit(metadata.entry); + assert!(config.resolve_entry(&metadata, load).is_err()); + config.format = FitFormat::Bin; + assert!(config.resolve_load(&metadata).is_err()); } #[test] From e7b566e3564ebec80601367c83850d40a81db464 Mon Sep 17 00:00:00 2001 From: Niu Zhihong Date: Fri, 9 Oct 2026 15:31:52 +0800 Subject: [PATCH 8/8] =?UTF-8?q?fix(ostool):=20=E6=A0=A1=E9=AA=8C=20ELF=20?= =?UTF-8?q?=E5=AE=B9=E5=99=A8=E5=9C=B0=E5=9D=80=E5=B9=B6=E4=BF=9D=E7=95=99?= =?UTF-8?q?=20FIT=20=E8=BE=93=E5=87=BA=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ELF/Linux 自动加载地址校验完整文件的装载基址;符号地址冲突、入口映射不明或加载位置变化时要求显式覆盖,避免生成不可可靠启动的 FIT。 FIT 输出保留已有文件权限,新文件遵循 umask。最终入口统一由 FitInput 传递,保持默认 Linux 与 LoongArch 镜像头处理兼容。 验证:ARM64 devbox 格式、Clippy、构建及 ostool 测试通过。真实 ELF/BIN FIT 内容、地址、LoongArch 入口、文件权限与原子替换的本地验收通过。未运行 x86_64 CI matrix 或硬件启动。 Co-authored-by: OpenAI Codex Signed-off-by: Niu Zhihong --- ostool/src/boot/fit.rs | 41 ++++++++++++++++++++++++++----- ostool/src/boot/fit_config.rs | 45 ++++++++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 17 deletions(-) diff --git a/ostool/src/boot/fit.rs b/ostool/src/boot/fit.rs index 348141b3..a08ac2a9 100644 --- a/ostool/src/boot/fit.rs +++ b/ostool/src/boot/fit.rs @@ -141,6 +141,8 @@ pub(crate) async fn generate_configured_fit( if kernel_data.is_empty() { anyhow::bail!("cannot build FIT with an empty kernel payload"); } + // The Linux image header takes precedence over metadata-only inference. + // These branches are exclusive: an explicit entry never reads the header. let entry = if config.entry == FitAddress::Auto && config.format == FitFormat::Bin && config.os == FitOs::Linux @@ -157,7 +159,28 @@ pub(crate) async fn generate_configured_fit( .parent() .filter(|parent| !parent.as_os_str().is_empty()) .unwrap_or(Path::new(".")); - let temporary = tempfile::NamedTempFile::new_in(output_dir) + #[cfg(unix)] + let output_permissions = match fs::metadata(&output).await { + Ok(metadata) => Some(metadata.permissions()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, + Err(error) => { + return Err(error).with_path("failed to read FIT output permissions", &output); + } + }; + let builder = tempfile::Builder::new(); + #[cfg(unix)] + let builder = { + use std::os::unix::fs::PermissionsExt; + let mut builder = builder; + // New outputs follow normal file creation (0666 masked by umask). + // Keep replacements private until their existing mode is restored. + if output_permissions.is_none() { + builder.permissions(std::fs::Permissions::from_mode(0o666)); + } + builder + }; + let temporary = builder + .tempfile_in(output_dir) .with_path("failed to stage FIT output", &output)?; write_fit_image( FitInput { @@ -171,10 +194,16 @@ pub(crate) async fn generate_configured_fit( output_path: Some(temporary.path().to_path_buf()), }, kernel_data, - entry, config.os, ) .await?; + #[cfg(unix)] + if let Some(permissions) = output_permissions { + temporary + .as_file() + .set_permissions(permissions) + .with_path("failed to preserve FIT output permissions", &output)?; + } temporary .persist(&output) .map_err(|error| error.error) @@ -216,7 +245,7 @@ fn validate_elf_load_data(metadata: &ElfMetadata, file_size: usize) -> anyhow::R } /// Existing U-Boot path: Linux payload with its historical header handling. -pub(crate) async fn generate_fit_image(input: FitInput) -> anyhow::Result { +pub(crate) async fn generate_fit_image(mut input: FitInput) -> anyhow::Result { info!("Making FIT image..."); let kernel_data = fs::read(&input.kernel_path) @@ -239,13 +268,13 @@ pub(crate) async fn generate_fit_image(input: FitInput) -> anyhow::Result, - kernel_entry_addr: u64, os: FitOs, ) -> anyhow::Result { let arch_name = fit_arch_name(input.arch)?; @@ -284,7 +313,7 @@ async fn write_fit_image( dtb_data, initramfs_data, input.kernel_load_addr, - kernel_entry_addr, + input.kernel_entry_addr, input.fdt_load_addr, ); diff --git a/ostool/src/boot/fit_config.rs b/ostool/src/boot/fit_config.rs index e8642e14..17313a1a 100644 --- a/ostool/src/boot/fit_config.rs +++ b/ostool/src/boot/fit_config.rs @@ -98,10 +98,18 @@ impl FitConfig { } Ok(base) } - FitAddress::Auto => match metadata.executable_start { - Some(address) => Ok(address), - None => elf_load_base(metadata), - }, + FitAddress::Auto => { + let base = elf_load_base(metadata)?; + if metadata + .executable_start + .is_some_and(|address| address != base) + { + bail!( + "cannot derive ELF FIT load address: __executable_start differs from the ELF container base; set load explicitly" + ); + } + Ok(base) + } } } @@ -118,17 +126,20 @@ impl FitConfig { } match self.entry { FitAddress::Explicit(address) => Ok(address), - FitAddress::Auto if self.format == FitFormat::Bin => bin_entry(metadata, load), - FitAddress::Auto => Ok(metadata.entry), + FitAddress::Auto => direct_entry(metadata, load, self.format), } } } -fn bin_entry(metadata: &ElfMetadata, load: u64) -> Result { +fn direct_entry(metadata: &ElfMetadata, load: u64, format: FitFormat) -> Result { // Raw bytes carry no relocation or address-mapping contract. Only preserve // e_entry when it still addresses its original bytes without either. - if load != bin_load_base(metadata)? { - bail!("cannot derive BIN FIT entry for a relocated payload; set entry explicitly"); + let base = match format { + FitFormat::Bin => bin_load_base(metadata)?, + FitFormat::Elf => elf_load_base(metadata)?, + }; + if load != base { + bail!("cannot derive FIT entry for a relocated payload; set entry explicitly"); } let mut found = false; for section in &metadata.load_sections { @@ -142,7 +153,7 @@ fn bin_entry(metadata: &ElfMetadata, load: u64) -> Result { { if section_lma(section, &metadata.load_segments)? != section.virtual_address { bail!( - "cannot derive BIN FIT entry: ELF entry VMA differs from its LMA; set entry explicitly" + "cannot derive FIT entry: ELF entry VMA differs from its LMA; set entry explicitly" ); } found = true; @@ -150,7 +161,7 @@ fn bin_entry(metadata: &ElfMetadata, load: u64) -> Result { } if !found { bail!( - "cannot derive BIN FIT entry: ELF entry is outside the file-backed payload; set entry explicitly" + "cannot derive FIT entry: ELF entry is outside the file-backed payload; set entry explicitly" ); } Ok(metadata.entry) @@ -326,6 +337,18 @@ mod tests { config.format = FitFormat::Elf; let load = config.resolve_load(&metadata).unwrap(); assert_eq!(load, 0x7fff_ff00); + // A VMA symbol cannot override the file base, even if it names entry. + metadata.executable_start = Some(metadata.entry); + assert!(config.resolve_load(&metadata).is_err()); + metadata.executable_start = Some(load); + assert_eq!(config.resolve_load(&metadata).unwrap(), load); + config.entry = FitAddress::Auto; + assert!(config.resolve_entry(&metadata, load).is_err()); + config.entry = FitAddress::Explicit(metadata.entry); + assert_eq!( + config.resolve_entry(&metadata, load).unwrap(), + metadata.entry + ); config.os = FitOs::Elf; assert!(config.resolve_load(&metadata).is_err()); config.load = FitAddress::Explicit(load);