I am building an emulator of the Risk-V CPU for own educational purposes. I have small POC working and want to build example programs and test them on the emulator.
I'm trying to build example program in Rust and seems like I made some decent progress, but I got stuck when I have to load compiled program to the memory of my emulator and transfer CPU execution to that program.
Test program:
#![no_std]
#![no_main]
use core::panic::PanicInfo;
#[no_mangle]
pub extern "C" fn _start() -> ! {
loop {
for i in 0..1000 {
unsafe {
let r = i as *mut u32;
// This can panic because (500 - i) can be 0
*r = 20000 % (500 - i);
}
}
}
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
build:
$ cargo build --target riscv32i-unknown-none-elf --release
generating binary image from elf target:
riscv32-unknown-linux-gnu-objcopy -g -O binary \
target/riscv32i-unknown-none-elf/release/sample1 \
target/riscv32i-unknown-none-elf/release/sample1.bin
This works fine so far and generates me binary file with size 5156 bytes.
I inspected .bin file and it looks "legit binary" to me.
I found some readable strings in the beginning of the file (like attempt to calculate the remainder with a divisor of zero
) - looks like they are related to code which handles panic which can happen if I'm doing % 0
.
In the end of file I found something that looks like riskv32i instructions (easy to notice them since least significant bits are 11
).
Rest of the file is filled with zeros.
Place where I stuck is I cannot figure out:
- At which offset am I supposed to load this bin image file into memory of my virtual CPU? I don't think it's OK to load it at 0x0 address, because there is useful info in the beginning of the image and I don't think it's cool for the program to read it from address 0x0.
- After program is loaded, I need to transfer CPU execution to the entry point of my program (
_start
). How can I find out which address is the entry point so I can put this address intopc
register before starting CPU cycles? It's obviously not in the beginning of the image (there are human readable strings there). - Is there a way to make this entry point address stable, so all programs which I write will have the same entry point address, so I don't have to do tweaks for each of programs I compile?
I may went wrong way when I used objcopy
. If that's the case, please let me know what's the appropriate way to load ELF file into a homemade CPU emulator.
Update: Linker arguments, (as provided by RUSTFLAGS="-Z print-link-args" cargo build --target riscv32i-unknown-none-elf --release --verbose
):
rust-lld \
-flavor \
gnu \
-L \
/home/kris/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/riscv32i-unknown-none-elf/lib \
/mnt/c/src/ws/cpu/sample1/target/riscv32i-unknown-none-elf/release/deps/sample1-4813691a581d1819.sample1.251h7tq6-cgu.0.rcgu.o \
/mnt/c/src/ws/cpu/sample1/target/riscv32i-unknown-none-elf/release/deps/sample1-4813691a581d1819.sample1.251h7tq6-cgu.1.rcgu.o -o \
/mnt/c/src/ws/cpu/sample1/target/riscv32i-unknown-none-elf/release/deps/sample1-4813691a581d1819 \
--gc-sections \
-L \
/mnt/c/src/ws/cpu/sample1/target/riscv32i-unknown-none-elf/release/deps \
-L \
/mnt/c/src/ws/cpu/sample1/target/release/deps \
-L \
/home/kris/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/riscv32i-unknown-none-elf/lib \
-Bstatic \
/home/kris/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/riscv32i-unknown-none-elf/lib/librustc_std_workspace_core-6d1cf467df9db3bb.rlib \
/home/kris/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/riscv32i-unknown-none-elf/lib/libcore-a1a0b4993598bfe4.rlib \
/home/kris/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/riscv32i-unknown-none-elf/lib/libcompiler_builtins-a229bbbccd019775.rlib \
-Bdynamic
I know that there are some important things missing in the program, like initializing stack pointer register. I'm planning to take care about that after I figure out loading logic