具有 JIT 支持的 Qemu.js:您仍然可以向后转动肉末

几年前法布里斯·贝拉德 由 jslinux 编写 是一个用 JavaScript 编写的 PC 模拟器。 之后至少还有更多 虚拟x86。 但据我所知,它们都是解释器,而 Qemu(很早由同一位 Fabrice Bellard 编写)以及可能任何有自尊的现代模拟器都使用来宾代码的 JIT 编译为主机系统代码。 在我看来,是时候实现与浏览器解决的任务相反的任务了:将机器代码 JIT 编译为 JavaScript,为此移植 Qemu 似乎是最合乎逻辑的。 看起来,为什么 Qemu 有更简单且用户友好的模拟器 - 例如相同的 VirtualBox - 安装并运行。 但 Qemu 有几个有趣的功能

  • 开源
  • 无需内核驱动程序即可工作的能力
  • 能够在口译模式下工作
  • 支持大量主机和来宾架构

关于第三点,我现在可以解释一下,其实在TCI模式下,解释的并不是客户机器指令本身,而是从中获得的字节码,但这并没有改变本质——为了构建和运行新架构上的 Qemu,如果幸运的话,一个 C 编译器就足够了——可以推迟编写代码生成器。

现在,经过两年空闲时间悠闲地修改 Qemu 源代码,一个工作原型出现了,您已经可以在其中运行 Kolibri OS 等。

什么是 Emscripten

现在出现了很多编译器,最终的结果就是JavaScript。 有些(例如 Type Script)最初旨在成为网络编写的最佳方式。 同时,Emscripten 是一种获取现有 C 或 C++ 代码并将其编译为浏览器可读形式的方法。 在 此页 我们收集了很多知名程序的端口: 这里例如,您可以查看 PyPy - 顺便说一句,他们声称已经有了 JIT。 事实上,并不是每个程序都可以简单地在浏览器中编译和运行 - 有很多程序 特点,但是,您必须忍受这一点,因为同一页上的铭文说“Emscripten 可以用来编译几乎任何 手提 C/C++ 代码到 JavaScript”。也就是说,有许多操作根据标准是未定义的行为,但通常在 x86 上工作 - 例如,对变量的未对齐访问,这在某些体系结构上通常是禁止的。一般来说,Qemu 是一个跨平台程序,而且,我想相信,它还没有包含很多未定义的行为 - 接受它并编译,然后用 JIT 修改一下 - 你就完成了!但这不是案件...

先试试

总的来说,我并不是第一个提出将 Qemu 移植到 JavaScript 的想法的人。 ReactOS 论坛上有人询问是否可以使用 Emscripten 实现这一点。 甚至更早之前,就有传言说 Fabrice Bellard 亲自做到了这一点,但我们谈论的是 jslinux,据我所知,这只是一次尝试在 JS 中手动实现足够的性能,并且是从头开始编写的。 后来,编写了 Virtual x86 - 为其发布了未混淆的源代码,并且如上所述,仿真的更大“真实性”使得使用 SeaBIOS 作为固件成为可能。 此外,至少有一次尝试使用 Emscripten 移植 Qemu - 我尝试这样做 套接字对,但据我了解,发展被冻结了。

所以,看起来,这里是源代码,这里是 Emscripten - 获取并编译。 但也有Qemu所依赖的库,以及那些库所依赖的库等等,其中之一就是 库菲,glib 取决于哪个。 网上有传言说 Emscripten 的大量移植库中有一个,但有点难以置信:首先,它并不是要成为一个新的编译器,其次,它的级别太低了。直接获取库并编译为 JS。 这不仅仅是汇编插入的问题 - 也许,如果你扭转它,对于某些调用约定,你可以在堆栈上生成必要的参数并在没有它们的情况下调用函数。 但 Emscripten 是一件棘手的事情:为了使生成的代码对于浏览器 JS 引擎优化器来说看起来很熟悉,使用了一些技巧。 特别是,所谓的重新循环 - 使用接收到的 LLVM IR 和一些抽象转换指令的代码生成器尝试重新创建合理的 if、循环等。 那么,参数是如何传递给函数的呢? 当然,作为JS函数的参数,也就是说,如果可能的话,不要通过堆栈。

