Skip to content

Commit

Permalink
Feat: expose base_address and instructions decoded from ELF (#487)
Browse files Browse the repository at this point in the history
This PR aims to extract more information from ELF file. The program
table circuit needs the whole instructions to build the fixed program
table.

Meanwhile, we also distinguish two values of program counter: 
- `pc_base` (was former called `pc_start` incorrectly): It's address of
the first opcode in the program. This is equal to
`Platform::rom_start()`.
- `entrypoint`(this should be `pc_start`): It's the address of `.start`
label in the assembly source of guest program and address of the first
opcode to be executed.
  • Loading branch information
kunxian-xia authored Oct 29, 2024
1 parent 70fbefc commit 045338d
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 85 deletions.
5 changes: 3 additions & 2 deletions ceno_emul/src/addr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use std::{fmt, ops};

pub const WORD_SIZE: usize = 4;
pub const PC_WORD_SIZE: usize = 4;
pub const PC_STEP_SIZE: usize = 4;

// Type aliases to clarify the code without wrapper types.
Expand All @@ -26,10 +27,10 @@ pub type Addr = u32;
pub type Cycle = u64;
pub type RegIdx = usize;

#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ByteAddr(pub u32);

#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct WordAddr(u32);

impl From<ByteAddr> for WordAddr {
Expand Down
68 changes: 64 additions & 4 deletions ceno_emul/src/elf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,47 @@ use alloc::collections::BTreeMap;

use crate::addr::WORD_SIZE;
use anyhow::{Context, Result, anyhow, bail};
use elf::{ElfBytes, endian::LittleEndian, file::Class};
use elf::{
ElfBytes,
abi::{PF_R, PF_W, PF_X},
endian::LittleEndian,
file::Class,
};

/// A RISC Zero program
#[derive(Clone, Debug)]
pub struct Program {
/// The entrypoint of the program
pub entry: u32,

/// This is the lowest address of the program's executable code
pub base_address: u32,
/// The instructions of the program
pub instructions: Vec<u32>,
/// The initial memory image
pub image: BTreeMap<u32, u32>,
}

impl Program {
/// Create program
pub fn new(
entry: u32,
base_address: u32,
instructions: Vec<u32>,
image: BTreeMap<u32, u32>,
) -> Program {
Self {
entry,
base_address,
instructions,
image,
}
}
/// Initialize a RISC Zero Program from an appropriate ELF file
pub fn load_elf(input: &[u8], max_mem: u32) -> Result<Program> {
let mut instructions: Vec<u32> = Vec::new();
let mut image: BTreeMap<u32, u32> = BTreeMap::new();
let mut base_address = None;

let elf = ElfBytes::<LittleEndian>::minimal_parse(input)
.map_err(|err| anyhow!("Elf parse error: {err}"))?;
if elf.ehdr.class != Class::ELF32 {
Expand All @@ -58,7 +84,18 @@ impl Program {
if segments.len() > 256 {
bail!("Too many program headers");
}
for segment in segments.iter().filter(|x| x.p_type == elf::abi::PT_LOAD) {
for (idx, segment) in segments
.iter()
.filter(|x| x.p_type == elf::abi::PT_LOAD)
.enumerate()
{
tracing::debug!(
"loadable segement {}: PF_R={}, PF_W={}, PF_X={}",
idx,
segment.p_flags & PF_R != 0,
segment.p_flags & PF_W != 0,
segment.p_flags & PF_X != 0,
);
let file_size: u32 = segment
.p_filesz
.try_into()
Expand All @@ -77,6 +114,13 @@ impl Program {
.p_vaddr
.try_into()
.map_err(|err| anyhow!("vaddr is larger than 32 bits. {err}"))?;
if (segment.p_flags & PF_X) != 0 {
if base_address.is_none() {
base_address = Some(vaddr);
} else {
return Err(anyhow!("only support one executable segment"));
}
}
if vaddr % WORD_SIZE as u32 != 0 {
bail!("vaddr {vaddr:08x} is unaligned");
}
Expand Down Expand Up @@ -104,9 +148,25 @@ impl Program {
word |= (*byte as u32) << (j * 8);
}
image.insert(addr, word);
if (segment.p_flags & PF_X) != 0 {
instructions.push(word);
}
}
}
}
Ok(Program { entry, image })

if base_address.is_none() {
return Err(anyhow!("does not have executable segment"));
}
let base_address = base_address.unwrap();
assert!(entry >= base_address);
assert!((entry - base_address) as usize <= instructions.len() * WORD_SIZE);

Ok(Program {
entry,
base_address,
image,
instructions,
})
}
}
30 changes: 20 additions & 10 deletions ceno_emul/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,48 @@ use crate::addr::{Addr, RegIdx};
/// - the layout of virtual memory,
/// - special addresses, such as the initial PC,
/// - codes of environment calls.
pub struct Platform;
#[derive(Copy, Clone)]
pub struct Platform {
pub rom_start: Addr,
pub rom_end: Addr,
pub ram_start: Addr,
pub ram_end: Addr,
}

pub const CENO_PLATFORM: Platform = Platform;
pub const CENO_PLATFORM: Platform = Platform {
rom_start: 0x2000_0000,
rom_end: 0x3000_0000 - 1,
ram_start: 0x8000_0000,
ram_end: 0xFFFF_FFFF,
};

impl Platform {
// Virtual memory layout.

pub const fn rom_start(&self) -> Addr {
0x2000_0000
self.rom_start
}

pub const fn rom_end(&self) -> Addr {
0x3000_0000 - 1
self.rom_end
}

pub fn is_rom(&self, addr: Addr) -> bool {
(self.rom_start()..=self.rom_end()).contains(&addr)
}

pub const fn ram_start(&self) -> Addr {
let ram_start = 0x8000_0000;
if cfg!(feature = "forbid_overflow") {
// -1<<11 == 0x800 is the smallest negative 'immediate'
// offset we can have in memory instructions.
// So if we stay away from it, we are safe.
assert!(ram_start >= 0x800);
assert!(self.ram_start >= 0x800);
}
ram_start
self.ram_start
}

pub const fn ram_end(&self) -> Addr {
0xFFFF_FFFF
self.ram_end
- if cfg!(feature = "forbid_overflow") {
// (1<<11) - 1 == 0x7ff is the largest positive 'immediate'
// offset we can have in memory instructions.
Expand Down Expand Up @@ -69,7 +79,7 @@ impl Platform {

// Startup.

pub const fn pc_start(&self) -> Addr {
pub const fn pc_base(&self) -> Addr {
self.rom_start()
}

Expand Down Expand Up @@ -122,7 +132,7 @@ mod tests {
#[test]
fn test_no_overlap() {
let p = CENO_PLATFORM;
assert!(p.can_execute(p.pc_start()));
assert!(p.can_execute(p.pc_base()));
// ROM and RAM do not overlap.
assert!(!p.is_rom(p.ram_start()));
assert!(!p.is_rom(p.ram_end()));
Expand Down
2 changes: 2 additions & 0 deletions ceno_emul/src/rv32im.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@ impl Emulator {
let decoded = DecodedInstruction::new(word);
let insn = self.table.lookup(&decoded);
ctx.on_insn_decoded(&decoded);
tracing::trace!("pc: {:x}, kind: {:?}", pc.0, insn.kind);

if match insn.category {
InsnCategory::Compute => self.step_compute(ctx, insn.kind, &decoded)?,
Expand Down Expand Up @@ -775,6 +776,7 @@ impl Emulator {
let addr = ByteAddr(rs1.wrapping_add(decoded.imm_s()));
let shift = 8 * (addr.0 & 3);
if !ctx.check_data_store(addr) {
tracing::error!("mstore: addr={:x?},rs1={:x}", addr, rs1);
return ctx.trap(TrapCause::StoreAccessFault);
}
let mut data = ctx.peek_memory(addr.waddr());
Expand Down
42 changes: 29 additions & 13 deletions ceno_emul/src/vm_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ use crate::{
tracer::{Change, StepRecord, Tracer},
};
use anyhow::{Result, anyhow};
use std::iter::from_fn;
use std::{iter::from_fn, ops::Deref, sync::Arc};

/// An implementation of the machine state and of the side-effects of operations.
pub struct VMState {
program: Arc<Program>,
platform: Platform,
pc: Word,
/// Map a word-address (addr/4) to a word.
Expand All @@ -24,28 +25,39 @@ pub struct VMState {
}

impl VMState {
pub fn new(platform: Platform) -> Self {
let pc = platform.pc_start();
Self {
platform,
pub fn new(platform: Platform, program: Program) -> Self {
let pc = program.entry;
let program = Arc::new(program);

let mut vm = Self {
pc,
platform,
program: program.clone(),
memory: HashMap::new(),
registers: [0; 32],
halted: false,
tracer: Tracer::new(),
};

// init memory from program.image
for (&addr, &value) in program.image.iter() {
vm.init_memory(ByteAddr(addr).waddr(), value);
}

vm
}

pub fn new_from_elf(platform: Platform, elf: &[u8]) -> Result<Self> {
let mut state = Self::new(platform);
let program = Program::load_elf(elf, u32::MAX).unwrap();
for (addr, word) in program.image.iter() {
let addr = ByteAddr(*addr).waddr();
state.init_memory(addr, *word);
}
if program.entry != state.platform.pc_start() {
return Err(anyhow!("Invalid entrypoint {:x}", program.entry));
let state = Self::new(platform, program);

if state.program.base_address != state.platform.rom_start() {
return Err(anyhow!(
"Invalid base_address {:x}",
state.program.base_address
));
}

Ok(state)
}

Expand All @@ -57,7 +69,11 @@ impl VMState {
&self.tracer
}

/// Set a word in memory without side-effects.
pub fn program(&self) -> &Program {
self.program.deref()
}

/// Set a word in memory without side effects.
pub fn init_memory(&mut self, addr: WordAddr, value: Word) {
self.memory.insert(addr, value);
}
Expand Down
35 changes: 26 additions & 9 deletions ceno_emul/tests/test_vm_trace.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
#![allow(clippy::unusual_byte_groupings)]
use anyhow::Result;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};

use ceno_emul::{
ByteAddr, CENO_PLATFORM, Cycle, EmuContext, InsnKind, StepRecord, Tracer, VMState, WordAddr,
CENO_PLATFORM, Cycle, EmuContext, InsnKind, Program, StepRecord, Tracer, VMState, WORD_SIZE,
WordAddr,
};

#[test]
fn test_vm_trace() -> Result<()> {
let mut ctx = VMState::new(CENO_PLATFORM);

let pc_start = ByteAddr(CENO_PLATFORM.pc_start()).waddr();
for (i, &inst) in PROGRAM_FIBONACCI_20.iter().enumerate() {
ctx.init_memory(pc_start + i as u32, inst);
}
let program = Program::new(
CENO_PLATFORM.pc_base(),
CENO_PLATFORM.pc_base(),
PROGRAM_FIBONACCI_20.to_vec(),
PROGRAM_FIBONACCI_20
.iter()
.enumerate()
.map(|(insn_idx, &insn)| {
(
CENO_PLATFORM.pc_base() + (WORD_SIZE * insn_idx) as u32,
insn,
)
})
.collect(),
);
let mut ctx = VMState::new(CENO_PLATFORM, program);

let steps = run(&mut ctx)?;

Expand All @@ -35,7 +46,13 @@ fn test_vm_trace() -> Result<()> {

#[test]
fn test_empty_program() -> Result<()> {
let mut ctx = VMState::new(CENO_PLATFORM);
let empty_program = Program::new(
CENO_PLATFORM.pc_base(),
CENO_PLATFORM.pc_base(),
vec![],
BTreeMap::new(),
);
let mut ctx = VMState::new(CENO_PLATFORM, empty_program);
let res = run(&mut ctx);
assert!(matches!(res, Err(e) if e.to_string().contains("IllegalInstruction(0)")));
Ok(())
Expand Down
Loading

0 comments on commit 045338d

Please sign in to comment.