A few years ago, Fabrice Bellard — a PC emulator written in JavaScript. After that, there was at least . But as far as I know, they were all interpreters, while the earlier Qemu written by the same Fabrice Bellard, and probably any self-respecting modern emulator, uses JIT compilation of guest code into host system code. I thought it was time to implement the opposite task to what browsers solve: JIT compilation of machine code into JavaScript, for which porting Qemu seemed the most logical. You might wonder why Qemu in particular, as there are simpler and more user-friendly emulators — VirtualBox, for example — just install it and it works. But Qemu has several interesting features
- open source
- ability to work without a kernel driver
- ability to operate in interpreter mode
- support for a wide range of both host and guest architectures
Regarding the third point, I can now clarify that in TCI mode, it is not the guest machine instructions themselves that are interpreted, but the bytecode generated from them. However, this doesn't change the essence — to build and run Qemu on a new architecture, if you're lucky, a C compiler is enough — you can postpone writing a code generator.
And so, after two years of slow tinkering with the Qemu source code in my free time, a working prototype emerged where you can already run, for example, Kolibri OS.
What is Emscripten
Nowadays, many compilers have emerged, the end result of which is JavaScript. Some, such as TypeScript, were originally designed as a better way to write for the web. Meanwhile, Emscripten is a way to take existing C or C++ code and compile it into a form understandable by the browser. So far, a considerable number of ports of well-known programs have been made: , for example, you can take a look at PyPy — by the way, they claim to already have JIT. In reality, not every program can simply be compiled and run in a browser — there are a number of , which one has to deal with; however, as the inscription on this page says, "Emscripten can be used to compile almost any portable C/C++ code to JavaScript. There are a number of operations that result in undefined behavior according to the standard but usually work on x86 — for example, unaligned access to variables, which is completely prohibited on some architectures. Overall, Qemu is a cross-platform program and, one hoped, does not contain a lot of undefined behavior — just take it and compile, then fiddle a bit with JIT — and it's ready! But it turns out it wasn't that simple…
First attempt
Generally speaking, I am not the first to think about porting Qemu to JavaScript. The question was raised on the ReactOS forum about whether this could be done using Emscripten. Earlier, there were rumors that Fabrice Bellard himself had done it, but it was about jslinux, which, as far as I know, is an attempt to achieve sufficient performance on JS manually and was written from scratch. Later, Virtual x86 was created — its unobfuscated source code was released, and it was claimed that the increased "realism" of the emulation allowed for the use of SeaBIOS as firmware. Additionally, there was at least one attempt to port Qemu using Emscripten — someone tried to do this. , but the development, as I understood, was frozen.
So, it seemed that here are the sources, and here is Emscripten — just take it and compile. But there are also libraries that Qemu depends on, and libraries that those libraries depend on, etc., one of which is , which glib depends on. There were rumors on the internet that there is one in a large collection of library ports for Emscripten, but it was hard to believe: firstly, it wasn’t compiling with the new compiler, secondly, it’s too low-level a library to just be taken and compiled to JS. And it’s not just about assembler inserts — probably, if you get creative, for some calling conventions you can form the required arguments on the stack and call the function without them. But Emscripten is a tricky thing: in order for the generated code to look familiar to the JS engine optimizer in the browser, certain tricks are employed. In particular, so-called relooping — the code generator takes the received LLVM IR with some abstract transition instructions and attempts to recreate plausible if-statements, loops, etc. And how are the arguments in functions passed? Naturally, as arguments of JS functions, meaning as much as possible not through the stack.
At first, I thought about simply writing a replacement for libffi in JS and running the standard tests, but in the end, I got tangled up in how to make my header files work with the existing code — what can you do, as they say, "Either the tasks are too complex, or we are too dumb." I had to port libffi to yet another architecture, so to speak — fortunately, in Emscripten, there are both macros for inline assembly (in JavaScript, yes — well, whatever architecture, that’s the assembler) and the ability to execute generated code on the fly. So, after toiling with platform-dependent fragments of libffi for a while, I received some compilable code and ran it on the first test I came across. To my surprise, the test passed successfully. Bewildered by my genius — it’s no joke, it worked on the first try — I, still not believing my eyes, took another look at the resulting code to evaluate where to dig further. Here I was stunned a second time — the only thing my function did ffi_call — was report a successful call. There was no actual call. Thus, I sent my first pull request, fixing an error in the test that was obvious to any Olympiad participant — real numbers shouldn’t be compared as a == b and not even as a - b < EPS — we shouldn't forget the module, otherwise 0 might just equal 1/3… In general, I managed to create a kind of libffi port that passes the simplest tests and compiles glib — I decided that if needed, I'll finish it later. To jump ahead, I should mention that, as it turned out, the final code of the libffi function wasn't even included by the compiler.
But as I've mentioned, there are some limitations, and among the free use of various undefined behaviors, there's a particularly unpleasant feature — JavaScript, by design, does not support multithreading with shared memory. Overall, this can usually be considered a decent idea, but not when porting code that is architected around C threads. Generally speaking, experiments are ongoing in Firefox to support shared workers, and a pthread implementation for them exists in Emscripten, but I didn't want to depend on that. I had to slowly extract multithreading from the Qemu code — that is, find where threads are launched, extract the body of the loop executing in that thread into a separate function, and call such functions in sequence from the main loop.
Second attempt
At some point, it became clear that things had not progressed, and that the random stuffing of hacks into the code wouldn't lead to good results. The conclusion: I needed a way to systematize the process of adding hacks. So, I took the latest version 2.4.1 at that time (not 2.5.0, because who knows, it might have some untracked bugs in the new version, and I have enough of my own bugs), and the first thing I did was safely rewrite thread-posix.c. I mean in a safe way: if someone tried to perform an operation that would lead to a block, the function was immediately called abort() — of course, this didn't immediately solve all problems, but at least it felt better than silently encountering data inconsistency.
In general, when porting code to JS, Emscripten options are very helpful -s ASSERTIONS=1 -s SAFE_HEAP=1 — they catch certain types of undefined behavior like accesses to unaligned addresses (which does not align with code for typed arrays like HEAP32[addr >> 2] = 1) or calling a function with the wrong number of arguments.
By the way, alignment issues are a separate topic. As I mentioned earlier, Qemu has a "degenerate" interpreting backend, TCI (tiny code interpreter), and to build and run Qemu on a new architecture, if you're lucky, a C compiler is enough. Key words "if you're lucky". Unfortunately, I wasn't lucky, and it turned out that TCI, when parsing its bytecode, uses unaligned access. This means that on architectures like ARM and others that require aligned access, Qemu compiles because there is a proper TCG backend generating native code, but whether TCI will actually work on them is still a question. However, as it turns out, something similar was clearly stated in the TCI documentation. As a result, function calls for unaligned reading were added to the code, which were found in another part of Qemu.
Heap corruption
. In the end, unaligned access in TCI was fixed, a main loop was created, sequentially calling the processor, RCU, and some minor tasks. Now I'm running Qemu with the option -d exec,in_asm,out_asm, indicating that it should report which code blocks are being executed, and at the time of translation, note what guest code was used and what host code became (in this case, bytecode). It starts, executes several translation blocks, outputs the debug message I left, stating that RCU is about to start, and… crashes at abort() inside the function free(). By probing the function free() , it was found that in the header of the heap block, which lies in the eight bytes preceding the allocated memory, instead of the block size or something similar, there was garbage.
Heap corruption – how charming... In such cases, there’s a useful tool – to (if possible) create a native binary from (the same) source files and run it under Valgrind. After a while, the binary was ready. I run it with the same options – it crashes again during initialization, not even reaching the actual execution. It’s unpleasant, of course – it seems the source files weren't exactly the same, which isn't surprising, as configure found a few different options. But I have Valgrind – I’ll fix this bug first, and then, if I'm lucky, the original one might appear. I run everything under Valgrind… Whoa, it launched, initialized properly and moved forward past the original bug without a single warning about incorrect memory access, let alone crashing. Life has not prepared me for this – a crashing program stops crashing when launched under Valgrind. What was that – a mystery. My hypothesis is that since there was a valid pointer involved with the use of either memset- with a valid pointer using either mmx, or xmm registers, it might have been some kind of alignment error, although it's still hard to believe.
Okay, Valgrind doesn't seem to be a helper here. And here's where the most unpleasant part began — everything seemed to start, but it crashed for absolutely unknown reasons due to an event that could have happened millions of instructions ago. For a long time, it was unclear how to even approach it. Eventually, I had to sit down and debug it. Printing what was written in the header showed that it looked more like some binary data rather than a number. And, lo and behold, this binary string was found in the BIOS file — which means it could now be said with sufficient confidence that it was a buffer overflow, and it was even clear what was being written to that buffer. Well, then it was like this — in Emscripten, fortunately, there is no address space randomization, and there are no holes in it either, so it’s possible to write somewhere in the middle of the code to output data via a pointer from the previous run, check the data, look at the pointer, and if it hasn't changed, gain some information for reflection. However, two minutes are spent on linking after any changes, but what can you do. As a result, a specific line was found that copies BIOS from the temporary buffer to the guest memory — and indeed, there was not enough space in the buffer. Tracing the source of that strange buffer address led to the function qemu_anon_ram_alloc in the file oslib-posix.c — the logic was like this: sometimes it can be beneficial to align the address to a huge page size of 2 MB, for this we will ask for mmap a little more at first, and then return the excess using munmap. And if such alignment is not required, then we specify instead of 2 MB the result getpagesize() — mmap still gives an aligned address… So, in Emscripten mmap it just calls malloc, and it naturally does not align by the page. In general, the bug that frustrated me for a couple of months was fixed by a change in two lines.
Function calling conventions
And now the processor is calculating something, Qemu doesn't crash, but the screen is not turning on, and the processor quickly enters a loop, judging by the output. -d exec,in_asm,out_asmA hypothesis has emerged: timer interrupts (or perhaps all interrupts) are not being received. Indeed, if we disable the interrupts from the native build, which for some reason worked, we end up with a similar picture. However, the solution was not at all related to this: comparing the traces generated with the aforementioned option showed that the execution paths diverge very early. It's worth mentioning that comparing the debug output recorded with the launcher emrun with the output of the native build is not a straightforward process. I am not entirely sure how a program running in the browser connects with emrun, but some lines in the output appear to be swapped, so a difference in the diff does not necessarily mean that the execution paths have diverged. Overall, it became clear that under the instruction ljmpl there is a transition to different addresses, and the bytecode is fundamentally different: one contains a call to a C helper function, while the other does not. After googling the instructions and studying the code that translates these instructions, it became clear that, firstly, directly before it in the register cr0 there was a writing operation — also performed by a helper — that transitions the processor to protected mode, and secondly, that the js version never actually transitioned to protected mode. The issue is that another feature of Emscripten is its unwillingness to accept code like the implementation of the instruction call in TCI, which converts any function pointer to the type long long f(int arg0, .. int arg9) — functions must be called with the correct number of arguments. If this rule is violated, depending on the debugging settings, the program will either crash (which is good) or call the wrong function (which will be sad to debug). There's also a third option — to enable the generation of wrappers that add/remove arguments, but overall these wrappers take up quite a bit of space, given that I only need a little over a hundred wrappers. Just this alone is quite unfortunate, but there turned out to be a more serious problem: in the generated code of the wrapper functions, arguments were converted and converted, but the function with the generated arguments was sometimes not called — just like in my implementation of libffi. In other words, some helpers simply did not execute.
Fortunately, Qemu has machine-readable helper lists in the form of a header file like
DEF_HELPER_0(lock, void)
DEF_HELPER_0(unlock, void)
DEF_HELPER_3(write_eflags, void, env, tl, i32)They are used quite amusingly: first, the macros are overridden in the most peculiar way DEF_HELPER_n, and then it gets included helper.h. All the way to the point where the macro expands into a structure initializer and a comma, and then an array is defined, but instead of elements — #include <helper.h> As a result, I finally had a reason to try out the library , and I wrote a script that generates wrappers exactly for those functions that are needed.
And so, after this, the processor seemed to have started working. Seemed to, because the screen never initialized, although I managed to run memtest86+ in the native build. I should clarify here that the Qemu block I/O code is written using coroutines. Emscripten has its own rather convoluted implementation, but it still needed to be supported in the Qemu code, and the processor could be debugged right now: Qemu supports the options -kernel, -initrd, -append, which allow loading Linux or, for example, memtest86+ without using block devices at all. But here’s the catch: in the native build, the output of the Linux kernel could be observed on the console with the option -nographic, but from the browser, there was no output to the terminal from which it was launched emrun, so it was unclear: is the processor not working or the graphics output? Then it occurred to me to wait a bit. It turned out that "the processor is not asleep, but just blinking slowly," and after about five minutes, the kernel dumped a batch of messages to the console and continued to hang. It became clear that the processor, on the whole, works, and I needed to dig into the SDL2 handling code. Unfortunately, I am not familiar with this library, so I had to operate somewhat blindly at times. At one point, a line parallel0 flashed on the blue background, which led to some thoughts. In the end, it turned out that the issue was that Qemu opens several virtual windows in one physical window, which can be switched using Ctrl-Alt-n: in the native build, it works, but in Emscripten — it does not. After getting rid of the unnecessary windows using the options -monitor none -parallel none -serial none and forcing the entire screen to redraw on each frame, everything suddenly started working.
Coroutines
So, the browser emulation works, but there’s nothing interesting to run in it because there’s no block I/O — support for coroutines needs to be implemented. Qemu already has several coroutine backends, but due to the specifics of JavaScript and the Emscripten code generator, you can't just take it and start juggling stacks. It may seem like everything is lost, but the Emscripten developers have already taken care of everything. It’s been implemented quite amusingly: let’s call suspicious function calls like emscripten_sleep and several others using the Asyncify mechanism, as well as pointer calls and calls to any function where one of the previous two cases could occur further down the stack. Now, before each suspicious call, we allocate an async context, and immediately after the call, we check if an asynchronous call occurred, and if it did, we save all local variables in this async context, indicate which function to pass control to when execution needs to continue, and exit the current function. That is where there’s room to explore the effect of — for the purpose of resuming code execution after returning from an asynchronous call, the compiler generates "stubs" of functions, starting after the suspicious call — like this: if there are n suspicious calls, the function will be fragmented about n/2 times — and this is without considering that part of the local variables needs to be saved after each potentially asynchronous call. Eventually, I even had to write a simple Python script that, for a given set of particularly fragmented functions, which presumably "do not pass through asynchronicity" (meaning they don’t trigger stack unwinding and everything I just described), specifies which pointer calls should be ignored by the compiler so that those functions aren’t treated as asynchronous. After all, JS files of 60 MB — that’s clearly excessive — let’s at least keep it to 30. Although, once I configured the build script and accidentally removed the linker options, among which was -O3I run the generated code, and Chromium hogs memory and crashes. Later, I accidentally looked at what it was trying to load... Well, what can I say, I would also freeze if I were asked to thoughtfully study and optimize JavaScript at over 500 MB.
Unfortunately, the checks in the Asyncify support library code did not quite mesh with longjmp-s used in the virtual processor's code, but after a small patch that disables these checks and forcibly restores contexts as if everything were fine, the code worked. And then the strange part began: sometimes the checks in the synchronization code triggered — the ones that terminate the code if logically it should block — someone tried to acquire an already acquired mutex. Fortunately, this was not a logical problem in the serialized code — I simply used the built-in main loop functionality provided by Emscripten, but sometimes an asynchronous call fully unwound the stack, and at that moment the setTimeout from the main loop would trigger — thus, the code would enter an iteration of the main loop without exiting the previous iteration. I rewrote it in an infinite loop, and emscripten_sleep, and the mutex problems ceased. The code even became more logical — after all, I essentially don’t have some code that prepares the next frame of animation — the processor just computes something and the screen updates periodically. However, the problems did not stop there: sometimes the execution of Qemu would just quietly terminate without any exceptions or errors. At that moment, I let it go, but to get ahead of myself, the problem was this: the coroutine code actually does not use setTimeout (or at least not as often as one might think): the function emscripten_yield simply sets an asynchronous call flag. The crux of the matter is that emscripten_coroutine_next is not an asynchronous function: it checks the flag, resets it, and hands control where it needs to go. So, the stack unwinding ends there. The problem was that due to a use-after-free that appeared when the coroutine pool was disabled because I didn’t copy an important line of code from the existing coroutine backend, the function qemu_in_coroutine returned true when it should have returned false. This led to a call. emscripten_yield, above which there was no stack. emscripten_coroutine_next, the stack stretched all the way to the top, but there were no setTimeout, as I mentioned before, it wasn't exposed.
JavaScript code generation
And here's the promised 'reversing the grind'. Actually not. Of course, if you run Qemu in a browser and in it — Node.js, then naturally, after code generation in Qemu, we will get a completely different JavaScript. But still, it's some form of reverse transformation.
First, a bit about how Qemu works. I must ask for your forgiveness right away: I am not a professional Qemu developer, and my conclusions may be incorrect at times. As they say, 'a student's opinion does not have to coincide with that of the teacher, Peano's axioms, and common sense.' Qemu has a certain number of supported guest architectures, and for each, there is a directory like target-i386. During compilation, you can specify support for multiple guest architectures, but as a result, you will simply get several binaries. The code for supporting the guest architecture, in turn, generates certain internal operations of Qemu, which TCG (Tiny Code Generator) then transforms into machine code of the host architecture. As stated in the readme file located in the tcg directory, this was originally part of a regular C compiler, which was later adapted for JIT. Therefore, for example, the target architecture in terms of this document is no longer the guest architecture, but the host architecture. At some point, another component appeared — Tiny Code Interpreter (TCI), which is supposed to execute the code (practically the same internal operations) in the absence of a code generator for the specific host architecture. In fact, as mentioned in its documentation, this interpreter may not always work as well as the JIT code generator, both quantitatively in terms of speed and qualitatively. Although I'm not sure its description is entirely current.
At first, I tried to create a full-fledged TCG backend but quickly got confused in the source code and the not entirely clear description of the bytecode instructions, so I decided to wrap the TCI interpreter. This provided several advantages immediately:
- when implementing the code generator, it was possible to look at the code of the interpreter instead of the instruction descriptions.
- Functions can be generated not for every encountered translation block, but for example, only after the hundredth execution.
- In case the generated code changes (which seems possible, judging by functions with names containing the word patch), I will need to invalidate the generated JS code, but at least I will have a basis to regenerate it.
Regarding the third point, I'm not sure if patching is possible after the code has been executed for the first time, but the first two points are sufficient.
Initially, the code was generated as a large switch statement based on the address of the original bytecode instruction, but later, recalling an article about Emscripten, optimizing the generated JS, and relooping, I decided to generate more human-readable code, especially since empirically, it turned out that the only entry point into the translation block is its beginning. Said and done, after some time a code generator was created, generating code with if statements (though without loops). But there was a problem; it crashed, giving a message that the instruction had some incorrect length. Meanwhile, the last instruction at this level of recursion was brcond. Alright, I will add an identical check in the generation of this instruction before the recursive call and after, and... neither of them executed, but after the switch on assert, it still crashed. Ultimately, studying the generated code, I realized that after the switch the pointer to the current instruction is reloaded from the stack and is likely being overwritten by the generated JavaScript code. And it turned out to be the case. Increasing the buffer from one megabyte to ten changed nothing, and it became clear that the code generator was running in circles. I had to check that we didn’t go beyond the current TB, and if we did, to output the address of the next TB with a minus sign so that execution could continue. Also, this solves the problem of "which generated functions to invalidate if this snippet of bytecode has changed?" — we need to invalidate only the function that corresponds to this translation block. By the way, although I debugged everything in Chromium (since I use Firefox and it's easier for me to use a separate browser for experiments), Firefox helped me fix compatibility issues with the asm.js standard, after which the code started to run faster in Chromium.
Example of generated code
Compiling 0x15b46d0:
CompiledTB[0x015b46d0] = function(stdlib, ffi, heap) {
"use asm";
var HEAP8 = new stdlib.Int8Array(heap);
var HEAP16 = new stdlib.Int16Array(heap);
var HEAP32 = new stdlib.Int32Array(heap);
var HEAPU8 = new stdlib.Uint8Array(heap);
var HEAPU16 = new stdlib.Uint16Array(heap);
var HEAPU32 = new stdlib.Uint32Array(heap);
var dynCall_iiiiiiiiiii = ffi.dynCall_iiiiiiiiiii;
var getTempRet0 = ffi.getTempRet0;
var badAlignment = ffi.badAlignment;
var _i64Add = ffi._i64Add;
var _i64Subtract = ffi._i64Subtract;
var Math_imul = ffi.Math_imul;
var _mul_unsigned_long_long = ffi._mul_unsigned_long_long;
var execute_if_compiled = ffi.execute_if_compiled;
var getThrew = ffi.getThrew;
var abort = ffi.abort;
var qemu_ld_ub = ffi.qemu_ld_ub;
var qemu_ld_leuw = ffi.qemu_ld_leuw;
var qemu_ld_leul = ffi.qemu_ld_leul;
var qemu_ld_beuw = ffi.qemu_ld_beuw;
var qemu_ld_beul = ffi.qemu_ld_beul;
var qemu_ld_beq = ffi.qemu_ld_beq;
var qemu_ld_leq = ffi.qemu_ld_leq;
var qemu_st_b = ffi.qemu_st_b;
var qemu_st_lew = ffi.qemu_st_lew;
var qemu_st_lel = ffi.qemu_st_lel;
var qemu_st_bew = ffi.qemu_st_bew;
var qemu_st_bel = ffi.qemu_st_bel;
var qemu_st_leq = ffi.qemu_st_leq;
var qemu_st_beq = ffi.qemu_st_beq;
function tb_fun(tb_ptr, env, sp_value, depth) {
tb_ptr = tb_ptr|0;
env = env|0;
sp_value = sp_value|0;
depth = depth|0;
var u0 = 0, u1 = 0, u2 = 0, u3 = 0, result = 0;
var r0 = 0, r1 = 0, r2 = 0, r3 = 0, r4 = 0, r5 = 0, r6 = 0, r7 = 0, r8 = 0, r9 = 0;
var r10 = 0, r11 = 0, r12 = 0, r13 = 0, r14 = 0, r15 = 0, r16 = 0, r17 = 0, r18 = 0, r19 = 0;
var r20 = 0, r21 = 0, r22 = 0, r23 = 0, r24 = 0, r25 = 0, r26 = 0, r27 = 0, r28 = 0, r29 = 0;
var r30 = 0, r31 = 0, r41 = 0, r42 = 0, r43 = 0, r44 = 0;
r14 = env|0;
r15 = sp_value|0;
START: do {
r0 = HEAPU32[((r14 + (-4))|0) >> 2] | 0;
r42 = 0;
result = ((r0|0) != (r42|0))|0;
HEAPU32[1445307] = r0;
HEAPU32[1445321] = r14;
if(result|0) {
HEAPU32[1445322] = r15;
return 0x0345bf93|0;
}
r0 = HEAPU32[((r14 + (16))|0) >> 2] | 0;
r42 = 8;
r0 = ((r0|0) - (r42|0))|0;
HEAPU32[(r14 + (16)) >> 2] = r0;
r1 = 8;
HEAPU32[(r14 + (44)) >> 2] = r1;
r1 = r0|0;
HEAPU32[(r14 + (40)) >> 2] = r1;
r42 = 4;
r0 = ((r0|0) + (r42|0))|0;
r2 = HEAPU32[((r14 + (24))|0) >> 2] | 0;
HEAPU32[1445307] = r0;
HEAPU32[1445308] = r1;
HEAPU32[1445309] = r2;
HEAPU32[1445321] = r14;
HEAPU32[1445322] = r15;
qemu_st_lel(env|0, r0|0, r2|0, 34, 22759218);
if(getThrew() | 0) abort();
r0 = 3241038392;
HEAPU32[1445307] = r0;
r0 = qemu_ld_leul(env|0, r0|0, 34, 22759233)|0;
if(getThrew() | 0) abort();
HEAPU32[(r14 + (24)) >> 2] = r0;
r1 = HEAPU32[((r14 + (12))|0) >> 2] | 0;
r2 = HEAPU32[((r14 + (40))|0) >> 2] | 0;
HEAPU32[1445307] = r0;
HEAPU32[1445308] = r1;
HEAPU32[1445309] = r2;
qemu_st_lel(env|0, r2|0, r1|0, 34, 22759265);
if(getThrew() | 0) abort();
r0 = HEAPU32[((r14 + (24))|0) >> 2] | 0;
HEAPU32[(r14 + (40)) >> 2] = r0;
r1 = 24;
HEAPU32[(r14 + (52)) >> 2] = r1;
r42 = 0;
result = ((r0|0) == (r42|0))|0;
if(result|0) {
HEAPU32[1445307] = r0;
HEAPU32[1445308] = r1;
}
HEAPU32[1445307] = r0;
HEAPU32[1445308] = r1;
return execute_if_compiled(22759392|0, env|0, sp_value|0, depth|0) | 0;
return execute_if_compiled(23164080|0, env|0, sp_value|0, depth|0) | 0;
break;
} while(1); abort(); return 0|0;
}
return {tb_fun: tb_fun};
}(window, CompilerFFI, Module.buffer)["tb_fun"]Conclusion
So, the work is still not finished, but secretly I got tired of perfecting this long-term project. Therefore, I decided to publish what I have for now. The code is a bit scary in places as this is an experiment, and it's unclear what needs to be done in advance. Perhaps later it would be worth creating proper atomic commits on top of some more modern version of Qemu. For now, there is a branch in the git in blog format: each 'level' reached has a detailed comment in Russian. Essentially, this article is largely a retelling of the output. git log.
You can try all of this (caution, traffic).
What works right now:
- The virtual x86 processor works
- There is a working prototype of a JIT code generator from machine code to JavaScript
- There is a framework for building other 32-bit guest architectures: you can currently admire Linux for the MIPS architecture hanging in the browser during the loading stage
What else can be done
- Speed up the emulation. Even in JIT mode, it seems to work slower than Virtual x86 (but potentially there is a whole Qemu with a large amount of emulated hardware and architectures)
- Create a proper interface — to be honest, I am not a great web developer, so for now, I have modified the standard Emscripten shell as best as I could
- Try to run more complex Qemu functions — networking, VM migration, etc.
- UPD: I will need to submit my few contributions and bug reports to the upstream Emscripten, as previous Qemu porters and other projects did. Thanks to them for enabling me to implicitly benefit from their contributions to Emscripten within my task.
Source: habr.com
