Skip to content
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

Merged
merged 15 commits into from
Nov 2, 2023
Merged
88 changes: 88 additions & 0 deletions scripts/run_signature.py
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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe os.path.join()?

"-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()
40 changes: 40 additions & 0 deletions test/external/rvtest_env/link.ld
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 = .;
}
64 changes: 64 additions & 0 deletions test/external/rvtest_env/model_test.h
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
28 changes: 28 additions & 0 deletions test/regression/cocotb/signature.Makefile
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
25 changes: 25 additions & 0 deletions test/regression/cocotb/signature_entrypoint.py
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)
63 changes: 39 additions & 24 deletions test/regression/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a clumsily named align_to_power_of_two function in coreblocks.utils.utils, which rounds up, you could use that. Rounding down can be calculated by masking; but maybe this should get an utility function as well? The code you've written is fine, but it's not very readable.

Copy link
Member

Choose a reason for hiding this comment

The 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] = []

Expand All @@ -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
63 changes: 63 additions & 0 deletions test/regression/signature.py
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")