文/ 阿里淘系 F(x)Team - 旭伦,今天小编就来聊一聊关于汇编语言程序设计教程心得体会?接下来我们就一起去研究一下吧!

汇编语言程序设计教程心得体会(大神赞过的学习)

汇编语言程序设计教程心得体会

文/ 阿里淘系 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 http://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中的函数只需要三步:

  1. 通过WebAssembly.compile编译buffer中的二进行wasm

  2. 通过WebAssembly.instantiate建立Web Assembly的实例

  3. 通过实例的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.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的一部分,详情请参看:http://github.com/WebAssembly/simd/tree/main/proposals

    小结

    本文简要介绍了WebAssembly抽象机的指令集和汇编语言的写法和运行方法。 有了这个基础,我们再看通过emsdk编译出来的wasm代码就不心慌了,看到v8相关的代码也容易理解到底在做些什么。