From 492330b7e8b744400fd042830aa860a23708e3f8 Mon Sep 17 00:00:00 2001 From: fractasy <89866610+fractasy@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:44:40 +0100 Subject: [PATCH] Feature/documentation3 (#183) * More documentation --- book/developer/doc.md | 2 +- core/src/zisk_definitions.rs | 1 + emulator/src/emulator.rs | 106 ++++++++++++----- emulator/src/lib.rs | 18 ++- emulator/src/stats.rs | 131 +++++++++++++++++----- riscv/src/riscv_inst.rs | 2 +- ziskos/entrypoint/src/syscalls/keccakf.rs | 8 ++ ziskos/entrypoint/src/syscalls/mod.rs | 2 +- 8 files changed, 209 insertions(+), 61 deletions(-) diff --git a/book/developer/doc.md b/book/developer/doc.md index 92b7d33f..40eff95e 100644 --- a/book/developer/doc.md +++ b/book/developer/doc.md @@ -37,4 +37,4 @@ Some basic hints: `//! ...` * Documentation for a public element must be placed right before it, starting with `/// ...` * Wrap code with triple spike: `//! \`\`\`` -* To avoid cargo doc to compile the code, use ignore after the priple spike: `//! \`\`\` ignore` \ No newline at end of file +* To avoid cargo doc to compile the code, use `text` after the priple spike: `//! \`\`\`text` \ No newline at end of file diff --git a/core/src/zisk_definitions.rs b/core/src/zisk_definitions.rs index 4c197e31..98aafbeb 100644 --- a/core/src/zisk_definitions.rs +++ b/core/src/zisk_definitions.rs @@ -35,4 +35,5 @@ pub const P2_30: u64 = 0x40000000; pub const P2_31: u64 = 0x80000000; /// Constant values used in operation functions and state machine executors +pub const M3: u64 = 0x7; pub const M64: u64 = 0xFFFFFFFFFFFFFFFF; diff --git a/emulator/src/emulator.rs b/emulator/src/emulator.rs index aa952c1f..36d3bceb 100644 --- a/emulator/src/emulator.rs +++ b/emulator/src/emulator.rs @@ -1,3 +1,20 @@ +//! ZiskEmulator +//! +//! ```text +//! ziskemu.main() +//! \ +//! emulate() +//! \ +//! process_directory() -> lists *dut*.elf files +//! \ +//! process_elf_file() +//! \ +//! - Riscv2zisk::run() +//! - process_rom() +//! \ +//! Emu::run() +//! ``` + use crate::{ Emu, EmuOptions, EmuStartingPoints, EmuTrace, EmuTraceStart, ErrWrongArguments, ParEmuOptions, ZiskEmulatorErr, @@ -26,22 +43,9 @@ use rayon::prelude::*; pub struct ZiskEmulator; -/* -ziskemu.main() -\ - emulate() - \ - process_directory() - \ - process_elf_file() - \ - Riscv2zisk::run() - process_rom() - \ - Emu::run() -*/ - impl ZiskEmulator { + /// Lists all device-under-test riscof files in a directory (*dut*.elf) and calls + /// process_elf_file with each of them fn process_directory( directory: String, inputs: &[u8], @@ -51,8 +55,12 @@ impl ZiskEmulator { println!("process_directory() directory={}", directory); } + // List all files in the directory let files = Self::list_files(&directory).unwrap(); + + // For every file for file in files { + // If file follows the riscof dut file name convention, then call process_elf_file() if file.contains("dut") && file.ends_with(".elf") { Self::process_elf_file(file, inputs, options, None::>)?; } @@ -61,6 +69,7 @@ impl ZiskEmulator { Ok(Vec::new()) } + /// Processes an RISC-V ELF file fn process_elf_file( elf_filename: String, inputs: &[u8], @@ -71,16 +80,18 @@ impl ZiskEmulator { println!("process_elf_file() elf_file={}", elf_filename); } - // Convert the ELF file to ZisK ROM - // Create an instance of the RISCV -> ZisK program converter + // Create an instance of the RISC-V -> ZisK program transpiler (Riscv2zisk) with the ELF + // file name let riscv2zisk = Riscv2zisk::new(elf_filename, String::new(), String::new(), String::new()); - // Convert program to rom + // Convert the ELF file to ZisK ROM calling the transpiler run() method let zisk_rom = riscv2zisk.run().map_err(|err| ZiskEmulatorErr::Unknown(err.to_string()))?; + // Process the Zisk rom with the provided inputs, according to the configured options Self::process_rom(&zisk_rom, inputs, options, callback) } + // To be implemented fn process_rom_file( rom_filename: String, inputs: &[u8], @@ -96,6 +107,7 @@ impl ZiskEmulator { Self::process_rom(&rom, inputs, options, callback) } + /// Processes a Zisk rom with the provided inputs, according to the configured options pub fn process_rom( rom: &ZiskRom, inputs: &[u8], @@ -106,17 +118,22 @@ impl ZiskEmulator { println!("process_rom() rom size={} inputs size={}", rom.insts.len(), inputs.len()); } - // Create a emulator instance with this rom and inputs + // Create a emulator instance with the Zisk rom let mut emu = Emu::new(rom); + + // Get the current time, to be used to calculate the metrics let start = Instant::now(); - // Run the emulation + // Run the emulation, using the input and the options emu.run(inputs.to_owned(), options, callback); + // Check that the emulation completed, either successfully or not, but it must reach the end + // of the program if !emu.terminated() { return Err(ZiskEmulatorErr::EmulationNoCompleted); } + // Store the duration of the emulation process as a difference vs. the start time let duration = start.elapsed(); // Log performance metrics @@ -160,17 +177,21 @@ impl ZiskEmulator { Ok(output) } + /// Process a Zisk rom with the provided input data, according to the configured options, in + /// order to generate a histogram of the program counters used during the emulation. pub fn process_rom_pc_histogram( rom: &ZiskRom, inputs: &[u8], options: &EmuOptions, ) -> Result { - // Create a emulator instance with this rom and inputs + // Create a emulator instance with the rom let mut emu = Emu::new(rom); - // Run the emulation + // Run the emulation and get the pc histogram let pc_histogram = emu.run_pc_histogram(inputs.to_owned(), options); + // Check that the emulation completed, either successfully or not, but it must reach the end + // of the program if !emu.terminated() { return Err(ZiskEmulatorErr::EmulationNoCompleted); } @@ -240,6 +261,8 @@ impl ZiskEmulator { Ok((vec_traces, emu_slices)) } + /// Process a Zisk rom with the provided input data, according to the configured options, in + /// order to generate a set of required operation data. #[inline] pub fn process_slice_required( rom: &ZiskRom, @@ -248,20 +271,28 @@ impl ZiskEmulator { emu_trace_start: &EmuTraceStart, num_rows: usize, ) -> Vec { - // Create a emulator instance with this rom + // Create a emulator instance with the rom let mut emu = Emu::new(rom); + // Run the emulation emu.run_slice_required::(vec_traces, op_type, emu_trace_start, num_rows) } + /// Finds all files in a directory and returns a vector with their full paths fn list_files(directory: &str) -> std::io::Result> { + // Define an internal function to call it recursively fn _list_files(vec: &mut Vec, path: &Path) -> std::io::Result<()> { + // Only search if the path is a directory if path.is_dir() { + // List all contained paths for entry in fs::read_dir(path)? { let entry = entry?; let full_path = entry.path(); + + // If it is a directory, call list files recursively if full_path.is_dir() { _list_files(vec, &full_path)?; + // If it is a file, add it to the vector } else { vec.push(full_path); } @@ -270,13 +301,19 @@ impl ZiskEmulator { Ok(()) } + // Define an empty vector let mut paths = Vec::new(); + + // Call the internal function _list_files(&mut paths, Path::new(directory))?; + + // Return the paths Ok(paths.into_iter().map(|p| p.display().to_string()).collect()) } } impl Emulator for ZiskEmulator { + /// Implement the emulate method of the Emulator trait for ZiskEmulator fn emulate( &self, options: &EmuOptions, @@ -287,7 +324,7 @@ impl Emulator for ZiskEmulator { println!("emulate()\n{}", options); } - // Check options are valid + // Check options if options.rom.is_some() && options.elf.is_some() { return Err(ZiskEmulatorErr::WrongArguments(ErrWrongArguments::new( "ROM file and ELF file are incompatible; use only one of them", @@ -298,9 +335,8 @@ impl Emulator for ZiskEmulator { ))); } - // INPUTs: - // build inputs data either from the provided inputs path, or leave it empty (default - // inputs) + // Build an input data buffer either from the provided inputs path (if provided), or leave + // it empty let mut inputs = Vec::new(); if options.inputs.is_some() { // Read inputs data from the provided inputs path @@ -308,9 +344,12 @@ impl Emulator for ZiskEmulator { inputs = fs::read(path).expect("Could not read inputs file"); } + // If a rom file path is provided, load the rom from it if options.rom.is_some() { + // Get the rom file name let rom_filename = options.rom.clone().unwrap(); + // Check the file exists and it is not a directory let metadata = fs::metadata(&rom_filename).map_err(|_| { ZiskEmulatorErr::WrongArguments(ErrWrongArguments::new("ROM file does not exist")) })?; @@ -320,16 +359,25 @@ impl Emulator for ZiskEmulator { ))); } + // Call process_rom_file() Self::process_rom_file(rom_filename, &inputs, options, callback) - } else { + } + // Process the ELF file + else { + // Get the ELF file name let elf_filename = options.elf.clone().unwrap(); + // Get the file metadata let metadata = fs::metadata(&elf_filename).map_err(|_| { ZiskEmulatorErr::WrongArguments(ErrWrongArguments::new("ELF file does not exist")) })?; + + // If it is a directory, call process_directory() if metadata.is_dir() { Self::process_directory(elf_filename, &inputs, options) - } else { + } + // If it is a file, call process_elf_file() + else { Self::process_elf_file(elf_filename, &inputs, options, callback) } } diff --git a/emulator/src/lib.rs b/emulator/src/lib.rs index d450af0b..61e55e87 100644 --- a/emulator/src/lib.rs +++ b/emulator/src/lib.rs @@ -1,3 +1,17 @@ +//! The Zisk emulator executes the Zisk program rom with the provided input data and generates +//! the corresponding output data, according to the configured options. +//! +//! ```text +//! ELF file --> riscv2zisk --> ZiskRom \ +//! | +//! ZiskRom ------------------> ZiskInst's | +//! \--> RO data > Emu --> Output data, statistics, metrics, logs... +//! \ | +//! Input file ---------------> Mem | +//! | +//! User configuration -------> EmuOptions / +//! ``` + mod emu; mod emu_context; mod emu_full_trace; @@ -6,9 +20,9 @@ mod emu_par_options; mod emu_segment; pub mod emu_slice; pub mod emu_trace; -mod emulator; +pub mod emulator; mod emulator_errors; -mod stats; +pub mod stats; pub use emu::*; pub use emu_context::*; diff --git a/emulator/src/stats.rs b/emulator/src/stats.rs index 9a8072e6..c6a16fee 100644 --- a/emulator/src/stats.rs +++ b/emulator/src/stats.rs @@ -1,4 +1,11 @@ -use zisk_core::{zisk_ops::ZiskOp, ZiskInst, REG_FIRST, REG_LAST}; +//! Emulator execution statistics +//! +//! Statistics include: +//! * Memory read/write counters (aligned and not aligned) +//! * Registers read/write counters (total and per register) +//! * Operations counters (total and per opcode) + +use zisk_core::{zisk_ops::ZiskOp, ZiskInst, M3, REG_FIRST, REG_LAST}; const AREA_PER_SEC: f64 = 1000000_f64; const COST_MEM: f64 = 10_f64 / AREA_PER_SEC; @@ -9,35 +16,56 @@ const COST_MEMA_W2: f64 = 80_f64 / AREA_PER_SEC; const COST_USUAL: f64 = 8_f64 / AREA_PER_SEC; const COST_STEP: f64 = 50_f64 / AREA_PER_SEC; +/// Keeps counters for every type of memory operation (including registers). +/// +/// Since RISC-V registers are mapped to memory, memory operations include register access +/// operations. #[derive(Default, Debug, Clone)] -struct MemoryOperations { - mread_a: u64, // Aligned +pub struct MemoryOperations { + /// Counter of reads from aligned memory addresses + mread_a: u64, + /// Counter of writes to aligned memory addresses mwrite_a: u64, - mread_na1: u64, // Not aligned + /// Counter of reads from non-aligned memory addresses (1) + mread_na1: u64, + /// Counter of writes to non-aligned memory addresses (1) mwrite_na1: u64, - mread_na2: u64, // Not aligned + /// Counter of reads from non-aligned memory addresses (2) + mread_na2: u64, + /// Counter of writes to non-aligned memory addresses (2) mwrite_na2: u64, } +/// Keeps counter for register read and write operations #[derive(Default, Debug, Clone)] -struct RegistryOperations { +pub struct RegistryOperations { + /// Counter of reads from registers reads: u64, + /// Counter of writes to registers writes: u64, } +/// Keeps statistics of the emulator operations #[derive(Debug, Clone)] pub struct Stats { + /// Counters of memory read/write operations, both aligned and non-aligned mops: MemoryOperations, + /// Counters of register read/write operations rops: RegistryOperations, + /// Counter of usual operations usual: u64, + /// Counter of steps steps: u64, + /// Counters of operations, one per possible u8 opcode (many remain unused) ops: [u64; 256], + /// Counters or register writes, split per register (32) reg_writes: [u64; 32], + /// Counters or register reads, split per register (32) reg_reads: [u64; 32], } -/// Default constructor for Stats structure impl Default for Stats { + /// Default constructor for Stats structure. Sets all counters to zero. fn default() -> Self { Self { mops: MemoryOperations::default(), @@ -52,59 +80,101 @@ impl Default for Stats { } impl Stats { + /// Called every time some data is read from memory, is statistics are enabled pub fn on_memory_read(&mut self, address: u64, width: u64) { - if (address % 8) != 0 { - if ((address + width) / 8) < (address / 8) { + // If the memory is alligned to 8 bytes, i.e. last 3 bits are zero, then increase the + // aligned memory read counter + if ((address & M3) == 0) && (width == 8) { + self.mops.mread_a += 1; + } else { + // If the memory read operation requires reading 2 aligned chunks of 8 bytes to build + // the requested width, i.e. the requested slice crosses an 8-bytes boundary, then + // increase the non-aligned counter number 2 + if ((address + width - 1) >> 3) > (address >> 3) { self.mops.mread_na2 += 1; - } else { + } + // Otherwise increase the non-aligned counter number 1 + else { self.mops.mread_na1 += 1; } - } else { - self.mops.mread_a += 1; } + + // If the address is within the range of register addresses, increase register counters if (REG_FIRST..=REG_LAST).contains(&address) { + // Increase total register reads counter self.rops.reads += 1; + + // Increase the specific reads counter for this register self.reg_reads[((address - REG_FIRST) / 8) as usize] += 1; } } + /// Called every time some data is writen to memory, is statistics are enabled pub fn on_memory_write(&mut self, address: u64, width: u64) { - if (address % 8) != 0 { - if ((address + width) / 8) < (address / 8) { + // If the memory is alligned to 8 bytes, i.e. last 3 bits are zero, then increase the + // aligned memory read counter + if ((address & M3) == 0) && (width == 8) { + self.mops.mwrite_a += 1; + } else { + // If the memory write operation requires writing 2 aligned chunks of 8 bytes to build + // the requested width, i.e. the requested slice crosses an 8-bytes boundary, then + // increase the non-aligned counter number 2 + if ((address + width - 1) >> 3) > (address >> 3) { self.mops.mwrite_na2 += 1; - } else { + } + // Otherwise increase the non-aligned counter number 1 + else { self.mops.mwrite_na1 += 1; } - } else { - self.mops.mwrite_a += 1; } + + // If the address is within the range of register addresses, increase register counters if (REG_FIRST..=REG_LAST).contains(&address) { + // Increase total register writes counter self.rops.writes += 1; + + // Increase the specific writes counter for this register self.reg_writes[((address - REG_FIRST) / 8) as usize] += 1; } } + /// Called at every step with the current number of executed steps, if statistics are enabled pub fn on_steps(&mut self, steps: u64) { + // Store the number of executed steps self.steps = steps; } + /// Called every time an operation is executed, if statistics are enabled pub fn on_op(&mut self, instruction: &ZiskInst, a: u64, b: u64) { + // If the operation is a usual operation, then increase the usual counter if self.is_usual(instruction, a, b) { self.usual += 1; - } else { + } + // Otherwise, increase the counter corresponding to this opcode + else { self.ops[instruction.op as usize] += 1; } } + /// Returns true if the provided operation is a usual operation fn is_usual(&self, instruction: &ZiskInst, a: u64, b: u64) -> bool { - (instruction.op != 0xF1) && instruction.is_external_op && (a < 256) && (b < 256) + // ecall/system call functions are not candidates to be usual + (instruction.op != 0xF1) && + // Internal functions are not candidates to be usual + instruction.is_external_op && + // If both a and b parameters have low values (they fit into a byte) then the operation can + // be efficiently proven using lookup tables + (a < 256) && (b < 256) } + /// Returns a string containing a human-readable text showing all caunters pub fn report(&self) -> String { const AREA_PER_SEC: f64 = 1000000_f64; - // Result of his function + + // The result of his function is accumulated in this string let mut output = String::new(); + // First, log the cost constants output += "Cost definitions:\n"; output += &format!(" AREA_PER_SEC: {} steps\n", AREA_PER_SEC); output += &format!(" COST_MEMA_R1: {:02} sec\n", COST_MEMA_R1); @@ -114,6 +184,7 @@ impl Stats { output += &format!(" COST_USUAL: {:02} sec\n", COST_USUAL); output += &format!(" COST_STEP: {:02} sec\n", COST_STEP); + // Calculate some aggregated counters to be used in the logs let total_mem_ops = self.mops.mread_na1 + self.mops.mread_na2 + self.mops.mread_a + @@ -131,38 +202,43 @@ impl Stats { self.mops.mwrite_na1 as f64 * COST_MEMA_W1 + self.mops.mwrite_na2 as f64 * COST_MEMA_W2; + // Declare some total counters for the opcodes let mut total_opcodes: u64 = 0; let mut opcode_steps: [u64; 256] = [0; 256]; let mut total_opcode_steps: u64 = 0; let mut opcode_cost: [f64; 256] = [0_f64; 256]; let mut total_opcode_cost: f64 = 0_f64; + + // For every possible opcode value for opcode in 0..256 { - // Skip zero counters + // Skip opcode counters that are zero if self.ops[opcode] == 0 { continue; } - // Increase total + // Increase total opcodes counter total_opcodes += self.ops[opcode]; - // Get the Zisk instruction corresponding to this opcode + // Get the Zisk instruction corresponding to this opcode; if the counter has been + // increased, then the opcode must be a valid one let inst = ZiskOp::try_from_code(opcode as u8).unwrap(); - // Increase steps + // Increase steps, both per opcode and total opcode_steps[opcode] += inst.steps(); total_opcode_steps += inst.steps(); - // Increse cost + // Increse cost, both per opcode and total let value = self.ops[opcode] as f64; opcode_cost[opcode] += value * inst.steps() as f64 / AREA_PER_SEC; total_opcode_cost += value * inst.steps() as f64 / AREA_PER_SEC; } + // Calculate some costs let cost_usual = self.usual as f64 * COST_USUAL; let cost_main = self.steps as f64 * COST_STEP; - let total_cost = cost_main + cost_mem + cost_mem_align + total_opcode_cost + cost_usual; + // Build the memory usage counters and cost values output += &format!("\nTotal Cost: {:.2} sec\n", total_cost); output += &format!(" Main Cost: {:.2} sec {} steps\n", cost_main, self.steps); output += &format!(" Mem Cost: {:.2} sec {} steps\n", cost_mem, total_mem_ops); @@ -206,6 +282,7 @@ impl Stats { reg_total_percentage ); + // Build the registers usage counters and cost values for reg in 0..32 { let reads = self.reg_reads[reg]; let writes = self.reg_writes[reg]; @@ -222,8 +299,8 @@ impl Stats { ); } + // Build the operations usage counters and cost values output += "\nOpcodes:\n"; - for opcode in 0..256 { // Skip zero counters if self.ops[opcode] == 0 { diff --git a/riscv/src/riscv_inst.rs b/riscv/src/riscv_inst.rs index 2216b29d..485ceb36 100644 --- a/riscv/src/riscv_inst.rs +++ b/riscv/src/riscv_inst.rs @@ -11,7 +11,7 @@ //! * J-type: unconditional jumps, a variation of U-type //! //! RV32I instruction formats showing immediate variants: -//! ```ignore +//! ```text //! 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00 //! | funct7 | rs2 | rs1 | funct3 | rd | opcode | R-type //! | imm[11:0] | rs1 | funct3 | rd | opcode | I-type diff --git a/ziskos/entrypoint/src/syscalls/keccakf.rs b/ziskos/entrypoint/src/syscalls/keccakf.rs index 06d57504..fb399966 100644 --- a/ziskos/entrypoint/src/syscalls/keccakf.rs +++ b/ziskos/entrypoint/src/syscalls/keccakf.rs @@ -1,8 +1,16 @@ +//! Keccak system call interception + #[cfg(target_os = "ziskos")] use core::arch::asm; /// Executes the Keccak256 permutation on the given state. /// +/// The keccak system call writes the KECCAKF constant to the A7 register, the address of the +/// input/output memory buffer to the A0 register, and a 0 to the A1 register. +/// The Zisk +/// The Keccak-f code will get the input state data (1600 bits = 200 bytes) from that address, hash +/// the bits, and write the output state data (same size) to the same address as a result. +/// /// ### Safety /// /// The caller must ensure that `state` is valid pointer to data that is aligned along a four diff --git a/ziskos/entrypoint/src/syscalls/mod.rs b/ziskos/entrypoint/src/syscalls/mod.rs index d97c0786..0329166a 100644 --- a/ziskos/entrypoint/src/syscalls/mod.rs +++ b/ziskos/entrypoint/src/syscalls/mod.rs @@ -1,3 +1,3 @@ -mod keccakf; +pub mod keccakf; /// Executes `KECCAK_PERMUTE`. pub const KECCAKF: u32 = 0x00_01_01_01;