3.2 指令分析
前面的介绍可能比较抽象,这一节将通过一些实例来具体分析指令。为了让例子尽可能简单,我们不再使用前两章用过的Rust版“Hello,World!”程序,而是用Wasm文本格式(也就是WAT语言)编写一些有针对性的代码。请读者注意,这一节我们的目标是掌握各种指令的编码格式,所以即使不能完全理解这些例子也没关系,在第4章,我们将详细讨论Wasm文本格式,从第5章开始,我们再详细讨论并实现各种指令。
由于两条参数指令都比较简单(也没有立即数),本节就不专门分析了,在第5章会做具体介绍。下面先来分析较为简单的数值指令,然后分析变量和内存指令,最后再分析控制指令。
3.2.1 数值指令
数值指令中的饱和截断指令比较特殊。在写作本书时,该指令正式加入Wasm规范。饱和截断指令其实是一组指令,共8条,和普通的浮点数截断指令一一对应。这组指令的格式为前缀操作码(0x0F)+子操作码,可以将其当作是8条指令,每条指令的操作码占两个字节;也可以当作一条指令,子操作码是该指令的立即数。为了和其他指令统一,本书将其当作一条指令处理。因此,在全部133条数值指令中,只有4条常量指令和饱和截断指令有立即数。
1)i32.const(操作码0x41):带一个s32类型的立即数,使用LEB128有符号格式编码。
2)i64.const(操作码0x42):带一个s64类型的立即数,使用LEB128有符号格式编码。
3)f32.const(操作码0x43):带一个f32类型的立即数,固定占4字节。
4)f32.const(操作码0x44):带一个f32类型的立即数,固定占8字节。
5)trunc_sat(操作码0xFC):带一个单字节的立即数。该指令比较特殊,详见第5章。
关于LEB128编码格式的详细内容请参考2.1.3节。第5章将详细介绍数值指令,下面给出数值指令的编码格式(沿用上一章的描述方式)。
i32.const: 0x41|s32 i64.const: 0x42|s64 f32.const: 0x43|f32 f64.const: 0x44|f64 trunc_sat: 0xfc|byte num_instr: opcode
数值指令比较多,无法逐一介绍,下面的例子仅展示常量指令、加法指令和饱和截断指令。
(module (func (f32.const 12.3) (f32.const 45.6) (f32.add) (i32.trunc_sat_f32_s) (drop) ) )
WAT示例代码在code/wat目录下,本章的示例全部以ch03开头,这些例子只是为了展示指令编码格式,除此之外没有实际的意义。
使用wat2wasm命令编译上面的例子,然后使用xxd命令观察二进制模块代码段(ID是0x0A),就能很容易地找出两条f32.const指令,以及跟在这两条指令后面的f32.add指令(操作码0x92),以及饱和截断指令(操作码0xFC)。
$ wat2wasm code/wat/ch03_eg1_num.wat $ xxd -u -g 1 ch03_eg1_num.wasm 00000000: 00 61 73 6D 01 00 00 00 01 04 01 60 00 00 03 02 .asm.......`.... 00000010: 01 00 0A 12 01 10 00 43 CD CC 44 41 43 66 66 36 .......C..DACff6 00000020: 42 92 FC 00 1A 0B B.....
下面是wasm-objdump命令打印出的结果(使用-d选项开启字节码反编译)。
$ wasm-objdump -d ch03_eg1_num.wasm ... 000016 func[0]: 000017: 43 cd cc 44 41 | f32.const 0x1.89999ap+3 00001c: 43 66 66 36 42 | f32.const 0x1.6cccccp+5 000021: 92 | f32.add 000022: fc 00 | i32.trunc_sat_f32_s 000024: 1a | drop 000025: 0b | end
3.2.2 变量指令
变量指令共5条,其中3条用于读写局部变量,立即数是局部变量索引;另外2条用于读写全局变量,立即数是全局变量索引。第7章将详细介绍变量指令,下面给出变量指令的编码格式。
local.get : 0x20|local_idx local.set : 0x21|local_idx local.tee : 0x22|local_idx global.get: 0x23|global_idx global.set: 0x24|global_idx
我们来看一个具体的例子。
(module (global $g1 (mut i32) (i32.const 1)) (global $g2 (mut i32) (i32.const 1)) (func (param $a i32) (param $b i32) (global.get $g1) (global.set $g2) (local.get $a) (local.set $b) ) )
编译WAT文件,然后用xxd命令观察二进制模块代码段。由第2章可知,索引在Wasm二进制格式中按LEB128无符号整数格式编码。采用这种编码格式,小于128的整数编码后就是它本身(2的补码表示),编码后的数据只占一个字节。根据这些线索就能很容易地找到4条变量指令。
$ wat2wasm code/wat/ch03_eg2_var.wat $ xxd -u -g 1 ch03_eg2_var.wasm 00000000: 00 61 73 6D 01 00 00 00 01 06 01 60 02 7F 7F 00 .asm.......`.... 00000010: 03 02 01 00 06 0B 02 7F 01 41 01 0B 7F 01 41 01 .........A....A. 00000020: 0B 0A 0C 01 0A 00 23 00 24 01 20 00 21 01 0B ......#.$. .!..
下面是wasm-objdump命令打印出的结果。
$ wasm-objdump -d ch03_eg2_var.wasm ... 000025 func[0]: 000026: 23 00 | global.get 0 000028: 24 01 | global.set 1 00002a: 20 00 | local.get 0 00002c: 21 01 | local.set 1 00002e: 0b | end
3.2.3 内存指令
内存指令共25条,其中14条是加载指令,用于将内存数据载入操作数栈,还有9条是存储指令,用于将操作数栈顶数据写回内存,这23条指令统一带有两个立即数:对齐提示和内存偏移量。剩余两条指令用于获取和扩展内存页数,立即数是内存索引。由于Wasm规范规定模块只能导入或定义一块内存,所以这个内存索引目前只起到占位作用,索引值必须为0。第6章将详细介绍内存指令,下面给出内存指令的编码格式。
load_instr : opcode|align|offset # align: u32, offset: u32 store_instr: opcode|align|offset memory.size: 0x3f|0x00 memory.grow: 0x40|0x00
我们来看一个具体的例子。
(module (memory 1 8) (data (offset (i32.const 100)) "hello") (func (i32.const 1) (i32.const 2) (i32.load offset=100) (i32.store offset=100) (memory.size) (drop) (i32.const 4) (memory.grow) (drop) ) )
编译WAT文件,然后用xxd命令观察二进制模块代码段。根据i32.load和i32.store指令的操作码(0x28和0x36)可以找到这两条指令。这两条指令的对齐提示都是2,全局变量索引都是100(0x64)。后面是memory.size和memory.grow指令(操作码0x3F和0x40,中间隔着drop和一条常量指令),内存索引值为0。
$ wat2wasm code/wat/ch03_eg3_mem.wat $ xxd -u -g 1 ch03_eg3_mem.wasm 00000000: 00 61 73 6D 01 00 00 00 01 04 01 60 00 00 03 02 .asm.......`.... 00000010: 01 00 05 04 01 01 01 08 0A 16 01 14 00 41 01 41 .............A.A 00000020: 02 28 02 64 36 02 64 3F 00 1A 41 04 40 00 1A 0B .(.d6.d?..A.@... 00000030: 0B 0C 01 00 41 E4 00 0B 05 68 65 6C 6C 6F ....A....hello
下面是wasm-objdump命令打印出的结果。
$ wasm-objdump -d ch03_eg3_mem.wasm ... 00001c func[0]: 00001d: 41 01 | i32.const 1 00001f: 41 02 | i32.const 2 000021: 28 02 64 | i32.load 2 100 000024: 36 02 64 | i32.store 2 100 000027: 3f 00 | memory.size 0 000029: 1a | drop 00002a: 41 04 | i32.const 4 00002c: 40 00 | memory.grow 0 00002e: 1a | drop 00002f: 0b | end
3.2.4 结构化控制指令
控制指令共13条,包括结构化控制指令、跳转指令、函数调用指令等。其中结构化控制指令有3条,分别是block(操作码0x02)、loop(操作码0x03)和if(操作码0x04)。这3条指令必须和end指令(操作码0x0B)搭配,成对出现。如果if指令有两条分支,则中间由else指令(操作码0x05)分隔。由于end和else指令比较特殊,只起分隔作用,所以也可以称为伪指令。第8章将详细介绍结构化控制指令,下面给出这3条指令的编码格式。
block_instr: 0x02|block_type|instr*|0x0b loop_instr : 0x03|block_type|instr*|0x0b if_instr : 0x04|block_type|instr*|(0x05|instr*)?|0x0b block_type : s32
我们来看一个具体的例子。
(module (func (result i32) (block (result i32) (i32.const 1) (loop (result i32) (if (result i32) (i32.const 2) (then (i32.const 3)) (else (i32.const 4)) ) ) (drop) ) ) )
编译WAT文件,然后用xxd命令观察二进制模块代码段。根据这几条指令的操作码,可以很容易地在代码段中找到它们的启始和结束。
$ wat2wasm code/wat/ch03_eg4_block.wat $ xxd -u -g 1 ch03_eg4_block.wasm 00000000: 00 61 73 6D 01 00 00 00 01 05 01 60 00 01 7F 03 .asm.......`.... 00000010: 02 01 00 0A 17 01 15 00 02 7F 41 01 03 7F 41 02 ..........A...A. 00000020: 04 7F 41 03 05 41 04 0B 0B 1A 0B 0B ..A..A......
用wasm-objdump命令可以观察得更清楚一些,下面是打印出的结果。
$ wasm-objdump -d ch03_eg4_block.wasm ... 000017 func[0]: 000018: 02 7f | block i32 00001a: 41 01 | i32.const 1 00001c: 03 7f | loop i32 00001e: 41 02 | i32.const 2 000020: 04 7f | if i32 000022: 41 03 | i32.const 3 000024: 05 | else 000025: 41 04 | i32.const 4 000027: 0b | end 000028: 0b | end 000029: 1a | drop 00002a: 0b | end 00002b: 0b | end
3.2.5 跳转指令
跳转指令共4条,其中br指令(操作码0x0C)进行无条件跳转,立即数是目标标签索引;br_if指令(操作码0x0D)进行有条件跳转,立即数也是目标标签索引;br_table指令(操作码0x0E)进行查表跳转,立即数是目标标签索引表和默认标签索引。return指令(操作码0x0F)只是br指令的一种特殊形式,执行效果是直接跳出最外层循环并且导致整个函数返回,没有立即数。第8章将详细介绍跳转指令,下面给出这4条指令的编码格式。
br_instr : 0x0c|label_idx br_if_instr : 0x0d|label_idx br_table : 0x0e|vec<label_idx>|label_idx return_instr: 0x0f
来看一个具体的例子。
(module (func (block (block (block (br 1) (br_if 2 (i32.const 100)) (br_table 0 1 2 3) (return) ))) ) )
编译WAT文件,然后用xxd命令观察二进制模块代码段。根据操作码不难找出这4条跳转指令。
$ wat2wasm code/wat/ch03_eg5_br.wat $ xxd -u -g 1 ch03_eg5_br.wasm 00000000: 00 61 73 6D 01 00 00 00 01 04 01 60 00 00 03 02 .asm.......`.... 00000010: 01 00 0A 1B 01 19 00 02 40 02 40 02 40 0C 01 41 ........@.@.@..A 00000020: E4 00 0D 02 0E 03 00 01 02 03 0F 0B 0B 0B 0B ...............
下面是wasm-objdump命令打印出的结果。
$ wasm-objdump -d ch03_eg5_br.wasm ... 000016 func[0]: 000017: 02 40 | block 000019: 02 40 | block 00001b: 02 40 | block 00001d: 0c 01 | br 1 00001f: 41 e4 00 | i32.const 100 000022: 0d 02 | br_if 2 000024: 0e 03 00 01 02 03 | br_table 0 1 2 3 00002a: 0f | return 00002b: 0b | end 00002c: 0b | end 00002d: 0b | end 00002e: 0b | end
3.2.6 函数调用指令
Wasm支持直接和间接两种函数调用方式。call指令(操作码0x10)进行直接函数调用,函数索引由立即数指定。call_indirect指令(操作码0x11)进行间接函数调用,函数签名的索引由立即数指定,到运行时才能知道具体调用哪个函数。第7章将详细介绍直接函数调用指令,第9章将详细介绍间接函数调用指令,下面给出这两条指令的编码格式。
call_instr : 0x10|func_idx call_indirect: 0x11|type_idx|0x00
间接函数调用指令需要查表才能完成,由第2个立即数指定查哪张表。我们已经知道,模块最多只能导入或定义一张表,所以这个立即数只起占位作用,必须是0。下面来看一个具体的例子。
(module (type $ft1 (func)) (type $ft2 (func)) (table funcref (elem $f1 $f1 $f1)) (func $f1 (call $f1) (call_indirect (type $ft2) (i32.const 2)) ) )
编译WAT文件,然后用xxd命令观察二进制模块代码段。根据操作码,不难找出这两条函数调用指令。
$ wat2wasm code/wat/ch03_eg6_call.wat $ xxd -u -g 1 ch03_eg6_call.wasm 00000000: 00 61 73 6D 01 00 00 00 01 07 02 60 00 00 60 00 .asm.......`..`. 00000010: 00 03 02 01 00 04 05 01 70 01 03 03 09 09 01 00 ........p....... 00000020: 41 00 0B 03 00 00 00 0A 0B 01 09 00 10 00 41 02 A.............A. 00000030: 11 01 00 0B ....
下面是wasm-objdump命令打印出的结果。
$ wasm-objdump -d ch03_eg6_call.wasm ... 00002b func[0]: 00002c: 10 00 | call 0 00002e: 41 02 | i32.const 2 000030: 11 01 00 | call_indirect 1 0 000033: 0b | end