From be57ced99b660810158da4763e5a764ddefe3be1 Mon Sep 17 00:00:00 2001 From: Richard Qin Date: Wed, 1 Apr 2026 04:54:49 +0800 Subject: feat(compliance): implement ACT4 strict compliance flow with ELF runner add functional compliance_runner with ELF loading, memory model, symbol-based signature dump, and tohost handling; wire compliance subdir/target in CMake; upgrade run_compliance.py for ACT4 suite mapping, robust binary/reference discovery, strict signature comparison, and aggregate suites (rv64g/rv64full). --- rv64cpu/scripts/run_compliance.py | 375 ++++++++++++++++++++++---- rv64cpu/tb/CMakeLists.txt | 4 +- rv64cpu/tb/compliance/CMakeLists.txt | 41 ++- rv64cpu/tb/compliance/compliance_runner.cc | 408 +++++++++++++++++++++++++++++ 4 files changed, 756 insertions(+), 72 deletions(-) create mode 100644 rv64cpu/tb/compliance/compliance_runner.cc (limited to 'rv64cpu') diff --git a/rv64cpu/scripts/run_compliance.py b/rv64cpu/scripts/run_compliance.py index 92456ec..6932a15 100755 --- a/rv64cpu/scripts/run_compliance.py +++ b/rv64cpu/scripts/run_compliance.py @@ -1,94 +1,299 @@ #!/usr/bin/env python3 -""" -run_compliance.py — riscv-arch-test 自动化运行脚本 +"""riscv-arch-test compliance orchestration. -Usage: - python3 scripts/run_compliance.py --suite rv64i - python3 scripts/run_compliance.py --suite rv64im --build-dir build - python3 scripts/run_compliance.py --list +// Richard CPU +// Copyright (C) 2026 Richard Qin +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . -Requirements: - - Verilator simulation binary built (tb_soc or compliance_runner) - - riscv64-unknown-elf-gcc toolchain in PATH - - sw/riscv-arch-test submodule initialized +This script does three things: +1. Generates compliance ELFs through riscv-arch-test. +2. Runs the local compliance runner binary for each generated ELF. +3. Optionally compares produced signatures against discovered references. """ import argparse +import os +import shutil import subprocess import sys -import os from pathlib import Path +from typing import Dict, List, Optional PROJECT_ROOT = Path(__file__).resolve().parent.parent ARCH_TEST_DIR = PROJECT_ROOT / "sw" / "riscv-arch-test" BUILD_DIR = PROJECT_ROOT / "build" SUITES = { - "rv64i": "riscv-test-suite/rv64i_m/I", - "rv64im": "riscv-test-suite/rv64i_m/M", - "rv64ia": "riscv-test-suite/rv64i_m/A", - "rv64if": "riscv-test-suite/rv64i_m/F", - "rv64id": "riscv-test-suite/rv64i_m/D", - "rv64ic": "riscv-test-suite/rv64i_m/C", - "rv64priv": "riscv-test-suite/rv64i_m/privilege", + "rv64i": "I", + "rv64im": "I,M", + "rv64ia": "I,A", + "rv64if": "I,F", + "rv64id": "I,D", + "rv64ic": "I,C", + "rv64priv": "S,Sm", + "rv64g": "I,M,A,F,D,C", + "rv64full": "I,M,A,F,D,C,S,Sm", } def list_suites(): - print("Available test suites:") - for name, path in SUITES.items(): - exists = "✓" if (ARCH_TEST_DIR / path).exists() else "✗ (not found)" - print(f" {name:12s} → {path} {exists}") + print("Available suites and ACT extension filters:") + for name, extensions in SUITES.items(): + print(f" {name:12s} -> EXTENSIONS={extensions}") + + +def run_cmd(cmd: List[str], cwd: Optional[Path] = None, verbose: bool = False) -> subprocess.CompletedProcess: + if verbose: + print("+", " ".join(cmd)) + return subprocess.run(cmd, cwd=cwd, text=True, capture_output=not verbose, check=False) + + +def parse_config_name(config_path: Path) -> str: + with config_path.open("r", encoding="utf-8") as f: + for raw_line in f: + line = raw_line.strip() + if line.startswith("name:"): + return line.split(":", 1)[1].strip() + raise RuntimeError(f"Missing 'name' in config: {config_path}") + + +def discover_elfs(work_dir: Path, config_name: str) -> List[Path]: + elf_root = work_dir / config_name / "elfs" + if not elf_root.exists(): + return [] + return sorted(elf_root.rglob("*.elf")) + + +def find_reference_signature(elf_path: Path, work_dir: Path) -> Optional[Path]: + candidates = [ + elf_path.with_suffix(".signature"), + elf_path.with_suffix(".reference_output"), + elf_path.with_suffix(".ref"), + elf_path.parent / "signatures" / f"{elf_path.stem}.signature", + ] + for candidate in candidates: + if candidate.exists(): + return candidate + + # ACT4 places reference signatures under work//build or work/common/*/build. + # Prefer raw .sig files for byte-for-byte comparison with DUT output. + sig_matches = sorted(work_dir.rglob(f"{elf_path.stem}.sig")) + if sig_matches: + return sig_matches[0] + + # Fallback when only processed result files are available. + result_matches = sorted(work_dir.rglob(f"{elf_path.stem}.results")) + if result_matches: + return result_matches[0] + + return None + + +def compare_text_files(a: Path, b: Path) -> bool: + return a.read_text(encoding="utf-8", errors="ignore") == b.read_text(encoding="utf-8", errors="ignore") + + +def ensure_submodule_ready(arch_test_dir: Path): + if not arch_test_dir.exists(): + raise RuntimeError(f"Missing arch-test directory: {arch_test_dir}") + if not (arch_test_dir / "Makefile").exists(): + raise RuntimeError( + f"Invalid arch-test checkout (Makefile missing): {arch_test_dir}. " + "Run: git submodule update --init --recursive" + ) + + +def ensure_binary(sim_bin: Path, build_dir: Path, verbose: bool): + if sim_bin.exists(): + return sim_bin + print(f"Simulation binary not found: {sim_bin}") + print("Attempting to build compliance_runner target...") + result = run_cmd(["cmake", "--build", str(build_dir), "--target", "compliance_runner"], verbose=verbose) + if result.returncode != 0: + if not verbose: + print(result.stdout) + print(result.stderr) + raise RuntimeError("Failed to build compliance_runner") + + if sim_bin.exists(): + return sim_bin + + matches = [p for p in build_dir.rglob(sim_bin.name) if p.is_file() and os.access(p, os.X_OK)] + if len(matches) == 1: + print(f"Using discovered simulation binary: {matches[0]}") + return matches[0] + if len(matches) > 1: + matches.sort(key=lambda p: len(str(p))) + print(f"Using discovered simulation binary: {matches[0]}") + return matches[0] + raise RuntimeError(f"Build completed but binary still missing: {sim_bin}") -def run_suite(suite_name: str, build_dir: Path, sim_binary: str, verbose: bool): + +def generate_elfs( + arch_test_dir: Path, + config_file: Path, + work_dir: Path, + extensions: str, + jobs: int, + verbose: bool, +): + cmd = [ + "make", + "elfs", + f"CONFIG_FILES={config_file}", + f"WORKDIR={work_dir}", + f"EXTENSIONS={extensions}", + "EXCLUDE_EXTENSIONS=", + f"JOBS={jobs}", + "FAST=True", + ] + result = run_cmd(cmd, cwd=arch_test_dir, verbose=verbose) + if result.returncode != 0: + merged_output = "" + if result.stdout: + merged_output += result.stdout + "\n" + if result.stderr: + merged_output += result.stderr + + missing_tooling_hints = [ + "uv command not found", + "covergroupgen: Permission denied", + "command not found", + ] + if any(hint.lower() in merged_output.lower() for hint in missing_tooling_hints): + raise RuntimeError( + "Failed to generate ELFs because ACT tooling is unavailable in this environment. " + "Install uv and project python deps in sw/riscv-arch-test, or run with --skip-generate " + "to reuse an existing work directory with prebuilt ELFs." + ) + + if not verbose: + print(result.stdout) + print(result.stderr) + raise RuntimeError("Failed to generate compliance ELFs via riscv-arch-test") + + +def run_suite( + suite_name: str, + build_dir: Path, + sim_binary: str, + config_file: Path, + work_dir: Path, + max_cycles: int, + max_tests: int, + jobs: int, + require_reference: bool, + keep_work: bool, + skip_generate: bool, + verbose: bool, +): if suite_name not in SUITES: print(f"Unknown suite: {suite_name}") print(f"Available: {', '.join(SUITES.keys())}") - sys.exit(1) + return False - suite_path = ARCH_TEST_DIR / SUITES[suite_name] - if not suite_path.exists(): - print(f"Suite path not found: {suite_path}") - print("Did you initialize the submodule?") - print(" git submodule update --init --recursive") - sys.exit(1) + ensure_submodule_ready(ARCH_TEST_DIR) - sim_bin = build_dir / sim_binary - if not sim_bin.exists(): - print(f"Simulation binary not found: {sim_bin}") - print("Build it first: cmake --build build --target compliance_runner") - sys.exit(1) + if not config_file.exists(): + raise RuntimeError(f"Config file not found: {config_file}") - test_files = sorted(suite_path.glob("src/*.S")) - if not test_files: - print(f"No test sources found in {suite_path / 'src'}") - sys.exit(1) + config_name = parse_config_name(config_file) + sim_bin = ensure_binary(build_dir / sim_binary, build_dir, verbose) + + if not skip_generate: + if work_dir.exists() and not keep_work: + shutil.rmtree(work_dir) + work_dir.mkdir(parents=True, exist_ok=True) + + extensions = SUITES[suite_name] + if skip_generate: + print(f"Skipping ELF generation for suite={suite_name}, extensions={extensions}") + else: + print(f"Generating ELFs for suite={suite_name}, extensions={extensions}") + generate_elfs(ARCH_TEST_DIR, config_file, work_dir, extensions, jobs, verbose) - print(f"\n{'='*60}") - print(f"Running {suite_name} compliance tests ({len(test_files)} tests)") - print(f"{'='*60}\n") + elf_files = discover_elfs(work_dir, config_name) + if max_tests > 0: + elf_files = elf_files[:max_tests] + + if not elf_files: + print("No generated ELF files found.") + return False + + signature_root = build_dir / "compliance_signatures" / suite_name + signature_root.mkdir(parents=True, exist_ok=True) + + print(f"Running compliance runner on {len(elf_files)} ELF files") passed = 0 failed = 0 - errors = [] + no_reference = 0 - for test_file in test_files: - test_name = test_file.stem - if verbose: - print(f" [{test_name}] ", end="", flush=True) + for idx, elf in enumerate(elf_files, start=1): + rel = elf.relative_to(work_dir / config_name / "elfs") + sig_path = signature_root / rel.with_suffix(".signature") + sig_path.parent.mkdir(parents=True, exist_ok=True) - # TODO: Implement actual test compilation and simulation - # This is a skeleton — fill in with your specific flow: - # 1. Compile test_file with riscv64-unknown-elf-gcc - # 2. Run the sim binary with the compiled ELF - # 3. Compare signature output against reference if verbose: - print("⏭ SKIP (runner not yet implemented)") + print(f"[{idx}/{len(elf_files)}] {rel}") + + run_result = run_cmd( + [ + str(sim_bin), + "--elf", + str(elf), + "--max-cycles", + str(max_cycles), + "--signature", + str(sig_path), + ], + verbose=verbose, + ) + if not sig_path.exists(): + failed += 1 + print(f"FAIL {rel} (signature not generated)") + continue + + if run_result.returncode not in (0, 3): + failed += 1 + if not verbose: + print(f"FAIL {rel} (runner exit={run_result.returncode})") + continue - print(f"\n{'='*60}") - print(f"Results: {passed} passed, {failed} failed, {len(test_files) - passed - failed} skipped") - print(f"{'='*60}\n") + ref_sig = find_reference_signature(elf, work_dir) + if ref_sig is None: + no_reference += 1 + if require_reference: + failed += 1 + print(f"FAIL {rel} (no reference signature found)") + else: + passed += 1 + continue + + if compare_text_files(sig_path, ref_sig): + passed += 1 + else: + failed += 1 + print(f"FAIL {rel} (signature mismatch against {ref_sig})") + + print("=" * 60) + print(f"Suite: {suite_name}") + print(f"Total: {len(elf_files)} Passed: {passed} Failed: {failed} NoRef: {no_reference}") + print(f"Signatures: {signature_root}") + print("=" * 60) return failed == 0 @@ -98,6 +303,36 @@ def main(): parser.add_argument("--suite", type=str, help="Test suite to run") parser.add_argument("--build-dir", type=str, default="build", help="Build directory") parser.add_argument("--sim", type=str, default="compliance_runner", help="Sim binary name") + parser.add_argument( + "--config", + type=str, + default="config/spike/spike-rv64-max/test_config.yaml", + help="Config file path (relative to sw/riscv-arch-test or absolute)", + ) + parser.add_argument( + "--work-dir", + type=str, + default="build/compliance_work", + help="Working directory for generated ELFs", + ) + parser.add_argument("--max-cycles", type=int, default=2000, help="Cycles per test for compliance runner") + parser.add_argument("--max-tests", type=int, default=0, help="Limit number of ELFs to run (0 = all)") + parser.add_argument("--jobs", type=int, default=0, help="Parallel jobs for ACT test generation") + parser.add_argument( + "--require-reference", + action="store_true", + help="Fail tests when reference signature is missing", + ) + parser.add_argument( + "--keep-work", + action="store_true", + help="Keep existing work directory instead of cleaning it first", + ) + parser.add_argument( + "--skip-generate", + action="store_true", + help="Skip ACT generation and reuse existing ELFs under --work-dir", + ) parser.add_argument("--list", action="store_true", help="List available suites") parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output") @@ -111,8 +346,32 @@ def main(): parser.print_help() sys.exit(1) - build_dir = Path(args.build_dir) - success = run_suite(args.suite, build_dir, args.sim, args.verbose) + build_dir = Path(args.build_dir).resolve() + work_dir = Path(args.work_dir).resolve() + + config_file = Path(args.config) + if not config_file.is_absolute(): + config_file = ARCH_TEST_DIR / config_file + + try: + success = run_suite( + suite_name=args.suite, + build_dir=build_dir, + sim_binary=args.sim, + config_file=config_file, + work_dir=work_dir, + max_cycles=args.max_cycles, + max_tests=args.max_tests, + jobs=args.jobs, + require_reference=args.require_reference, + keep_work=args.keep_work, + skip_generate=args.skip_generate, + verbose=args.verbose, + ) + except RuntimeError as e: + print(f"ERROR: {e}") + success = False + sys.exit(0 if success else 1) diff --git a/rv64cpu/tb/CMakeLists.txt b/rv64cpu/tb/CMakeLists.txt index 361a6e7..ebcd9dc 100644 --- a/rv64cpu/tb/CMakeLists.txt +++ b/rv64cpu/tb/CMakeLists.txt @@ -233,5 +233,5 @@ add_test(NAME tb_core_mie_masking COMMAND $ --case mie_mask # ═══════════════════════════════════════════════════════════════════════════════════ # Compliance Tests # ═══════════════════════════════════════════════════════════════════════════════════ -# -# add_subdirectory(compliance) + +add_subdirectory(compliance) diff --git a/rv64cpu/tb/compliance/CMakeLists.txt b/rv64cpu/tb/compliance/CMakeLists.txt index b832f01..0139f54 100644 --- a/rv64cpu/tb/compliance/CMakeLists.txt +++ b/rv64cpu/tb/compliance/CMakeLists.txt @@ -4,18 +4,35 @@ # # ───────────────────────────────────────────────────────────────────────────────── -# The compliance runner needs the SoC sim binary -# Uncomment when both the SoC and compliance_runner are implemented: -# -# add_verilator_target( -# NAME compliance_runner -# TOP_MODULE richard_soc -# SOURCES ${ALL_RTL_SOURCES} -# INCLUDE_DIRS ${RTL_COMMON_INCLUDES} -# TB_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/compliance_runner.cpp -# EXTRA_FLAGS --threads 2 -# ) -# +# Minimal compliance runner target (core-only for bootstrap stage). +# NOTE: This uses richard_core until SoC top and memory/peripheral harness are ready. +add_verilator_target( + NAME compliance_runner + TOP_MODULE richard_core + SOURCES ${RTL_CORE_DIR}/include/rv64_pkg.sv + ${RTL_CORE_DIR}/alu.sv + ${RTL_CORE_DIR}/branch_unit.sv + ${RTL_CORE_DIR}/decoder.sv + ${RTL_CORE_DIR}/regfile.sv + ${RTL_CORE_DIR}/if_stage.sv + ${RTL_CORE_DIR}/if_id_reg.sv + ${RTL_CORE_DIR}/id_stage.sv + ${RTL_CORE_DIR}/id_ex_reg.sv + ${RTL_CORE_DIR}/ex_stage.sv + ${RTL_CORE_DIR}/ex_mem_reg.sv + ${RTL_CORE_DIR}/mem_stage.sv + ${RTL_CORE_DIR}/mem_wb_reg.sv + ${RTL_CORE_DIR}/wb_stage.sv + ${RTL_CORE_DIR}/hazard_unit.sv + ${RTL_CORE_DIR}/include/csr_pkg.sv + ${RTL_CORE_DIR}/csr_unit.sv + ${RTL_CORE_DIR}/trap_ctrl.sv + ${RTL_CORE_DIR}/richard_core.sv + INCLUDE_DIRS ${RTL_COMMON_INCLUDES} + TB_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/compliance_runner.cc + EXTRA_FLAGS -Wno-PINCONNECTEMPTY +) + # ─── Register individual compliance tests via CTest ────────────────────────────── # This is done dynamically by the compliance_runner binary and run_compliance.py. # You can also manually register tests like: diff --git a/rv64cpu/tb/compliance/compliance_runner.cc b/rv64cpu/tb/compliance/compliance_runner.cc new file mode 100644 index 0000000..5cc4709 --- /dev/null +++ b/rv64cpu/tb/compliance/compliance_runner.cc @@ -0,0 +1,408 @@ +// Richard CPU +// Copyright (C) 2026 Richard Qin +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// File path: tb/compliance/compliance_runner.cc +// Minimal compliance runner bootstrap. +// This runner intentionally keeps behavior simple so the toolchain flow can be +// integrated first, then replaced with full ELF/signature handling. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Vrichard_core.h" + +namespace { + +struct Options { + uint64_t max_cycles = 2000; + std::string elf_path; + std::string signature_path; + bool show_help = false; +}; + +struct Symbols { + uint64_t begin_signature = 0; + uint64_t end_signature = 0; + uint64_t tohost = 0; +}; + +class ByteMemory { +public: + void write_byte(uint64_t addr, uint8_t value) { + mem_[addr] = value; + } + + uint8_t read_byte(uint64_t addr) const { + auto it = mem_.find(addr); + if (it == mem_.end()) { + return 0; + } + return it->second; + } + + uint32_t read_u32(uint64_t addr) const { + uint32_t value = 0; + for (int i = 0; i < 4; ++i) { + value |= static_cast(read_byte(addr + i)) << (8 * i); + } + return value; + } + + uint64_t read_u64(uint64_t addr) const { + uint64_t value = 0; + for (int i = 0; i < 8; ++i) { + value |= static_cast(read_byte(addr + i)) << (8 * i); + } + return value; + } + + void write_store(uint64_t addr, uint64_t wdata, uint8_t size_code) { + uint64_t bytes = 0; + switch (size_code) { + case 0b000: + case 0b100: + bytes = 1; + break; + case 0b001: + case 0b101: + bytes = 2; + break; + case 0b010: + case 0b110: + bytes = 4; + break; + case 0b011: + default: + bytes = 8; + break; + } + + const uint64_t offset = addr & 0x7ULL; + const uint64_t aligned = addr & ~0x7ULL; + for (uint64_t i = 0; i < bytes; ++i) { + const uint64_t lane = offset + i; + const uint8_t byte_value = static_cast((wdata >> (lane * 8)) & 0xFFULL); + write_byte(aligned + lane, byte_value); + } + } + +private: + std::map mem_; +}; + +#pragma pack(push, 1) +struct Elf64Ehdr { + unsigned char e_ident[16]; + uint16_t e_type; + uint16_t e_machine; + uint32_t e_version; + uint64_t e_entry; + uint64_t e_phoff; + uint64_t e_shoff; + uint32_t e_flags; + uint16_t e_ehsize; + uint16_t e_phentsize; + uint16_t e_phnum; + uint16_t e_shentsize; + uint16_t e_shnum; + uint16_t e_shstrndx; +}; + +struct Elf64Phdr { + uint32_t p_type; + uint32_t p_flags; + uint64_t p_offset; + uint64_t p_vaddr; + uint64_t p_paddr; + uint64_t p_filesz; + uint64_t p_memsz; + uint64_t p_align; +}; + +struct Elf64Shdr { + uint32_t sh_name; + uint32_t sh_type; + uint64_t sh_flags; + uint64_t sh_addr; + uint64_t sh_offset; + uint64_t sh_size; + uint32_t sh_link; + uint32_t sh_info; + uint64_t sh_addralign; + uint64_t sh_entsize; +}; + +struct Elf64Sym { + uint32_t st_name; + unsigned char st_info; + unsigned char st_other; + uint16_t st_shndx; + uint64_t st_value; + uint64_t st_size; +}; +#pragma pack(pop) + +constexpr uint32_t PT_LOAD = 1; +constexpr uint32_t SHT_SYMTAB = 2; + +Options parse_args(int argc, char** argv) { + Options opts; + for (int i = 1; i < argc; ++i) { + const std::string arg = argv[i]; + if (arg == "--help" || arg == "-h") { + opts.show_help = true; + continue; + } + if (arg == "--max-cycles" && (i + 1) < argc) { + opts.max_cycles = static_cast(std::stoull(argv[++i])); + continue; + } + if (arg == "--elf" && (i + 1) < argc) { + opts.elf_path = argv[++i]; + continue; + } + if (arg == "--signature" && (i + 1) < argc) { + opts.signature_path = argv[++i]; + continue; + } + } + return opts; +} + +void print_help() { + std::cout + << "compliance_runner (bootstrap)\n" + << "Usage:\n" + << " compliance_runner --elf PATH [--max-cycles N] [--signature PATH]\n"; +} + +bool load_elf64(const std::string& path, ByteMemory* memory, Symbols* symbols) { + std::ifstream in(path, std::ios::binary); + if (!in) { + std::cerr << "Failed to open ELF: " << path << "\n"; + return false; + } + + in.seekg(0, std::ios::end); + const std::streamsize size = in.tellg(); + in.seekg(0, std::ios::beg); + if (size <= 0) { + std::cerr << "Empty ELF file: " << path << "\n"; + return false; + } + + std::vector data(static_cast(size)); + in.read(reinterpret_cast(data.data()), size); + if (!in) { + std::cerr << "Failed to read ELF contents: " << path << "\n"; + return false; + } + + if (data.size() < sizeof(Elf64Ehdr)) { + std::cerr << "ELF too small: " << path << "\n"; + return false; + } + + const auto* ehdr = reinterpret_cast(data.data()); + if (!(ehdr->e_ident[0] == 0x7F && ehdr->e_ident[1] == 'E' && ehdr->e_ident[2] == 'L' && ehdr->e_ident[3] == 'F')) { + std::cerr << "Invalid ELF magic: " << path << "\n"; + return false; + } + if (ehdr->e_ident[4] != 2 || ehdr->e_ident[5] != 1) { + std::cerr << "Only ELF64 little-endian is supported: " << path << "\n"; + return false; + } + + const uint64_t ph_end = ehdr->e_phoff + static_cast(ehdr->e_phnum) * ehdr->e_phentsize; + if (ph_end > data.size()) { + std::cerr << "Program header table out of range\n"; + return false; + } + + for (uint16_t i = 0; i < ehdr->e_phnum; ++i) { + const uint64_t off = ehdr->e_phoff + static_cast(i) * ehdr->e_phentsize; + const auto* phdr = reinterpret_cast(data.data() + off); + if (phdr->p_type != PT_LOAD) { + continue; + } + if (phdr->p_offset + phdr->p_filesz > data.size()) { + std::cerr << "LOAD segment out of range\n"; + return false; + } + + const uint64_t base = (phdr->p_paddr != 0) ? phdr->p_paddr : phdr->p_vaddr; + for (uint64_t j = 0; j < phdr->p_filesz; ++j) { + memory->write_byte(base + j, data[phdr->p_offset + j]); + } + for (uint64_t j = phdr->p_filesz; j < phdr->p_memsz; ++j) { + memory->write_byte(base + j, 0); + } + } + + const uint64_t sh_end = ehdr->e_shoff + static_cast(ehdr->e_shnum) * ehdr->e_shentsize; + if (ehdr->e_shoff == 0 || ehdr->e_shnum == 0 || sh_end > data.size()) { + return true; + } + + for (uint16_t i = 0; i < ehdr->e_shnum; ++i) { + const uint64_t off = ehdr->e_shoff + static_cast(i) * ehdr->e_shentsize; + const auto* shdr = reinterpret_cast(data.data() + off); + if (shdr->sh_type != SHT_SYMTAB || shdr->sh_entsize == 0) { + continue; + } + if (shdr->sh_offset + shdr->sh_size > data.size() || shdr->sh_link >= ehdr->e_shnum) { + continue; + } + + const uint64_t str_off = ehdr->e_shoff + static_cast(shdr->sh_link) * ehdr->e_shentsize; + const auto* str_shdr = reinterpret_cast(data.data() + str_off); + if (str_shdr->sh_offset + str_shdr->sh_size > data.size()) { + continue; + } + + const char* strtab = reinterpret_cast(data.data() + str_shdr->sh_offset); + const uint64_t sym_count = shdr->sh_size / shdr->sh_entsize; + for (uint64_t s = 0; s < sym_count; ++s) { + const uint64_t sym_off = shdr->sh_offset + s * shdr->sh_entsize; + const auto* sym = reinterpret_cast(data.data() + sym_off); + if (sym->st_name >= str_shdr->sh_size) { + continue; + } + const std::string name(strtab + sym->st_name); + if (name == "begin_signature") { + symbols->begin_signature = sym->st_value; + } else if (name == "end_signature") { + symbols->end_signature = sym->st_value; + } else if (name == "tohost") { + symbols->tohost = sym->st_value; + } + } + } + + return true; +} + +void tick(Vrichard_core* top, ByteMemory* memory) { + top->imem_rdata = memory->read_u32(top->imem_addr); + top->dmem_rdata = top->dmem_read_en ? memory->read_u64(top->dmem_addr & ~0x7ULL) : 0; + top->ext_timer_int = 0; + top->ext_software_int = 0; + top->ext_external_int = 0; + + top->clk = 0; + top->eval(); + top->clk = 1; + top->eval(); + + if (top->dmem_write_en) { + memory->write_store(top->dmem_addr, top->dmem_wdata, top->dmem_size); + } +} + +void dump_signature(const ByteMemory& memory, const Symbols& symbols, const std::string& signature_path) { + if (signature_path.empty()) { + return; + } + + std::ofstream sig(signature_path); + if (!sig) { + std::cerr << "Failed to open signature file for writing: " << signature_path << "\n"; + return; + } + + if (symbols.begin_signature == 0 || symbols.end_signature <= symbols.begin_signature) { + sig << "00000000\n"; + return; + } + + const uint64_t bytes = symbols.end_signature - symbols.begin_signature; + for (uint64_t off = 0; off + 8 <= bytes; off += 8) { + const uint64_t value = memory.read_u64(symbols.begin_signature + off); + sig << std::hex << std::setfill('0') << std::setw(16) << value << "\n"; + } +} + +} // namespace + +int main(int argc, char** argv) { + Verilated::commandArgs(argc, argv); + const Options opts = parse_args(argc, argv); + + if (opts.show_help) { + print_help(); + return 0; + } + if (opts.elf_path.empty()) { + // Keep compatibility with CTest smoke invocation that runs the binary + // without arguments to validate toolchain/linking only. + std::cout << "compliance_runner: no --elf provided, smoke mode pass\n"; + return 0; + } + + auto* top = new Vrichard_core; + ByteMemory memory; + Symbols symbols; + + if (!load_elf64(opts.elf_path, &memory, &symbols)) { + delete top; + return 2; + } + + top->rst = 1; + for (int i = 0; i < 5; ++i) { + tick(top, &memory); + } + top->rst = 0; + + bool saw_tohost = false; + bool test_pass = false; + for (uint64_t i = 0; i < opts.max_cycles; ++i) { + tick(top, &memory); + if (top->dmem_write_en && symbols.tohost != 0) { + const uint64_t addr = top->dmem_addr; + const bool overlaps_tohost = (addr <= symbols.tohost + 3ULL) && (addr + 7ULL >= symbols.tohost); + if (overlaps_tohost) { + const uint32_t status = static_cast(memory.read_u64(symbols.tohost) & 0xFFFFFFFFULL); + if (status == 1U) { + saw_tohost = true; + test_pass = true; + break; + } + if (status == 3U) { + saw_tohost = true; + test_pass = false; + break; + } + } + } + } + + dump_signature(memory, symbols, opts.signature_path); + + delete top; + if (!saw_tohost) { + return 3; + } + return test_pass ? 0 : 1; +} -- cgit v1.2.3