Implement loading of ELF files
This commit is contained in:
parent
184c379e96
commit
0c8d1bf257
8 changed files with 163 additions and 42 deletions
|
@ -13,6 +13,7 @@ target_sources(vm_lib
|
||||||
PRIVATE
|
PRIVATE
|
||||||
src/vm.cpp
|
src/vm.cpp
|
||||||
src/debug.cpp
|
src/debug.cpp
|
||||||
|
src/elf.cpp
|
||||||
|
|
||||||
PUBLIC
|
PUBLIC
|
||||||
FILE_SET HEADERS
|
FILE_SET HEADERS
|
||||||
|
@ -20,6 +21,7 @@ target_sources(vm_lib
|
||||||
FILES
|
FILES
|
||||||
src/vm.hpp
|
src/vm.hpp
|
||||||
src/debug.hpp
|
src/debug.hpp
|
||||||
|
src/elf.hpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(rve src/rve.cpp)
|
add_executable(rve src/rve.cpp)
|
||||||
|
|
26
README.md
26
README.md
|
@ -1,7 +1,9 @@
|
||||||
# A simple RISC-V emulator
|
# A simple RISC-V emulator
|
||||||
|
|
||||||
This is a toy emulator for RISC-V, made for educational purposes.
|
This is a toy emulator for RISC-V, made for educational purposes.
|
||||||
The goal is to have a base rv32i instruction set (the bare minimum) plus a M-extension for division and multiplication. In theory, it should be enough to execute simple C programs compiled with GCC and sprinkled with a few linker scripts. Of course, no libc because there's no OS.
|
The goal is to have a base rv32i instruction set (the bare minimum) plus a M-extension for division and multiplication. It is capable of running normal ELF binaries produced by compiling C programs with GCC. It also has support for attaching the GDB debugger to the GDB stub port, so you can debug your programs running in the virtual machine.
|
||||||
|
|
||||||
|
The code is small and compact on purpose, to make the implementation easy to understand.
|
||||||
|
|
||||||
## Compiling and running
|
## Compiling and running
|
||||||
|
|
||||||
|
@ -33,10 +35,28 @@ cd example
|
||||||
make
|
make
|
||||||
```
|
```
|
||||||
|
|
||||||
As a result, you'll get an `example.raw` binary. To execute it:
|
As a result, you'll get an `example` binary. To execute it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./rve ../example/example.raw
|
./rve ../example/example
|
||||||
```
|
```
|
||||||
|
|
||||||
The expected output of the example program is `40320`.
|
The expected output of the example program is `40320`.
|
||||||
|
|
||||||
|
## Debugging programs under GDB
|
||||||
|
|
||||||
|
The virtual machine contains an implementation of GDB stub protocol. To run the program in debug mode, execute:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./rve --debug ../example/example
|
||||||
|
```
|
||||||
|
|
||||||
|
The program would load, and stop at first instruction. It will then prompt you to connect the debugger.
|
||||||
|
Then run `riscv32-none-elf-gdb`, and in the gdb prompt, type:
|
||||||
|
|
||||||
|
```
|
||||||
|
file ../example/example
|
||||||
|
target remote :1234
|
||||||
|
```
|
||||||
|
|
||||||
|
From now on, you can set breakpoints, examine variables, registers and memory as you would expect under GDB.
|
||||||
|
|
|
@ -2,8 +2,3 @@ example: example.c Makefile boot.s linker.ld
|
||||||
riscv32-none-elf-as -march=rv32i -mabi=ilp32 boot.s -o boot.o
|
riscv32-none-elf-as -march=rv32i -mabi=ilp32 boot.s -o boot.o
|
||||||
riscv32-none-elf-gcc -fno-builtin -fvisibility=hidden -nostdlib -nostartfiles -march=rv32im -mabi=ilp32 -c example.c -o example.o -g
|
riscv32-none-elf-gcc -fno-builtin -fvisibility=hidden -nostdlib -nostartfiles -march=rv32im -mabi=ilp32 -c example.c -o example.o -g
|
||||||
riscv32-none-elf-ld boot.o example.o -T linker.ld -o example -g
|
riscv32-none-elf-ld boot.o example.o -T linker.ld -o example -g
|
||||||
#riscv32-none-elf-strip -R .riscv.attributes example
|
|
||||||
#riscv32-none-elf-strip -R .comment example
|
|
||||||
riscv32-none-elf-objcopy -O binary example example.raw
|
|
||||||
#riscv32-none-elf-objcopy -O binary -j .text example example.text
|
|
||||||
#riscv32-none-elf-objcopy -O binary -j .sdata example example.data
|
|
||||||
|
|
127
src/elf.cpp
Normal file
127
src/elf.cpp
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
#include "elf.hpp"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// ELF file constants
|
||||||
|
constexpr uint8_t ELF_MAGIC[] = {0x7F, 'E', 'L', 'F'};
|
||||||
|
constexpr uint16_t ELF_TYPE_EXECUTABLE = 2;
|
||||||
|
constexpr uint16_t ELF_MACHINE_RISCV = 243;
|
||||||
|
constexpr uint8_t ELF_CLASS_32 = 1;
|
||||||
|
constexpr uint8_t ELF_LITTLE_ENDIAN = 1;
|
||||||
|
|
||||||
|
struct Elf32Header {
|
||||||
|
uint8_t e_ident[16]; // Magic number and other info
|
||||||
|
uint16_t e_type; // Object file type
|
||||||
|
uint16_t e_machine; // Architecture
|
||||||
|
uint32_t e_version; // Object file version
|
||||||
|
uint32_t e_entry; // Entry point virtual address
|
||||||
|
uint32_t e_phoff; // Program header table file offset
|
||||||
|
uint32_t e_shoff; // Section header table file offset
|
||||||
|
uint32_t e_flags; // Processor-specific flags
|
||||||
|
uint16_t e_ehsize; // ELF header size in bytes
|
||||||
|
uint16_t e_phentsize; // Program header table entry size
|
||||||
|
uint16_t e_phnum; // Program header table entry count
|
||||||
|
uint16_t e_shentsize; // Section header table entry size
|
||||||
|
uint16_t e_shnum; // Section header table entry count
|
||||||
|
uint16_t e_shstrndx; // Section header string table index
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Elf32Section {
|
||||||
|
uint32_t sh_name; // Section name (string table index)
|
||||||
|
uint32_t sh_type; // Section type
|
||||||
|
uint32_t sh_flags; // Section attributes
|
||||||
|
uint32_t sh_addr; // Virtual address in memory
|
||||||
|
uint32_t sh_offset; // Offset in file
|
||||||
|
uint32_t sh_size; // Size of section
|
||||||
|
uint32_t sh_link; // Link to another section
|
||||||
|
uint32_t sh_info; // Additional section information
|
||||||
|
uint32_t sh_addralign; // Section alignment
|
||||||
|
uint32_t sh_entsize; // Entry size if section holds table
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<uint8_t> load_elf(const std::string& filename, size_t memory_size) {
|
||||||
|
std::ifstream file(filename, std::ios::binary);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
throw std::runtime_error("Failed to open ELF file");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the ELF header
|
||||||
|
Elf32Header ehdr;
|
||||||
|
file.read(reinterpret_cast<char*>(&ehdr), sizeof(ehdr));
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("Failed to read ELF header");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ELF magic and basic properties
|
||||||
|
if (std::memcmp(ehdr.e_ident, ELF_MAGIC, sizeof(ELF_MAGIC)) != 0) {
|
||||||
|
throw std::runtime_error("Invalid ELF magic number");
|
||||||
|
}
|
||||||
|
if (ehdr.e_ident[4] != ELF_CLASS_32 || ehdr.e_ident[5] != ELF_LITTLE_ENDIAN) {
|
||||||
|
throw std::runtime_error("Unsupported ELF class or endianness");
|
||||||
|
}
|
||||||
|
if (ehdr.e_type != ELF_TYPE_EXECUTABLE ||
|
||||||
|
ehdr.e_machine != ELF_MACHINE_RISCV) {
|
||||||
|
throw std::runtime_error("Unsupported ELF type or machine");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read section headers
|
||||||
|
file.seekg(ehdr.e_shoff);
|
||||||
|
std::vector<Elf32Section> sectionHeaders(ehdr.e_shnum);
|
||||||
|
for (size_t i = 0; i < ehdr.e_shnum; ++i) {
|
||||||
|
file.read(reinterpret_cast<char*>(§ionHeaders[i]),
|
||||||
|
sizeof(Elf32Section));
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("Failed to read section headers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load section string table
|
||||||
|
const Elf32Section& strTabHdr = sectionHeaders[ehdr.e_shstrndx];
|
||||||
|
std::vector<char> sectionStrTable(strTabHdr.sh_size);
|
||||||
|
file.seekg(strTabHdr.sh_offset);
|
||||||
|
file.read(sectionStrTable.data(), strTabHdr.sh_size);
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("Failed to read section string table");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine memory range for allocation
|
||||||
|
uint32_t memoryEnd = 0;
|
||||||
|
for (const Elf32Section& shdr : sectionHeaders) {
|
||||||
|
const char* sectionName = §ionStrTable[shdr.sh_name];
|
||||||
|
if (std::strcmp(sectionName, ".text") == 0 ||
|
||||||
|
std::strcmp(sectionName, ".sdata") == 0) {
|
||||||
|
memoryEnd = std::max(memoryEnd, shdr.sh_addr + shdr.sh_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memoryEnd == 0) {
|
||||||
|
throw std::runtime_error("No loadable sections found");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> loadedData(memoryEnd, 0);
|
||||||
|
|
||||||
|
// Load .text and .sdata sections
|
||||||
|
for (const Elf32Section& shdr : sectionHeaders) {
|
||||||
|
const char* sectionName = §ionStrTable[shdr.sh_name];
|
||||||
|
if (std::strcmp(sectionName, ".text") == 0 ||
|
||||||
|
std::strcmp(sectionName, ".sdata") == 0) {
|
||||||
|
std::vector<uint8_t> sectionData(shdr.sh_size);
|
||||||
|
file.seekg(shdr.sh_offset);
|
||||||
|
file.read(reinterpret_cast<char*>(&loadedData[shdr.sh_addr]),
|
||||||
|
shdr.sh_size);
|
||||||
|
if (!file) {
|
||||||
|
throw std::runtime_error("Failed to read section data");
|
||||||
|
}
|
||||||
|
loadedData.insert(loadedData.end(), sectionData.begin(),
|
||||||
|
sectionData.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadedData;
|
||||||
|
}
|
7
src/elf.hpp
Normal file
7
src/elf.hpp
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
std::vector<uint8_t> load_elf(const std::string& filename, size_t memory_size);
|
|
@ -5,6 +5,7 @@
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
#include "debug.hpp"
|
#include "debug.hpp"
|
||||||
|
#include "elf.hpp"
|
||||||
#include "vm.hpp"
|
#include "vm.hpp"
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
|
@ -26,12 +27,14 @@ int main(int argc, char *argv[]) {
|
||||||
|
|
||||||
std::vector<uint8_t> memory;
|
std::vector<uint8_t> memory;
|
||||||
try {
|
try {
|
||||||
memory = load_program(program_filename, MEMORY_SIZE);
|
memory = load_elf(program_filename, MEMORY_SIZE);
|
||||||
} catch (const std::exception &e) {
|
} catch (const std::exception &e) {
|
||||||
std::cerr << e.what() << std::endl;
|
std::cerr << e.what() << std::endl;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
memory.resize(MEMORY_SIZE, 0);
|
||||||
|
|
||||||
VM vm(memory);
|
VM vm(memory);
|
||||||
|
|
||||||
if (!debug) {
|
if (!debug) {
|
||||||
|
|
30
src/vm.cpp
30
src/vm.cpp
|
@ -12,36 +12,6 @@ inline int32_t sign_extend(int32_t value, int bits) {
|
||||||
return (value ^ mask) - mask;
|
return (value ^ mask) - mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> load_program(const std::string& filename,
|
|
||||||
size_t memory_size) {
|
|
||||||
std::vector<uint8_t> memory(memory_size, 0);
|
|
||||||
|
|
||||||
std::ifstream file(filename, std::ios::binary | std::ios::ate);
|
|
||||||
|
|
||||||
if (!file.is_open()) {
|
|
||||||
throw std::runtime_error("Failed to open file: " + filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t file_size = file.tellg();
|
|
||||||
|
|
||||||
if (file_size > memory_size) {
|
|
||||||
throw std::runtime_error("File is too big");
|
|
||||||
}
|
|
||||||
|
|
||||||
file.seekg(0, std::ios::beg);
|
|
||||||
|
|
||||||
file.read(reinterpret_cast<char*>(&memory[0]), file_size);
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
throw std::runtime_error(
|
|
||||||
"Failed to read the complete program into memory.");
|
|
||||||
}
|
|
||||||
|
|
||||||
file.close();
|
|
||||||
|
|
||||||
return memory;
|
|
||||||
}
|
|
||||||
|
|
||||||
VM::VM(std::vector<uint8_t> memory) : memory_(memory) {}
|
VM::VM(std::vector<uint8_t> memory) : memory_(memory) {}
|
||||||
|
|
||||||
std::vector<uint8_t> VM::read_memory(size_t start, size_t size) {
|
std::vector<uint8_t> VM::read_memory(size_t start, size_t size) {
|
||||||
|
|
|
@ -9,9 +9,6 @@ class EbreakException : std::exception {};
|
||||||
|
|
||||||
const int NUM_REGISTERS = 32; // Standard RISC-V has 32 registers
|
const int NUM_REGISTERS = 32; // Standard RISC-V has 32 registers
|
||||||
|
|
||||||
std::vector<uint8_t> load_program(const std::string& filename,
|
|
||||||
size_t memory_size);
|
|
||||||
|
|
||||||
class VM {
|
class VM {
|
||||||
public:
|
public:
|
||||||
VM(std::vector<uint8_t> memory);
|
VM(std::vector<uint8_t> memory);
|
||||||
|
|
Loading…
Reference in a new issue