# Testing Memory Leak of rv32emu Steps of the experiment in detailed: 1. Install [Debian GNU/Linux](https://www.debian.org/) 12 (bookworm) in VirtualBox 7.0.12 in Windows 10 Home. 1. Install the dependencies with `apt` command. For the commands in this document: `curl git make`. For compiling [rv32emu](https://github.com/sysprog21/rv32emu): `libsdl2-dev libsdl2-mixer-dev`. For [Valgrind](https://valgrind.org/) to debug programs: `libc6-dbg`. ```bash sudo apt install -y curl git make libsdl2-dev libsdl2-mixer-dev libc6-dbg ``` 1. Get and build the latest version of [sysprog21/rv32emu](https://github.com/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](https://valgrind.org/) for the correctness of debugging. ```bash mkdir -p ~/ca cd ~/ca git clone https://github.com/sysprog21/rv32emu cd rv32emu sed -i Makefile -e "s/\(CFLAGS.*\)-O2\(.*\)/\1-O0 -g\2/" make ``` 1. Get, build and install the latest release of [Valgrind](https://valgrind.org/) from [its official website](https://valgrind.org/downloads/current.html). (It is 3.22.0 on 2023-12-11.) ```bash cd /tmp curl -o valgrind.tar.bz2 https://sourceware.org/pub/valgrind/valgrind-3.22.0.tar.bz2 tar -xjf valgrind.tar.bz2 mv valgrind-3.22.0 valgrind echo 3.22.0 > valgrind/version.txt rm valgrind.tar.bz2 cd valgrind mkdir -p ~/.local/valgrind ./configure --prefix=$HOME/.local/valgrind make make install echo export PATH="~/.local/valgrind/bin/:$PATH" >> ~/.bashrc . ~/.bashrc cd rm -rf /tmp/valgrind ``` 1. In Debian, 4 out of 5 tests in rv32emu yielded segmentation faults. The test of `hello.elf` yielded Failed. ```bash cd ~/ca/rv32emu make check ``` Results: ``` Segmentation fault Segmentation fault Segmentation fault Segmentation fault Running hello.elf ... Failed. make: *** [Makefile:185: check] Error 1 ``` 1. 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. 1. Follow the instructions from [Valgrind™ Developers (2023)](https://valgrind.org/docs/manual/quick-start.html), execute the same command that gives segmentation fault in `valgrind`. ```bash valgrind --leak-check=yes build/rv32emu build/hello.elf ``` <details> <summary>Output (click to expand/hide):</summary> ``` ==24784== Memcheck, a memory error detector ==24784== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al. ==24784== Using Valgrind-3.22.0 and LibVEX; rerun with -h for copyright info ==24784== Command: build/rv32emu build/hello.elf ==24784== ==24784== Invalid read of size 8 ==24784== at 0x11F826: memory_write.lto_priv.1 (io.h:49) ==24784== by 0x11FE72: rv_reset (riscv.c:206) ==24784== by 0x11FCE0: rv_create (riscv.c:126) ==24784== by 0x123400: main (main.c:232) ==24784== Address 0x0 is not stack'd, malloc'd or (recently) free'd ==24784== ==24784== ==24784== Process terminating with default action of signal 11 (SIGSEGV) ==24784== Access not within mapped region at address 0x0 ==24784== at 0x11F826: memory_write.lto_priv.1 (io.h:49) ==24784== by 0x11FE72: rv_reset (riscv.c:206) ==24784== by 0x11FCE0: rv_create (riscv.c:126) ==24784== by 0x123400: main (main.c:232) ==24784== If you believe this happened as a result of a stack ==24784== overflow in your program's main thread (unlikely but ==24784== possible), you can try to increase the size of the ==24784== main thread stack using the --main-stacksize= flag. ==24784== The main thread stack size used in this run was 8388608. ==24784== ==24784== HEAP SUMMARY: ==24784== in use at exit: 121,693 bytes in 281 blocks ==24784== total heap usage: 380 allocs, 99 frees, 131,285 bytes allocated ==24784== ==24784== LEAK SUMMARY: ==24784== definitely lost: 0 bytes in 0 blocks ==24784== indirectly lost: 0 bytes in 0 blocks ==24784== possibly lost: 0 bytes in 0 blocks ==24784== still reachable: 119,677 bytes in 260 blocks ==24784== suppressed: 0 bytes in 0 blocks ==24784== Reachable blocks (those to which a pointer was found) are not shown. ==24784== To see them, rerun with: --leak-check=full --show-leak-kinds=all ==24784== ==24784== For lists of detected and suppressed errors, rerun with: -s ==24784== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0) Segmentation fault ``` </details> In short, the error is reading from an invalid memory location. Its trace is as follows. ``` Invalid read of size 8 at 0x11F826: memory_write.lto_priv.1 (io.h:49) by 0x11FE72: rv_reset (riscv.c:206) by 0x11FCE0: rv_create (riscv.c:126) by 0x123400: main (main.c:232) Address 0x0 is not stack'd, malloc'd or (recently) free'd ``` 1. 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.) ```c static inline void memory_write(memory_t *m, uint32_t addr, const uint8_t *src, uint32_t size) { #include <stdio.h> printf("src: %p\n", src); fflush(stdout); printf("m: %p\n", m); fflush(stdout); printf("m->mem_base: %p\n", m->mem_base); fflush(stdout); memcpy(m->mem_base + addr, src, size); } ``` 1. Build `rv32emu` and run `hello.elf`. ```bash make && build/rv32emu build/hello.elf ``` Output: ``` ... (bulding info) src: 0x7ffd91d49b40 m: (nil) Segmentation fault ``` Aha! `m` is nil. We need to find out why `m` is nil. 1. In other words, trace back to find out the place where `m` is initialized. 1. `src/riscv.c:206`: `s->mem` is the argument `m` of `memory_write()`. 1. `src/riscv.c:196`: `state_t *s = rv_userdata(rv);`. 1. `rv_userdata(rv)`: If `rv` is not nil, return `rv->userdata`. This is not our target, but tells us to trace `rv->userdata`. 1. `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()`. 1. `src/riscv.c:126`: `rv` is the argument `rv` of `rv_reset()`.` 1. `src/riscv.c:112`: `rv->userdata = userdata;`. 1. `src/riscv.c:99`: `userdata` is the parameter of `rv_create()`, so trace its caller, `main`. 1. `main.c:232`: `state` is the argument `userdata` of `rv_create()`. 1. `main.c:223`: `state_t *state = state_new();`. 1. `src/state.h:28`: In `state_new()`, `s->mem = memory_new();`. This is our target! Look into it. 1. `src/io.c:50`: `return mem;`. 1. `src/io.c:33`: `memory_t *mem = malloc(sizeof(memory_t));`, where `sizeof(memory_t)` is 16, which shouldn't cause segmentation fault. 1. `src/io.c:42`: However, in the same function, this line `data_memory_base = malloc(MEM_SIZE);` is interesting, for it tries to allocate $\left(2^{32} - 1\right)$ bytes (~ 4 GiB) of memory at once. <div id=conclusion></div> 1. 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. :::warning 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.