Steps of the experiment in detailed:
Install Debian GNU/Linux 12 (bookworm) in VirtualBox 7.0.12 in Windows 10 Home.
Install the dependencies with apt
command.
For the commands in this document: curl git make
.
For compiling rv32emu: libsdl2-dev libsdl2-mixer-dev
.
For Valgrind to debug programs: libc6-dbg
.
Get and build the latest version of sysprog21/rv32emu from its GitHub repository. (The latest commit on the master branch is 90b42a6
on 2023-12-11.) The sed
command is used to replace -O2
flag to cc
with -O0 -g
, which is suggested by Valgrind for the correctness of debugging.
Get, build and install the latest release of Valgrind from its official website. (It is 3.22.0 on 2023-12-11.)
In Debian, 4 out of 5 tests in rv32emu yielded segmentation faults. The test of hello.elf
yielded Failed.
Results:
According to the target check
in Makefile
, the core to execute binaries is $(BIN) $(OUT)/$(e).elf
. If we focus on a faulty binary hello
, the command above is equivalent to build/rv32emu build/hello.elf
by tracing through these variables.
Follow the instructions from Valgrind™ Developers (2023), execute the same command that gives segmentation fault in valgrind
.
In short, the error is reading from an invalid memory location. Its trace is as follows.
Right before src/io.h:49
, print all pointers to check which one is the bad apple. So, the function is as follows after editing. (Forgive me including headers here. This is to make gcc
happy.)
Build rv32emu
and run hello.elf
.
Output:
Aha! m
is nil. We need to find out why m
is nil.
In other words, trace back to find out the place where m
is initialized.
src/riscv.c:206
: s->mem
is the argument m
of memory_write()
.src/riscv.c:196
: state_t *s = rv_userdata(rv);
.rv_userdata(rv)
: If rv
is not nil, return rv->userdata
. This is not our target, but tells us to trace rv->userdata
.src/riscv.c:153
: rv
is the parameter of rv_reset()
, which is the current function. So, we need to trace its caller, rv_create()
.src/riscv.c:126
: rv
is the argument rv
of rv_reset()
.`src/riscv.c:112
: rv->userdata = userdata;
.src/riscv.c:99
: userdata
is the parameter of rv_create()
, so trace its caller, main
.main.c:232
: state
is the argument userdata
of rv_create()
.main.c:223
: state_t *state = state_new();
.src/state.h:28
: In state_new()
, s->mem = memory_new();
. This is our target! Look into it.src/io.c:50
: return mem;
.src/io.c:33
: memory_t *mem = malloc(sizeof(memory_t));
, where sizeof(memory_t)
is 16, which shouldn't cause segmentation fault.src/io.c:42
: However, in the same function, this line data_memory_base = malloc(MEM_SIZE);
is interesting, for it tries to allocate bytes (~ 4 GiB) of memory at once. With the fact that rv32emu
assumes that the operating system commit at least 4 GiB of memory without traps or returning nil, trying to boot a Debian or Ubuntu VM with different memory size gives the following conclusion.
If the size of the available memory (Mem_total - Mem_used + Swap_available) is smaller than 4 GiB, rv32emu
yields segmentation fault in some cases in Debian. Nevertheless, Ubuntu doesn't care about it.
If rv32emu
raises an exception when malloc()
-related functions return nil, segmentation fault will not occur.