-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Script to generate signature from test ELFs #466
Changes from 12 commits
c5c4985
8041a96
6c63e7a
da912ca
c4acb21
dd1b762
11a9391
8901051
fdac4fd
202512b
14aa970
2dd30d5
23cf60c
be074c4
3d24f88
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import asyncio | ||
import argparse | ||
import sys | ||
import os | ||
import subprocess | ||
from typing import Literal | ||
|
||
if __name__ == "__main__": | ||
parent = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||
sys.path.insert(0, parent) | ||
|
||
import test.regression.signature # noqa: E402 | ||
from test.regression.pysim import PySimulation # noqa: E402 | ||
|
||
|
||
def run_with_cocotb(test_name: str, traces: bool, output: str) -> bool: | ||
arglist = [ | ||
"make", | ||
"-C", | ||
parent + "/" if parent else "" + "test/regression/cocotb", | ||
"-f", | ||
"signature.Makefile", | ||
"--no-print-directory", | ||
] | ||
|
||
if os.path.isfile(output): | ||
os.remove(output) | ||
|
||
arglist += [f"TESTNAME={test_name}"] | ||
arglist += [f"OUTPUT={output}"] | ||
|
||
if traces: | ||
arglist += ["TRACES=1"] | ||
|
||
subprocess.run(arglist) | ||
|
||
return os.path.isfile(output) # completed successfully if signature file was created | ||
|
||
|
||
def run_with_pysim(test_name: str, traces: bool, verbose: bool, output: str) -> bool: | ||
traces_file = None | ||
if traces: | ||
traces_file = os.path.basename(test_name) | ||
try: | ||
asyncio.run( | ||
test.regression.signature.run_test(PySimulation(verbose, traces_file=traces_file), test_name, output) | ||
) | ||
except RuntimeError as e: | ||
print("RuntimeError:", e) | ||
return False | ||
return True | ||
|
||
|
||
def run_test(test: str, backend: Literal["pysim", "cocotb"], traces: bool, verbose: bool, output: str) -> bool: | ||
if backend == "cocotb": | ||
return run_with_cocotb(test, traces, output) | ||
elif backend == "pysim": | ||
return run_with_pysim(test, traces, verbose, output) | ||
return False | ||
|
||
|
||
def main(): | ||
parser = argparse.ArgumentParser() | ||
parser.add_argument("-t", "--trace", action="store_true", help="Dump waveforms") | ||
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output") | ||
parser.add_argument("-b", "--backend", default="pysim", choices=["cocotb", "pysim"], help="Simulation backend") | ||
parser.add_argument("-o", "--output", default=None, help="Selects output file to write test signature to") | ||
parser.add_argument("path") | ||
|
||
args = parser.parse_args() | ||
|
||
output = args.output if args.output else args.path + ".signature" | ||
|
||
success = run_test(args.path, args.backend, args.trace, args.verbose, output) | ||
if not success: | ||
print(f"{args.path}: Program execution failed") | ||
|
||
if output is not None: # create empty file on failure for checker scripts | ||
with open(output, "w"): | ||
pass | ||
|
||
sys.exit(1) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
OUTPUT_ARCH( "riscv" ) | ||
ENTRY(rvtest_entry_point) | ||
|
||
MEMORY | ||
{ | ||
text (rxai!w) : ORIGIN = 0x00000000, LENGTH = 2M | ||
data (wxa!ri) : ORIGIN = 0x10000000, LENGTH = 1M | ||
mmio (wa!rxi) : ORIGIN = 0x80000000, LENGTH = 1K | ||
signature (wa!rxi) : ORIGIN = 0x81000000, LENGTH = 16K | ||
|
||
} | ||
|
||
PHDRS | ||
{ | ||
text PT_LOAD; | ||
data_init PT_LOAD; | ||
data PT_NULL; | ||
mmio PT_LOAD; | ||
signature PT_LOAD; | ||
} | ||
|
||
SECTIONS | ||
{ | ||
.text.init : { *(.text.init) } >text AT>text :text | ||
. = ALIGN(0x1000); | ||
.text : { *(.text) } >text AT>text :text | ||
|
||
. = ALIGN(0x1000); | ||
.data : { *(.data) } >data AT>data :data_init | ||
.data.string : { *(.data.string)} >data AT>data :data_init | ||
.bss : { *(.bss) } >data AT>data :data | ||
|
||
. = ALIGN(0x1000); | ||
.hostmmio : { *(.hostmmio) } >mmio AT>mmio :mmio | ||
|
||
. = ALIGN(0x1000); | ||
.signature : { *(.signature) } >signature AT>signature :signature | ||
|
||
_end = .; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
#ifndef _COMPLIANCE_MODEL_H | ||
#define _COMPLIANCE_MODEL_H | ||
#define RVMODEL_DATA_SECTION \ | ||
.pushsection .hostmmio,"aw",@progbits; \ | ||
.align 8; .global tohost; tohost: .dword 0; \ | ||
.align 8; .global fromhost; fromhost: .dword 0; \ | ||
.popsection; \ | ||
.align 8; .global begin_regstate; begin_regstate: \ | ||
.word 128; \ | ||
.align 8; .global end_regstate; end_regstate: \ | ||
.word 4; | ||
|
||
//RV_COMPLIANCE_HALT | ||
#define RVMODEL_HALT \ | ||
li x1, 1; \ | ||
write_tohost: \ | ||
sw x1, tohost, t5; \ | ||
j write_tohost; | ||
|
||
#define RVMODEL_BOOT | ||
|
||
//RV_COMPLIANCE_DATA_BEGIN | ||
#define RVMODEL_DATA_BEGIN \ | ||
RVMODEL_DATA_SECTION \ | ||
.pushsection .signature,"aw",@progbits; \ | ||
.align 2; \ | ||
.global begin_signature; begin_signature: | ||
|
||
//RV_COMPLIANCE_DATA_END | ||
#define RVMODEL_DATA_END \ | ||
.global end_signature; end_signature: \ | ||
.popsection; | ||
|
||
//RVTEST_IO_INIT | ||
#define RVMODEL_IO_INIT | ||
//RVTEST_IO_WRITE_STR | ||
#define RVMODEL_IO_WRITE_STR(_R, _STR) | ||
//RVTEST_IO_CHECK | ||
#define RVMODEL_IO_CHECK() | ||
//RVTEST_IO_ASSERT_GPR_EQ | ||
#define RVMODEL_IO_ASSERT_GPR_EQ(_S, _R, _I) | ||
//RVTEST_IO_ASSERT_SFPR_EQ | ||
#define RVMODEL_IO_ASSERT_SFPR_EQ(_F, _R, _I) | ||
//RVTEST_IO_ASSERT_DFPR_EQ | ||
#define RVMODEL_IO_ASSERT_DFPR_EQ(_D, _R, _I) | ||
|
||
// empty macros to supress warnings | ||
#define RVMODEL_SET_MSW_INT | ||
#define RVMODEL_CLEAR_MSW_INT | ||
#define RVMODEL_CLEAR_MTIMER_INT | ||
#define RVMODEL_CLEAR_MEXT_INT | ||
#define RVMODEL_CLR_MSW_INT | ||
#define RVMODEL_CLR_MTIMER_INT | ||
#define RVMODEL_CLR_MEXT_INT | ||
#define RVMODEL_SET_SSW_INT | ||
#define RVMODEL_CLR_SSW_INT | ||
#define RVMODEL_CLR_STIMER_INT | ||
#define RVMODEL_CLR_SEXT_INT | ||
#define RVMODEL_SET_VSW_INT | ||
#define RVMODEL_CLR_VSW_INT | ||
#define RVMODEL_CLR_VTIMER_INT | ||
#define RVMODEL_CLR_VEXT_INT | ||
|
||
#endif // _COMPLIANCE_MODEL_H |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# Makefile | ||
|
||
# defaults | ||
SIM ?= verilator | ||
TOPLEVEL_LANG ?= verilog | ||
|
||
VERILOG_SOURCES += $(PWD)/../../../core.v | ||
# use VHDL_SOURCES for VHDL files | ||
|
||
# TOPLEVEL is the name of the toplevel module in your Verilog or VHDL file | ||
TOPLEVEL = top | ||
|
||
# MODULE is the basename of the Python test file | ||
MODULE = signature_entrypoint | ||
|
||
SIM_BUILD = build/signature | ||
|
||
# Yosys/Amaranth borkedness workaround | ||
ifeq ($(SIM),verilator) | ||
EXTRA_ARGS += -Wno-CASEINCOMPLETE -Wno-CASEOVERLAP -Wno-WIDTHEXPAND -Wno-WIDTHTRUNC | ||
endif | ||
|
||
ifeq ($(TRACES),1) | ||
EXTRA_ARGS += --trace-fst --trace-structs | ||
endif | ||
|
||
# include cocotb's make rules to take care of the simulator setup | ||
include $(shell cocotb-config --makefiles)/Makefile.sim |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import os | ||
import sys | ||
import cocotb | ||
from pathlib import Path | ||
|
||
top_dir = Path(__file__).parent.parent.parent.parent | ||
sys.path.insert(0, str(top_dir)) | ||
|
||
from test.regression.cocotb import CocotbSimulation # noqa: E402 | ||
from test.regression.signature import run_test # noqa: E402 | ||
|
||
|
||
@cocotb.test() | ||
async def do_test(dut): | ||
cocotb.logging.getLogger().setLevel(cocotb.logging.INFO) | ||
|
||
test_name = os.environ["TESTNAME"] | ||
if test_name is None: | ||
raise RuntimeError("No ELF file provided") | ||
|
||
output = os.environ["OUTPUT"] | ||
if output is None: | ||
output = test_name + ".signature" | ||
|
||
await run_test(CocotbSimulation(dut), test_name, output) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,8 @@ | |
from typing import Optional, TypeVar | ||
from dataclasses import dataclass, replace | ||
from elftools.elf.constants import P_FLAGS | ||
from elftools.elf.elffile import ELFFile | ||
from elftools.elf.elffile import ELFFile, Segment | ||
from coreblocks.params.configurations import CoreConfiguration | ||
|
||
all = [ | ||
"ReplyStatus", | ||
|
@@ -137,6 +138,42 @@ def write(self, req: WriteRequest) -> WriteReply: | |
return WriteReply(status=ReplyStatus.ERROR) | ||
|
||
|
||
def load_segment(segment: Segment, *, disable_write_protection: bool = False) -> RandomAccessMemory: | ||
paddr = segment.header["p_paddr"] | ||
memsz = segment.header["p_memsz"] | ||
flags_raw = segment.header["p_flags"] | ||
|
||
seg_start = paddr | ||
seg_end = paddr + memsz | ||
|
||
data = segment.data() | ||
|
||
flags = SegmentFlags(0) | ||
if flags_raw & P_FLAGS.PF_R: | ||
flags |= SegmentFlags.READ | ||
if flags_raw & P_FLAGS.PF_W or disable_write_protection: | ||
flags |= SegmentFlags.WRITE | ||
if flags_raw & P_FLAGS.PF_X: | ||
flags |= SegmentFlags.EXECUTABLE | ||
|
||
if flags_raw & P_FLAGS.PF_X: | ||
# align only instruction section to full icache lines | ||
alignment = 2 ** CoreConfiguration().icache_block_size_bits | ||
|
||
def align_down(n: int) -> int: | ||
return (n // alignment) * alignment | ||
|
||
align_front = seg_start - align_down(seg_start) | ||
align_back = align_down(seg_end + alignment - 1) - seg_end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a clumsily named There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, this strategy might fail if multiple segments use the same cache line. As this is unlikely to occur (?), that's a fine workaround for now. |
||
|
||
data = b"\x00" * align_front + data + b"\x00" * align_back | ||
|
||
seg_start -= align_front | ||
seg_end += align_back | ||
|
||
return RandomAccessMemory(range(seg_start, seg_end), flags, data) | ||
|
||
|
||
def load_segments_from_elf(file_path: str, *, disable_write_protection: bool = False) -> list[RandomAccessMemory]: | ||
segments: list[RandomAccessMemory] = [] | ||
|
||
|
@@ -145,28 +182,6 @@ def load_segments_from_elf(file_path: str, *, disable_write_protection: bool = F | |
for segment in elffile.iter_segments(): | ||
if segment.header["p_type"] != "PT_LOAD": | ||
continue | ||
|
||
paddr = segment.header["p_paddr"] | ||
alignment = segment.header["p_align"] | ||
memsz = segment.header["p_memsz"] | ||
flags_raw = segment.header["p_flags"] | ||
|
||
def align_down(n: int) -> int: | ||
return (n // alignment) * alignment | ||
|
||
seg_start = align_down(paddr) | ||
seg_end = align_down(paddr + memsz + alignment - 1) | ||
|
||
data = b"\x00" * (paddr - seg_start) + segment.data() + b"\x00" * (seg_end - (paddr + len(segment.data()))) | ||
|
||
flags = SegmentFlags(0) | ||
if flags_raw & P_FLAGS.PF_R: | ||
flags |= SegmentFlags.READ | ||
if flags_raw & P_FLAGS.PF_W or disable_write_protection: | ||
flags |= SegmentFlags.WRITE | ||
if flags_raw & P_FLAGS.PF_X: | ||
flags |= SegmentFlags.EXECUTABLE | ||
|
||
segments.append(RandomAccessMemory(range(seg_start, seg_end), flags, data)) | ||
segments.append(load_segment(segment, disable_write_protection=disable_write_protection)) | ||
|
||
return segments |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
from .memory import * | ||
from .common import SimulationBackend | ||
|
||
|
||
class ToHostMMIO(MemorySegment): | ||
def __init__(self, addr: range, on_finish: Callable[[], None]): | ||
super().__init__(addr, SegmentFlags.READ | SegmentFlags.WRITE) | ||
self.on_finish = on_finish | ||
|
||
def read(self, _) -> ReadReply: | ||
return ReadReply() | ||
|
||
def write(self, _) -> WriteReply: | ||
self.on_finish() | ||
return WriteReply() | ||
|
||
|
||
def map_mem_segments( | ||
elf_path: str, stop_callback: Callable[[], None] | ||
) -> tuple[list[MemorySegment], RandomAccessMemory]: | ||
mem_segments = [] | ||
signature_ram = RandomAccessMemory(range(0, 0), SegmentFlags.WRITE, bytearray()) | ||
|
||
with open(elf_path, "rb") as f: | ||
elffile = ELFFile(f) | ||
|
||
signature_section = elffile.get_section(elffile.get_section_index(".signature")) | ||
tohost_section = elffile.get_section(elffile.get_section_index(".hostmmio")) | ||
|
||
for segment in elffile.iter_segments(): | ||
# .signature and .tohost sections have direct segment mapping | ||
addr_range = range(segment.header["p_vaddr"], segment.header["p_vaddr"] + segment.header["p_memsz"]) | ||
if segment.section_in_segment(signature_section): | ||
signature_ram = load_segment(segment) | ||
elif segment.section_in_segment(tohost_section): | ||
mem_segments.append(ToHostMMIO(addr_range, stop_callback)) | ||
elif segment.header["p_type"] == "PT_LOAD": | ||
mem_segments.append(load_segment(segment)) | ||
|
||
return (mem_segments, signature_ram) | ||
|
||
|
||
async def run_test(sim_backend: SimulationBackend, test_path: str, signature_path: str): | ||
(mem_segments, signature_ram) = map_mem_segments(test_path, sim_backend.stop) | ||
|
||
mem_segments.append(signature_ram) | ||
mem_model = CoreMemoryModel(mem_segments) | ||
|
||
success = await sim_backend.run(mem_model, timeout_cycles=100000) | ||
|
||
if not success: | ||
raise RuntimeError(f"{test_path}: Simulation timed out") | ||
|
||
print(f"{test_path}: Program execution finished! Signature: {signature_path}") | ||
|
||
# generate signature file in riscv-torture format (used also by riscof) | ||
# 32-bit little endian memory dump from data between .begin_signature and .end_signature | ||
# symbols, mapped to .signature section in our linker script | ||
with open(signature_path, "w") as sig_file: | ||
data = signature_ram.data.zfill(((len(signature_ram.data) + 3) // 4) * 4) | ||
for idx in range(0, len(data), 4): | ||
num = int.from_bytes(data[idx : idx + 4], "little") | ||
sig_file.write(hex(num)[2:].zfill(8) + "\n") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe
os.path.join()
?