当前位置: 首页 > 科技观察

WebAssembly小知识

时间:2023-03-11 22:42:56 科技观察

随着互联网的发展,网络应用变得越来越复杂,比如3d可视化、音视频软件、大型网络游戏等。因此,代码的效率和安全性变得更加重要。WebAssembly是一种可移植的低级字节码,通过提供紧凑的表示、高效的验证和编译以及低开销甚至零开销的安全执行来满足这些需求。它不仅是一种特定的编程模型,而且是一种独立于语言和平台的现代硬件抽象。1.起源由于历史原因,JavaScript是Web上唯一受本地支持的编程语言。它已成为许多其他语言的编译目标,因为它在实现中无处不在,性能快速提升,也许是纯粹的必要性。然而,JavaScript存在性能不一致和其他各种问题。WebAssembly(简称“Wasm”)用低级代码解决了Web上的安全性、速度和可移植性问题。从ActiveX到NativeClient再到asm.js,这种代码格式应该具备的属性都没有:安全、快速、可移植语义:可以安全快速执行,与语言、硬件、平台无关.确定性结果和易于推理,同时易于与Web平台互操作安全有效的表示:结构紧凑,易于解码、验证和编译,易于开发人员生成,支持流式处理和并行处理。为什么这些目标很重要?为什么难?安全性代码安全性在Web上至关重要,因为代码通常来自不受信任的来源。传统上,代码保护是通过提供托管语言运行时来实现的,例如浏览器的JavaScript虚拟机或语言插件。托管增强了内存的安全性,防止程序损害用户数据或系统状态。然而,托管运行时传统上并没有为底层代码提供太多,例如C/C++程序。类似c/c++的快速静态语言,编译器会提前优化底层代码。本机代码,无论是手工编写还是编译器的优化输出,都可以充分利用机器的性能。运行时托管和沙盒通常会给这些代码带来巨大的性能开销。统一性除了不可避免的硬件限制外,还有许多良好的编程范例不应受到代码格式的限制。然而,大多数运行时托管旨在很好地支持特定语言或编程范例,同时对其他语言造成巨大成本。易于移植Web不仅跨越许多设备,而且还跨越不同的机器体系结构、操作系统和浏览器。Web代码必须独立于硬件和平台,以允许应用程序在所有浏览器和硬件类型上以相同的确定性行为运行。以前的低级代码解决方案绑定到单一架构或存在其他可移植性问题。紧凑机制在网络上传输的代码应该很小,以减少负载、节省带宽并提高整体响应能力。Web上的代码通常作为JavaScript源代码传输,即使经过压缩,也远不如二进制格式紧凑。二进制代码格式也不总是针对大小进行优化。WebAssembly是Web的第一个低级代码解决方案,实现了上述所有设计目标,是所有主要浏览器供应商和在线社区为构建高性能应用程序的通用解决方案协作的结果。虽然网络是WebAssembly的发源地,但它旨在避免对网络的任何依赖。它是一种可以嵌入各种环境的开放标准,并且可能是第一种从头开始设计的具有形式语义的工业强度语言。2.语言概述WebAssembly虽然是一种二进制代码格式,但本质上它仍然是一种具有语法和结构的编程语言,这使得它更容易解释和理解。2.1.基础这里是WebAssembly中的一些基本概念。模块WebAssembly二进制文件采用模块的形式。它包含函数、全局变量、表和内存的定义,可以通过导入和导出来重复使用。虽然模块对应于程序的静态表示,但模块的动态表示是一个实例,具有完全可变的状态。实例化模块需要通过调用导出函数来启动计算,为所有导入提供定义,这些导入可能已经从先前创建的实例中导出。模块提供封装和沙盒,客户端只能访问模块的导出,其他内部都被保护不被篡改;同时,模块只能通过客户端提供的导入与其环境交互,因此客户端无法控制给定模块的功能拥有完全控制权。这两个方面都是代码安全的重要组成部分。功能模块中的代码被组织成单独的函数,这些函数接受参数并返回由其函数类型定义的结果。函数可以相互调用,包括递归调用,并且正在运行的WebAssembly程序无法直接访问进行调用的堆栈。指令WebAssembly在概念上是一个基于堆栈的机器,函数的代码由一系列操作堆栈上的值的指令组成。然而,类型系统的布局可以在代码中的任何一点静态确定,因此可以直接编译指令之间的数据流,而无需实现操作堆栈。堆栈的组织只是实现紧凑表示的一种方式,它小于基于寄存器的机器的大小。trapexception某些指令可能会产生一个异常陷阱,它会立即中止当前的计算。WebAssembly代码可能无法处理异常陷阱,嵌入器通常会提供处理此类情况的方法,例如通过将它们具体化为JavaScript异常。数值类型WebAssembly只有四种可以计算的原始值类型。这些是整数和浮点数,每个都有32位或64位,可用于通用硬件。大多数WebAssembly指令为这些数字类型提供简单的运算符,例如一元和二元运算符、比较和转换。与硬件一样,WebAssembly不区分有符号/无符号整数类型。Variadic函数可以声明可变的局部变量,这实际上提供了一组零初始化的虚拟寄存器。模块还可以声明类型化的全局变量,这些变量可以是可变的或不可变的,并且需要显式的初始化器。导入全局变量允许有限种类的可配置性,例如链接。与WebAssembly中的所有实体一样,变量由整数索引引用。2.2内存WebAssembly的主内存是一个大的字节数组,线性内存,或者简单内存。通过加载和存储指令访问内存,其中地址只是无符号整数。创建和扩展每个模块最多只能定义一个内存区域,通过导入/导出可以与其他实例共享。创建的内存区域具有初始大小,但可以动态增长。增长的单位是页,定义为64kb,这将允许重用虚拟内存硬件来对硬件进行边界检查。页面大小是固定的而不是特定于系统的,以防止可移植性危害。Endianness由于大多数现代硬件都专注于小端,或者至少可以处理它,WebAssembly的内存区域同样是小端。因此,内存访问的语义是完全确定的,并且可以跨所有引擎和平台移植。内存安全所有的内存访问都会根据内存大小进行动态检查,越界访问会导致异常陷阱。线性内存与引擎的代码空间、执行堆栈和数据结构分离,因此编译后的程序不能破坏其执行环境、跳转到任意位置或执行其他未定义的行为。快速进程内隔离对于以高效方式与不受信任的JavaScript和各种WebAPI进行交互是必不可少的。同时,WebAssembly引擎被安全地嵌入到其他托管语言运行时中。2.3.控制流WebAssembly表达控制流的方式与大多数基于堆栈的机器不同。它不提供任意跳转,而是提供更类似于编程语言的结构化控制流。这种结构确保控制流不会形成不可简化的循环、包含堆栈高度未对齐的块分支或分支到多字节指令的中间。这些属性允许WebAssembly代码一次性验证和编译。控制结构块、循环和if结构必须由结束操作码终止,并且必须正确嵌套才能被视为格式良好的结构。这些结构中的一系列内部指令形成一个块。请注意,循环不会自动迭代,但允许手动构造具有显式分支的循环。每个控制结构都用一个函数进行了类型注释,描述了它对堆栈的影响,类型为Pop/Push值。分支分支可以是无条件的、有条件的或索引的。它们具有“标签”的即时性,不表示在指令流中的位置,而是通过相对嵌套深度引用外部控制结构。因此,标签有效地限定了范围:分支只能引用它们嵌套在其中的结构。如果分支从构建的块中中断,则效果取决于目标构造:对于块,或者它是否向前跳转到其末尾(如break语句);对于一个循环,它是一个向后跳转到它的开头(就像一个continue语句)。分支通过隐式弹出所有未使用的运算符来理清运算符堆栈,类似于函数调用的返回。表达式结构化控制流似乎是一个严重的限制,但大多数高级控制结构都可以通过适当的块嵌套轻松表达。例如,C风格的switch语句需要更多技巧才能在无序条件之间失败。各种形式的循环也可以用分支的组合来表示。将非结构化控制流转换为结构化形式是开发人员的责任。这是Web编译的既定方法,其中JavaScript也仅限于结构化控件。这种限制的好处是引擎中的许多算法更简单、更快。2.4.函数调用和表函数体是一个块。执行可以通过到达函数的结果值在堆栈上的块的末尾来完成,也可以通过从函数块中分支出来来完成,返回指令只是后者的简写。可以使用调用指令直接调用调用函数,调用指令可以用函数指针模拟,函数指针引用模块定义的函数表中的运行时索引。表中的函数不需要具有相同的类型。相反,在不匹配的情况下,函数的类型会根据提供的指令和陷阱的预期类型进行动态检查,从而保持执行环境的完整性。表的异构性允许更准确地表示函数指针并简化动态链接。为了进一步帮助动态链接的场景,可以通过外部API更改导出的表。外部调用的函数可以导入到模块中,直接调用和间接调用都可以调用导入的函数,通过export/import实现多个模块实例之间的通信。此外,导入机制作为一个安全的外部函数接口(),WebAssembly程序可以通过它与其嵌入式环境进行通信。例如,在Web上导入的函数可能是由JavaScript定义的宿主函数。跨语言边界的值将自动为2.5。确定性结果WebAssembly试图在不牺牲性能的情况下为低级代码提供一个可移植的目标。硬件行为不同的地方通常包括整数除以零、溢出或浮点转换以及对齐。WebAssembly的设计以最小的执行开销为所有此类硬件提供确定性语义。然而,仍然有三个依赖于实现的行为来源可以被认为是不确定的:NaN有效负载:WebAssembly遵循IEEE754浮点运算标准。然而,IEEE并没有为所有情况下的NaN值指定确切的位模式,cpu之间存在显着差异,并且每次数值运算后归一化的开销太高。根据JavaScript引擎的经验,可以提供足够的保证来支持NaN标记等技术。资源耗尽:资源总是有限的,并且在设备之间差异很大。特别是引擎可能内存不足,调用指令也可能因为栈溢出产生异常陷阱,但是,WebAssembly本身无法观察到这些情况,它只是暂停计算。宿主函数:一个WebAssembly程序可以调用一个宿主函数,它本身并不能确定或改变WebAssembly的状态。当然,调用宿主函数的结果也超出了WebAssembly的语义范围。2.6.二进制格式WebAssembly作为抽象语法的二进制编码传输,旨在最小化大小和解码时间。二进制文件表示单个模块,根据其中声明的不同实体类型分为几个部分。函数体的代码在所有声明之后被推迟到一个单独的部分,以便在函数体开始通过网络到达时启用流式编译。该引擎还可以并行编译函数体。.该格式还允许用户定义的部分,这些部分可能会被引擎忽略。3.语义意识WebAssembly语义由两部分组成:定义验证的静态语义和定义执行的动态语义。在这两种情况下,它都是一种方便有效的声明性规范工具。最后,形式化使得证明WebAssembly规范、机器验证结果正确性以及构建可证明正确的解释器变得容易。执行执行是根据标准的小步缩减关系定义的,其中每个计算步骤都被描述为指令的一系列重写规则。堆栈仅由指令序列中的所有前导标识指令组成。当指令序列减少到与结果值堆栈对应的常量时,执行终止。在恢复期间临时,帧本质上是函数调用的调用帧。该存储对程序的全局状态进行建模,并跟踪已分配函数、全局变量、表和内存实例的列表。存储组件之一的索引称为地址,模块实例将指令中出现的静态索引映射到存储中它们各自的动态地址。为此,除了函数局部变量的状态之外,每个框架还携带一个指向它所在的模块实例的链接,实现可以通过将生成的机器代码专门化到模块实例来消除这些闭包。存储、框架和指令序列的三元组一起形成一个配置,表示WebAssembly抽象机在给定时间点的完整状态。一般的缩减规则是重写配置,而不仅仅是指令序列。身份验证在Web上,代码是从不受信任的来源获得的,必须经过身份验证。Webasembly的验证规则被简洁地定义为类型系统。这种类型的系统设计用于在单个线性中进行有效检查。指令的类型是指定其所需输入堆栈和提供的输出堆栈的函数类型。每条规则都包含一个结论和一个可能为空的前提列表。可以理解为:如果所有前提都为真,则结论为真。每个指令都有一个规则来定义它何时是正确类型的。一个程序是有效的,当且仅当规则归纳地推断出它是一个类型良好的程序。例如,常量和数字运算符的规则是甚至不需要前提的公理。管理构造的规则要求它们的类型匹配显式注释并在检查内部块时使用本地标签扩展上下文。键入分支指令时,将在上下文中查找标签类型,这需要堆栈上的适当运算符与连接点处的堆栈相匹配。可靠性WebAssembly系统类型具有标准的可靠性属性。归约规则实际上涵盖了有效程序的所有可能执行状态。这意味着不存在类型安全违规,例如无效调用或对局部变量的非法访问,它保证了内存安全并确保代码地址或调用堆栈的不可访问性。这也意味着运算符堆栈的使用是结构化的,其布局在所有程序点都是静态确定的,这对于在基于寄存器的机器上进行高效编译至关重要。此外,它还建立了内存和状态封装、模块和函数边界上的抽象属性,不会泄露信息。机械化证明WebAssembly中的参考解释器涉及将正式规则直接翻译成可执行代码。虽然这两项任务从根本上来说都很简单,但它们总是容易出现测试无法发现的细微错误。基于机器的语义验证不仅仅是验证WebAssembly本身,还为其他形式化方法的应用提供基础,例如验证针对WebAssembly的编译器或证明程序属性、程序等价性和安全性。4.标准化WebAssembly的形式语义可以促进标准化的形成。核心语言WebAssembly语言的定义遵循形式化并指定抽象语法、类型规则、归约规则和抽象存储。二进制和文本格式作为属性语法给出,准确描述它们产生的抽象语法。对于工业级语言来说,这种严谨和精确的程度可能是前所未有的。形式化:一个广泛使用的标准不能假定所有读者都熟悉语义的形式化符号,不直接阅读规则的开发人员受益于将形式化规则集中在标准文档中。参考解释器:随着WebAssembly在浏览器中的生产实现,参考解释器被用作近乎“可执行的规范”来开发测试套件以测试具体实现和正式规范,并构建新功能的原型。显然,形式化语义并非在所有情况下都是直截了当的。WebAssembly的实现包括一个多阶段提案过程。在提案的各个阶段,维护者必须提供:(1)非正式描述,(2)非正式规范,(3)原型实现,(4)综合测试套件,(5)正式规范,(6)引用在解释器中的实现,(7)在独立生产系统中的实现。嵌入式执行环境WebAssembly类似于虚拟指令集的程序架构,因为它没有定义程序如何加载到执行引擎中,也没有定义程序如何进行I/O。这种设计分离可以将WebAssembly实现嵌入到执行环境中。嵌入机制定义了如何加载模块、如何解析导入和导出、如何处理陷阱,并提供用于访问环境的外部函数。为了加强平台独立性,WebAssembly标准被分层到单独的文档中:核心规范仅定义了虚拟指令集架构,而单独的嵌入式规范定义了它与特定主机环境的交互。在浏览器中,可以通过JavaScriptAPI加载、编译和调用WebAssembly模块。一种粗略的方法是(1)从给定源获取二进制模块,例如作为网络资源,(2)实例化它,提供必要的导入,以及(3)调用所需的导出函数。由于编译和实例化可能很慢,因此它们作为异步方法提供,其结果包含在承诺中。JavaScriptAPI还允许在外部创建和初始化内存或表,或将它们作为导出来访问。作为一种低级语言,WebAssembly不提供任何内置的对象模型。这种设计为开发人员提供了最大的灵活性,并且不会像以前的虚拟机那样锁定任何特定的编程范式/对象模型。生产者可以在WebAssembly之上定义一个通用的ABI,这样模块就可以跨不同的应用程序进行互操作。这种关注点分离对于WebAssembly作为通用代码格式至关重要。5.一些实现方面的考虑WebAssembly的主要设计目标是在不牺牲安全性和可移植性的情况下实现高性能。V8(Chrome)、SpiderMonkey(Firefox)和JavaScriptCore(WebKit)重用它们的JS编译器来提前编译WebAssembly模块。这会导致可预测的高性能、更快的启动和可能更低的内存消耗。在这些实现中,使用了使用抽象控制和运算符堆栈的相同算法策略。在解码过程中,对传入的字节码进行一次验证,不需要额外的中间表示。SpiderMonkey引擎包括两个WebAssembly编译层。第一个是快速基线JIT,它不会创建中间表示(IR),但会跟踪寄存器状态并尝试在正向传递中执行简单的寄存器分配。baselineJIT仅用于快速启动,而optimizingJIT在后台并行编译模块。V8在原型配置中包含一个类似的基线JIT。优化JIT获得JavaScript的顶级执行并将其重用于WebAssembly。V8和SpiderMonkey都使用基于SSA的中间表示。WebAssembly的结构化控制流对此有很大帮助,使解码算法更简单、更高效,并避免了通常的JIT限制。按照设计,WebAssembly中的所有内存访问都通过动态边界检查是安全的,这相当于根据当前内存大小检查地址。引擎将从进程中的某个基地址开始,在一个大的连续范围内分配内存。为了快速访问,基地址可以存储在专用的机器寄存器中,更积极的策略是将每个实例的机器代码专门化为特定的基地址,将其作为常量直接嵌??入代码中。在64位平台上,引擎可以利用虚拟内存来完全消除对内存访问的边界检查。引擎只预留了8GB的虚拟地址空间,除了临近启动的有效内存部分外,所有页面都被标记为不可访问。由于WebAssembly内存地址和偏移量是32位整数加上一个静态常量,因此访问不能超过8GB地址空间。因此,JIT可以简单地发出普通的加载/存储指令,并依靠硬件保护机制来捕获越界访问。通过提前编译,WebAssembly模块的编译可以并行化,每个功能分配给不同的线程,从而显着提高性能。比如v8和SpiderMonkey都使用了8个编译线程,都将编译速度提高了5-6倍。此外,WebAssembly二进制格式旨在支持流式传输,引擎可以在加载完整二进制文件之前开始编译各个函数。当与并行化相结合时,这可以最大限度地减少冷启动时间。代码缓存除了冷启动时间,热启动时间也很重要,因为用户可能会重复访问同一个网页。IndexedDB数据库的JavaScriptAPI允许JavaScript操作和编译WebAssembly模块,并将它们的编译表示存储为不透明的blob。这允许JavaScript应用程序在下载和编译之前首先查询IndexedDB以获取WebAssembly模块的缓存版本。在v8和SpiderMonkey中,这种机制可以将启动时间缩短几个数量级。字节码校验的速度和简单性是性能好和保证高的关键,WebAssembly没有像JVM字节码校验那样150页的指令,而只有一页形式化的符号。6.总结WebAssembly侧重于支持低级代码,尤其是从c/c++编译的代码。WebAssembly可能会通过包含尾调用、堆栈切换或协程等相关原语而演变成一种高级语言,但是,一个非常重要的目标是提供对所有Web浏览器内置的垃圾收集器的访问,从而消除编译时的相关缺陷网络和JavaScript。在Web之外,WebAssembly可能会在其他领域得到广泛使用,例如内容分发网络中的沙盒、智能合约或区块链上的去中心化计算,作为移动设备的代码格式,甚至只是作为一种方式来提供可移植的独立引擎运行。[参考及相关阅读]https://webassembly.org/https://webassembly.github.io/spec/.http://asmjs.orghttps://cacm.acm.org/magazines/2018/12/23288-使用webassembly/fulltext使web跟上速度