《WebAssembly 原理与核心技术》笔记

WebAssembly编译之(1)-asm.js及WebAssembly原理介绍(wasm) asm.js及WebAssembly原理介绍 阅读详情

读了一本关于 WebAssembly 虚拟机实现的书 ——《WebAssembly 原理与核心技术》,这本书写得非常详细,也有实现的源码讲解。本文是对阅读时所做的粗略的笔记。

1、Wasm (WebAssembly)

1.1、asm.js

asm.js 是 JavaScript 语言的一个严格子集,试图通过减少动态特性和添加类型提示的方式帮助浏览器提升 JavaScript 优化空间。相较于完整的 JavaScript 语言,裁剪后的 asm.js 更靠近底层,更适合作为编译器目标语言。

function MyAsmModule() {    
    "use asm"; // 告诉浏览器这是一个asm.js模块  

    function add(x, y) {        
        x = x|0;        // x是整数        
        y = y|0;        // y也是整数        
        return (x+y)|0; // 返回值也是整数    
    }    

    return { add: add };
}

asm.js 有优点也有缺点。

优点非常明显:asm.js 代码就是 JavaScript 代码,因此完全可以跨浏览器运行。能识别特殊标记的“聪明”浏览器可以根据提示进行激进的 JIT 优化,甚至是 AOT 编译,大幅提升性能。不能识别特殊标记的“笨”浏览器也可以忽略这些提示,直接按普通 JavaScript 代码来执行。

asm.js 的缺点也很明显,那就是“底层”得不够彻底,例如代码仍然是文本格式;代码编写仍然受 JavaScript 语法限制;浏览器仍然需要完成解析脚本、解释执行、收集性能指标、JIT 编译等一系列步骤。

1.2、Wasm

如果采用像 Java 类文件那样的二进制格式,不仅能缩小文件体积,减少网络传输时间和解析时间,还能选用更接近机器的字节码,这样 AOT/JIT 编译器实现起来会更轻松,效果也更好。

按字面意思理解,WebAssembly 就是 Web 汇编,是为 Web 浏览器定制的汇编语言。

1.2.1、编译器支持

除了四大浏览器的一致支持,Wasm 也获得了主流编程语言的强力支持。C/C++ 是最先可以编译为 Wasm 的语言,因为 Emscripten 已经支持 asm.js,只要把 asm.js 编译成 Wasm 即可。由于两项技术比较相似,这个编译工作并不难。

Emscripten 改为默认使用 LLVM 提供的 Wasm 编译后端直接生成 Wasm。

现代的高级编程语言要么是由编译器编译成机器码(Machine Code)然后执行,要么就是由解释器直接解释执行。前者可以理解为预先编译(Ahead-of-Time compilation,AOT),后者可以在执行时将部分“热”代码即时编译成机器码并执行,以提升性能,这就是所谓的即时编译(Just-in-Time Compilation,JIT)。

为了降低代码实现难度、提高可扩展性,现代编译器一般都会按模块化的方式设计和实现。典型的做法是把编译器分成前端(Front End)、中端(Middle End)和后端(Back End)。前端主要负责预处理、词法分析、语法分析、语义分析,生成便于后续处理的中间表示(Intermediate Representation,IR)。中端对 IR 进行分析和各种优化,例如常量折叠、死代码消除、函数内联。后端生成目标代码,把 IR 转化成平台相关的汇编代码,最终由汇编器编译为机器码。

1.2.2、设计理念

第一,层次必须低,尽量接近机器语言,这样浏览器才更容易进行 AOT/JIT 编译,以趋近原生应用的速度运行 Wasm 程序。

第二,要适合作为目标代码,由其他高级语言编译器生成。

而要在浏览器运行,Wasm 又必须满足其他一些要求。首先代码必须安全可控,不能像真正的汇编代码那样可以执行任意操作。然后,代码必须是平台无关的,这样才可以跨浏览器执行。

从高级语言编译器的角度来看,Wasm 是目标代码。但从浏览器的角度来看,Wasm 却更像 IR,最终会被 AOT/JIT 编译器编译成平台相关的机器码。基于以上介绍的这些因素,Wasm 最终采用了虚拟机/字节码技术,并且定义了紧凑的二进制格式。

Wasm 规范定义了两种模块格式:二进制格式和文本格式。如果和传统汇编语言做类比,那么 Wasm 模块的二进制格式相当于目标文件或可执行文件格式,文本格式则相当于汇编语言。使用汇编器可以把文本格式编译为二进制格式,使用反汇编器可以把二进制格式反编译成文本格式。

和 Java 虚拟机(Java Virtual Machine,JVM)一样,Wasm 也采用了栈式虚拟机和字节码。

Wasm 模块必须是安全可靠的,绝不允许有任何恶意行为。为了保证这一点,Wasm 模块包含了大量类型信息,这样绝大多数问题就可以通过静态分析在代码执行前被发现,只有少数问题需要推迟到运行时进行检查。

2、二进制格式介绍

Wasm 二进制格式也是以魔数(Magic Number)和版本号开头。在魔数和版本号之后是模块的主体内容,这些内容被分门别类放在不同的段(Section,也叫Segment)中。