一开始有一个想法,简单地用 JS 编写 libffi 的替代品并运行标准测试,但最终我对如何制作头文件以便它们能够与现有代码一起工作感到困惑 - 我能做什么,正如他们所说,“任务有那么复杂吗?”“我们有那么愚蠢吗?” 我必须将 libffi 移植到另一个体系结构,可以这么说 - 幸运的是,Emscripten 具有用于内联汇编的宏(在 Javascript 中,是的 - 好吧,无论什么体系结构,所以汇编器),以及运行动态生成的代码的能力。 总的来说,在对依赖于平台的 libffi 片段进行了一段时间的修改之后,我得到了一些可编译的代码,并在我遇到的第一个测试中运行了它。 令我惊讶的是,测试成功了。 被我的天才震惊了 - 不是开玩笑,它从第一次启动就起作用了 - 我仍然不敢相信自己的眼睛,再次查看生成的代码,以评估下一步该在哪里挖掘。 在这里我第二次发疯了——我的函数唯一做的就是 ffi_call - 这报告了一次成功的呼叫。 本身没有电话。 因此,我发送了第一个拉取请求,该请求纠正了测试中的一个错误,任何奥林匹克学生都清楚这一错误 - 真实数字不应与 a == b 甚至如何 a - b < EPS - 你还需要记住模块,否则 0 将非常等于 1/3... 一般来说,我想出了 libffi 的某个端口,它通过了最简单的测试,并且 glib 与它一起使用已编译 - 我认为这是必要的,稍后我会添加它。 展望未来,我要说的是,事实证明,编译器甚至没有在最终代码中包含 libffi 函数。

但是,正如我已经说过的,存在一些限制,并且在各种未定义行为的自由使用中,隐藏了一个更令人不快的功能 - JavaScript 在设计上不支持共享内存的多线程。 原则上,这通常甚至可以被称为一个好主意,但不适用于移植其体系结构与 C 线程相关的代码。 一般来说,Firefox 正在尝试支持共享工作线程,Emscripten 为它们提供了 pthread 实现,但我不想依赖它。 我必须慢慢地从 Qemu 代码中根除多线程 - 也就是说,找出线程在哪里运行,将在该线程中运行的循环体移动到一个单独的函数中,并从主循环中一一调用这些函数。

第二次尝试

在某些时候,很明显问题仍然存在,随意地在代码周围推拐杖不会带来任何好处。 结论:我们需要以某种方式系统化添加拐杖的过程。 因此,采用了当时新鲜的2.4.1版本(不是2.5.0,因为,你永远不知道,新版本中会有尚未捕获的bug,而我自己的已经够多了) bug),我做的第一件事就是安全地重写它 thread-posix.c。 嗯,也就是说,同样安全:如果有人试图执行导致阻塞的操作,该函数会立即被调用 abort() ——当然,这并不能一次性解决所有问题,但至少比悄悄收到不一致的数据要令人愉快一些。

总的来说,Emscripten 选项对于将代码移植到 JS 非常有帮助 -s ASSERTIONS=1 -s SAFE_HEAP=1 - 它们捕获某些类型的未定义行为,例如对未对齐地址的调用(这与类型化数组的代码完全不一致,例如 HEAP32[addr >> 2] = 1) 或调用参数数量错误的函数。

顺便说一句,对齐错误是一个单独的问题。 正如我已经说过的,Qemu 有一个用于代码生成 TCI(微型代码解释器)的“退化”解释后端,并且要在新架构上构建和运行 Qemu,如果幸运的话,一个 C 编译器就足够了。 “如果你幸运的话”。 我很不幸,结果发现 TCI 在解析其字节码时使用了未对齐的访问。 也就是说,在各种 ARM 和其他具有必要级别访问权限的架构上,Qemu 可以编译,因为它们有一个生成本机代码的普通 TCG 后端,但 TCI 是否适用于它们是另一个问题。 然而,事实证明,TCI 文档清楚地表明了类似的内容。 结果,代码中添加了未对齐读取的函数调用,这是在 Qemu 的另一部分中发现的。

堆破坏

