This was only reproducible if the VM was built natively, on a Raspberry Pi, inside an Ubuntu18-derived container. Buiding natively on Raspberry Pi OS was fine. Cross-building was fine too. The difference is the default instruction set the toolchain uses. We don't explicitly specify `-mthumb` or `-marm`, so we use the toolchain's default. That default seems to depend on how GCC itself was built. Ubuntu ships a GCC that has been built in thumb mode, thus defaulting to `-mthumb`, whereas Rasperry Pi OS and Fedora ship GCCs that default to `-marm`.
So, the VM proper is compiled either to arm or thumb code. The `SafeFetch32` assembly function itself uses arm code always. Why this is I don't know for sure, I assume if I wanted thumb I need to specify `.thumb_func` in the assembly.
If the VM uses thumb, it needs to call SafeFetch32 with a switching branch instruction (BX). But the compiler-generated BL. The instruction set was not switched upon entering SafeFetch32 and garbage thumb code was executed. VM crashes soon after.
This seems to be a common problem when writing arm assembly by hand, the solution is specify `.type function`. See also : "As of GCC 4.7, the .type directive is pretty much required for functions. Or, rather, it is required if you want ARM and Thumb interworking to work."
A remaining question is whether we should specify the instruction set explicitly when building on arm32, to prevent surprises like this. Preferably with a configure option.