先放个 go 的源码案例,方便对二进制格式里面的内容进行说明。

package main
import "fmt"

// 全局变量
const PI float32 = 3.14

// 类型定义
type type0 = func(a, b int32) int32
type type1 = func()
type type2 = func(ptr, len int32)

// 函数
func Add(a, b int32) int32 { return a + b }
func Sub(a, b int32) int32 { return a - b }
func Mul(a, b int32) int32 { return a * b }
func Div(a, b int32) int32 { return a / b }

func main() {
    fmt.Println("Hello, World!")
}

下面简单介绍 Wasm 二进制模块可能包含的 12 种段。

类型段(ID是1)

该段列出 Wasm 模块用到的所有函数类型(又叫作函数签名,或者函数原型)。

假设函数签名在类型段中的排列顺序和它们在源代码中首次出现的顺序一致,那么前两个签名应该是 (int32,int32)→(int32) 和 ()→()。

导入段和导出段(ID是2和7)

这两个段分别列出模块所有的导入项和导出项,多个模块可以通过导入和导出项链接在一起

有一个全局变量和 4 个函数被导出(首字母大写),因此导出段中应该包含 5 个项目:一个全局变量和 4 个函数。

函数段和代码段(ID是3和10)

内部函数信息被分开存储在两个段中,其中函数段实际上是一个索引表,列出内部函数所对应的签名索引;代码段存储内部函数的局部变量信息和字节码。不难看出,函数和代码段中的项目数量必须一致,且一一对应。

假设函数在函数段和代码段中的排列顺序也和它们在源代码中定义的顺序一致,那么函数段的内容应该是 [0,0,0,0,1],其中前 4 个 0 是加减乘除这 4 个函数的类型索引,1 是main()函数的类型索引(注意并不包含导入的函数)。相应的,代码段也应该包含 5 个项目,依次存放这 5 个函数的信息。

表段和元素段(ID是4和9)

表段列出模块内定义的所有表,元素段列出表初始化数据。Wasm 规范规定模块最多只能导入或定义一张表,所以即使模块有表段,里面也只能有一个项目。表主要和间接函数调用有关

内存段和数据段(ID是5和11)

内存段列出模块内定义的所有内存,数据段列出内存初始化数据。Wasm 规范规定模块最多只能导入或定义一块内存,所以即使模块有内存段,里面也只能有一个项目。

全局段(ID是6)

该段列出模块内定义的所有全局变量信息,包括值类型、可变性(Mutability)和初始值。上面的 Go 代码只定义了一个全局变量(准确地说是一个不可变常量),因此全局段中应该只有一个项目。

起始段(ID是8)

该段给出模块的起始函数索引。和其他段有所不同,起始段只能有一个项目。起始函数主要起到两个作用,一个是在模块加载后进行一些初始化工作;另一个是把模块变成可执行程序。如果模块有起始段,那么 Wasm 实现在加载模块后会自动执行起始函数。上面的 Go 代码有主函数,因此编译后的模块有起始段,里面放着该函数的索引。

自定义段(ID是0)

该段是给编译器等工具使用的,里面可以存放函数名等调试信息,或者其他任何附加信息。自定义段不参与 Wasm 语义,所以即便是完全忽略自定义段也不影响模块的执行。

Note

除了自定义段,其他段必须按 ID 递增的顺序出现。之所以有这样的规定,是因为很多段之间存在信息的依赖关系。

例如,由于导入段、函数段、代码段等都需要知道函数类型信息,所以类型段必须在这 3 个段之前出现;由于导入的函数、表、内存、全局变量在各自索引空间的最前面,所以导入段必须在这 4 个段之前出现;由于导出函数、表、内存、全局变量时需要知道其索引,所以导出段必须在这 4 个段之后出现。

Wasm 二进制格式的设计原则之一是可以一遍(One-pass)完成模块的解析、验证和编译(指AOT或JIT编译)。换句话说,Wasm 实现(比如浏览器)可以在下载模块的同时进行解码、验证和编译。可流式处理(Streamable)是 Wasm 的特点之一,二进制模块中各个段的排列方式一定程度上是为了满足这一特点。另外,之所以要把函数的签名信息和其他信息分别存放在两个段里,也是为了满足这一特点

下面是模块的结构体定义。

type Module struct {    
    Magic      uint32      
    Version    uint32      
    CustomSecs []CustomSec 
    TypeSec    []FuncType  
    ImportSec  []Import    
    FuncSec    []TypeIdx   
    TableSec   []TableType 
    MemSec     []MemType   
    GlobalSec  []Global    
    ExportSec  []Export   
    StartSec   *FuncIdx    
    ElemSec    []Elem      
    CodeSec    []Code      
    DataSec    []Data      
}

这些字段在后面会详细介绍,但这里有两点需要说明:第一,如前文所述,Wasm 规范规定模块最多只能导入或定义一张表,内存也有同样限制。但这些限制只是暂时的

第二,由于起始段只需要记录一个函数索引,所以我们把它定义成指针类型。如果该指针是 nil,则表示没有起始段。

2.1、二进制格式分析

