Serverless MCU Lambdas - A Working Emulator - Part 1
A follow-up of my Serverless MCU Lambdas idea. It is comprised of 3 phases :
- HTTP/CGI
- Generic RV32I emulation
- Specific RV32I emulation (curl, I/O, memory functions, …)
Generic RV32I emulation
There’s actually many RV32I emulators out there, but they are quite complex usually, and focused on interactive execution.
The 2 promising ones are :
- mini-rv32ima from the awesome Charles Lohr
- Rv32iEmulator from ずし
mini-rv32ima
I started with mini-rv32ima at first, as it is a tiny C header-only risc-v emulator therefore looked like a good fit. It is actually rather well written, but after a while, I discovered that it is really geared to running Linux NOMMU. Which means that it has some quirks, notably not emulating a MCU, but a full featured CPU with peripherals. The amount of work that went into it is rather amazing as it delivers on its promised.*
Rv32iEmulator
I found out Rv32iEmulator while struggling to implement a R/O ROM for mini-rv32ima. The code is written in a very naive way. Almost taking the reference ISA and coding straight from it. Without much optimization. It is also coded in a simpler form as mini-rv32ima, since it uses functions much more than preprocessor macros. This does fully leverage modern compilers, in the same vein as my RTS project
The code itself had several critical bugs, such as not emulating correctly load & stores from less than 32 bits integer sizes. This prompted me to convert the codebase to my famous 1SLOC paradigm, and it showed several other bugs. Mostly in shifting & bitmasks that were not done in the correct order.
The end result is available on my github fork.
Interactions with CGI
To interact with CGI, the caller creates 3 temporary files in the filesystem, along with the binary file that represents the ROM.
Files
Calling the emulator is very easy with a simple rv32i rom.bin ram in out
.
file name | max size | Offset | ACL |
---|---|---|---|
rom.bin |
1MiB | 0x00000000 |
R/O |
ram |
1MiB | 0x01000000 |
R/W |
in |
1MiB | 0x08000000 |
R/O |
out |
1MiB | 0x08100000 |
R/W |
The offset is set in the interpreter. Those are MMAP-ed directly into the process address space with the corresponding access protection. This means that the ROM area cannot be written to, nor the IN area.
File format
The rom.bin
is a very simple binary format1. It has no headers. It is the
output of objcopy -O binary
.
The ram
file is intended to be an opaque state file. It shall not be
parsed or generated outside of the emulator, but can be examined for
debugging purposes. No compatibility guaranties are provided. It is always safe
to zero-fill it in doubt. It does serve as a state between calls. Subsequent
calls have to be implemented as if the RST
pin was lowered on a real MCU.
This will lead to some tricks later as we see, mostly about handling static
vars initial values.
The in
file is simple. It can contain a little endian uint32_t
as header to
indicate the size of the file. The header is only useful if you want to
leverage the standard tooling on the area. You can simply ignore it or use
another header. Any buffer overrun is safe, as there’s an un-mmaped zone just
behind it. Therefore the only harmful effect is a SIGSEGV
that will
brutally kill the emulator process.
The out
file is also simple. It starts also with a little endian uint32_t
as header to indicate the size of the file. Again buffer overrun is safe, but
setting this header to a lower value will risk data truncation.
A simple way to have a peek into memory mapping of the emulator is to leverage
xxd -a -o
such as
xxd -a -o 0x00000000 rom.bin
xxd -a -o 0x01000000 ram
xxd -a -o 0x08000000 in
xxd -a -o 0x08100000 out
It will show the offset correct addresses, along with compressing any zero-filled area.
Builtool
A very simple toolchain is provided to create a ROM that will run on bare metal
It is based on gcc-riscv64-unknown-elf
& picolibc-riscv64-unknown-elf
.
-
Intel HEX, Motorola S-record or even the ELF format might be supported in the future, but for now simplicity is key. ↩