diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..1d4ea4a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +on: [push, pull_request] +name: build +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Build the application + uses: actions-rs/cargo@v1 + with: + command: build + + - name: Run tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --workspace + + - name: Lint the code + uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Check if code formatted + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1de5659 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a2d5c35 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,118 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "do-core" +version = "0.1.0" + +[[package]] +name = "do-core1" +version = "0.1.0" +dependencies = [ + "clap", + "do-core", +] + +[[package]] +name = "hermit-abi" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +dependencies = [ + "libc", +] + +[[package]] +name = "libc" +version = "0.2.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..24c9be2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "do-core1" +version = "0.1.0" +authors = ["Samuel Ortiz "] +edition = "2018" + +[dependencies] +clap = "2.33.3" +do-core = { path = "do-core" } + +[workspace] +members = [ + "do-core" +] diff --git a/README.md b/README.md index b5d22e9..43e7133 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,53 @@ # do-core1 -DO processor + +The `do-core1` is a simple processor architecture, mostly for educational purposes. + +It aims at being a support for system programming and computer architecture fundamentals courses. + +## Instruction Set Architecture + +The do-core1 [Instruction Set Architecture (ISA)](https://en.wikipedia.org/wiki/Instruction_set) is a simple [Reduced Instruction Set Computer (RISC)](https://en.wikipedia.org/wiki/Reduced_instruction_set_computer) +processor architecture, with a very limited memory model and instruction and register set. + +### Registers + +`do-core1` exposes **8 general purpose registers**: `R0`, `R1`, `R2`, `R3`, `R4`, `R5`, `R6`, `R7`. + +It also uses one Instruction Pointer (`RIP`) register and a operation flags (`RFLAGS`) register. + +All `do-core1` registers are **16 bits wide**. + +### Memory Model + +`do-core1` can address up to **4KiB (4096 bytes) of physical memory**. + +### Instruction Set + +`do-core1` is a [RISC](https://en.wikipedia.org/wiki/Reduced_instruction_set_computer) architecture and executes fixed-length +instructions of 16 bits. + +The `do-core1` is a 2-operand architecture, i.e. its instruction takes at most 2 operands. +`do-core1` operands are register indexes. + +A `do-core1` instruction can be split into an operation code (opcode), the first operand (op0) +and the second operand (op1). The opcode is 8 bits long, and both operands are 4 bits long: + +``` +do-core instruction (16 bits) + +Bits |15 8|7 4|3 0| + ------------------------------------------------------------- + | Opcode (bits 15-8) | op0 (bits 7-4) | op1 (bits 3-0) | + ------------------------------------------------------------- +``` + +The `do-core1` is a [load-store](https://en.wikipedia.org/wiki/Load%E2%80%93store_architecture) +architecture and supports the following instructions: + + +| Opcode | Instruction | Description | +|--------|--------------|----------------------------------------------------------------------| +| `0x00` | `LD Rn, Rm` | Load the value at the memory address contained in `Rm` into `Rn` | +| `0x01` | `ST Rn, Rm` | Store the value from `Rn` into the memory address contained in `Rm` | +| `0x02` | `ADD Rn, Rm` | Add the value contained in `Rm` into `Rn` (`Rn = Rn + Rm`) | +| `0x03` | `XOR Rn, Rm` | Perform a bitwise exclusive OR between `Rn` and `Rm`(`Rn = Rn ^ Rm`) | diff --git a/do-core/Cargo.toml b/do-core/Cargo.toml new file mode 100644 index 0000000..7c6caf1 --- /dev/null +++ b/do-core/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "do-core" +version = "0.1.0" +authors = ["Samuel Ortiz "] +edition = "2018" + +[dependencies] diff --git a/do-core/src/core.rs b/do-core/src/core.rs new file mode 100644 index 0000000..c1a1dd5 --- /dev/null +++ b/do-core/src/core.rs @@ -0,0 +1,140 @@ +use crate::instruction::{Instruction, OpCode}; +use crate::memory::Memory; +use crate::{Error, MAX_REGISTER_INDEX, MEMORY_SIZE}; + +pub struct Core { + registers: [u16; MAX_REGISTER_INDEX as usize + 1], + memory: Memory, +} + +impl Core { + pub fn new() -> Self { + let mut core = Core { + registers: [0u16; MAX_REGISTER_INDEX as usize + 1], + memory: Memory::new(MEMORY_SIZE), + }; + + // Arbitrary initial registers value. + // Registers will eventually be initialized through memory loads. + for (index, register) in core.registers.iter_mut().enumerate() { + *register = index as u16 * 0x10; + } + + core + } + + pub fn register(&self, index: u8) -> Result { + if index > MAX_REGISTER_INDEX { + return Err(Error::Op0OutOfRange); + } + + Ok(self.registers[index as usize]) + } + + pub fn dump(&self, preamble: &str) { + println!("do-core1: {}:", preamble); + for (index, register) in self.registers.iter().enumerate() { + println!("\tR{}: {:#x?}", index, *register); + } + } + + pub fn decode(&mut self, insn: u16) -> Result { + Instruction::disassemble(insn) + } + + pub fn execute(&mut self, insn: Instruction) -> Result<(), Error> { + let opcode = insn.opcode(); + + match opcode { + OpCode::ADD => self.add(insn)?, + OpCode::XOR => self.xor(insn)?, + OpCode::LD => self.load(insn)?, + OpCode::ST => self.store(insn)?, + } + + Ok(()) + } + + fn add(&mut self, insn: Instruction) -> Result<(), Error> { + let op0 = insn.op0() as usize; + let op1 = insn.op1() as usize; + + self.registers[op0] = + self.registers[op0] + .checked_add(self.registers[op1]) + .ok_or(Error::AdditionOverflow( + self.registers[op0], + self.registers[op1], + ))?; + + Ok(()) + } + + fn xor(&mut self, insn: Instruction) -> Result<(), Error> { + let op0 = insn.op0() as usize; + let op1 = insn.op1() as usize; + + self.registers[op0] ^= self.registers[op1]; + + Ok(()) + } + + fn load(&mut self, insn: Instruction) -> Result<(), Error> { + let op0 = insn.op0() as usize; + let op1 = insn.op1() as usize; + + self.registers[op0] = self.memory.load(self.registers[op1])?.into(); + + Ok(()) + } + + fn store(&mut self, insn: Instruction) -> Result<(), Error> { + let op0 = insn.op0() as usize; + let op1 = insn.op1() as usize; + + self.memory + .store(self.registers[op1], self.registers[op0] as u8) + } +} + +#[cfg(test)] +mod tests { + use crate::core::Core; + use crate::Error; + + #[test] + fn test_core_add_r4_r5() -> Result<(), Error> { + let insn = 0x245; + let mut cpu = Core::new(); + + let r4 = cpu.register(4)?; + let r5 = cpu.register(5)?; + + let decoded_insn = cpu.decode(insn)?; + cpu.execute(decoded_insn)?; + + let new_r4 = cpu.register(4)?; + + assert_eq!(new_r4, r4 + r5); + + Ok(()) + } + + #[test] + fn test_core_xor_r1_r7() -> Result<(), Error> { + let insn = 0x317; + let mut cpu = Core::new(); + + let r1 = cpu.register(1)?; + let r7 = cpu.register(7)?; + + let decoded_insn = cpu.decode(insn)?; + cpu.execute(decoded_insn)?; + + let new_r1 = cpu.register(1)?; + + assert_eq!(new_r1, r1 ^ r7); + + Ok(()) + } +} diff --git a/do-core/src/instruction.rs b/do-core/src/instruction.rs new file mode 100644 index 0000000..dee6aab --- /dev/null +++ b/do-core/src/instruction.rs @@ -0,0 +1,167 @@ +use crate::{Error, MAX_REGISTER_INDEX}; + +#[allow(dead_code)] +#[derive(Clone, Debug, PartialEq)] +pub enum OpCode { + LD = 0x00, + ST = 0x01, + ADD = 0x02, + XOR = 0x03, +} + +impl OpCode { + pub fn from_u8(opcode: u8) -> Result { + match opcode { + 0x00 => Ok(OpCode::LD), + 0x01 => Ok(OpCode::ST), + 0x02 => Ok(OpCode::ADD), + 0x03 => Ok(OpCode::XOR), + _ => Err(Error::InvalidOpCode(opcode)), + } + } +} + +impl std::str::FromStr for OpCode { + type Err = Error; + fn from_str(s: &str) -> Result { + match s { + "LD" => Ok(OpCode::LD), + "ST" => Ok(OpCode::ST), + "ADD" => Ok(OpCode::ADD), + "XOR" => Ok(OpCode::XOR), + _ => Err(Error::ParseOpError), + } + } +} + +#[derive(Debug)] +pub struct Instruction { + opcode: OpCode, + op0: u8, + op1: u8, +} + +impl Instruction { + // Instruction constructor, a.k.a. disassembler. + pub fn disassemble(insn: u16) -> Result { + let opcode = OpCode::from_u8((insn >> 8) as u8)?; + let op0 = ((insn & 0xf0) >> 4) as u8; + let op1: u8 = (insn & 0xf) as u8; + + if op0 > MAX_REGISTER_INDEX { + return Err(Error::Op0OutOfRange); + } + + if op1 > MAX_REGISTER_INDEX { + return Err(Error::Op1OutOfRange); + } + + Ok(Instruction { opcode, op0, op1 }) + } + + pub fn opcode(&self) -> OpCode { + self.opcode.clone() + } + + pub fn op0(&self) -> u8 { + self.op0 + } + + pub fn op1(&self) -> u8 { + self.op1 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Error; + + use std::str::FromStr; + + #[test] + fn test_instruction_disassemble_add_r0_r1() -> Result<(), Error> { + let insn_bytes: u16 = 0x201; + let insn = Instruction::disassemble(insn_bytes)?; + + assert_eq!(insn.opcode, OpCode::ADD); + assert_eq!(insn.op0, 0); + assert_eq!(insn.op1, 1); + + Ok(()) + } + + #[test] + fn test_instruction_disassemble_add_r9_r1() -> Result<(), Error> { + let insn_bytes: u16 = 0x291; + assert!(Instruction::disassemble(insn_bytes).is_err()); + + Ok(()) + } + + #[test] + fn test_instruction_disassemble_add_r0_r10() -> Result<(), Error> { + let insn_bytes: u16 = 0x20a; + assert!(Instruction::disassemble(insn_bytes).is_err()); + + Ok(()) + } + + #[test] + fn test_instruction_disassemble_add_r7_r2() -> Result<(), Error> { + let insn_bytes: u16 = 0x272; + let insn = Instruction::disassemble(insn_bytes)?; + + assert_eq!(insn.opcode, OpCode::ADD); + assert_eq!(insn.op0, 7); + assert_eq!(insn.op1, 2); + + Ok(()) + } + + #[test] + fn test_instruction_disassemble_ld_r0_r1() -> Result<(), Error> { + let insn_bytes: u16 = 0x01; + let insn = Instruction::disassemble(insn_bytes)?; + + assert_eq!(insn.opcode, OpCode::LD); + assert_eq!(insn.op0, 0); + assert_eq!(insn.op1, 1); + + Ok(()) + } + + #[test] + fn test_instruction_disassemble_xor_r2_r3() -> Result<(), Error> { + let insn_bytes: u16 = 0x323; + let insn = Instruction::disassemble(insn_bytes)?; + + assert_eq!(insn.opcode, OpCode::XOR); + assert_eq!(insn.op0, 2); + assert_eq!(insn.op1, 3); + + Ok(()) + } + + #[test] + fn test_instruction_disassemble_st_r5_r0() -> Result<(), Error> { + let insn_bytes: u16 = 0x150; + let insn = Instruction::disassemble(insn_bytes)?; + + assert_eq!(insn.opcode, OpCode::ST); + assert_eq!(insn.op0, 5); + assert_eq!(insn.op1, 0); + + Ok(()) + } + + #[test] + fn test_opcode_from_string() -> Result<(), Error> { + assert_eq!(OpCode::from_str("ADD").unwrap(), OpCode::ADD); + assert_eq!(OpCode::from_str("LD").unwrap(), OpCode::LD); + + assert!(OpCode::from_str("GIBBERISH").is_err()); + + Ok(()) + } +} diff --git a/do-core/src/lib.rs b/do-core/src/lib.rs new file mode 100644 index 0000000..b0665d7 --- /dev/null +++ b/do-core/src/lib.rs @@ -0,0 +1,22 @@ +use crate::instruction::OpCode; + +#[derive(Debug)] +pub enum Error { + InvalidOpCode(u8), + UnsupportedOpCode(OpCode), + Op0OutOfRange, + Op1OutOfRange, + AdditionOverflow(u16, u16), + MemoryOverflow(u16), + ParseOpError, +} + +// do-core register indexes range from 0 to 7. +pub const MAX_REGISTER_INDEX: u8 = 7; + +// do-core only support 4K of memory +pub const MEMORY_SIZE: usize = 0x1000; + +pub mod core; +pub mod instruction; +pub mod memory; diff --git a/do-core/src/memory.rs b/do-core/src/memory.rs new file mode 100644 index 0000000..6acbc35 --- /dev/null +++ b/do-core/src/memory.rs @@ -0,0 +1,58 @@ +use crate::Error; + +pub struct Memory { + size: usize, + memory: Vec, +} + +impl Memory { + pub fn new(size: usize) -> Self { + Memory { + size, + memory: vec![0; size], + } + } + + pub fn load(&self, address: u16) -> Result { + if address > self.size as u16 { + return Err(Error::MemoryOverflow(address)); + } + + Ok(self.memory[address as usize]) + } + + pub fn store(&mut self, address: u16, value: u8) -> Result<(), Error> { + if address > self.size as u16 { + return Err(Error::MemoryOverflow(address)); + } + + self.memory[address as usize] = value; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::memory::Memory; + use crate::Error; + + #[test] + fn test_memory_store_load() -> Result<(), Error> { + let mut memory = Memory::new(4096); + + memory.store(0x100, 0xf)?; + assert_eq!(memory.load(0x100)?, 0xf); + + Ok(()) + } + + #[test] + fn test_memory_overflow() -> Result<(), Error> { + let mut memory = Memory::new(4096); + + assert!(memory.store(0x2000, 0xf).is_err()); + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..28145dd --- /dev/null +++ b/src/main.rs @@ -0,0 +1,32 @@ +extern crate clap; + +use clap::{App, Arg}; +use do_core::core::Core; +use do_core::Error; + +fn main() -> Result<(), Error> { + let mut cpu = Core::new(); + let arguments = App::new("do-core1") + .about("do-core1 emulator") + .arg( + Arg::with_name("instruction") + .long("instruction") + .help("do-core1 instruction to execute") + .takes_value(true), + ) + .get_matches(); + + let insn_string = arguments + .value_of("instruction") + .expect("Missing --instruction argument") + .trim_start_matches("0x"); + + cpu.dump("Initial CPU state"); + + let insn = cpu.decode(u16::from_str_radix(insn_string, 16).unwrap())?; + cpu.execute(insn)?; + + cpu.dump("Final CPU state"); + + Ok(()) +}