Wasm 二进制格式的魔数占 4 个字节,内容是 \0asm 。Wasm 二进制格式的版本号也占 4 个字节,当前版本是 1。

在 Wasm 二进制格式里,每个段都以 1 字节的段 ID 开始。除了自定义段,其他段的结构都完全定义好了。由于自定义段的内容可能是未知的,所以段 ID 后面存储了段的实际内容字节数,这样 Wasm 实现就可以根据字节数跳过不认识的自定义段。对于非自定义段,内容字节数并非解码必须的,但却有助于在需要时快速跳过某些段。

3、指令集介绍

Wasm 指令包含两部分信息:操作码(Opcode)和操作数(Operands)。操作码是指令的 ID,决定指令将执行的操作;操作数则相当于指令的参数,决定指令的执行结果。

Wasm 指令的操作码固定为一个字节,因此指令集最多只能包含 256 条指令。像这样的代码有一个响亮的名称:字节码(最有名的就是 Java 字节码)。

Wasm 规范一共定义了 178 条指令,这些指令按功能可以分为 5 大类,分别是:
1)控制指令(Control Instructions)
2)参数指令(Parametric Instructions)
3)变量指令(Variable Instructions)
4)内存指令(Memory Instructions)
5)数值指令(Numeric Instructions)

Wasm 只支持 4 种数值类型:i32、i64、f32、f64。Wasm 指令集中很大一部分指令是以这 4 种数值中的某一种为主进行操作,这些指令的操作码助记符以类型和点号为前缀,比如i32.load、i64.const、f32.add、f64.max。

Wasm 规范要求整数采用 2 的补码表示。使用这种编码格式,数据里并没有显式编码整数的符号位,所以数值可以被解释为正数,也可以被解释为负数。对于某些计算(比如加、减、乘),无论怎么解释,最终的计算结果都是一样的;对于另外一些计算(比如除、求余、比较),就必须指定数值有无符号。如果整数指令的结果不受符号影响,则操作码助记符无特别后缀,比如 i32.add。否则,由指令决定将整数解释为有符号(Signed,操作码助记符以 _s 结尾)还是无符号(Unsigned,操作码助记符以 _u 结尾)。强调符号的指令一般成对出现,比如 i64.div_s 和 i64.div_u。

操作数又分为两种:静态操作数和动态操作数。静态操作数直接编码在指令里,跟在操作码的后面。动态操作数在运行时从操作数栈获取。为了避免混淆,我们把静态操作数称为指令的静态立即参数(Static Immediate Arguments),后文简称立即数。如无特别说明,后文出现的操作数特指动态操作数(Dynamic Operands)。

和 Java 虚拟机一样,Wasm 规范实际上也定义了一台概念上的栈式虚拟机。绝大多数的 Wasm 指令都是基于一个虚拟栈工作:从栈顶弹出若干个数,进行计算,然后把结果压栈。如上文所述,我们把这些运行时位于栈顶并被指令操纵的数叫作指令的动态操作数,简称操作数。很自然的,我们称这个栈为操作数栈。为了实现控制指令,Wasm 还需要一个控制栈

由于采用了栈式虚拟机,大部分 Wasm 指令(特别是数值指令)都很短,只有一个操作码,这是因为操作数已经隐含在栈上了。举例来说,i32.add 指令只有一个操作码 0x6A。在执行时,这条指令从栈顶弹出两个 i32 类型的整数,这两个整数相加,然后把结果(也是 i32 类型)压栈。这一设计使得 Wasm 字节码非常紧凑。

还有一些编程语言采用寄存器虚拟机,比如 Lua 语言和 Android 早期的 Dalvik 虚拟机。因为指令中需要包含寄存器索引,所以寄存器虚拟机的指令一般比较长。以 Lua 虚拟机为例,指令固定长度为 4 字节,加法指令可以写成 ADD A B C,表示将寄存器 B 和寄存器 C 中的数相加,写入寄存器 A。

3.1、指令分析

Wasm 支持直接和间接两种函数调用方式。

call 指令(操作码 0x10)进行直接函数调用,函数索引由立即数指定。

call_indirect 指令(操作码 0x11)进行间接函数调用,函数签名的索引由立即数指定,到运行时才能知道具体调用哪个函数。

4、基本结构

从整体上看 Wasm 文本格式和二进制格式基本是一致的。除了表现形式明显不同以外,在结构上,两种格式还有几个较大的不同之处。

1)二进制格式是以段(Section)为单位组织数据的,文本格式则是以域(Field,为了和编程语言中的字段进行区别,本书将其称为域)为单位组织内容。域相当于二进制段中的项目,但不一定要连续出现,WAT 编译器会把同类型的域收集起来,合并成二进制段。

2)在二进制格式中,除了自定义段以外,其他段必须按照 ID 递增的顺序排列,文本格式中的域则没有这么严格的限制。不过,导入域必须出现在函数域、表域、内存域和全局域之前。

3)文本格式中的域和二进制格式中的段基本是一一对应的,但是有两种情况例外。第一种是文本格式没有单独的代码域,只有函数域。WAT 编译器会将函数域收集起来,分别生成函数段和代码段。第二种是文本格式没有自定义域,没办法描述自定义段(已经有提案建议增强 WAT 语法,支持表达自定义数据)。

