正如我对 WebAssembly 初学者笔记的后续承诺,这里有一些关于 WebAssembly 的笔记,特别是关于 C/C++ 的笔记。
授权。 Clang 内置了对生成 Wasm 代码的支持,但是构建代码不仅仅是将 C++ 转换为适当的机器代码。 Emscripten是管理这个的 C++ 到浏览器 Wasm 工具链,为您提供从 C 源代码到.wasm
输出的emcc
编译器。对于一些示例,emscripten 提供了链接器和malloc
的实现,以及支持 wasm 加载所需的 .js 文件,甚至还可以生成 HTML 文件。
Emscripten 管理的另一件大事是让现有的 C++ 代码工作,例如puts()
之类的 C++ 调用填充到调用 JS console.log()
中。再举一个例子,当你编写 C++ GL 代码时,它会将 C++ GL API 填充到浏览器 WebGL 调用上,对于许多其他 C++ API 也是如此;请参阅他们关于移植的文档部分。但是对于 Figma 如何使用 C++,我们并没有使用太多,因为我们通常是从头开始编写代码到目标浏览器。
总而言之,Emscripten 运行良好,但也很笨重。我并不是要批评它——作为一个业余爱好者,我知道制作东西非常困难——而是要说它有惊人的表面积,例如用于与 JavaScript 和编译器标志交互的多个 API具有类似EMULATE_FUNCTION_POINTER_CASTS
的名称。具有讽刺意味的是,Emscripten 是一种 JS 工具,但它的JS 库是由一堆全局变量构成的。
空指针令人惊讶。在典型的 C 环境中,取消引用空指针会崩溃。在 Wasm 中,空指针仅引用memory[0]
,它是一个合法的读写地址。读/写空指针的 C 代码是我们通常尝试避免的,但在 Wasm 环境中,空指针感觉有点像On Error Resume Next
。
对于这个非专家,我很想知道在将 C 编译为 Wasm 时是否可以恢复围绕空指针处理的 C 语义,如果您可以改为让空指针引用某个内存地址高到可以陷入陷阱。例如,我记得有一些关于 C 的 NULL 实际上是否为 0 的语言律师规则。或者,我想知道您是否可以对每个内存引用进行代码生成,而不是引用theAddress-0x1000
,从而在运行时有效地将所有内存向下移动,这将导致空指针下溢到类似的非法高地址。 (我认为它甚至可能不会花费太多——大多数Wasm 内存指令已经采用了一个恒定的偏移参数……)
安全。一方面,空指针不崩溃对于运行 C 非常不利。但另一方面,有趣的是,反思 C 从现代硬件和操作系统中获得了多少支持。使空指针崩溃的不仅仅是保护页面,还有过度使用和 W^X 页面以及 ASLR 和 CFI 等,所有这些都在某种意义上减轻了其他地方的编译器/运行时解决的缺陷。 (例如,基本上所有非 C 语言中的 null 处理都不依赖于 CPU 的帮助!)
Wasm 环境实际上并没有同样的支持,这意味着语言级别的安全性在 Wasm 下可能会变得更加重要。另一方面,Wasm 的哈佛架构也只是通过构造排除了一堆 C 的问题,例如 ROP;我使用它的次数越多,就越觉得它是“正确”的选择。并且错误的后果也可能更低,因为缓冲区溢出不会直接导致 Wasm 沙箱逃逸。这是我最喜欢的博客中最近一篇关于这些因素之间平衡的论文的一个很好的分析。
在我看来,我们将来可能会看到“将 Wasm 沙箱转入浏览器评估”类型的 XSS 错误,因为这就是安全性无处不在的工作方式:对于任何特定于 Wasm 的安全属性存在,这只意味着错误将是在 Wasm 和主机系统之间的边界。尽管 Wasm 引起的 XSS 是一种与攻击浏览器本身截然不同的妥协,但基于 Wasm 的 XSS 等价物针对 nodejs 很容易升级为任意代码执行(参见上面的论文)。
内存布局。 Wasm 内存是一个大的平面缓冲区。为了将 C 映射到此,emscripten 将堆栈放置在某个固定偏移处并使其向下增长,并使堆从同一点开始并向上增长。如果您查看weave 中的 C++ 文件,您可以看到在“全局”部分中定义的初始堆栈指针。 (上一节中链接的博客文章链接的论文对此进行了更多介绍。)
虚拟机。在另一篇文章中,我提到了陷阱,这是 Wasm 机器停止您的程序的情况。在 Figma,我们以一种有趣的方式遇到了与虚拟调用相关的陷阱。
在 C++ 中,给定一些struct Iface { void foo(); }
,如果您在ptr
为空时调用ptr->foo()
,则不会立即出错;它只是用this == nullptr
调用foo
。像this->someMember
这样的取消引用只是读取低内存地址,这在 Wasm 中都是合法的。
但是如果foo
是一个虚方法,事情就变得更复杂了;在这里查看生成的输出。根据 C++ 语义,虚拟调用首先通过取消引用指针来查找 vtable,然后从该表中获取目标函数的地址。 (您可以在i32.load
指令对的 Godbolt 输出中看到这一点。)如果此处涉及任何空值,那么 Wasm 仍然没有问题,因为您只需返回一个垃圾索引。最后,还有一个 Wasm 操作码call_indirect
,它调用由运行时计算索引选择的函数。
如果该索引引用了程序声明的函数数组之外的函数,它将捕获。但是,如果在取消引用一些空值后,您碰巧不小心引用了某个现有函数怎么办?在上面的输出中,请注意预期目标函数的类型如何包含在call_indirect
指令中。
函数类型出现在 Wasm 语义中的原因与 Wasm 的健全性属性有关,据我了解,它需要随时知道堆栈上的值类型。但在这种情况下,这意味着您有一个垃圾函数索引,如果该垃圾索引引用的函数类型与您预期的不同,Wasm 将捕获。
总结本节:空指针通常不会崩溃,但指向虚拟的空指针可以,如果它们碰巧引用了具有不同 Wasm 级别类型的函数。令人毛骨悚然的是,您可以让一些涉及空指针的代码愉快地在整个内存中乱写并继续运行,您会注意到的唯一方法是它最终是否陷入虚拟调用。
间接缓冲区。我们在 Figma 依赖的一种有趣的模式称为IndirectBuffer 。基本思想是您可以在 JavaScript 中分配一个数组,并且仍然可以通过公开对它的调用从 C++ 操作它,即使数组的字节本身从未存在于 C++ 内存中。这最终很重要,因为 Wasm 内存限制为 ~4gb,但 JS 内存不是。
特别是 Figma 文档处理大量图像,因此我们尝试将图像像素保留在 JS 或 GPU 内存中,同时仍然使用 C++ 渲染场景。例如,我在 Figma 从事的一个项目与“将当前文档另存为文件”功能相关,该功能需要将当前文档的所有内容(包括图像像素)序列化到一个大缓冲区中。加载的文档本身已经占用了大部分 Wasm 空间,因此我们将大量序列化输出移动到 JS 级别的缓冲区中,尽管所有序列化代码仍然存在于 C++ 中。
小时候,我从 DOS 时代开始编程,我仍然记得段寄存器和远指针。整件事让我想起了那个时候的有趣回忆。但是这个问题也可能是 Figma 特有的,它会很乐意吃掉你能给它的所有 RAM。
现在还早。将 DWARF 调试信息挂接到浏览器开发工具中的工作正在进行中。它看起来很有希望,但工具还很早。这超出了这篇文章的范围,但 Figma 有一个非常惊人/疯狂的设置,我们可以使用本机工具链构建和调试应用程序 C++ 部分,尽管该应用程序主要是用 HTML 编写的,我们之所以保持活力,主要是因为本机开发体验是还是好多了。
除此之外,如果您有一个调用 JS 的 C++ 调用堆栈,然后调用new Error()
,则生成的对象上有 C++ 帧,并且使用足够多的机制,您甚至可以获得符号。我提到调试主要是为了提到它正在迅速改进(通过 C++ 堆栈在浏览器中单步执行非常简洁!)并且还将您链接到另一篇非常详尽的博客文章。
再举一个例子,在 emscripten 工具链中,用于链接的工具堆栈是 Wasm/emscripten 特定的,这意味着他们还没有看到多年的改进,既能快速运行又能生成在其他平台(尤其是 Linux)上获得的紧凑代码.
还有一个“现在还早”的例子,虽然我想现在我正在讨论这个主题,但我意识到这与 C++ 没有什么特别的关系:我们最近在 Firefox 的 Wasm 优化器中遇到了一个代码生成错误。我仍然对我的同事能够将运行时的 Figma 错误提炼成如此小的复制品感到惊讶,并且对 Firefox 工程师如何能够在报告后 90 分钟为它编写补丁(?!)印象深刻。
总而言之,在 Wasm 上开发 C++ 感觉就像我想象的为嵌入式系统开发 C++ 可能是这样的:它肯定仍然是 C++,但工具有点弱,并且与你熟悉的工具有随机的不同。
C++的命运。到今年夏天,我第一份编程工作已经过去了 25 年,为一家互联网创业公司编写 C++,而今天我仍在为一家互联网创业公司编写 C++。从这个角度来看,学习 C++ 是一项明智的投资。但我认为许多具有类似 C++ 经验的人会同意我的观点,即如今它很少能真正成为用于软件的正确工具。
特别是,我认为 C++ 是一种糟糕的专家工具,如果你犯了错误,它就会恶意失败:要么是静默的内存损坏,要么是由于意外复制而导致的静默性能不佳。同时,正如Zaplib 事后分析所发现的那样,性能更好的语言实际上并不容易转化为性能上的胜利,如果不是为了性能,编写 C++ 的理由就更少了。
考虑到这一点,我认为 C++ 和 Wasm 的未来主要是整合遗留 C++ 代码,而不是编写新代码。这主要是关于我对 C++ 的总体感受的陈述,但 Wasm 尤其为能够将旧的 C++ 代码插入新的上下文中注入了新的活力。例如,Mozilla 发布了一个非常惊人的 hack ,它使用 Wasm 将 Firefox 本身使用的 C++ 代码沙箱化:他们将 C++ 编译为 Wasm,然后将其链接回更大的 C++ 应用程序,运行时效果几乎就像在其中运行 C++ 子模块一个模拟器。
原文: http://neugierig.org/software/blog/2022/06/wasm-c++.html