Module sim

Source
Expand description

Simulating and execution for LC-3 assembly.

This module is focused on executing fully assembled code (i.e., ObjectFile).

This module consists of:

  • Simulator: The struct that simulates assembled code.
  • mem: The module handling memory relating to the registers.
  • device: The module handling simulator IO, interrupts, and general handling of external devices.
  • debug: The module handling types of breakpoints for the simulator.
  • frame: The module handling the frame stack and call frame management.

§Usage

To simulate some code, you need to instantiate a Simulator and load an object file to it:

use lc3_ensemble::sim::Simulator;
 
let mut simulator = Simulator::new(Default::default());
simulator.load_obj_file(&obj_file);
simulator.run().unwrap();

§Flags

Here, we define simulator to have the default flags. We could also configure the simulator by editing the flags. For example, if we wish to enable real traps, we can edit the flags like so:

let mut simulator = Simulator::new(SimFlags { use_real_traps: true, ..Default::default() });

All of the available flags can be found in SimFlags.

§Execution

Beyond the basic Simulator::run (which runs until halting), there are also:

use lc3_ensemble::parse::parse_ast;
use lc3_ensemble::asm::assemble;
use lc3_ensemble::sim::Simulator;
use lc3_ensemble::ast::Reg::R0;
 
let src = "
    .orig x3000
    AND R0, R0, #0
    ADD R0, R0, #1
    ADD R0, R0, #1
    ADD R0, R0, #1
    HALT
    .end
";
let ast = parse_ast(src).unwrap();
let obj_file = assemble(ast).unwrap();
 
let mut sim = Simulator::new(Default::default());
sim.load_obj_file(&obj_file);
 
// Running step by step:
sim.step_in().unwrap();
assert_eq!(sim.reg_file[R0].get(), 0);
sim.step_in().unwrap();
assert_eq!(sim.reg_file[R0].get(), 1);
sim.step_in().unwrap();
assert_eq!(sim.reg_file[R0].get(), 2);
sim.step_in().unwrap();
assert_eq!(sim.reg_file[R0].get(), 3);

§Querying State

You can query (or set) a variety of different state values from the simulator.

  • If you wish to access the PC, it can simply be done through the sim.pc field.
  • If you wish to access the PSR or MCR, the Simulator::psr and Simulator::mcr methods are present to query those values.
  • If you wish to access the register file, you can access it through the sim.reg_file field.

RegFile holds its values in a Word, which is a memory cell which keeps track of initialization state. Accessing can simply be done with Word::get and Word::set:

use lc3_ensemble::sim::Simulator;
use lc3_ensemble::ast::Reg::R0;
 
let mut sim = Simulator::new(Default::default());
 
sim.reg_file[R0].set(0x1234);
assert_eq!(sim.reg_file[R0].get(), 0x1234);
  • If you wish to access the memory, the simulator provides two pairs of memory access:
    • Direct access to the memory array (via the mem) field, which does not trigger access violations or IO.
    • Simulator::read_mem and Simulator::write_mem, which are used for accesses which do trigger access violations and IO.
use lc3_ensemble::sim::Simulator;
 
let mut sim = Simulator::new(Default::default());
 
// Raw memory access:
sim.mem[0x3000].set(0x5678);
assert_eq!(sim.mem[0x3000].get(), 0x5678);
 
// Through read/write:
use lc3_ensemble::sim::mem::Word;
 
assert!(sim.write_mem(0x0000, Word::new_init(0x9ABC), sim.default_mem_ctx()).is_err());
assert!(sim.write_mem(0x3000, Word::new_init(0x9ABC), sim.default_mem_ctx()).is_ok());
assert!(sim.read_mem(0x0000, sim.default_mem_ctx()).is_err());
assert!(sim.read_mem(0x3000, sim.default_mem_ctx()).is_ok());
  • Other state can be accessed. Consult the Simulator docs for more information.

§Frames

The simulator also keeps track of subroutine frame information, accessible on the frame_stack field of Simulator.

If debug_frames is not enabled in SimFlags, the only information the Simulator keeps track of is the number of subroutine frames deep the simulator is (via FrameStack::len):

  • During a JSR instruction, the frame count increases by 1.
  • During a RET instruction, the frame count decreases by 1.

If debug_frames is enabled in SimFlags, the frame information is significantly extended. The simulator then keeps track of several frame values (such as caller and callee address). These are accessible via the FrameStack::frames method.

Debug frame information by default includes caller and callee addresses, but can be configured to also include frame pointer and argument information. See the frame module for details.

§Debugging with breakpoints

Breakpoints are accessible through the breakpoints field on Simulator.

To add a breakpoint, simply insert a Breakpoint and it will break if its condition is met during all execution functions (except Simulator::step_in).

use lc3_ensemble::parse::parse_ast;
use lc3_ensemble::asm::assemble;
use lc3_ensemble::sim::Simulator;
use lc3_ensemble::sim::debug::Breakpoint;
 