4)为了便于编写,文本格式提供了多种内联写法。例如:函数域、表域、内存域、全局域可以内联导入或导出域,表域可以内联元素域,内存域可以内联数据域,函数域和导入域可以内联类型域。这些内联写法只是“语法糖”,WAT 编译器会做妥善处理。

5、操作数栈

Wasm 程序的执行环境是一台栈式虚拟机,绝大多数 Wasm 指令都要借助操作数栈来工作:从上面弹出若干数值,对数值进行计算,然后再把计算结果压栈。这台虚拟机还可以附加一块内存,鉴于操作数栈只能存放临时数据,生命力更强的数据则可以放在内存里。此外,这台虚拟机还可以操作表和全局变量。

对于计算机来说,一切信息终究只是 0 和 1 组成的序列。如果把数据的单位放大一些,那么一切信息都只是字节序列而已。一串 0 和 1 代表的含义,取决于我们如何解释它。例如二进制序列 10101001,我们可以认为它是一个无符号整数,表示 169(或者 0xA9);也可以认为它是一个有符号整数,表示 -87;还可以认为它是一个 ASCII 字符,表示 ©。当我们说一个数是整数或者浮点数时,并不是说它所对应的字节序列有什么特别之处,只是因为我们知道它表示一个整数或者符点数,并且要通过某种方式告诉计算机这一点。

5.1、参数指令

drop 指令(操作码 0x1A)从栈顶弹出一个操作数并把它“扔掉”。drop 指令没有立即数,也不会检查操作数的类型。

select 指令(操作码 0x1B)从栈顶弹出 3 个操作数,然后根据最先弹出的操作数从其他两个操作数中选择一个压栈。最先弹出的操作数必须是 i32 类型,其他 2 个操作数是相同类型的就可以。如果最先弹出的操作数不为 0,则把最后弹出的操作数压栈;如果为 0,则把中间的操作数压栈。

6、内存介绍

和真实的机器一样,Wasm 内存是一个随机存取存储器(Random Access Memory,简称RAM)。从本质上讲,它就是一个线性的字节数组,可以按偏移量(也就是内存地址)读写任意字节。数值在 Wasm 内存中按小端方式存储。我们已经知道,Wasm 操作数栈是类型安全的。每一条指令都按照规定的方式改变栈顶操作数,因此在任意时刻我们都知道操作数栈里有多少操作数,分别是何种类型。与操作数栈不同,Wasm 内存则完全是无类型的,没办法在编译器对它做任何类型检查。在运行时能做的检查也只是确保内存读写不会越界。

6.1、内存指令

内存指令一共有 25 条,按操作又可以分为 3 组。第一组是加载指令,共 14 条,从内存中读取数据,压入操作数栈。第二组是存储指令,共 9 条,从栈顶弹出数值,写入内存。第三组是页数获取和增长指令,共 2 条,不读写内存,只获取或者增长内存页数。

加载指令从内存加载数据,转换为适当类型的值,再压入操作数栈。Wasm 使用立即数+操作数的内存寻址方式。所有的加载和存储指令都带有两个立即数:对齐方式和内存偏移量。其中对齐方式存放的是以 2 为底,对齐字节数的对数。

加载和存储指令还需要从操作数栈上弹出一个 i32 类型的数,把它和立即数偏移量相加,即可得到实际内存地址。由于静态的立即数偏移量和动态的操作数偏移量都被解释为 32 位无符号整数,所以 Wasm 实际上拥有 33 比特的地址空间。

6.2、Note

Wasm 内存是一块抽象的 RAM(本质上就是一个线性的字节数组),并且可以在限制范围内按页动态增长。Wasm 提供了丰富的内存指令,用于读写各种基本类型的数值,这些数值在 Wasm 内存中按小端方式存储。简而言之,Wasm 内存和真实内存非常接近,只具备最基本的读写功能,像内存管理、垃圾回收这样的高级功能都要靠高级语言解决。也正是因为贴近底层,Wasm 程序才能够以接近本地程序的速度执行。

7、函数调用介绍

Wasm 提供了直接函数调用和间接函数调用两种函数调用方式。前一种方式通过指令立即数指定的函数索引直接调用函数,因此也可以称为静态函数调用。后一种方式要借助栈顶操作数和表间接调用函数,因此也可以称为动态函数调用。

1.参数和返回值函数的参数由调用方准备。在调用函数之前,调用方应该将参数准备好。更准确地说,是把实际参数按顺序压栈(第一个参数在最下面)。函数调用完毕后,这些参数已经被弹出,取而代之的是函数的返回值(如果有的话)。Wasm 函数可以返回多个值,这些值会按顺序出现在栈顶(第一个返回值在最下面)。

2.局部变量函数指令可以操纵操作数、局部变量、全局变量和内存。操作数的生命周期最短暂,可能只持续几条指令。局部变量的生命周期就是整个函数执行的过程,我们在前文也提到过,函数的参数实际上也是局部变量。全局变量和内存数据的生命最长,在整个模块执行期间都有效。如果全局变量或内存是从外部导入的,那么它的生命周期将更长,很可能会跨越多个模块实例的生命周期。