结果,对 TCI 的未对齐访问得到了纠正,创建了一个主循环,依次调用处理器、RCU 和其他一些小东西。 所以我启动 Qemu 并选择 -d exec,in_asm,out_asm,这意味着您需要说明正在执行哪些代码块,并且在广播时写入来宾代码是什么,主机代码变成了什么(在本例中为字节码)。 它启动,执行几个翻译块,写入我留下的调试消息,RCU 现在将启动,然后......崩溃 abort() 在函数内部 free()。 通过修改函数 free() 我们设法发现,在堆块的标头(位于分配的内存之前的八个字节)中,存在垃圾,而不是块大小或类似的东西。

堆的破坏 - 多么可爱......在这种情况下,有一个有用的补救措施 - 从(如果可能)相同的来源,组装一个本机二进制文件并在 Valgrind 下运行它。 一段时间后,二进制文件就准备好了。 我使用相同的选项启动它 - 即使在初始化期间,在实际执行之前它也会崩溃。 当然,这令人不愉快 - 显然,来源并不完全相同,这并不奇怪,因为配置发现了略有不同的选项,但我有 Valgrind - 首先我会修复这个错误,然后,如果我幸运的话,就会出现原来的。 我在 Valgrind 下运行同样的东西...Y-y-y,y-y-y,呃-呃,它开始了,正常地进行了初始化,并继续过去的原始错误,没有关于不正确的内存访问的任何警告,更不用说跌倒了。 正如他们所说,生活并没有让我为此做好准备——一个崩溃的程序在 Walgrind 下启动时就不再崩溃了。 那是什么是个谜。 我的假设是,一旦在初始化过程中崩溃后位于当前指令附近,gdb 就会显示工作 memset-a 使用有效指针 mmx,无论是 xmm 寄存器,那么也许是某种对齐错误,尽管仍然很难相信。

好吧,Valgrind 似乎在这里没有帮助。 最恶心的事情开始了——一切似乎都开始了,但是由于一个可能在数百万条指令之前发生的事件而由于绝对未知的原因而崩溃。 很长一段时间,连如何接近都不清楚。 最终我还是要坐下来调试。 打印标题被重写的内容表明它看起来不像数字,而是某种二进制数据。 而且,你瞧,这个二进制字符串是在 BIOS 文件中找到的 - 也就是说,现在可以有合理的信心说它是缓冲区溢出,甚至很清楚它被写入了这个缓冲区。 好吧,然后是这样的 - 在 Emscripten 中,幸运的是,地址空间没有随机化,也没有漏洞,所以你可以在代码中间的某个地方编写,通过上次启动的指针输出数据,看数据,看指针,如果没有变化,就深思熟虑。 确实,任何更改后都需要几分钟才能链接,但是您能做什么呢? 结果,发现了一条特定的行将 BIOS 从临时缓冲区复制到客户内存 - 事实上,缓冲区中没有足够的空间。 找到那个奇怪的缓冲区地址的来源产生了一个函数 qemu_anon_ram_alloc 在文件中 oslib-posix.c - 逻辑是这样的:有时将地址与 2 MB 大小的大页面对齐可能很有用,为此我们会询问 mmap 先多一点,然后我们会在帮助下退还多余的部分 munmap。 如果不需要这样的对齐,那么我们将指示结果而不是 2 MB getpagesize() - mmap 它仍然会给出一个对齐的地址...所以在 Emscripten 中 mmap 只是打电话 malloc,但它当然不会在页面上对齐。 一般来说,一个让我沮丧了几个月的错误已经通过以下更改得到纠正: 线。

调用函数的特点

