大神赞过的:学习 WebAssembly 汇编语言程序设计
wptr33 2025-05-24 17:33 28 浏览
文/ 阿里淘系 F(x)Team - 旭伦
随着前端页面变得越来越复杂,javascript的性能问题一再被诟病。而Javascript设计时就不是为了性能优化设计的,这使得浏览器上可以运行的本地语言一再受到青睐。
从兼容性上看 WebAssembly,从 canIUse 数据看,已经达到了94.7%的高覆盖率。这个值跟 Javascipt 的await 支持程序差不多。基本上2017年以后的浏览器都支持,距现在已经5年了。主流的 Chrome, Chrome for Android, Android Browser, Safari, Safari on iOS, Edge, Firefox, Opera 全部都支持。
既然落地有戏,那我们就正式开始WebAssembly的破冰之旅。
初识WebAssembly
WebAssembly是定义在抽象机器上的一种汇编语言,浏览器负责将其编译成本地代码。负责这部分工作的一般仍然是v8这样的js引擎。 既然是个抽象机,那么就可以跨平台运行。我们既可以用工具链提供的解释器来运行,也可以通过本地的Node.js来运行。
与x86汇编使用命令式的汇编语言不同,WebAssembly使用类似于Lisp语言的S表达式来编写。S表达式就是以括号括起来语句的语言。
我们来看个最简单的例子:
(module
(func (result i32)
(i32.const 666)
)
(export "const_i32" (func 0))
)
最外面的括号是module,WebAssembly的代码以模块的方式组织。 模块里我们就可以通过func来定义函数。大家可以发现,函数没有名字。 如果想要给外界一个可以访问的名字,要通过export将其绑定到一个名字上。
WebAssembly二进制工具链
我们都知道,要将汇编语言编译机器指令需要汇编器。同时,还需要一大堆二进制的工具链,比如objdump,反汇编工具之类的来打辅助。
WebAssembly Community Group为我们提供了WebAssembly Binary Toolkit(wabt).
wabt是个多git库的工程,下载代码的方法如下:
git clone --recursive https://github.com/WebAssembly/wabt
cd wabt
git submodule update --init
然后可以通过cmake进行编译:
$ mkdir build
$ cd build
$ cmake ..
$ cmake --build .
编译成功之后,汇编器wat2wasm等工具就可以使用了。
比如我们把上节的例子存为test001.wat,就可以这样编译:
wabt/build/wat2wasm test001.wat
编译成功之后,将生成test001.wasm。
我们可以使用wasm-objdump来查看wasm的内容:
wabt/build/wasm-objdump -s -d -x test001.wasm
其中:-d是反汇编,-x是显示section详情,-s是显示原始数据。
输出内容如下:
test001.wasm: file format wasm 0x1
Section Details:
Type[1]:
- type[0] () -> i32
Function[1]:
- func[0] sig=0 <const_i32>
Export[1]:
- func[0] <const_i32> -> "const_i32"
Code[1]:
- func[0] size=5 <const_i32>
Code Disassembly:
000026 func[0] <const_i32>:
000027: 41 9a 05 | i32.const 666
00002a: 0b | end
Contents of section Type:
000000a: 0160 0001 7f .`...
Contents of section Function:
0000011: 0100 ..
Contents of section Export:
0000015: 0109 636f 6e73 745f 6933 3200 00 ..const_i32..
Contents of section Code:
0000024: 0105 0041 9a05 0b ...A...
将wasm代码运行起来
通过wasm解释器运行
wabt提供了wasm解释器wasm-interp. 我们可以运行所有export出来的函数,例如:
wabt/build/wasm-interp --run-all-exports ./test001.wasm
输出如下:
const_i32() => i32:666
在Node.js环境中运行
因为WebAssembly是标准,所以不需要安装任何三方包就可以使用。
运行WebAssembly中的函数只需要三步:
- 通过WebAssembly.compile编译buffer中的二进行wasm
- 通过WebAssembly.instantiate建立Web Assembly的实例
- 通过实例的exports中的方法来运行功能
在Node环境中,我们只要用fs API将文件读出来就可以直接运行了:
const {readFileSync} = require('fs')
const outputWasm = './test001.wasm';
async function run(){
const buffer = readFileSync(outputWasm);
const module = await WebAssembly.compile(buffer);
const instance = await WebAssembly.instantiate(module);
console.log(instance.exports.const_i32());
}
run();
在浏览器中运行
因为WebAssembly是标准,所以在浏览器中与Node.js中的API用法完全一样。不同的只是如何读取wasm文件。
下面我们启动一个本地服务器,使用fetch函数获取本地的wasm文件:
<!DOCTYPE html>
<html lang="en">
<meta charset="utf-8" />
<head>
<script type="text/javascript">
function fetchAndInstantiate(url) {
return fetch(url)
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes))
.then((results) => results.instance);
}
function test001() {
fetchAndInstantiate("./test001.wasm").then((instance) => {
alert(instance.exports.const_i32());
});
}
</script>
</head>
<body onload="test001()">
<p> WebAssembly测试页</p>
</body>
</html>
运行效果如下:
反汇编wasm
既然是汇编语言,能做汇编,也能比较容易地做反汇编。
wabt提供了wasm2wat工具反汇编wasm文件:
wabt/build/wasm2wat test001.wasm
我们看看反汇编出来的结果:
(module
(type (;0;) (func (result i32)))
(func (;0;) (type 0) (result i32)
i32.const 666)
(export "const_i32" (func 0)))
我们可以看到,反汇编出来的结果,比我们手写的多了类型的定义。 这类型既然汇编器可以推断出来,那么我们还是不用手写了。
函数传参
通过反汇编我们可以看到,函数是没有名字的。只有需要导出给外部调用的时候才绑定一个符号做名字。 同样,函数参数也是没有名字的。 但是,我们可以在写汇编的时候给一个形式参数。在wat中,函数参数要用param关键字声明,可以给一个"#34;开头的名字。
我们来看个例子:
(module
(func (param $a i32) (result i32)
(i32.add
(local.get $a)
(i32.const 1)
)
)
(export "inc_i32" (func 0))
)
用wasm2wat反汇编出来的结果如下:
(type (;0;) (func (param i32) (result i32)))
(func (;0;) (type 0) (param i32) (result i32)
local.get 0
i32.const 1
i32.add)
(export "inc_i32" (func 0)))
我们发现'$a'形参已经不见了,在声明时直接就不存在了,在调用的时候变成了序号0.
另外我们还发现指令顺序的变化。我们采用的前序表达式,或者是叫波兰表达法,i32.add指令在前,它的两个操作数在后面。 反汇编之后,变成了逆波兰表达式,也就是后序后达式,i32.add这个指令在后面,它的两个操作数在前面。
算术运算
WebAssembly,以下简称wasm,的数字有4种类型:
- i32: 有符号32位整数
- i64: 有符号64位整数
- f32: 有符号32位浮点数
- f64: 有符号32位浮点数
针对这4种类型,有完整的4套指令集。
没有无符号数字类型,但是整数类型有无符号计算的指令。
对应4种基本数字类型,有4条指令是将一个这种类型的常量压入栈的操作,它们是i32.const, i64.const, f32.const和f64.const。
加减乘法
加法共4种,每种类型一种:
- i32.add
- i64.add
- f32.add
- f64.add
减法也是4种,每种类型一种:
- i32.sub
- i64.sub
- f32.sub
- f64.sub
乘法也一样:
- i32.mul
- i64.mul
- f32.mul
- f64.mul
我们来看一个f32乘法的例子吧:
(module
(func (param $a f32) (result f32)
(f32.mul
(local.get $a)
(f32.const 1024)
)
)
(export "mul_1k_f32" (func 0))
)
写个Node.js脚本运行一下:
const {readFileSync} = require('fs')
const outputWasm = './test003.wasm';
async function run(){
const buffer = readFileSync(outputWasm);
const module = await WebAssembly.compile(buffer);
const instance = await WebAssembly.instantiate(module);
console.log(instance.exports.mul_1k_f32(3.14));
}
run();
除法
除法对于浮点数比较简单,只有div一条指令:
- f32.div
- f64.div
对于整数来说,除法分为有符号除法和无符号除法:
- i32.div_s
- i64.div_s
- i32.div_u
- i64.div_u
除此之外,还有有符号求余数和无符号求余数两条指令:
- i32.rem_s
- i64.rem_s
- i32.rem_u
- i64.rem_u
我们以64位求余数为例:
(module
(func (param $a i64) (param $b i64) (result i64)
(i64.rem_u
(local.get $a)
(local.get $b)
)
)
(export "rem_u_i64" (func 0))
)
i64类型在Node.js上运行的时候,需要输入为BigInt,在输入的时候要加一个"n"后缀:
const {readFileSync} = require('fs')
const outputWasm = './test_remu.wasm';
async function run(){
const buffer = readFileSync(outputWasm);
const module = await WebAssembly.compile(buffer);
const instance = await WebAssembly.instantiate(module);
console.log(instance.exports.rem_u_i64(1000n,256n));
}
run();
浮点数特有指令
- 绝对值f32.absf64.abs
- 取反f32.negf64.neg
- 取整向上取整向下取整向0取整向最接近的整数取整
- 平方根f32.sqrtf64.sqrt
- 最大最小值f32.minf64.minf32.maxf64.max
- 取符号位f32.copysignf64.copysign
我们挑不熟悉的copysign说起吧。它做的事情就是把当前数的正负号换成另一个数的正负号。
(module
(func (param $a f64) (result f64)
(f64.copysign
(local.get $a)
(f64.const -1.0)
)
)
(export "copysign_f64" (func 0))
)
我们是将-1.0的符号,复制给copysign_f64函数传来的64位整数。
我们传一个3.14试试:
const {readFileSync} = require('fs')
const outputWasm = './copysign.wasm';
async function run(){
const buffer = readFileSync(outputWasm);
const module = await WebAssembly.compile(buffer);
const instance = await WebAssembly.instantiate(module);
console.log(instance.exports.copysign_f64(3.14));
}
run();
运行结果为-3.14,果然换成了-1.0的符号。
比较指令
等于0
只有整数可以判断是否为0,所以是两种整数各一条指令:
- i32.eqz
- i64.eqz
我们来看个例子:
(module
(func (param $a i32) (result i32)
(i32.eqz
(local.get $a)
)
)
(export "i32_eqz" (func 0))
)
运行一下:
const {readFileSync} = require('fs')
const outputWasm = './cmp.wasm';
async function run(){
const buffer = readFileSync(outputWasm);
const module = await WebAssembly.compile(buffer);
const instance = await WebAssembly.instantiate(module);
console.log(instance.exports.i32_eqz(0));
console.log(instance.exports.i32_eqz(-1));
}
run();
输出为1,0.
相等与不相等
相等4条:
- i32.eq
- i64.eq
- f32.eq
- f64.eq
不等4条:
- i32.ne
- i64.ne
- f32.ne
- f64.ne
小于与大于
浮点数比较简单,小于是lt,大于是gt:
- f32.lt
- f32.gt
- f64.lt
- f64.gt
整数还分为有符号和无符号两种情况:
- i32.lt_s
- i32.lt_u
- i64.lt_s
- i64.lt_u
- i32.gt_s
- i32.gt_u
- i64.gt_s
- i64.gt_u
如果是小于或等于,将lt换成le;如果是大于或等于,则将gt换成ge。
流程控制语句
函数调用
函数虽然没有名字,但是我们可以用一个引用来标记它,然后使用call指令来调用它。
我们用i32_eqz2给上节的i32_eqz函数封装一下。
(module
(func $f1 (param $a i32) (result i32)
(i32.eqz
(local.get $a)
)
)
(func (param $b i32) (result i32)
(call $f1 (local.get $b))
)
(export "i32_eqz2" (func 1))
)
我们看反汇编的结果,引用值被翻译成了函数的索引号:
(module
(type (;0;) (func (param i32) (result i32)))
(func (;0;) (type 0) (param i32) (result i32)
local.get 0
i32.eqz)
(func (;1;) (type 0) (param i32) (result i32)
local.get 0
call 0)
(export "i32_eqz2" (func 1)))
分支判断
Wasm中提供了if指令,它会从栈顶中读取一个i32类型的整数,如果参数不为0,则执行then块的代码;否则执行else块的代码。
我们来看个例子,再重写一遍i32_eqz:
(module
(func (param $a i32)(result i32)
(local.get $a)
(if (result i32)
(then (i32.const 0))
(else (i32.const 1))
)
)
(export "i32_eqz3" (func 0))
)
if可以像一个函数一样返回一个值。这时需要then和else都要有。
强调下,if判断的条件不是立即数,需要事先放到栈中。否则汇编会报错。
我们看下反汇编之后的结果,then并不是一个关键字,我们也可以用if...else...end的结构来写:
(module
(type (;0;) (func (param i32) (result i32)))
(func (;0;) (type 0) (param i32) (result i32)
local.get 0
if (result i32) ;; label = @1
i32.const 0
else
i32.const 1
end)
(export "i32_eqz3" (func 0)))
我们可以像下面这样写,注意不要给if外面加括号,否则(if)块是期望(then)和(else)块的。
(module
(func (param $a i32)(result i32)
(local.get $a)
if (result i32)
i32.const 0
else
i32.const 1
end
)
(export "i32_eqz4" (func 0))
)
循环
loop指令用于循环。 如果想要提前进行下一轮循环,可以使用br指令,这时候相当于C语言中的continue语句。
如果想要退出循环,可以使用br_if指令。
循环比较命令化,我就直接按照命令式语言的方式来写汇编了:
(module
(func (param $a i32)(result i32)
(local $sum i32)
(local.set $sum (i32.const 0))
loop
local.get $a
i32.const -1
i32.add
local.set $a
local.get $a
local.get $sum
i32.add
local.set $sum
local.get $a
br_if 0
end
(return (local.get $sum))
)
(export "i32_sum" (func 0))
)
local指令用于定义局部变量。 local.set指令用于为局部变量赋值。 return指令用于函数返回。
SIMD指令
WebAssembly虽然是个抽象的机器,但是也要发恢硬件的能力。 SIMD是单指令多数据的缩写。 说起SIMD,在Intel CPU上最早始于1996年的MMX指令集。它能将一个64位寄存器当成2个32位寄存器或者8个8位寄存器一起使用。 1999年,在Pentiun III处理器上引入了支持128位的寄存器的SSE指令集。 2008年,Intel在第二代Core处理器Sandy Bridge上引入了支持256位寄存器的AVX指令集。 2013年,Intel发布512位寄存器的AVX 512指令集。AVX 512指令集会导致功耗大大增加,被Linus Torvalds评论:"I hope AVX-512 dies a painful death". 扯远了,WebAssembly也支持128位的SIMD的指令集,称为向量指令集。
空说有点抽象,我们来看个例子:
(module
(func (result i32)
v128.const i32x4 1 1 1 1
v128.const i32x4 2 2 2 2
i32x4.add
v128.any_true
return
)
(export "v128_anytrue" (func 0))
)
不同于i32,i64,f32,f64这样的数值常量,v128的常量要指令是如何解释128位的用法的。比如本例中我们将其当作4个32位寄存器使用。 这时候,我们做操作就要使用i32x4的指令集。
但是同时,我们也可以针对v128整体进行处理,使用v128的指令集。
再举个例子,我们想使用swizzle指令给8x16个数字重新排一下序:
用8x16的指令写成如下:
v128.const i8x16 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
v128.const i8x16 1 2 0 3 4 5 6 7 8 9 10 11 12 13 14 15
i8x16.swizzle
反汇编一下我们发现,原来被反汇编成了i32x4格式的。但是完全不影响i8x16指令的使用。
v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d
v128.const i32x4 0x03000201 0x07060504 0x0b0a0908 0x0f0e0d0c
i8x16.swizzle
Wasm中的SIMD指令最早是作为Javascript的扩展SIMD.js开发的,现在作为wasm的一部分,详情请参看:
https://github.com/WebAssembly/simd/tree/main/proposals
小结
本文简要介绍了WebAssembly抽象机的指令集和汇编语言的写法和运行方法。 有了这个基础,我们再看通过emsdk编译出来的wasm代码就不心慌了,看到v8相关的代码也容易理解到底在做些什么。
相关推荐
- MySQL进阶五之自动读写分离mysql-proxy
-
自动读写分离目前,大量现网用户的业务场景中存在读多写少、业务负载无法预测等情况,在有大量读请求的应用场景下,单个实例可能无法承受读取压力,甚至会对业务产生影响。为了实现读取能力的弹性扩展,分担数据库压...
- 3分钟短文 | Laravel SQL筛选两个日期之间的记录,怎么写?
-
引言今天说一个细分的需求,在模型中,或者使用laravel提供的EloquentORM功能,构造查询语句时,返回位于两个指定的日期之间的条目。应该怎么写?本文通过几个例子,为大家梳理一下。学习时...
- 一文由浅入深带你完全掌握MySQL的锁机制原理与应用
-
本文将跟大家聊聊InnoDB的锁。本文比较长,包括一条SQL是如何加锁的,一些加锁规则、如何分析和解决死锁问题等内容,建议耐心读完,肯定对大家有帮助的。为什么需要加锁呢?...
- 验证Mysql中联合索引的最左匹配原则
-
后端面试中一定是必问mysql的,在以往的面试中好几个面试官都反馈我Mysql基础不行,今天来着重复习一下自己的弱点知识。在Mysql调优中索引优化又是非常重要的方法,不管公司的大小只要后端项目中用到...
- MySQL索引解析(联合索引/最左前缀/覆盖索引/索引下推)
-
目录1.索引基础...
- 你会看 MySQL 的执行计划(EXPLAIN)吗?
-
SQL执行太慢怎么办?我们通常会使用EXPLAIN命令来查看SQL的执行计划,然后根据执行计划找出问题所在并进行优化。用法简介...
- MySQL 从入门到精通(四)之索引结构
-
索引概述索引(index),是帮助MySQL高效获取数据的数据结构(有序),在数据之外,数据库系统还维护者满足特定查询算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构...
- mysql总结——面试中最常问到的知识点
-
mysql作为开源数据库中的榜一大哥,一直是面试官们考察的重中之重。今天,我们来总结一下mysql的知识点,供大家复习参照,看完这些知识点,再加上一些边角细节,基本上能够应付大多mysql相关面试了(...
- mysql总结——面试中最常问到的知识点(2)
-
首先我们回顾一下上篇内容,主要复习了索引,事务,锁,以及SQL优化的工具。本篇文章接着写后面的内容。性能优化索引优化,SQL中索引的相关优化主要有以下几个方面:最好是全匹配。如果是联合索引的话,遵循最...
- MySQL基础全知全解!超详细无废话!轻松上手~
-
本期内容提醒:全篇2300+字,篇幅较长,可搭配饭菜一同“食”用,全篇无废话(除了这句),干货满满,可收藏供后期反复观看。注:MySQL中语法不区分大小写,本篇中...
- 深入剖析 MySQL 中的锁机制原理_mysql 锁详解
-
在互联网软件开发领域,MySQL作为一款广泛应用的关系型数据库管理系统,其锁机制在保障数据一致性和实现并发控制方面扮演着举足轻重的角色。对于互联网软件开发人员而言,深入理解MySQL的锁机制原理...
- Java 与 MySQL 性能优化:MySQL分区表设计与性能优化全解析
-
引言在数据库管理领域,随着数据量的不断增长,如何高效地管理和操作数据成为了一个关键问题。MySQL分区表作为一种有效的数据管理技术,能够将大型表划分为多个更小、更易管理的分区,从而提升数据库的性能和可...
- MySQL基础篇:DQL数据查询操作_mysql 查
-
一、基础查询DQL基础查询语法SELECT字段列表FROM表名列表WHERE条件列表GROUPBY分组字段列表HAVING分组后条件列表ORDERBY排序字段列表LIMIT...
- MySql:索引的基本使用_mysql索引的使用和原理
-
一、索引基础概念1.什么是索引?索引是数据库表的特殊数据结构(通常是B+树),用于...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
程序员的开源月刊《HelloGitHub》第 71 期
-
详细介绍一下Redis的Watch机制,可以利用Watch机制来做什么?
-
假如有100W个用户抢一张票,除了负载均衡办法,怎么支持高并发?
-
如何将AI助手接入微信(打开ai手机助手)
-
Java面试必考问题:什么是乐观锁与悲观锁
-
SparkSQL——DataFrame的创建与使用
-
redission YYDS spring boot redission 使用
-
一文带你了解Redis与Memcached? redis与memcached的区别
-
如何利用Redis进行事务处理呢? 如何利用redis进行事务处理呢英文
-
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)