3.调用栈和调用帧我们知道,函数的指令需要使用操作数栈,函数也需要为参数和局部变量分配空间。另外,为了实现函数调用,往往还需要记录一些其他信息(后面会详细介绍)。我们把这些数据看成一个整体,把它叫作函数的调用帧(Call Frame)。每调用一个函数,就需要创建一个调用帧;当函数执行结束,再把调用帧销毁。我们可以把这一系列调用帧理解成栈结构,将其称为函数的调用栈(Call Stack)。一连串的函数调用就是不停创建和销毁调用帧的过程,但是在任一时刻,只有位于栈顶的调用帧是活跃的,我们称之为当前帧,与之关联的函数称为当前函数。

7.1、函数调用实现

我们需要一个程序计数器(Program Counter,PC)来记录指令执行的位置。当一个控制帧被重新激活(成为栈顶控制帧),可以根据PC恢复指令执行。

铺垫了这么多,终于可以实现 call 指令(操作码 0x10)了。我们已经知道,该指令带有一个立即数,给出被调用函数的索引。指令执行时,会根据被调用函数的类型从栈顶弹出参数。指令执行结束后,被调用函数的返回值会出现在栈顶。

7.2、局部变量指令

local.set 指令(操作码 0x21)用于设置局部变量的值。局部变量的索引由立即数指定,新的值从栈顶弹出(必须和要修改的局部变量是相同类型)。

8、控制指令介绍

对于虚拟机来说,goto/jump 指令已经没那么大危害了。因为大多数情况下,指令是由编译器生成的,程序员基本不需要直接接触代码。但是,允许任意跳转还是存在一个问题:代码不太好验证。仍然以 Java 虚拟机为例,由于 goto 等指令的存在,Java 字节码验证起来非常麻烦。作为对比,Wasm 彻底废弃了任意跳转指令,只支持结构化控制指令和受限制的跳转指令,因此字节码很容易验证,可以用几百行甚至更少的代码实现

跳转标签索引是相对的,是一个抽象的概念,只有针对具体跳转指令才有具体含义。比如某条跳转指令,对它来说标签索引 0 表示该指令所在的控制块定义的跳转标签,1 表示往外一层控制块定义的跳转标签,2 表示再往外一层控制块定义的跳转标签,以此类推。下面这个例子展示了多层 block 指令和跳转标签索引的用法。

8.1、控制指令实现

br_table 指令进行无条件查表跳转。br 和 br_if 指令只能指定一个跳转目标,且在编译期就已经确定。与这两条指令不同,br_table 指令可以指定多个跳转目标,最终使用哪个跳转目标要到运行期间才能决定。更具体地说,br_table 指令的立即数给定了 n+1 个跳转标签索引。其中前 n 个标签索引构成了一个索引表,后一个标签索引是默认索引。当 br_table 指令执行时,要先从操作数栈顶弹出一个 i32 类型的值(假设它为 m)。如果 m 小于等于 n,则跳转到索引表第 m 个索引指向的标签处,否则跳转到默认索引指定的标签处。

9、本地函数调用

我们已经对 Wasm 函数和本地函数进行了足够细致的讨论,现在总结一下二者的异同之处。相同之处:都是函数,都可以被调用,都要(按照签名)从栈顶拿走参数并放回返回值。不同之处:Wasm 函数是在模块内部定义的,有自己的字节码;本地函数是从外部导入的,用本地语言编写。为了便于实现,有必要将二者统一起来。

对于 Wasm 函数,其类型可以通过函数段获得,代码可以从代码段获得。这两个字段都可以通过函数索引获取,不过也可以简化实现。对于本地函数(和其他外部函数),其类型在导入段中描述,实现则是在模块链接时给定。

如果是外部或本地函数,是不需要创建调用帧的,只需要先根据函数签名把参数从栈顶弹出,然后调用本地函数,最后把返回值压栈即可,代码如下所示。

func callExternalFunc(vm *vm, f vmFunc) {    
    args := popArgs(vm, f._type)    
    results := f.goFunc(args)    
    pushResults(vm, f._type, results)
}

参数和返回值的弹出和压入由 popArgs()/pushResults() 函数完成

9.1、间接函数调用

直接函数调用非常简单,要调用哪个函数是在编译期确定的。换句话说,被调用函数的索引硬编码在 call 指令的立即数中。间接函数调用则有所不同:在编译期只能确定被调用函数的类型(call_indirect 指令的立即数里存放的是被调用函数的类型索引),具体调用哪个函数只有在运行期间根据栈顶操作数才能确定。由于这个原因,我们也可以称直接函数调用为静态调用,称间接函数调用为动态调用。

指令执行时,需要先从栈顶弹出一个 i32 类型的操作数(假设是 1),以此为索引查表就可以找到函数引用,继而找到并调用函数。

现在问题来了,为什么不能直接用函数索引作为函数引用呢?如果函数引用只指向内部函数或者导入的外部函数,的确可以这样执行。但实际情况却没有这么简单。表是可以被导入或导出的,所以同一张表可以被多个模块操作。换句话说,对于某个模块来说,其表内函数引用指向的函数可能并不在该模块的函数集合(外部导入的函数+内部定义的函数)之内。

