WebAssembly汇编语言程序设计初步
Posted Jtag特工
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WebAssembly汇编语言程序设计初步相关的知识,希望对你有一定的参考价值。
WebAssembly汇编语言程序设计初步
随着前端页面变得越来越复杂,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建立WebAssembly的实例
- 通过实例的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关键字声明,可以给一个"$"开头的名字。
我们来看个例子:
(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.abs
- f64.abs
- 取反
- f32.neg
- f64.neg
- 取整
- 向上取整
- 向下取整
- 向0取整
- 向最接近的整数取整
- 平方根
- f32.sqrt
- f64.sqrt
- 最大最小值
- f32.min
- f64.min
- f32.max
- f64.max
- 取符号位
- f32.copysign
- f64.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相关的代码也容易理解到底在做些什么。
以上是关于WebAssembly汇编语言程序设计初步的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Go 语言开发的宿主程序中嵌入 WebAssembly
Oracle在GraalVM中实现WebAssembly引擎GraalWasm
WebAssembly,可以作为任何编程语言的编译目标,使应用程序可以运行在浏览器或其它代理中——浏览器里运行其他语言的程序?