现在处理器正在计算一些东西,Qemu没有崩溃,但是屏幕没有打开,并且处理器很快进入循环,从输出来看 -d exec,in_asm,out_asm。 出现了一个假设:定时器中断(或者一般来说,所有中断)没有到达。 事实上,如果您从本机程序集中拧下中断(由于某种原因该中断有效),您会得到类似的图片。 但这根本不是答案:与上述选项发出的轨迹的比较表明,执行轨迹很早就出现了分歧。 这里必须说一下使用启动器记录的内容的比较 emrun 使用本机程序集的输出调试输出并不是一个完全机械的过程。 我不知道浏览器中运行的程序如何连接到 emrun,但输出中的某些行被重新排列,因此 diff 的差异还不是假设轨迹已发散的理由。 总的来说,很明显,根据说明 ljmpl 存在到不同地址的转换,并且生成的字节码根本不同:一个包含调用辅助函数的指令,另一个则不包含。 在谷歌上搜索指令并研究翻译这些指令的代码后,很明显,首先,紧邻寄存器中的指令之前 cr0 进行了记录 - 也使用了帮助程序 - 将处理器切换到保护模式,其次,js 版本从未切换到保护模式。 但事实是 Emscripten 的另一个特点是它不愿意容忍指令的执行等代码 call 在TCI中,任何函数指针都会产生类型 long long f(int arg0, .. int arg9) - 必须使用正确数量的参数调用函数。 如果违反此规则,根据调试设置,程序将崩溃(这很好)或根本调用错误的函数(这对于调试来说会很糟糕)。 还有第三个选项 - 启用添加/删除参数的包装器的生成,但这些包装器总共占用了大量空间,尽管事实上我只需要一百多个包装器。 仅此一点就非常令人悲伤,但结果出现了一个更严重的问题:在包装函数的生成代码中,参数被转换再转换,但有时带有生成参数的函数没有被调用 - 好吧,就像我的 libffi 实现。 也就是说,有些帮手根本就没有被处决。

幸运的是,Qemu 有机器可读的帮助器列表,其形式是头文件,例如

DEF_HELPER_0(lock, void)
DEF_HELPER_0(unlock, void)
DEF_HELPER_3(write_eflags, void, env, tl, i32)

它们的使用非常有趣:首先,宏以最奇怪的方式重新定义 DEF_HELPER_n,然后打开 helper.h。 在某种程度上,宏被扩展为结构初始值设定项和逗号,然后定义一个数组,而不是元素 - #include <helper.h> 结果,我终于有机会在工作中尝试一下图书馆了 解析,并编写了一个脚本来准确生成那些包装器,以准确地满足所需的功能。

因此,在那之后处理器似乎开始工作了。 这似乎是因为屏幕从未初始化,尽管 memtest86+ 能够在本机程序集中运行。 这里有必要澄清一下,Qemu块I/O代码是用协程编写的。 Emscripten 有自己非常棘手的实现,但它仍然需要在 Qemu 代码中得到支持,并且您现在可以调试处理器:Qemu 支持选项 -kernel, -initrd, -append,您可以使用它来引导 Linux 或 memtest86+,而无需使用块设备。 但问题是:在本机程序集中,可以通过选项看到 Linux 内核输出到控制台 -nographic,并且浏览器没有输出到启动它的终端 emrun,没有来。 也就是说,不清楚:处理器不工作或图形输出不工作。 然后我想到要等一会儿。 事实证明,“处理器没有休眠,只是缓慢地闪烁”,大约五分钟后,内核将一堆消息扔到控制台上并继续挂起。 很明显,处理器总体上是可以工作的,我们需要深入研究使用 SDL2 的代码。 不幸的是,我不知道如何使用这个库,所以在某些地方我不得不随意行动。 在某个时刻,parallel0 行在屏幕上以蓝色背景闪烁,这表明了一些想法。 最后发现问题在于 Qemu 在一个物理窗口中打开了多个虚拟窗口,您可以使用 Ctrl-Alt-n 在这些虚拟窗口之间进行切换:它在本机构建中有效,但在 Emscripten 中无效。 使用选项摆脱不必要的窗口后 -monitor none -parallel none -serial none 以及在每一帧上强制重绘整个屏幕的指令,一切突然都正常了。

协程