10、定义实例接口

模块从二进制格式到最终执行分为 3 个语义阶段:解码、验证、执行,执行阶段又可以分为实例化和调用两个小阶段。解码阶段将二进制模块解码为内存表示,验证阶段对模块进行严格的验证,实例化阶段根据模块内存创建并准备好模块实例,然后就可以调用模块的公开函数了。

11、AOT 编译器

字节码程序(比如 Wasm 二进制模块或者 Java 类文件)通常有 3 种执行方式:解释执行、预先编译为本地可执行程序,以及在运行时即时编译为本地机器代码。其中预先(Ahead-of-Time)编译方式一般简称为 AOT,即时(Just-in-Time)编译方式一般简称为 JIT。简单来说,解释执行不编译字节码,AOT 在执行前把全部字节码编译为本地代码,JIT 在执行中将部分(热点)字节码编译为本地代码。

11.1、AOT 介绍

AOT 可以预先生成执行效果较好的本地代码,但由于是静态编译,所以无法准确预测代码在运行时的行为,不能达到性能极致。相比而言,JIT 结合了解释器和 AOT 的优点,在运行时收集各种信息和指标,既可以快速启动,又可以生成更加优化的代码,所以能在总体上达到更佳的运行效果,这也是解释器 + JIT 成为目前主流执行方式的原因之一。

生成更高效的代码是需要花费很多时间的,为了在快速启动和高度优化之间取得平衡,很多 JIT/AOT 实现(比如 Java 虚拟机和 Firefox 浏览器)使用了分层(Tiered)编译技术。分层编译一般分为两层,第一层(tier1,叫法因实现而异)编译器可以较快地生成本地代码并执行;而第二层(tier2)编译器在后台线程中生成执行效果更好的代码,并替换 tier 生成的代码。

【WebAssembly】WebAssembly概念介绍和在js中使用 记录下WebAssembly的概念和在JavaScript中的使用方法。 阅读详情

相关推荐

JIT加速:WebAssembly集成方案

WebAssembly为Go生态开辟了全新的可能性:从浏览器到服务端,从边缘计算到插件系统。通过深度集成JIT加速技术,Go应用能够在保持安全隔离的同时,突破性能边界实现接近原生的执行效率。随着WASI规范的演进和组件模型的落地,我们正步入一个多语言、跨平台、高性能计算的崭新时代。在这个融合架构中,Go不再仅是WASM的宿主,更成为构建下一代分布式应用的基石——安全地扩展能力边界,高效地整合异构算力,智能地优化资源利用,最终重塑云原生计算的效能标杆。

caishuangxi111的博客 2092

WebAssembly 基本了解

现代浏览器朝着第二操作系统一路狂奔,网页在线 PS、3D 地图、音视频编解码、游戏引擎等等应用,都在浏览器实现了,这都是基于 WebAssembly。WebAssem应不应该卷起来?

wade3po的博客 7358

WebAssembly:开启高性能 Web 应用的新篇章

在互联网技术飞速发展的浪潮中,Web应用的性能一直是一个重要的优化目标。Emscripten: 一个C/C++到WebAssembly的编译工具链,可以将C/C++代码编译成Wasm,并生成JavaScript胶水代码(glue code),使Wasm模块能够与JavaScript互操作。Rustc + wasm-pack: Rust语言官方支持WebAssembly,通过rustc编译器可以生成Wasm目标,wasm-pack工具则可以打包Rust生成的Wasm模块,并生成JavaScript绑定。

WSHAN_GEO的博客 931

什么是WebAssembly(Wasm)?

文章目录WebAssembly是什么?WebAssembly和JavaScript WebAssembly是什么? WASM是什么?带你了解WebAssembly的前世今生 参考URL: http://www.mabiji.com/webassembly/wasm.html WebAssembly是什么?WebAssembly即WASM, WebAssembly是一种新的编码格式并且可以在浏览器中运行,WASM可以与JavaScript并存,WASM更类似一种低级的汇编语言。 WebAssembly(又名w

西京刀客 4万+

WebAssembly相关技术介绍

本文将介绍Wasm的背景、工具栈、研发示例、工程应用以及未来发展。

leyang0910的博客 2153

WebAssembly逆向分析:WASM格式解析、反编译工具链与JS-WASM交互深度分析

WebAssembly(WASM)作为一种可移植、高性能的二进制指令格式,自2017年发布以来已成为Web平台的关键技术。它允许C/C++、Rust等语言编译的代码在浏览器中以接近原生的速度运行,同时保持与JavaScript的互操作性。对于爬虫工程师而言,WASM既是机遇也是挑战——许多网站将核心算法(如加密、签名、数据校验)编译为WASM模块,既提升了执行效率,也增加了逆向分析的难度。

从事移动开发八年,csdn博客专家,阿里云特邀专家,华为云云享专家,高级网络信息安全工程师,大数据工程师(高级),python技术开发(高级),HarmonyOS 应用开发者高级认证,现在某学校任教。 879

Jupyter 社区会议技术解读:JupyterLite 纯浏览器发行版、扩展国际化工作流与终端笔记本 Nbterm

