曾几何时,出于笑声,我决定证明过程的可逆性,并学习如何从机器代码生成JavaScript(或更确切地说,是Asm.js)。 选择了QEMU作为实验,一段时间后,在Habr上写了一篇文章。 在评论中,建议我在WebAssembly上重新制作该项目,但我本人不希望自己离开快完成的项目……工作在继续,但进展非常缓慢,现在,在该文章中,出现了关于“结果如何结束?”的评论。 对于我的详细回答,我听到“它来自一篇文章”。 好吧,如果它拉了,就会有一篇文章。 也许有人会派上用场。 通过它,读者可以了解有关设备后端QEMU代码生成的一些事实,以及如何为Web应用程序编写即时编译器。
任务
由于我已经学习了如何将QEMU移植到JavaScript,因此这次决定明智地这样做,并且不再重复旧的错误。
错误次数:从点释放分支
我的第一个错误是从上游版本2.4.1分支我的版本。 在我看来,这似乎是个好主意:如果存在点发布,那么它可能比简单的2.4更加稳定,甚至比master
分支更稳定。 而且由于我计划添加很多错误,所以我根本不需要陌生人。 所以它可能发生了。 但是,这是不幸的事情:QEMU并没有停滞不前,甚至在某个时候他们甚至宣布将生成的百分比代码优化10。“是的,现在我冻结了,”我想 摔断了 。 在这里,我们必须离题:由于QEMU.js的单线程性质以及原始QEMU并不意味着不存在多线程(也就是说,能够同时操作多个不相关的代码路径,而不仅仅是“循环所有内核”),这是线程的主要功能必须“出门”,以便有可能从外面打来电话。 这产生了一些自然的合并问题。 但是,从master
分支进行的一些更改(我尝试与之合并代码)也是在点发行中(因此在我的分支中)精心挑选的事实,也可能不会增加便利。
总的来说,我认为无论如何原型还是有意义的 扔掉 拆卸零件并根据较新的东西从头开始建造一个新版本,现在从master
那里买。
错误二:TLP方法论
实际上,通常这不是一个错误-只是在完全误解了“在何处以及如何移动?”的情况下创建项目的功能,而在一般情况下“我们会到达那里吗?”。 在这种情况下, 编程是一个合理的选择,但是,当然,我绝对不想不必要地重复它。 这次我想明智地做到这一点:原子提交,有意的代码更改(而不是“将随机字符串在一起,直到编译出来(带有警告)”,如Linus Torvalds曾经对某人说过的话,如果您相信Wikitatnik)。
错误三:不知道福特会爬到水里
我还没有完全摆脱这种情况,但是现在我决定不走阻力最小的道路,而要“以成人的方式”进行,即从头开始编写我的TCG后端,这样以后我就不会再说:“是的,当然,慢慢来,但我无能为力-TCI就是这样写的……”。 另外,起初这似乎是一个显而易见的解决方案,因为我正在生成二进制代码 。 俗话说:“我收集了根特,但不是我收集的”:代码当然是二进制的,但是控制不能像这样转移给它-需要将其显式推入浏览器进行编译,从而导致JS世界中的某个对象,仍然需要保存在某个地方。 但是,在 正常的 据我所知,对于RISC架构,典型的情况是需要显式刷新用于重新生成的代码的指令缓存-如果这不是我们所需要的,至少到此为止。 另外,从我的最后一次尝试中,我了解到控件似乎没有转移到转换块的中间,因此,我们实际上并不需要从任何偏移量解释字节码,并且可以在TB上简单地通过函数生成。
来踢了
尽管我从7月开始重新编写代码,但神奇的Pendel却没有引起人们的注意:通常,来自GitHub的信件是作为对Issues和Pull请求的响应的通知而来的,然后突然 , 作为上下文的qemu后端的Binaryen说道 :“这里是-他做了那样的事情,也许他会说些什么。” 这是关于使用与Emscripten相关的Binaryen库创建WASM JIT的。 好吧,我说过您那里拥有Apache 2.0许可证,并且QEMU作为一个整体是在GPLv2下分发的,它们不是很兼容。 突然发现许可证可以以某种方式更正 (我不知道:也许是变更,也许是双重许可,也许还有其他……)。 当然,这让我感到高兴,因为那时我已经看过几次二进制格式的 WebAssembly,这让我感到难过和难以理解。 这里有一个库,可以通过过渡图吞噬基本块,发布字节码,并在必要时甚至在解释器中启动它。
然后在QEMU邮件列表上还有一封信 ,但这更可能是一个问题:“谁需要它?” 突然间 ,这是必要的。 如果它或多或少地聪明地起作用,则至少可以将这样的用例拼凑起来:
- 无需安装就可以启动任何教学
- 在iOS上进行虚拟化,据传言,唯一有权即时生成代码的应用程序是JS引擎(是真的吗?)
- mini-OS演示-单磁盘,内置,各种固件等...
浏览器运行时的功能
如我所说,QEMU与多线程相关,但它不在浏览器中。 好吧,就是说,就像没有...最初根本不存在,然后出现了WebWorkers-据我所知,这是基于消息传递的多线程, 没有互变量 。 自然地,当基于共享内存模型移植现有代码时,这会带来严重的问题。 然后,在公众的压力下,它以SharedArrayBuffers
的名称SharedArrayBuffers
。 他们逐渐介绍了它,庆祝了它在不同浏览器中的发布,然后庆祝了新的一年,然后是Meltdown ...之后,他们得出结论,粗鲁,不粗鲁的时间计量,但是借助共享内存和增加计数器的流,它仍然非常准确 。 因此,他们关闭了共享内存的多线程。 似乎他们后来重新打开了它,但是,从第一个实验可以清楚地看到,没有它是有生命的,如果是这样,我们将尝试不依赖多线程来做到这一点。
第二个功能是不能对堆栈进行低级操作:您不能只获取,保存当前上下文并切换到具有新堆栈的新上下文。 调用堆栈由JS虚拟机管理。 似乎是什么问题,因为我们仍然决定完全手动管理以前的流程? 事实是,QEMU中的块输入输出是通过协程实现的,这里的低级堆栈操作对我们很有用。 幸运的是,Emscipten已经包含了用于异步操作的机制,甚至两种: Asyncify和Emterpreter 。 第一个工作原理是对生成的JavaScript代码进行大量膨胀,因此不再受支持。 第二种是当前的“正确方式”,并通过为其自己的解释器生成字节码来进行工作。 当然,它可以缓慢运行,但不会使代码膨胀。 的确,对此机制的协程支持必须单独归因(已经在Asyncify下编写了协程,并且有一个与Emterpreter大致相同的API的实现,您只需要连接它们即可)。
目前,我还没有设法将代码拆分成WASM编译的代码并使用Emterpreter进行解释,因此块设备还无法正常工作(请参阅下一个系列,正如他们所说的那样)。 也就是说,最后,您应该获得如此有趣的分层内容:
- 解释块I / O。 那么,您真的期望具有本机性能的模拟NVMe吗? :)
- 静态编译的主要QEMU代码(转换器,其他仿真设备等)
- WASM动态编译的来宾代码
QEMU来源的功能
您可能已经猜到了,来宾体系结构的仿真代码和用于从QEMU生成主机指令的代码是分开的。 实际上,还有一点棘手的问题:
- 有访客架构
- 有加速器 ,即用于Linux上的硬件虚拟化的KVM(用于兼容的来宾和主机系统),用于在任何地方生成JIT代码的TCG。 从QEMU 2.9开始,出现了Windows上对HAXM硬件虚拟化标准的支持( 详细信息 )
- 如果使用的是TCG,而不是硬件虚拟化,则它对每种主机体系结构的代码生成以及通用解释器具有单独的支持
- ...及其周围-模拟的外围设备,用户界面,迁移,记录重放等。
顺便说一句,您是否知道: QEMU不仅可以仿真整个计算机,还可以仿真主机内核中用于单独用户进程的处理器,例如,AFL模糊处理程序可使用该处理器来模拟二进制文件。 也许有人想将此QEMU操作模式移植到JS? ;)
像大多数长期存在的免费程序一样,QEMU是通过调用configure
和make
来make
。 假设您决定添加一些东西:TCG后端,线程实现等。 不要急于庆幸/惊恐(必要时加下划线)与Autoconf通信的前景-实际上,在QEMU上进行的configure
似乎是自写的,没有任何东西可产生。
网络组装
那么,这是什么东西-WebAssembly(又名WASM)? 这是Asm.js的替代,现在不再假装为有效的JavaScript代码。 相反,它是纯二进制的并且经过优化的,甚至只是将一个整数写入其中也不是那么简单:为了紧凑起见,它以LEB128格式存储。
您可能已经听说过Asm.js的重新循环算法-这是“高级”执行流控制指令(即if-then-else,循环等)的恢复,在该指令下,JS引擎从低级LLVM IR进行了调整,更接近处理器执行的机器代码。 自然,QEMU的中间表示更接近第二个。 看来这里是字节码,折磨的结局...然后是块,如果是,则是其他,然后循环!
这也是Binaryen之所以有用的另一个原因:它当然可以接受接近WASM中存储的高级块。 但是它也可以从基本块的图及其之间的转换中产生代码。 好吧,我已经说过,它将WebAssembly存储格式隐藏在方便的C / C ++ API之后。
TCG(微型代码生成器)
TCG 最初是 C编译器的后端,显然,它无法与GCC竞争,但最终它在QEMU中找到了自己的位置,成为主机平台的代码生成机制。 还有一个TCG后端生成一些抽象的字节码,该字节码由解释器立即执行,但这次我决定离开这个时间。 但是,QEMU已经可以通过tcg_qemu_tb_exec
函数启用向生成的TB的过渡这一tcg_qemu_tb_exec
对我非常有帮助。
要将新的TCG后端添加到QEMU,您需要创建一个tcg/< >
子目录(在本例中为tcg/binaryen
),并且其中有两个文件: tcg-target.h
和tcg-target.inc.c
并注册所有内容这是configure
。 您可以在其中放置其他文件,但是,根据这两个文件的名称可以猜测,它们都将包含在某个位置:一个作为常规头文件(它将包含在tcg/tcg.h
,而另一个文件已经存在于tcg
目录中, accel
不仅如此),另一个仅作为tcg/tcg.c
代码段,但它可以访问其静态功能。
决定花太多时间在详细的程序上以及它如何工作后,我简单地从另一个后端实现中复制了这两个文件的“骨架”,并在许可证标头中诚实地指出了这一点。
tcg-target.h
主要包含#define
形式的设置:
- 目标体系结构上有多少个寄存器以及有多少个寄存器(我们有-我们想要的,有这么多-问题不仅仅是浏览器将在“完全目标”体系结构上以更高效的代码生成什么?)
- 主机指令的对齐:在x86和TCI上,指令根本不对齐,但是我要放在代码缓冲区中的不是指令,而是指向Binaryen库结构的指针,所以我要说:4个字节
- 后端可以生成哪些可选指令-打开我们在Binaryen中找到的所有内容,让加速器将其余内容分解为更简单的内容
- 后端请求TLB缓存的大致大小。 事实是,在QEMU中,所有事情都是认真的:尽管加载/存储了辅助功能,但考虑到了来宾MMU(现在又没有它了?),但它们以结构形式保存了翻译缓存,处理起来很方便嵌入直接到翻译块。 问题是,通过小的快速命令序列可以最有效地处理此结构中的偏移量
- 在这里,您可以改变一个或两个保留寄存器的用途,通过一个函数启用对TB的调用,还可以选择描述几个小的
inline
函数,例如flush_icache_range
(但这不是我们的情况)
当然, tcg-target.inc.c
通常要大得多,并且包含一些必需的功能:
- 初始化,除其他外,它指示可以使用哪些操作数的指令的限制。 我从另一个后端狂妄地复制了
- 接受内部字节码的一条指令的函数
- 在这里您可以放置辅助功能,也可以在此处使用
tcg/tcg.c
静态功能
我自己选择了以下策略:在下一个翻译块的第一个单词中,我写下了四个指针:起始标记( 0xFFFFFFFF
附近的某个值,确定TB的当前状态),上下文,生成的模块以及调试的幻数。 首先,将标签设置为0xFFFFFFFF - n
,其中n
是一个小的正数,并且每次通过解释器将其增加1。当达到0xFFFFFFFE
,发生编译,该模块存储在函数表中,导入到一个小的“启动器”中,执行剩下tcg_qemu_tb_exec
,并且该模块已从QEMU内存中删除。
用经典来形容,“拐杖,这种发自内心的声音中交织着……”。 但是,内存在某处泄漏。 这是QEMU管理的内存! 我有一个代码,当写下下一条指令(好是一个指针)时,删除了先前指向该位置的链接,但这无济于事。 实际上,在最简单的情况下,QEMU在启动时分配内存,并将生成的代码写入那里。 当缓冲区结束时,将抛出该代码,然后开始在其位置写入下一个代码。
研究了代码之后,我意识到带有魔术数字的拐杖使我们不会落在堆的破坏上,从而在第一遍释放未初始化缓冲区中的错误。 但是谁稍后会绕过我的功能覆盖缓冲区呢? 正如Emscripten开发人员所建议的那样,遇到问题后,我将生成的代码移植回本机应用程序,在其上设置Mozilla Record-Replay ...因此,总的来说,我实现了一个简单的事情:为每个块分配一个带有其描述的struct TranslationBlock
。 猜猜在哪里...没错,就在缓冲区前面的块正前方。 意识到这一点后,我决定用拐杖(至少是一些拐杖)将其绑起来,然后简单地扔掉魔法数字,然后将剩余的单词转移到struct TranslationBlock
,从而创建一个单链接列表,您可以在重置翻译缓存并释放内存时快速进行浏览。
仍然存在一些拐杖:例如,代码缓冲区中的标记指针-其中一些只是BinaryenExpressionRef
,也就是说,它们查看需要线性放入生成的基本单元中的表达式,部分-WB之间的过渡条件,部分-去哪里。 好了,已经为Relooper准备了块,必须根据条件进行连接。 为了区分它们,假定它们全部对齐至少四个字节,因此可以安全地使用标签的低两位,只需要记住在必要时将其删除。 顺便说一句,此类标签已在QEMU中用于指示退出TCG周期的原因。
使用Binaryen
WebAssembly中的模块包含函数,每个函数都包含一个表示表达式的主体。 表达式是一元和二进制运算,由其他表达式的列表,控制流等组成的块。 正如我已经说过的,这里的控制流精确地组织为高级分支,循环,函数调用等。 与JS中一样,函数的参数不在堆栈上传递,而是显式传递。 有全局变量,但是我没有使用它们,所以我不再谈论它们。
函数还具有从头开始编号的局部变量,其类型为:int32 / int64 / float / double。 前n个局部变量是传递给函数的参数。 请注意,尽管就控制流程而言,此处的所有内容都不是完全底层的,但整数仍然不带有符号/无符号符号:数字的行为取决于操作代码。
一般来说,Binaryen提供了一个简单的C-API :创建一个模块, 在其中创建表达式-一元,二进制,来自其他表达式的块,控制流等。 然后创建一个函数,需要在其主体中指定一个表达式。 如果您像我一样具有低级的过渡图,那么relooper组件将为您提供帮助。 据我了解,可以对块中的执行流进行高级控制,只要它不超出块的限制即可,也就是说,可以在内置的TLB缓存处理代码中创建内部快速路径/慢路径分支,但不会干扰“外部”控制流。 当您释放relooper时,它的块也将释放,当您释放在其舞台上分配的模块,表达式,函数等时,它们也会消失。
但是,如果您想在旅途中解释代码而无需不必要的创建和删除解释器实例,则可以将此逻辑转移到C ++文件中,并从那里直接控制整个C ++ API库,而不用完成包装程序。
因此,要生成代码,您需要
… — , , — .
--, :
static char buf[1 << 20]; BinaryenModuleOptimize(MODULE); BinaryenSetMemory(MODULE, 0, -1, NULL, NULL, NULL, NULL, NULL, 0, 0); int sz = BinaryenModuleWrite(MODULE, buf, sizeof(buf)); BinaryenModuleDispose(MODULE); EM_ASM({ var module = new WebAssembly.Module(new Uint8Array(wasmMemory.buffer, $0, $1)); var fptr = $2; var instance = new WebAssembly.Instance(module, { 'env': { 'memory': wasmMemory,
- QEMU JS , ( ), . , translation block, , struct TranslationBlock
.
, ( ) Firefox. Chrome - , - WebAssembly, ...
. , , - . , . , WebAssembly , JS, , , .
: 32- , Binaryen, - - 2 32- . , Binaryen . ?
-, « , 32- Linux?» . , : 1 2 Gb.
- ( ). , — . « : , ...».
… Valgrind-, , , , , Valgrind :)
, - , ...