Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,24 @@ To run an LC-3 object file:
./lc3vm/build/lc3vm path/to/your_program.obj [path/to/another_program.obj ...]
```

## Disassembling Object Files

The LC-3 VM can also disassemble `.obj` files, showing you the LC-3 assembly instructions corresponding to the machine code in the file. This is useful for inspecting programs or debugging.

**How to use:**

To disassemble one or more object files, use the `-d` or `--disassemble` flag followed by the path(s) to your `.obj` file(s):

```bash
./lc3vm/build/lc3vm -d path/to/your_program.obj
```

Or for multiple files:

```bash
./lc3vm/build/lc3vm --disassemble program1.obj program2.obj
```

## Running Tests

To compile and run the unit tests (from within the `lc3vm` directory):
Expand Down
24 changes: 18 additions & 6 deletions lc3vm/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,40 @@ BUILD_DIR = build
TEST_DIR = tests
GTEST_FLAGS = -lgtest -lgtest_main -pthread

VM_TEST_SRCS = src/ls3.cpp src/memory.cpp
VM_SRCS = src/ls3.cpp src/memory.cpp src/terminal_input.cpp
VM_TEST_SRCS = $(VM_SRCS)

TEST_MAIN_SRC = src/main.cpp
TEST_MAIN_OBJ = $(BUILD_DIR)/test_main.o
TEST_FILES = tests/test_initialization.cpp tests/test_opcode_execution.cpp tests/test_disassembly.cpp
TEST_OBJS = $(TEST_FILES:tests/%.cpp=$(BUILD_DIR)/tests/%.o)

.PHONY: all clean test docs

all: $(BUILD_DIR)/lc3vm

$(BUILD_DIR)/lc3vm: $(VM_TEST_SRCS) src/main.cpp
$(BUILD_DIR)/lc3vm: $(VM_SRCS) src/main.cpp
mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(VM_TEST_SRCS) src/main.cpp -o $(BUILD_DIR)/lc3vm
$(CC) $(CFLAGS) $(VM_SRCS) src/main.cpp -o $(BUILD_DIR)/lc3vm

test: $(BUILD_DIR)/test_runner
$(BUILD_DIR)/test_runner

$(BUILD_DIR)/test_runner: $(TEST_DIR)/test_basic.cpp $(VM_TEST_SRCS) $(TEST_MAIN_SRC)
$(TEST_MAIN_OBJ): tests/test_main.cpp
mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) -Dmain=disabled_main $(TEST_DIR)/test_basic.cpp $(VM_TEST_SRCS) $(TEST_MAIN_SRC) -o $(BUILD_DIR)/test_runner $(GTEST_FLAGS) $(LDFLAGS_COVERAGE)
$(CC) $(CFLAGS) -c tests/test_main.cpp -o $(TEST_MAIN_OBJ)

$(BUILD_DIR)/tests/%.o: tests/%.cpp
mkdir -p $(BUILD_DIR)/tests
$(CC) $(CFLAGS) -c $< -o $@

$(BUILD_DIR)/test_runner: $(TEST_MAIN_OBJ) $(TEST_OBJS) $(VM_SRCS)
mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_MAIN_OBJ) $(TEST_OBJS) $(VM_SRCS) -o $(BUILD_DIR)/test_runner $(GTEST_FLAGS) $(LDFLAGS_COVERAGE)

docs:
@echo "Generating documentation with Doxygen..."
@doxygen Doxyfile

clean:
@echo "Cleaning build directory..."
rm -rf $(BUILD_DIR)
15 changes: 13 additions & 2 deletions lc3vm/include/lc3.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
#include "opcodes.hpp"
#include <string>
#include <array>
#include <vector>

/**
* @brief Represents a loaded code/data segment in memory.
*/
struct CodeSegment {
std::uint16_t start_address; ///< The starting memory address of the segment.
std::uint16_t size; ///< The size of the segment in words.
};

/**
* @brief Represents the state of an LC-3 virtual machine.
Expand All @@ -40,6 +49,7 @@ class LC3State {
void update_flags(std::uint16_t r);
bool running; ///< Flag indicating whether the LC-3 VM is currently running.

std::vector<CodeSegment> loaded_code_segments; ///< Stores info about loaded program segments.

/**
* @brief Table of function pointers for dispatching LC-3 opcodes.
Expand Down Expand Up @@ -88,10 +98,11 @@ class LC3State {
* @brief Disassembles the instruction at a given memory address.
* (Currently not implemented)
* @param address The memory address of the instruction to disassemble.
* @return A string representation of the disassembled instruction.
*/
void disassemble(std::uint16_t address);
std::string disassemble(std::uint16_t address);
/**
* @brief Disassembles all instructions in memory.
* @brief Disassembles all instructions in loaded memory segments.
* (Currently not implemented)
*/
void disassemble_all();
Expand Down
28 changes: 28 additions & 0 deletions lc3vm/include/terminal_input.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#ifndef LC3_TERMINAL_INPUT_H
#define LC3_TERMINAL_INPUT_H

/**
* @file terminal_input.hpp
* @brief Provides functions to control terminal input mode (raw/canonical).
*/

/**
* @brief Enables raw input mode for the terminal.
*
* In raw mode:
* - Input is unbuffered (character by character).
* - Input is not echoed to the screen.
* - Special character processing (like Ctrl+C for SIGINT) might be altered,
* so signal handling needs to be robust.
* @throw std::runtime_error if enabling raw mode fails.
*/
void enable_raw_mode();

/**
* @brief Disables raw input mode, restoring original terminal settings.
* This function should be called before the program exits to ensure the
* terminal is left in a usable state.
*/
void disable_raw_mode();

#endif // LC3_TERMINAL_INPUT_H
161 changes: 150 additions & 11 deletions lc3vm/src/ls3.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
#include <array>
#include "traps.hpp"
#include "flags.hpp"
#include <unistd.h>
#include <sstream>
#include <iomanip>

/**
* @brief Swaps the endianness of a 16-bit unsigned integer.
Expand Down Expand Up @@ -132,9 +135,10 @@ void LC3State::ins(LC3State& state, std::uint16_t instr) {
switch (instr & 0xFF) {
case TRAP_GETC:
{
char c_in;
std::cin.get(c_in);
state.reg[R_R0] = static_cast<std::uint16_t>(c_in);
char c_in = 0;
if (read(STDIN_FILENO, &c_in, 1) == 1) {
state.reg[R_R0] = static_cast<std::uint16_t>(c_in);
}
}
state.update_flags(R_R0);
break;
Expand All @@ -155,11 +159,13 @@ void LC3State::ins(LC3State& state, std::uint16_t instr) {
}
case TRAP_IN: {
std::cout << "Enter a character: ";
char c_in_trap;
std::cin.get(c_in_trap);
std::cout.put(c_in_trap);
std::cout.flush();
state.reg[R_R0] = static_cast<std::uint16_t>(c_in_trap);
char c_in_trap = 0;
if (read(STDIN_FILENO, &c_in_trap, 1) == 1) {
std::cout.put(c_in_trap);
std::cout.flush();
state.reg[R_R0] = static_cast<std::uint16_t>(c_in_trap);
}
state.update_flags(R_R0);
break;
}
Expand Down Expand Up @@ -235,6 +241,9 @@ void LC3State::load_image(const std::string &filename) {
throw std::runtime_error("Image too large for memory at specified origin.");
}
}
if (words_actually_read > 0) {
loaded_code_segments.push_back({origin, static_cast<std::uint16_t>(words_actually_read)});
}
}

LC3State::LC3State() : memory(), reg{}, running(true) {
Expand Down Expand Up @@ -279,11 +288,141 @@ void LC3State::update_flags(std::uint16_t r_idx) {
}
}

void LC3State::disassemble(std::uint16_t address) {
(void)address;
std::cout << "Disassembly not yet implemented." << std::endl;
std::string LC3State::disassemble(std::uint16_t address) {
std::uint16_t instr = memory.read(address);
std::uint16_t opcode = instr >> 12;
std::ostringstream oss;

oss << "0x" << std::nouppercase << std::hex << std::setfill('0') << std::setw(4) << address << ": ";

switch (opcode) {
case OP_ADD: {
std::uint16_t dr = (instr >> 9) & 0x7;
std::uint16_t sr1 = (instr >> 6) & 0x7;
oss << "ADD R" << dr << ", R" << sr1 << ", ";
if ((instr >> 5) & 0x1) {
std::int16_t imm5 = static_cast<std::int16_t>(sign_extend(instr & 0x1F, 5));
oss << "#" << std::dec << imm5;
} else {
std::uint16_t sr2 = instr & 0x7;
oss << "R" << sr2;
}
break;
}
case OP_AND: {
std::uint16_t dr = (instr >> 9) & 0x7;
std::uint16_t sr1 = (instr >> 6) & 0x7;
oss << "AND R" << dr << ", R" << sr1 << ", ";
if ((instr >> 5) & 0x1) {
std::int16_t imm5 = static_cast<std::int16_t>(sign_extend(instr & 0x1F, 5));
oss << "#" << std::dec << imm5;
} else {
std::uint16_t sr2 = instr & 0x7;
oss << "R" << sr2;
}
break;
}
case OP_NOT: {
std::uint16_t dr = (instr >> 9) & 0x7;
std::uint16_t sr = (instr >> 6) & 0x7;
oss << "NOT R" << dr << ", R" << sr;
break;
}
case OP_BR: {
std::uint16_t n = (instr >> 11) & 0x1;
std::uint16_t z = (instr >> 10) & 0x1;
std::uint16_t p = (instr >> 9) & 0x1;
std::int16_t pc_offset9 = static_cast<std::int16_t>(sign_extend(instr & 0x1FF, 9));
oss << "BR" << (n ? "n" : "") << (z ? "z" : "") << (p ? "p" : "") << " 0x" << std::nouppercase << std::hex << std::setfill('0') << std::setw(4) << (address + 1 + pc_offset9);
break;
}
case OP_JMP: {
std::uint16_t base_r = (instr >> 6) & 0x7;
if (base_r == 7) {
oss << "RET";
} else {
oss << "JMP R" << base_r;
}
break;
}
case OP_JSR: {
oss << "JSR ";
if ((instr >> 11) & 1) {
std::int16_t pc_offset11 = static_cast<std::int16_t>(sign_extend(instr & 0x7FF, 11));
oss << "0x" << std::nouppercase << std::hex << std::setfill('0') << std::setw(4) << (address + 1 + pc_offset11);
} else {
std::uint16_t base_r = (instr >> 6) & 0x7;
oss << "R" << base_r;
}
break;
}
case OP_LD: {
std::uint16_t dr = (instr >> 9) & 0x7;
std::int16_t pc_offset9 = static_cast<std::int16_t>(sign_extend(instr & 0x1FF, 9));
oss << "LD R" << dr << ", 0x" << std::nouppercase << std::hex << std::setfill('0') << std::setw(4) << (address + 1 + pc_offset9);
break;
}
case OP_LDI: {
std::uint16_t dr = (instr >> 9) & 0x7;
std::int16_t pc_offset9 = static_cast<std::int16_t>(sign_extend(instr & 0x1FF, 9));
oss << "LDI R" << dr << ", 0x" << std::nouppercase << std::hex << std::setfill('0') << std::setw(4) << (address + 1 + pc_offset9);
break;
}
case OP_LDR: {
std::uint16_t dr = (instr >> 9) & 0x7;
std::uint16_t base_r = (instr >> 6) & 0x7;
std::int16_t offset6 = static_cast<std::int16_t>(sign_extend(instr & 0x3F, 6));
oss << "LDR R" << dr << ", R" << base_r << ", #" << std::dec << offset6;
break;
}
case OP_LEA: {
std::uint16_t dr = (instr >> 9) & 0x7;
std::int16_t pc_offset9 = static_cast<std::int16_t>(sign_extend(instr & 0x1FF, 9));
oss << "LEA R" << dr << ", 0x" << std::nouppercase << std::hex << std::setfill('0') << std::setw(4) << (address + 1 + pc_offset9);
break;
}
case OP_ST: {
std::uint16_t sr = (instr >> 9) & 0x7;
std::int16_t pc_offset9 = static_cast<std::int16_t>(sign_extend(instr & 0x1FF, 9));
oss << "ST R" << sr << ", 0x" << std::nouppercase << std::hex << std::setfill('0') << std::setw(4) << (address + 1 + pc_offset9);
break;
}
case OP_STI: {
std::uint16_t sr = (instr >> 9) & 0x7;
std::int16_t pc_offset9 = static_cast<std::int16_t>(sign_extend(instr & 0x1FF, 9));
oss << "STI R" << sr << ", 0x" << std::nouppercase << std::hex << std::setfill('0') << std::setw(4) << (address + 1 + pc_offset9);
break;
}
case OP_STR: {
std::uint16_t sr = (instr >> 9) & 0x7;
std::uint16_t base_r = (instr >> 6) & 0x7;
std::int16_t offset6 = static_cast<std::int16_t>(sign_extend(instr & 0x3F, 6));
oss << "STR R" << sr << ", R" << base_r << ", #" << std::dec << offset6;
break;
}
case OP_TRAP: {
oss << "TRAP x" << std::nouppercase << std::hex << std::setfill('0') << std::setw(2) << (instr & 0xFF);
break;
}
case OP_RES:
case OP_RTI:
default:
oss << "BAD OPCODE";
break;
}
return oss.str();
}

void LC3State::disassemble_all() {
std::cout << "Disassembly not yet implemented." << std::endl;
if (loaded_code_segments.empty()) {
std::cout << "No program images loaded. Nothing to disassemble." << std::endl;
return;
}

for (const auto& segment : loaded_code_segments) {
for (std::uint16_t i = 0; i < segment.size; ++i) {
std::uint16_t current_address = segment.start_address + i;
std::cout << disassemble(current_address) << std::endl;
}
}
}
Loading