2021 年 4 月 27 日举行的 Jupyter 社区会议(Jupyter Community Call)围绕三个核心技术议题展开:Jeremy Tuloup 演示了完全运行在浏览器中的 JupyterLite 发行版;Gonzalo Peña-Castellanos 分享了 JupyterLab 扩展国际化(i18n)与语言包(language packs)的发布流程;David Broch

gitblog_00931的博客 37

瑞芯微RV1126平台人脸识别模型转换实战:从ONNX到RKNN的完整流程

本文详细介绍了在瑞芯微RV1126平台上,将人脸识别模型从ONNX格式转换为RKNN格式的完整实战流程。内容涵盖模型结构分析、RKNN-Toolkit环境搭建、量化数据集准备、核心转换脚本编写与参数解析,以及在设备端的部署测试与性能调优,旨在帮助嵌入式AI开发者高效完成模型转化,提升边缘设备上的人脸识别应用部署效率。

o5p6q的博客 570

WebAssembly二进制逆向分析完全指南:掌握wasm-decompile核心技术

你是否曾经面对一个WebAssembly二进制文件,却无从下手理解其内部逻辑?传统的十六进制查看器和反汇编工具在处理Wasm模块时往往力不从心,无法提供直观的代码结构。本文将带你深入探索WABT工具链中的秘密武器——wasm-decompile,彻底改变你对Wasm逆向分析的认知。 ## 传统困境 vs 现代解决方案 ### 传统方法的局限性 在wasm-decompile出现之前,开发者主要

gitblog_00558的博客 643

攻防世界 misc题GFSJ1129-【您看我还有机会吗?】

我们发现了隐藏内容,一开始我以为那个英文就是flag,提交后发现不对,然后我想只有一张图片然后图片上的隐写内容又不是flag,那必定还有什么文件被隐藏了,于是我们打开VMware并启动Ubuntu,使用binwalk分析一下有没有隐藏文件,我们要在Terminal中分析,如下。我们发现里面有一个flag.txt文件,我们尝试提取里面内容,但发现又要密码,我们想起了之前图片上的隐藏内容,我们将那段英文输入(包括空格和符号,在VMware中输入密码不会有显示,你看到的就像没输入一样),然后我们看到如下。

大数据专业,关注 AI 安全与数学建模。记录 Web 安全、对抗攻击、隐私保护等安全实践,以及从经典模型到智能方法的建模思考。用数学思维理解 AI,用 AI 方法反哺安全与建模。 1093

华为云Stack扩容实战:从CMDB配置到计算节点增减容,一份避坑指南

本文详细解析华为云Stack扩容实战,从CMDB配置到计算节点增减容的全流程操作指南。通过典型场景案例和避坑建议,帮助企业高效管理计算资源扩容与减容,确保业务连续性。特别针对华为云Stack的弹性扩容能力提供专业指导,涵盖存储、网络及自动化工具链的配合使用。

weixin_30591551的博客 479

wasmgo-book:WebAssembly原理与核心技术的实践指南

wasmgo-book:WebAssembly原理与核心技术的实践指南 项目介绍 《WebAssembly原理与核心技术》源代码项目(以下简称wasmgo-book)是一个致力于深入解析WebAssembly(简称Wasm)原理及核心技术的开源项目。它包含了丰富的学习资源和实践代码,旨在帮助开发者理解Wasm的底层架构和运作机制,从而更好地应用这一技术于现代Web开发中。 项目技术分析 wasmg...

gitblog_00234的博客 987

计算机组成原理课设

计算机组成原理课程设计

qq_63306482的博客 8054

【C语言WASM多线程实战指南】:掌握高性能WebAssembly并发编程核心技术

掌握C语言WASM的多线程支持,提升WebAssembly并发性能。本文详解多线程在浏览器端的实现机制、共享内存管理与线程同步方法,适用于高性能计算、音视频处理等场景,助你构建高效Web应用,值得收藏。

SimProceed的博客 1191

芒果YOLO11改进21:检测头Head篇:即插即用|新颖轻量化非对称多级压缩LADH检测头,原创改进适配YOLOv8高效检测头,检测头新颖性改进,全网首发独一份

检测头Head篇:即插即用|新颖轻量化非对称多级压缩LADH检测头,原创改进适配YOLOv8高效检测头,检测头新颖性改进,全网首发独一份

包括YOLOv5、YOLOv7、YOLOv8等模型改进 790

别再只当调包侠了!YOLO检测头到损失函数全链路拆解,看完才算真懂

YOLO的精妙不在于某个单一创新,而在于各模块围绕“实时精准检测”这一目标的协同设计。Backbone追求高效特征压缩,Neck保障多尺度信息流通,Head解耦任务冲突,Loss动态引导优化方向。只有理解这套内在逻辑,才能跳出“调包-试错”的低效循环,在面对新场景、新问题时做出有理有据的改进决策。技术深度决定职业天花板。下次再调YOLO时,不妨先问自己:我改的这个参数,到底影响了系统中的哪一环?参考资料。

专注于Python爬虫开发,分享爬虫技巧、项目实战与反爬经验,使用Scrapy、BeautifulSoup等工具,解决数据抓取难题。 185