let src = "
    .orig x3000
    ADD R0, R0, #0
    ADD R0, R0, #1
    ADD R0, R0, #2
    ADD R0, R0, #3
    HALT
    .end
";
let ast = parse_ast(src).unwrap();
let obj_file = assemble(ast).unwrap();
 
let mut sim = Simulator::new(Default::default());
sim.load_obj_file(&obj_file);
 
// Without breakpoint
sim.run().unwrap();
assert_eq!(sim.pc, 0x3004);
 
// With breakpoint
sim.reset();
sim.load_obj_file(&obj_file);
sim.breakpoints.insert(Breakpoint::PC(0x3002));
sim.run().unwrap();
assert_eq!(sim.pc, 0x3002);

§IO, interrupts, and external devices

IO and interrupts are handled by “external devices” (the trait ExternalDevice).

These can be added by registering the device in the Simulator’s device handler (DeviceHandler::add_device of the device_handler field).

When a load or store to a memory-mapped address (0xFE00-0xFFFF) occurs, the device handler sends the corresponding load/store to the device for it to handle.

The best IO for programmatic uses is device::BufferedKeyboard and device::BufferedDisplay, which exposes the IO to memory buffers that can be modified.

use lc3_ensemble::parse::parse_ast;
use lc3_ensemble::asm::assemble;
use lc3_ensemble::sim::Simulator;
use lc3_ensemble::sim::device::{BufferedKeyboard, BufferedDisplay};
use std::sync::Arc;
 
let src = "
    .orig x3000
    LOOP:
    GETC
    PUTC
    ADD R0, R0, #0
    BRnp LOOP
    HALT
    .end
";
let ast = parse_ast(src).unwrap();
let obj_file = assemble(ast).unwrap();
 
let mut sim = Simulator::new(Default::default());
sim.load_obj_file(&obj_file);
 
let input = BufferedKeyboard::default();
let output = BufferedDisplay::default();
sim.device_handler.set_keyboard(input.clone());
sim.device_handler.set_display(output.clone());
 
input.get_buffer().write().unwrap().extend(b"Hello, World!\0");
sim.run().unwrap();
 
assert_eq!(&*input.get_buffer().read().unwrap(), b"");
assert_eq!(&**output.get_buffer().read().unwrap(), b"Hello, World!\0");

These external devices also support interrupt-based IO.

If the device::BufferedKeyboard device is enabled, interrupts can be enabled by setting KBSR[14]. Once enabled, the keyboard can interrupt the simulator and run its interrupt service routine.

§Strictness (experimental)

This simulator also features uninitialized memory access checks (via the strict flag).

These strict memory checks verify that unintialized data is not written to the register files, memory, or other areas that do not expect uninitialized data. Uninitialized data here is defined as data that is unknown as it was never fully set and is dependent on the values the machine was initialized with.

The strict flag can currently detect:

  • Loads of uninitialized data into a register (excluding uninitialized reads from mem[R6 + offset]).
  • Stores of uninitialized data into memory (excluding uninitialized stores to mem[R6 + offset] and .blkw’d memory).
  • Stores of uninitialized data into memory-mapped IO
  • Loads and stores through an uninitialized memory address
  • Jumping to an uninitialized address (e.g., via JSRR or JMP)
  • Jumping to a memory location that is uninitialized
  • Decoding an instruction from uninitialized data
  • Setting the PSR to an uninitialized value

Note that this is considered experimental as false positives can still occur. Also note that the exceptions for loads and stores of uninitialized data are present to prevent typical value manipulation on the stack or in stored memory from triggering a strictness error.

use lc3_ensemble::parse::parse_ast;
use lc3_ensemble::asm::assemble;
use lc3_ensemble::sim::{Simulator, SimErr};
 
let src = "
    .orig x3000
    ADD R0, R0, #0
    ADD R0, R0, #15 ;; R0 = 15
    HALT
    .end
";
let ast = parse_ast(src).unwrap();
let obj_file = assemble(ast).unwrap();
 
let mut sim = Simulator::new(Default::default());
sim.load_obj_file(&obj_file);
sim.flags.strict = true;
 
// Strictness check detects `R0` was set without first being cleared.
assert!(matches!(sim.run(), Err(SimErr::StrictRegSetUninit)));
assert_eq!(sim.prefetch_pc(), 0x3000);

Modules§

debug
Utilities to debug simulation.
device
Handlers for external devices connected to the Simulator.
frame
The frame stack and call frame management.
mem
Memory handling for the LC-3 simulator.
observer
Module handles memory access observers, which store which accesses occur at a given memory location.

Structs§

MemAccessCtx
Context behind a memory access.
PSR
A wrapper over u16 in order to faciliate the PSR.
SimFlags
Configuration flags for Simulator.
Simulator
Executes assembled code.

Enums§

InternalRegister
A register which stores internal state in Simulator.
MMapInternalErr
Error in Simulator::mmap_internal.
SimErr
Errors that can occur during simulation.

Type Aliases§

MCR
A type alias for MCR.