因此,浏览器中的模拟可以工作,但您无法在其中运行任何有趣的单软盘,因为没有块 I/O - 您需要实现对协程的支持。 Qemu 已经有几个协程后端,但由于 JavaScript 和 Emscripten 代码生成器的性质,您不能只是开始处理堆栈。 看起来“一切都消失了,石膏正在被移除”,但 Emscripten 开发人员已经处理好了一切。 这实现起来很有趣:让我们调用这样一个可疑的函数 emscripten_sleep 以及使用 Asyncify 机制的其他几个,以及指针调用和对任何函数的调用,其中前两种情况之一可能发生在堆栈的更深处。 现在,在每个可疑调用之前,我们都会选择一个异步上下文,并且在调用之后,我们会立即检查是否发生了异步调用,如果有,我们将在这个异步上下文中保存所有局部变量,指示哪个函数当我们需要继续执行时将控制权转移到当前函数并退出。 这是有研究效果的地方 挥霍 — 为了从异步调用返回后继续执行代码的需要,编译器在可疑调用后开始生成函数的“存根” — 像这样:如果有 n 个可疑调用,则该函数将在某处展开 n/2次 - 这仍然是,如果不是请记住,在每次潜在的异步调用之后,您需要添加保存一些局部变量到原始函数。 随后,我什至不得不用 Python 编写一个简单的脚本,该脚本基于一组给定的特别过度使用的函数,这些函数据称“不允许异步通过自身传递”(即堆栈提升和我刚才描述的所有内容都不允许)在其中工作),指示通过指针进行的调用,其中的函数应被编译器忽略,以便这些函数不被视为异步。 然后 60 MB 以下的 JS 文件显然太多了 - 假设至少有 30 个。尽管如此,有一次我在设置汇编脚本时,不小心抛出了链接器选项,其中有 -O3。 我运行生成的代码,Chromium 耗尽内存并崩溃。 然后我无意中看到了他试图下载的内容...好吧,我能说什么,如果我被要求仔细研究和优化 500+ MB 的 Javascript,我也会冻结。

不幸的是,Asyncify 支持库代码中的检查并不完全友好 longjmp-s 在虚拟处理器代码中使用,但是在一个小补丁禁用这些检查并强制恢复上下文(就好像一切正​​常一样)之后,代码就可以工作了。 然后奇怪的事情开始了:有时会触发同步代码中的检查 - 如果根据执行逻辑,代码应该被阻止,则相同的检查会导致代码崩溃 - 有人试图获取已捕获的互斥体。 幸运的是,这在序列化代码中并不是一个逻辑问题——我只是使用 Emscripten 提供的标准主循环功能,但有时异步调用会完全解开堆栈,此时它会失败 setTimeout 从主循环 - 因此,代码进入主循环迭代而没有离开前一个迭代。 无限循环重写并 emscripten_sleep,互斥体的问题就停止了。 代码甚至变得更加合乎逻辑——毕竟,事实上,我没有一些准备下一个动画帧的代码——处理器只是计算一些东西,屏幕会定期更新。 然而,问题并没有就此停止:有时 Qemu 执行会简单地终止,没有任何异常或错误。 那一刻我放弃了它,但是,展望未来,我会说问题是这样的:协程代码实际上并没有使用 setTimeout (或者至少不像你想象的那么频繁):函数 emscripten_yield 只需设置异步调用标志即可。 重点是 emscripten_coroutine_next 不是异步函数:它在内部检查标志,重置它并将控制权转移到需要的地方。 也就是说,栈的提升到此结束。 问题是,由于我没有从现有协程后端复制重要的代码行,导致协程池被禁用时出现 use-after-free,该函数 qemu_in_coroutine 返回 true,而实际上它应该返回 false。 这导致了一个电话 emscripten_yield,上面堆栈上没有人 emscripten_coroutine_next,堆栈展开到最顶部,但没有 setTimeout正如我已经说过的,没有展出。

JavaScript 代码生成

事实上,这就是承诺的“把肉末变回来”。 并不真地。 当然,如果我们在浏览器中运行 Qemu,并在其中运行 Node.js,那么很自然地,在 Qemu 中生成代码后,我们会得到完全错误的 JavaScript。 但仍然是某种反向转换。

首先,简单介绍一下 Qemu 的工作原理。 请立即原谅我:我不是专业的 Qemu 开发人员,我的结论可能在某些地方是错误的。 正如他们所说,“学生的观点不必与老师的观点、皮亚诺的公理和常识相一致。” Qemu 有一定数量的受支持的客户架构,每个架构都有一个目录,例如 target-i386。 构建时,您可以指定对多个来宾架构的支持,但结果将只是几个二进制文件。 支持来宾架构的代码反过来会生成一些内部 Qemu 操作,TCG(微型代码生成器)已将其转换为主机架构的机器代码。 正如位于 tcg 目录中的自述文件中所述,这最初是常规 C 编译器的一部分,后来针对 JIT 进行了改编。 因此,例如,本文档中的目标架构不再是客户架构,而是主机架构。 在某个时候,出现了另一个组件 - Tiny Code Interpreter (TCI),它应该在没有特定主机架构的代码生成器的情况下执行代码(几乎相同的内部操作)。 事实上,正如其文档所述,该解释器的性能可能并不总是与 JIT 代码生成器一样好,不仅在速度上如此,而且在质量上也是如此。 尽管我不确定他的描述是否完全相关。