Flutter WebAssembly (Wasm) 的性能:与 Dart2JS 编译产物的运行效率对比

各位来宾,各位技术同仁,大家好!今天,我们齐聚一堂,共同探讨一个备受瞩目的议题:Flutter WebAssembly(Wasm)的性能,并将其与我们熟悉的Dart2JS编译产物的运行效率进行深入对比。Flutter在移动端取得了巨大的成功,如今它正大步迈向Web平台,而WebAssembly的出现,无疑为Flutter Web的未来描绘了一幅激动人心的蓝图。作为一名编程专家,我将以讲座的形式,带领大家剖析这两种技术栈的内在机制、性能表现,以及它们各自在Web世界中的定位与潜力。

weixin_41455464的博客 630

代谢组数据分析(二十四):基于tidymass包从质谱原始数据到代谢物注释结果的实践指南

本教程介绍了如何使用 R 语言中的 tidymass 包进行代谢组学数据分析。tidymass 是一个专为液相色谱-质谱(LC-MS)数据设计的开源工具,提供从数据准备到统计分析的全流程支持。教程详细讲解了数据准备、数据处理、数据探索、数据清洗、代谢物注释、统计分析及结果输出的步骤。用户需准备 MS1、MS2 数据和样本信息表,并通过 tidymass 进行峰识别、峰分组、保留时间校正等操作。数据处理后,用户可进行数据探索、清洗和代谢物注释,最终通过统计分析发现潜在的生物标志物,并输出结果。教程还提供了代码

专注生信领域 1635

突破WebAssembly内存限制:ffmpeg.wasm虚拟文件系统全解析

你还在为大文件处理时WebAssembly内存溢出烦恼吗?本文将详解ffmpeg.wasm如何通过虚拟文件系统(Virtual File System)技术突破浏览器内存限制,让4GB视频处理不再卡顿。读完你将掌握:虚拟文件系统工作原理、三步挂载流程、性能对比数据及生产级应用案例。 ## 内存瓶颈与解决方案 WebAssembly(WASM)作为浏览器端高性能计算的核心技术,默认内存限制通常在...

gitblog_00654的博客 1394

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

5分钟上手wasm-service:Rust开发者的WebAssembly前端开发指南

wasm-service是一个创新的前端开发方案,它将HTMX、WebAssembly、Rust和ServiceWorkers技术组合在一起,为Rust开发者提供了一条全新的Web前端开发路径。通过这个项目,开发者可以使用Rust编写WebAssembly模块,结合HTMX实现动态交互,并利用ServiceWorkers拦截网络请求,实现客户端的高效响应处理。 ## 为什么选择wasm-serv

gitblog_00262的博客 441

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

MPC-HC 封面显示修复:2.3.4 快速自查指南

MPC-HC 2.3.4 播放内嵌封面的音轨时,专辑封面会比例失调、边缘发虚,并且不再跟随播放器窗口一起伸缩。开发团队已在最新代码中确认了本次 **MPC-HC 封面显示修复**,正式版本尚在准备中。如果你正在使用 2.3.4 并播放带封面的音乐,这篇自查指南值得花两分钟读完。 [![MPC-HC 封面显示修复:图像缩放与插值处理效果示意](https://raw.gitcode.com/gh_

gitblog_01069的博客 841

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

【免费下载】 WebAssembly/wabt项目:wasm-decompile工具深度解析

wasm-decompile是WebAssembly二进制工具链(wabt)中的一个重要工具,它能够将二进制格式的Wasm模块反编译成一种更紧凑、更接近C语言风格的文本格式。相比于标准的WAT文本格式,这种反编译输出更适合人类阅读和理解,特别适合需要分析大量Wasm代码的开发人员。 ## 工具定位与设计目标 ### 核心目标 wasm-decompile主要服务于以下场景: - 语言运行时开

gitblog_01038的博客 857

ffmpeg.wasm 架构全解析:基于 WebAssembly 的浏览器端音视频处理方案

ffmpeg.wasm 是 FFmpeg 的纯 WebAssembly / JavaScript 移植版本,借助 Emscripten 工具链将 FFmpeg 源码及其依赖库编译为 WebAssembly,让视频与音频的录制、转码、流式处理能力完整落地到浏览器端。本文以 [apps/website/docs/overview.md](https://link.gitcode.com/i/88c61

gitblog_00897的博客 689

如何使用WebAssembly与C/Blazor构建高性能现代Web UI:2023完整指南

WebAssembly(简称Wasm)是一种二进制指令格式,为高级语言提供了一个高性能的编译目标,使C/C++、Rust等系统级语言能够在Web浏览器中以接近原生的速度运行。结合Blazor这一使用C#/Razor和HTML的Web UI框架,开发者可以构建既具备原生性能又拥有现代UI体验的Web应用。本指南将带你探索WebAssembly与C/Blazor的强大组合,掌握现代Web UI开发的核

gitblog_00446的博客 1120
上一篇: 代码分析属性图 CPG 介绍
下一篇: C++ 析构函数的坑
流左沙
博客等级 码龄11年 46粉丝 55原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值