A high‑level view of box86/box64 and a low‑level view of the dynarec
This article explains the technical details about how box86 and box64 works (at the high level for the start and a low level for the dynarec).
At the start, box86 (and box64) will extract useful informations about the executable being run: where is the executable code, where to put it, the needed libraries…
For each library, it will then try to load a native library if it is registered, or if it isn’t it will try to find a version to emulate. If the library is emulated, it will receive the same treatment as the executable, otherwise it will be treated separately and load the native library (the twist).
Finally, it will apply relocations: in the executable, function addresses are not hardcoded but left for the linker (or, here, box86/64) to give. This allows for box86/64 to do its magic: it will set the native functions’ address to point to a certain signature and metadata, which will alert box86/64’s emulator and dynarec about this so-called “bridge” (between the emulated and native world). This enables the “native calls” which is the strength of box86/64.
Then, once everything is loaded, the main loop will start. If the dynarec (dynamic recompiler) is enabled, it will try to recompile the next “block” in “four” passes (starting at pass 0), a block being a contiguous list of emulated instructions, ending “somewhere” (there is a lot of possibilities here):
- Pass 0 consists of counting the number of x86/amd64 instructions that are in the block.
- Some memory is allocated for each of those instructions.
- Pass 1 consists of checking all jumps (whether they go to the same block or require linking to another block), and setting the used and set flags for each instructions (some flags are costly to calculate, so it avoids calculating them if necessary).
- The real flags propagation happens: each instruction can require some/all flags, and can set some/all flags. This will make each instruction “know” if and which flags must be set after its execution.
- Pass 2 consists of counting the number of ARM (native) instructions in the block.
- Read-write-executable memory is allocated for those instructions.
- Pass 3 consists of actually generating all instructions.
Finally, once the block is generated, it is set in a global table containing a map of emulated address to native address, and finally execution starts.
If an instruction cannot be dynarec-ed, it will be emulated instead if possible, or otherwise box86 will simply crash.
Of course, the actual mechanism is more complex than that: for example, there are JITs (mono) and self-modifying code that need to be supported. But, this is the “basic” idea behind box86 (and box64).