起初,我尝试制作一个成熟的 TCG 后端,但很快就对源代码感到困惑,并且对字节码指令的描述也不完全清晰,因此我决定包装 TCI 解释器。 这带来了几个优点:

  • 在实现代码生成器时,您可以不看指令的描述,而可以看解释器代码
  • 您可以不为遇到的每个翻译块生成函数,而是仅在第一百次执行之后生成函数
  • 如果生成的代码发生变化(从名称中包含单词 patch 的函数来看,这似乎是可能的),我将需要使生成的 JS 代码无效,但至少我将有一些东西可以从中重新生成它

关于第三点,我不确定代码第一次执行后是否可以打补丁,但是前两点就足够了。

最初,代码是在原始字节码指令的地址处以大开关的形式生成的,但是后来,记住有关 Emscripten、生成的 JS 的优化和重新循环的文章,我决定生成更多的人类代码,特别是因为根据经验它事实证明,翻译块的唯一入口点是它的 Start。 说到做到,过了一会儿,我们就有了一个代码生成器,可以使用 ifs 生成代码(尽管没有循环)。 但运气不好,它崩溃了,并给出一条消息,说明指令的长度不正确。 此外,该递归级别的最后一条指令是 brcond。 好的,我将在递归调用之前和之后对该指令的生成添加相同的检查,并且......其中没有一个被执行,但在断言切换之后它们仍然失败。 最后,在研究生成的代码后,我意识到切换后,指向当前指令的指针会从堆栈中重新加载,并且可能会被生成的 JavaScript 代码覆盖。 结果就是这样。 将缓冲区从 XNUMX MB 增加到 XNUMX MB 并没有产生任何结果,而且很明显代码生成器正在循环运行。 我们必须检查是否超出了当前 TB 的边界,如果超出,则发出带有减号的下一个 TB 的地址,以便我们可以继续执行。 此外,这还解决了“如果这段字节码发生变化,哪些生成函数应该失效?”的问题。 — 只需要使与该翻译块对应的函数无效。 顺便说一句,虽然我在 Chromium 中调试了所有内容(因为我使用 Firefox,而且使用单独的浏览器进行实验对我来说更容易),但 Firefox 帮助我纠正了与 asm.js 标准的不兼容之处,之后代码开始在铬。

生成代码示例

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"]

结论

所以,工作还没有完成,但我厌倦了偷偷地把这个长期的建设做到完美。 因此,我决定发布我现在所拥有的内容。 代码有些地方有点吓人,因为这是一个实验,事先并不清楚需要做什么。 也许,在一些更现代的 Qemu 版本之上发布正常的原子提交是值得的。 与此同时,《梵歌》中以博客的形式有一条线索:对于至少以某种方式通过的每个“级别”,都添加了俄语的详细评论。 其实这篇文章很大程度上是对结论的复述 git log.

你可以全部尝试一下 这里 (注意交通)。

什么已经在工作:

  • x86 虚拟处理器运行
  • 有一个从机器代码到 JavaScript 的 JIT 代码生成器的工作原型
  • 有一个用于组装其他 32 位客户架构的模板:现在您可以欣赏 Linux 在加载阶段冻结在浏览器中的 MIPS 架构

你还能做什么

  • 加快仿真速度。 即使在 JIT 模式下,它的运行速度似乎也比 Virtual x86 慢(但可能存在一个包含大量模拟硬件和架构的完整 Qemu)
  • 为了制作一个正常的界面 - 坦率地说,我不是一个优秀的 Web 开发人员,所以现在我已经尽我所能重新制作了标准的 Emscripten shell
  • 尝试启动更复杂的 Qemu 功能 - 网络、VM 迁移等。
  • UPD: 您需要向 Emscripten 上游提交少量开发和错误报告,就像之前 Qemu 和其他项目的搬运工所做的那样。 感谢他们能够隐式地使用他们对 Emscripten 的贡献作为我任务的一部分。

来源: habr.com

添加评论