首先,这个东西虽然叫ASM但实际上只是个非常非常简单的计算机,它本身只能做纯运算,和外部的一切交互都得在模块初始化时给引入,从风控的角度看它没太大逆向的必要,不过前提还是先了解清楚它!所以下面先详细介绍它的语法,文件格式,指令集,再说怎么逆向~
语法
首先,它通常作为一个编译目标,即我们不会直接写WASM的汇编,而是用go/rust/c/cpp甚至JS开发,再由编译器(如emscripten / assemblyscript /golang/...)将其转换为wasm指令,当然了它执行的字节码有对应的汇编表示,即Wat(WebAssembly Text Format),Wat采用S表达式(S-expression)形式,代码里就暗含了语法树结构,这种格式更容易理解,不过它在汇编时会先被转换为flat-wat形式,即我们反汇编时看到的样子。
一些概念
1.Module: 每个wasm文件有且仅有一个模块,它实现一组特定的功能,模块是wasm的代码单元
2.import:wasm本身纯计算,要调外部的功能必须由import指定
3.export:wasm本身实现的功能或变量/内存可被导出给其他模块或外部环境使用
4.instance:模块需要先被实例化再使用,实例化时需要传入import,并且它会导出export
一个然破
(module
(; 这是一个块注释,如此处所示。 ;)
;; =================================
;; 1. 导入 (Imports)
;; =================================
;; 从 "env" 模块导入功能。
(import "env" "log" (func $log (param i32 i32))) ;; 导入日志函数,接收 (指针, 长度)
(import "env" "config" (global $config i32)) ;; 导入一个只读的 i32 全局配置变量
;; =================================
;; 2. 内存与数据段 (Memory & Data Segment)
;; =================================
(memory (export "memory") 1) ;; 定义并导出一个1页(64KB)的内存
(data (i32.const 0) "Module Initialized.") ;; 在内存地址0处预置一个字符串
;; =================================
;; 3. 全局变量 (Globals)
;; =================================
;; 定义并导出一个可变的全局变量,用于任务计数
(global $task_count (export "task_count") (mut i32) (i32.const 0))
;; =================================
;; 4. 表 (Table for Function Pointers)
;; =================================
(table (export "op_table") 2 funcref) ;; 定义并导出一个大小为2的函数引用表
(elem (i32.const 0) $add_op $div_op) ;; 初始化表元素,索引0是$add_op,索引1是$div_op
;; =================================
;; 5. 类型定义 (Type Definitions)
;; =================================
;; 为函数签名定义一个类型,用于 call_indirect 和函数声明
(type $binary_op (func (param i32 i32) (result i32)))
;; =================================
;; 6. 函数 (Functions)
;; =================================
;; 内部操作函数1:加法
(func $add_op (type $binary_op) ;; 使用已定义的类型
local.get 0 ;; 获取第一个参数
local.get 1 ;; 获取第二个参数
i32.add ;; 栈机操作:弹出两个值,相加,然后压入结果
)
;; 内部操作函数2:有符号除法
(func $div_op (param $a i32) (param $b i32) (result i32)
(i32.div_s (local.get $a) (local.get $b)) ;; 使用折叠格式 (folded form)
)
;; 主处理函数,将导出
(func (export "process_task")
(param $a i32) (param $b i32) (param $op_index i32) (param $out_ptr i32)
(result i32)
;; 定义一个局部变量并观察其零初始化
(local $temp_val i32)
;; 调用导入的 log 函数,打印初始化信息
;; (i32.const 0) 是 "Module Initialized." 的地址
;; (i32.const) 是其长度
(call $log (i32.const 0) (i32.const))
;; 更新全局任务计数器
(global.set $task_count
(i32.add
(global.get $task_count)
(i32.const 1)
)
)
;; 使用 block 来返回值
(block $result_block (result i32)
;; 控制语句:检查除数是否为0
(if (i32.and (i32.eq (local.get $op_index) (i32.const 1)) (i32.eq (local.get $b) (i32.const 0)))
(then
;; 如果是除法且除数为0,返回错误码 -1
(i32.const -1)
(br $result_block) ;; 跳转出 block,此时栈顶的 -1 成为 block 的返回值
)
)
;; 使用 loop 模拟一些工作
(local.set $temp_val (i32.const 5))
(loop $work_loop
(local.set $temp_val (i32.sub (local.get $temp_val) (i32.const 1)))
;; 当 $temp_val > 0 时继续循环
(br_if $work_loop (i32.gt_s (local.get $temp_val) (i32.const 0)))
)
;; 间接调用 (函数指针)
(call_indirect (type $binary_op)
(local.get $a)
(local.get $b)
(local.get $op_index) ;; 根据索引动态选择函数
)
;; 此时计算结果在栈顶,它将成为 block 的返回值
)
;; 此时 block 的返回值在栈顶
;; 将结果写入由 $out_ptr 指定的内存地址
(i32.store (local.get $out_ptr) (i32.add (global.get $config))) ;; 加上导入的配置值
;; drop 掉结果,因为函数签名要求返回一个 i32,而我们已经 store 了
;; drop
;; 返回状态码 0 表示成功
(i32.const 0)
)
)
它在被编译后,可在任何支持的环境里运行,但这里主要关心浏览器:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WASM Showcase</title>
<style>
body { font-family: monospace; padding: 1em; }
.output { border: 1px solid #ccc; padding: 1em; margin-top: 1em; white-space: pre-wrap; }
</style>
</head>
<body>
<h1>WASM Showcase</h1>
<button id="addBtn">执行加法 (100 +)</button>
<button id="divBtn">执行除法 (100 /)</button>
<button id="divZeroBtn">执行除零 (100 / 0)</button>
<div id="output" class="output">点击按钮开始...</div>
<script>
const outputLog = document.getElementById('output');
async function main() {
const memory = new WebAssembly.Memory({ initial: 1 });
const importObject = {
env: {
log: (ptr, len) => {
const buffer = new Uint8Array(memory.buffer, ptr, len);
const text = new TextDecoder('utf8').decode(buffer);
console.log(`[Log from WASM]: ${text}`);
outputLog.textContent += `[Log from WASM]: ${text}\n`;
},
config: new WebAssembly.Global({ value: "i32", mutable: false },), // 导入一个值为10的常量全局变量
memory: memory,
}
};
const { instance } = await WebAssembly.instantiateStreaming(fetch('showcase.wasm'), importObject);
const wasm = instance.exports;
outputLog.textContent = 'WASM 模块已准备就绪。\n';
outputLog.textContent += `导出的内存大小: ${wasm.memory.buffer.byteLength} bytes\n`;
outputLog.textContent += `导出的函数表大小: ${wasm.op_table.length}\n\n`;
const RESULT_POINTER =4; // 选一个内存地址用于存放结果
function runTask(a, b, opIndex, opName) {
outputLog.textContent += `--- Running Task: ${opName} ---\n`;
const status = wasm.process_task(a, b, opIndex, RESULT_POINTER);
if (status === 0) {
// 从内存中读取结果
const resultArray = new Int32Array(memory.buffer, RESULT_POINTER, 1);
outputLog.textContent += `计算结果 (已存入内存): ${resultArray[0]}\n`;
} else {
outputLog.textContent += `WASM 返回错误码: ${status}\n`;
}
// 读取导出的全局变量
outputLog.textContent += `任务总数 (全局变量): ${wasm.task_count.value}\n\n`;
}
document.getElementById('addBtn').addEventListener('click', () => runTask(100,, 0, "ADD"));
document.getElementById('divBtn').addEventListener('click', () => runTask(100,, 1, "DIV"));
document.getElementById('divZeroBtn').addEventListener('click', () => runTask(100, 0, 1, "DIV by Zero"));
}
main().catch(console.error);
</script>
</body>
</html>
在浏览器中,实例化wasm模块处最关键,当前浏览器存在四种方式去初始化,所以需要在这些位置获取:
WebAssembly.compile(bufferSource, compileOptions) // 将 WebAssembly 二进制代码编译为一个 WebAssembly.Module 对象
WebAssembly.compileStreaming(source, compileOptions) // 直接将流式传输的底层源码编译为一个 WebAssembly.Module 对象
WebAssembly.instantiate(bufferSource/module, importObject, compileOptions) // 编译和实例化 WebAssembly 代码,含两种重载
WebAssembly.instantiateStreaming(source, importObject, compileOptions) // 直接从流式底层源编译并实例化 WebAssembly 模块
更多基础语法可参考[5]。
文件结构
wasm的文件结构十分简单,里面由节组成,由于要实现流式加载,它里面的内置节存在固定的顺序,下面简单说明下,详细的可以找一个010模板文件自己看。
文件头
| Field Name | Type | Description |
|---|---|---|
magic_cookie |
[uint32] | 即0x6d736100 (\0asm) |
version |
[uint32] | 现在是0x1 |
节
| 节名称 (Section Name) | Opcode (ID) | 内容与用途 (Content and Purpose) | 关联关系 (Relationships) |
|---|---|---|---|
| Custom Section | 0x00 |
自定义节:存放任意元数据,如调试信息。WASM引擎会忽略其内容 | - |
| Type Section | 0x01 |
类型节:定义模块中用到的所有函数签名 | 被函数节、导入节、代码节(call_indirect)引用 |
| Import Section | 0x02 |
导入节:声明从外部环境导入的函数、表、内存和全局变量 | 依赖类型节(用于函数签名)。定义了函数、表、内存、全局变量等索引空间的起始部分 |
| Function Section | 0x03 |
函数节:声明模块内部函数,并关联到其在类型节中的签名索引 | 依赖类型节,与代码节一一对应,定义函数的签名部分 |
| Table Section | 0x04 |
表节:定义模块内部的表(通常用于函数指针) | 被元素节(用于初始化)和代码节(call_indirect)引用 |
| Memory Section | 0x05 |
内存节:定义模块内部的线性内存 | 被数据节(用于初始化)和代码节(load/store指令)引用 |
| Global Section | 0x06 |
全局变量节:定义模块内部的全局变量及其初始值 | 被代码节(global.get/set)和导出节引用。其初始值可能依赖导入的全局变量 |
| Export Section | 0x07 |
导出节:声明模块向外部环境导出的函数、表、内存和全局变量 | 依赖函数节、表节、内存节、全局变量节(以及导入节),它导出的是这些索引空间中的成员 |
| Start Section | 0x08 |
起始函数节:指定一个在模块实例化时自动执行的函数 | 依赖函数节(指定要执行的函数索引) |
| Element Section | 0x09 |
元素节:用于在实例化时初始化表的内容(即用函数引用填充表) | 依赖表节(指定要初始化的表)和函数节(提供用于填充的函数索引) |
| Code Section | 0x0a |
代码节:包含模块中每个函数的实际可执行指令(函数体) | 与函数节一一对应,提供函数的实现。依赖几乎所有节,它包含的指令会引用类型、函数、表、内存、全局变量等 |
| Data Section | 0x0b |
数据节:用于在实例化时初始化线性内存的内容 | 依赖内存节(指定要初始化的内存) |
指令集
先说下wasm的虚拟机是栈式虚拟机(虽然不纯,有本地变量),它有两个栈,操作数栈和控制流栈,前者很好懂,后者等哈看控制流指令就能更清晰的理解了,再说说类型,它只有4种值类型:i32, i64, f32, f64,其它的就是上面提到的函数引用funcref和签名类型了。
指令描述
Instruction Mnemonic Field (指令助记符)
即汇编的操作码,它的命名规则为:
- 类型前缀 (Type Prefix): 很多指令是针对特定数据类型的。助记符会以类型开头,如 i32.add (32位整数加法), f64.load (从内存加载64位浮点数)。
- 类型后缀 (Type Suffix): 类型转换指令会用后缀指明输入类型,如 i32.trunc_f64_s (将一个有符号64位浮点数f64截断成32位整数i32)。
- 有/无符号后缀 (Signed/Unsigned Suffix):
_s代表有符号 (signed) 整数运算,_u代表无符号 (unsigned) 整数运算。例如 i32.div_s (有符号除法) 和 i32.div_u (无符号除法)。
Instruction Opcode Field (指令操作码)
没啥说的,就是数值形式的操作码
Instruction Immediates Field (指令立即数)
跟随在 Opcode 后面的、作为指令一部分的“硬编码”参数。这些值不是从栈上动态获取的,而是指令自带的。如br 1的br 是跳转指令,1 就是一个立即数,表示要跳转到外层第1个 block 或 loop。
Instruction Signature Field (指令签名)
它描述了指令与操作数栈 (Operand Stack) 的交互方式。WASM 是一个基于栈的虚拟机,大部分指令都是从栈顶取出数据,运算后再将结果推回栈顶。它的格式为(operands) : (returns),即 (消耗的值) : (产生的值)。如i32.add 的签名是 (i32, i32) : (i32)。意思是:它会从栈顶弹出两个 i32 类型的值,将它们相加,然后把结果(一个 i32 值)推回到栈顶。
Instruction Families Field (指令家族)
为了更好地组织和理解,指令被按功能分成了不同的“家族”。主要家族举例:
- B (Branch), Q (Control-Flow Barrier), L (Call): 控制流家族。负责 if/else, loop 循环,函数调用 (call),返回 (return) 等改变程序执行顺序的操作。
- G, S, U, T, R (Integer): 整数运算家族。负责各种整数的加减乘除、位移、求余等。
- F, E (Floating-Point): 浮点数运算家族。负责浮点数的运算,严格遵循 IEEE 754 标准,保证了跨平台计算结果的一致性。
- M (Memory), Z (Memory Size): 内存访问家族。负责从一块被称为 Linear Memory 的连续内存中读取 (load) 和写入 (store) 数据。这是 WASM 与外部世界(如 JavaScript)交换复杂数据的主要方式。
- C (Comparison): 比较家族。负责比较两个值的大小或是否相等,结果是一个布尔值(0或1)。
指令详请
控制流指令
| 指令名称 | 操作码 | 立即数 | 签名 | 家族 | 说明 |
|---|---|---|---|---|---|
| block | 0x02 | $signature: [block signature type] |
() : () |
推送一个控制流条目到栈上,用于创建一个代码块。 | |
| loop | 0x03 | $signature: [block signature type] |
() : () |
绑定一个标签到当前位置并推送一个控制流条目,用于创建可跳转的循环。 | |
| br | 0x0c | $depth: [varuint32] |
($T[$block_arity]) : ($T[$block_arity]) |
[B] [Q] |
无条件跳转到由$depth指定的嵌套层级的标签。 |
| br_if | 0x0d | $depth: [varuint32] |
($T[$block_arity], $condition: i32) : ($T[$block_arity]) |
[B] |
如果栈顶的$condition为true,则跳转到由$depth指定的嵌套层级的标签。 |
| br_table | 0x0e | $table: [array] of [varuint32], $default: [varuint32] |
($T[$block_arity], $index: i32) : ($T[$block_arity]) |
[B] [Q] |
根据$index的值在跳转表$table中选择一个目标进行跳转,若越界则使用$default目标。 |
| if | 0x04 | $signature: [block signature type] |
($condition: i32) : () |
[B] |
开始一个if块。如果$condition为false,则直接跳转到对应的else或end。 |
| else | 0x05 | ($T[$any]) : ($T[$any]) |
[B] |
标记if块的'else'分支的开始。 | |
| end | 0x0b | ($T[$any]) : ($T[$any]) |
标记一个block, loop, if或else代码块的结束。 |
||
| return | 0x0f | ($T[$block_arity]) : ($T[$block_arity]) |
[B] [Q] |
从当前函数返回。 | |
| unreachable | 0x00 | () : () |
[Q] |
执行到此指令时,总是会触发一个陷阱(Trap),中止执行。 |
基本指令
| 指令名称 | 操作码 | 立即数 | 签名 | 家族 | 说明 |
|---|---|---|---|---|---|
| nop | 0x01 | () : () |
什么也不做。 | ||
| drop | 0x1a | ($T[1]) : () |
从栈顶丢弃一个值。 | ||
| i32.const | 0x41 | $value: [varsint32] |
() : (i32) |
将立即数$value作为i32常量推到栈顶。 |
|
| i64.const | 0x42 | $value: [varsint64] |
() : (i64) |
将立即数$value作为i64常量推到栈顶。 |
|
| f32.const | 0x43 | $value: [float32] |
() : (f32) |
将立即数$value作为f32常量推到栈顶。 |
|
| f64.const | 0x44 | $value: [float64] |
() : (f64) |
将立即数$value作为f64常量推到栈顶。 |
|
| local.get | 0x20 | $id: [varuint32] |
() : ($T[1]) |
获取索引为$id的局部变量的值,并将其推到栈顶。 |
|
| local.set | 0x21 | $id: [varuint32] |
($T[1]) : () |
从栈顶弹出一个值,并设置到索引为$id的局部变量。 |
|
| local.tee | 0x22 | $id: [varuint32] |
($T[1]) : ($T[1]) |
与local.set类似,但设置后不弹出值,而是将其保留在栈顶。 |
|
| global.get | 0x23 | $id: [varuint32] |
() : ($T[1]) |
获取索引为$id的全局变量的值,并将其推到栈顶。 |
|
| global.set | 0x24 | $id: [varuint32] |
($T[1]) : () |
从栈顶弹出一个值,并设置到索引为$id的全局变量。 |
|
| select | 0x1b | ($T[1], $T[1], $condition: i32) : ($T[1]) |
根据$condition的值(非0为true),从栈顶的两个值中选择一个返回。 |
||
| call | 0x10 | $callee: [varuint32] |
($T[$args]) : ($T[$returns]) |
[L] |
调用索引为$callee的函数。 |
| call_indirect | 0x11 | $signature: [varuint32], $reserved: [varuint1] |
($T[$args], $callee: i32) : ($T[$returns]) |
[L] |
间接调用函数。从栈顶获取函数在表中的索引$callee进行调用。 |
整数算数指令
| 指令名称 | 操作码 | 立即数 | 签名 | 家族 | 说明 |
|---|---|---|---|---|---|
| i32.add | 0x6a | (i32, i32) : (i32) |
[G] |
32位整数加法。 | |
| i64.add | 0x7c | (i64, i64) : (i64) |
[G] |
64位整数加法。 | |
| i32.sub | 0x6b | (i32, i32) : (i32) |
[G] |
32位整数减法。 | |
| i64.sub | 0x7d | (i64, i64) : (i64) |
[G] |
64位整数减法。 | |
| i32.mul | 0x6c | (i32, i32) : (i32) |
[G] |
32位整数乘法。 | |
| i64.mul | 0x7e | (i64, i64) : (i64) |
[G] |
64位整数乘法。 | |
| i32.div_s | 0x6d | (i32, i32) : (i32) |
[S] |
32位有符号整数除法。 | |
| i64.div_s | 0x7f | (i64, i64) : (i64) |
[S] |
64位有符号整数除法。 | |
| i32.div_u | 0x6e | (i32, i32) : (i32) |
[U] |
32位无符号整数除法。 | |
| i64.div_u | 0x80 | (i64, i64) : (i64) |
[U] |
64位无符号整数除法。 | |
| i32.rem_s | 0x6f | (i32, i32) : (i32) |
[S] [R] |
32位有符号整数取余。 | |
| i64.rem_s | 0x81 | (i64, i64) : (i64) |
[S] [R] |
64位有符号整数取余。 | |
| i32.rem_u | 0x70 | (i32, i32) : (i32) |
[U] [R] |
32位无符号整数取余。 | |
| i64.rem_u | 0x82 | (i64, i64) : (i64) |
[U] [R] |
64位无符号整数取余。 | |
| i32.and | 0x71 | (i32, i32) : (i32) |
[G] |
32位整数按位与。 | |
| i64.and | 0x83 | (i64, i64) : (i64) |
[G] |
64位整数按位与。 | |
| i32.or | 0x72 | (i32, i32) : (i32) |
[G] |
32位整数按位或。 | |
| i64.or | 0x84 | (i64, i64) : (i64) |
[G] |
64位整数按位或。 | |
| i32.xor | 0x73 | (i32, i32) : (i32) |
[G] |
32位整数按位异或。 | |
| i64.xor | 0x85 | (i64, i64) : (i64) |
[G] |
64位整数按位异或。 | |
| i32.shl | 0x74 | (i32, i32) : (i32) |
[T], [G] |
32位整数逻辑左移。 | |
| i64.shl | 0x86 | (i64, i64) : (i64) |
[T], [G] |
64位整数逻辑左移。 | |
| i32.shr_s | 0x75 | (i32, i32) : (i32) |
[T], [S] |
32位整数算术右移。 | |
| i64.shr_s | 0x87 | (i64, i64) : (i64) |
[T], [S] |
64位整数算术右移。 | |
| i32.shr_u | 0x76 | (i32, i32) : (i32) |
[T], [U] |
32位整数逻辑右移。 | |
| i64.shr_u | 0x88 | (i64, i64) : (i64) |
[T], [U] |
64位整数逻辑右移。 | |
| i32.rotl | 0x77 | (i32, i32) : (i32) |
[T], [G] |
32位整数循环左移。 | |
| i64.rotl | 0x89 | (i64, i64) : (i64) |
[T], [G] |
64位整数循环左移。 | |
| i32.rotr | 0x78 | (i32, i32) : (i32) |
[T], [G] |
32位整数循环右移。 | |
| i64.rotr | 0x8a | (i64, i64) : (i64) |
[T], [G] |
64位整数循环右移。 | |
| i32.clz | 0x67 | (i32) : (i32) |
[G] |
32位整数计算前导零。 | |
| i64.clz | 0x79 | (i64) : (i64) |
[G] |
64位整数计算前导零。 | |
| i32.ctz | 0x68 | (i32) : (i32) |
[G] |
32位整数计算末尾零。 | |
| i64.ctz | 0x7a | (i64) : (i64) |
[G] |
64位整数计算末尾零。 | |
| i32.popcnt | 0x69 | (i32) : (i32) |
[G] |
32位整数计算置1的位数。 | |
| i64.popcnt | 0x7b | (i64) : (i64) |
[G] |
64位整数计算置1的位数。 | |
| i32.eqz | 0x45 | (i32) : (i32) |
[G] |
判断32位整数是否等于零。 | |
| i64.eqz | 0x50 | (i64) : (i32) |
[G] |
判断64位整数是否等于零。 |
浮点数算数指令
| 指令名称 | 操作码 | 立即数 | 签名 | 家族 | 说明 |
|---|---|---|---|---|---|
| f32.add | 0x92 | (f32, f32) : (f32) |
[F] |
32位浮点数加法。 | |
| f64.add | 0xa0 | (f64, f64) : (f64) |
[F] |
64位浮点数加法。 | |
| f32.sub | 0x93 | (f32, f32) : (f32) |
[F] |
32位浮点数减法。 | |
| f64.sub | 0xa1 | (f64, f64) : (f64) |
[F] |
64位浮点数减法。 | |
| f32.mul | 0x94 | (f32, f32) : (f32) |
[F] |
32位浮点数乘法。 | |
| f64.mul | 0xa2 | (f64, f64) : (f64) |
[F] |
64位浮点数乘法。 | |
| f32.div | 0x95 | (f32, f32) : (f32) |
[F] |
32位浮点数除法。 | |
| f64.div | 0xa3 | (f64, f64) : (f64) |
[F] |
64位浮点数除法。 | |
| f32.sqrt | 0x91 | (f32) : (f32) |
[F] |
32位浮点数开平方根。 | |
| f64.sqrt | 0x9f | (f64) : (f64) |
[F] |
64位浮点数开平方根。 | |
| f32.min | 0x96 | (f32, f32) : (f32) |
[F] |
32位浮点数取较小值。 | |
| f64.min | 0xa4 | (f64, f64) : (f64) |
[F] |
64位浮点数取较小值。 | |
| f32.max | 0x97 | (f32, f32) : (f32) |
[F] |
32位浮点数取较大值。 | |
| f64.max | 0xa5 | (f64, f64) : (f64) |
[F] |
64位浮点数取较大值。 | |
| f32.ceil | 0x8d | (f32) : (f32) |
[F] |
32位浮点数向上取整。 | |
| f64.ceil | 0x9b | (f64) : (f64) |
[F] |
64位浮点数向上取整。 | |
| f32.floor | 0x8e | (f32) : (f32) |
[F] |
32位浮点数向下取整。 | |
| f64.floor | 0x9c | (f64) : (f64) |
[F] |
64位浮点数向下取整。 | |
| f32.trunc | 0x8f | (f32) : (f32) |
[F] |
32位浮点数向零取整。 | |
| f64.trunc | 0x9d | (f64) : (f64) |
[F] |
64位浮点数向零取整。 | |
| f32.nearest | 0x90 | (f32) : (f32) |
[F] |
32位浮点数取最近整数(中间值取偶)。 | |
| f64.nearest | 0x9e | (f64) : (f64) |
[F] |
64位浮点数取最近整数(中间值取偶)。 | |
| f32.abs | 0x8b | (f32) : (f32) |
[E] |
32位浮点数取绝对值。 | |
| f64.abs | 0x99 | (f64) : (f64) |
[E] |
64位浮点数取绝对值。 | |
| f32.neg | 0x8c | (f32) : (f32) |
[E] |
32位浮点数取负。 | |
| f64.neg | 0x9a | (f64) : (f64) |
[E] |
64位浮点数取负。 | |
| f32.copysign | 0x98 | (f32, f32) : (f32) |
[E] |
复制32位浮点数符号。 | |
| f64.copysign | 0xa6 | (f64, f64) : (f64) |
[E] |
复制64位浮点数符号。 |
整数比较指令
| 指令名称 | 操作码 | 立即数 | 签名 | 家族 | 说明 |
|---|---|---|---|---|---|
| i32.eq | 0x46 | (i32, i32) : (i32) |
[C], [G] |
判断两个32位整数是否相等。 | |
| i64.eq | 0x51 | (i64, i64) : (i32) |
[C], [G] |
判断两个64位整数是否相等。 | |
| i32.ne | 0x47 | (i32, i32) : (i32) |
[C], [G] |
判断两个32位整数是否不相等。 | |
| i64.ne | 0x52 | (i64, i64) : (i32) |
[C], [G] |
判断两个64位整数是否不相等。 | |
| i32.lt_s | 0x48 | (i32, i32) : (i32) |
[C], [S] |
32位有符号整数比较:是否小于。 | |
| i64.lt_s | 0x53 | (i64, i64) : (i32) |
[C], [S] |
64位有符号整数比较:是否小于。 | |
| i32.lt_u | 0x49 | (i32, i32) : (i32) |
[C], [U] |
32位无符号整数比较:是否小于。 | |
| i64.lt_u | 0x54 | (i64, i64) : (i32) |
[C], [U] |
64位无符号整数比较:是否小于。 | |
| i32.le_s | 0x4c | (i32, i32) : (i32) |
[C], [S] |
32位有符号整数比较:是否小于等于。 | |
| i64.le_s | 0x57 | (i64, i64) : (i32) |
[C], [S] |
64位有符号整数比较:是否小于等于。 | |
| i32.le_u | 0x4d | (i32, i32) : (i32) |
[C], [U] |
32位无符号整数比较:是否小于等于。 | |
| i64.le_u | 0x58 | (i64, i64) : (i32) |
[C], [U] |
64位无符号整数比较:是否小于等于。 | |
| i32.gt_s | 0x4a | (i32, i32) : (i32) |
[C], [S] |
32位有符号整数比较:是否大于。 | |
| i64.gt_s | 0x55 | (i64, i64) : (i32) |
[C], [S] |
64位有符号整数比较:是否大于。 | |
| i32.gt_u | 0x4b | (i32, i32) : (i32) |
[C], [U] |
32位无符号整数比较:是否大于。 | |
| i64.gt_u | 0x56 | (i64, i64) : (i32) |
[C], [U] |
64位无符号整数比较:是否大于。 | |
| i32.ge_s | 0x4e | (i32, i32) : (i32) |
[C], [S] |
32位有符号整数比较:是否大于等于。 | |
| i64.ge_s | 0x59 | (i64, i64) : (i32) |
[C], [S] |
64位有符号整数比较:是否大于等于。 | |
| i32.ge_u | 0x4f | (i32, i32) : (i32) |
[C], [U] |
32位无符号整数比较:是否大于等于。 | |
| i64.ge_u | 0x5a | (i64, i64) : (i32) |
[C], [U] |
64位无符号整数比较:是否大于等于。 |
浮点数比较指令
| 指令名称 | 操作码 | 立即数 | 签名 | 家族 | 说明 |
|---|---|---|---|---|---|
| f32.eq | 0x5b | (f32, f32) : (i32) |
[C], [F] |
判断两个32位浮点数是否相等。 | |
| f64.eq | 0x61 | (f64, f64) : (i32) |
[C], [F] |
判断两个64位浮点数是否相等。 | |
| f32.ne | 0x5c | (f32, f32) : (i32) |
[C], [F] |
判断两个32位浮点数是否不相等。 | |
| f64.ne | 0x62 | (f64, f64) : (i32) |
[C], [F] |
判断两个64位浮点数是否不相等。 | |
| f32.lt | 0x5d | (f32, f32) : (i32) |
[C], [F] |
32位浮点数比较:是否小于。 | |
| f64.lt | 0x63 | (f64, f64) : (i32) |
[C], [F] |
64位浮点数比较:是否小于。 | |
| f32.le | 0x5f | (f32, f32) : (i32) |
[C], [F] |
32位浮点数比较:是否小于等于。 | |
| f64.le | 0x65 | (f64, f64) : (i32) |
[C], [F] |
64位浮点数比较:是否小于等于。 | |
| f32.gt | 0x5e | (f32, f32) : (i32) |
[C], [F] |
32位浮点数比较:是否大于。 | |
| f64.gt | 0x64 | (f64, f64) : (i32) |
[C], [F] |
64位浮点数比较:是否大于。 | |
| f32.ge | 0x60 | (f32, f32) : (i32) |
[C], [F] |
32位浮点数比较:是否大于等于。 | |
| f64.ge | 0x66 | (f64, f64) : (i32) |
[C], [F] |
64位浮点数比较:是否大于等于。 |
转换指令
| 指令名称 | 操作码 | 立即数 | 签名 | 家族 | 说明 |
|---|---|---|---|---|---|
| i32.wrap_i64 | 0xa7 | (i64) : (i32) |
[G] |
将64位整数截断(wrap)为32位整数。 | |
| i64.extend_i32_s | 0xac | (i32) : (i64) |
[S] |
将32位有符号整数符号扩展为64位整数。 | |
| i64.extend_i32_u | 0xad | (i32) : (i64) |
[U] |
将32位无符号整数零扩展为64位整数。 | |
| i32.trunc_f32_s | 0xa8 | (f32) : (i32) |
[F], [S] |
将32位浮点数转为32位有符号整数(截断)。 | |
| i32.trunc_f64_s | 0xaa | (f64) : (i32) |
[F], [S] |
将64位浮点数转为32位有符号整数(截断)。 | |
| i64.trunc_f32_s | 0xae | (f32) : (i64) |
[F], [S] |
将32位浮点数转为64位有符号整数(截断)。 | |
| i64.trunc_f64_s | 0xb0 | (f64) : (i64) |
[F], [S] |
将64位浮点数转为64位有符号整数(截断)。 | |
| i32.trunc_f32_u | 0xa9 | (f32) : (i32) |
[F], [U] |
将32位浮点数转为32位无符号整数(截断)。 | |
| i32.trunc_f64_u | 0xab | (f64) : (i32) |
[F], [U] |
将64位浮点数转为32位无符号整数(截断)。 | |
| i64.trunc_f32_u | 0xaf | (f32) : (i64) |
[F], [U] |
将32位浮点数转为64位无符号整数(截断)。 | |
| i64.trunc_f64_u | 0xb1 | (f64) : (i64) |
[F], [U] |
将64位浮点数转为64位无符号整数(截断)。 | |
| f32.demote_f64 | 0xb6 | (f64) : (f32) |
[F] |
将64位浮点数降级(demote)为32位浮点数。 | |
| f64.promote_f32 | 0xbb | (f32) : (f64) |
[F] |
将32位浮点数升级(promote)为64位浮点数。 | |
| f32.convert_i32_s | 0xb2 | (i32) : (f32) |
[F], [S] |
将32位有符号整数转为32位浮点数。 | |
| f32.convert_i64_s | 0xb4 | (i64) : (f32) |
[F], [S] |
将64位有符号整数转为32位浮点数。 | |
| f64.convert_i32_s | 0xb7 | (i32) : (f64) |
[F], [S] |
将32位有符号整数转为64位浮点数。 | |
| f64.convert_i64_s | 0xb9 | (i64) : (f64) |
[F], [S] |
将64位有符号整数转为64位浮点数。 | |
| f32.convert_i32_u | 0xb3 | (i32) : (f32) |
[F], [U] |
将32位无符号整数转为32位浮点数。 | |
| f32.convert_i64_u | 0xb5 | (i64) : (f32) |
[F], [U] |
将64位无符号整数转为32位浮点数。 | |
| f64.convert_i32_u | 0xb8 | (i32) : (f64) |
[F], [U] |
将32位无符号整数转为64位浮点数。 | |
| f64.convert_i64_u | 0xba | (i64) : (f64) |
[F], [U] |
将64位无符号整数转为64位浮点数。 | |
| i32.reinterpret_f32 | 0xbc | (f32) : (i32) |
按位将32位浮点数重新解释为32位整数。 | ||
| i64.reinterpret_f64 | 0xbd | (f64) : (i64) |
按位将64位浮点数重新解释为64位整数。 | ||
| f32.reinterpret_i32 | 0xbe | (i32) : (f32) |
按位将32位整数重新解释为32位浮点数。 | ||
| f64.reinterpret_i64 | 0xbf | (i64) : (f64) |
按位将64位整数重新解释为64位浮点数。 | ||
| i32.extend8_s | 0xc0 | (i32) : (i32) |
[S] |
将i32的低8位作为有符号数,符号扩展到32位。 | |
| i32.extend16_s | 0xc1 | (i32) : (i32) |
[S] |
将i32的低16位作为有符号数,符号扩展到32位。 | |
| i64.extend8_s | 0xc2 | (i64) : (i64) |
[S] |
将i64的低8位作为有符号数,符号扩展到64位。 | |
| i64.extend16_s | 0xc3 | (i64) : (i64) |
[S] |
将i64的低16位作为有符号数,符号扩展到64位。 | |
| i64.extend32_s | 0xc4 | (i64) : (i64) |
[S] |
将i64的低32位作为有符号数,符号扩展到64位。 |
访存指令
| 指令名称 | 操作码 | 立即数 | 签名 | 家族 | 说明 |
|---|---|---|---|---|---|
| i32.load | 0x28 | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR) : (i32) |
[M], [G] |
从内存加载一个32位整数。 |
| i64.load | 0x29 | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR) : (i64) |
[M], [G] |
从内存加载一个64位整数。 |
| f32.load | 0x2a | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR) : (f32) |
[M], [E] |
从内存加载一个32位浮点数。 |
| f64.load | 0x2b | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR) : (f64) |
[M], [E] |
从内存加载一个64位浮点数。 |
| i32.store | 0x36 | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR, $value: i32) : () |
[M], [G] |
向内存存储一个32位整数。 |
| i64.store | 0x37 | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR, $value: i64) : () |
[M], [G] |
向内存存储一个64位整数。 |
| f32.store | 0x38 | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR, $value: f32) : () |
[M], [F] |
向内存存储一个32位浮点数。 |
| f64.store | 0x39 | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR, $value: f64) : () |
[M], [F] |
向内存存储一个64位浮点数。 |
| i32.load8_s | 0x2c | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR) : (i32) |
[M], [S] |
从内存加载8位有符号整数,符号扩展为32位。 |
| i32.load16_s | 0x2e | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR) : (i32) |
[M], [S] |
从内存加载16位有符号整数,符号扩展为32位。 |
| i64.load8_s | 0x30 | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR) : (i64) |
[M], [S] |
从内存加载8位有符号整数,符号扩展为64位。 |
| i64.load16_s | 0x32 | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR) : (i64) |
[M], [S] |
从内存加载16位有符号整数,符号扩展为64位。 |
| i64.load32_s | 0x34 | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR) : (i64) |
[M], [S] |
从内存加载32位有符号整数,符号扩展为64位。 |
| i32.load8_u | 0x2d | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR) : (i32) |
[M], [U] |
从内存加载8位无符号整数,零扩展为32位。 |
| i32.load16_u | 0x2f | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR) : (i32) |
[M], [U] |
从内存加载16位无符号整数,零扩展为32位。 |
| i64.load8_u | 0x31 | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR) : (i64) |
[M], [U] |
从内存加载8位无符号整数,零扩展为64位。 |
| i64.load16_u | 0x33 | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR) : (i64) |
[M], [U] |
从内存加载16位无符号整数,零扩展为64位。 |
| i64.load32_u | 0x35 | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR) : (i64) |
[M], [U] |
从内存加载32位无符号整数,零扩展为64位。 |
| i32.store8 | 0x3a | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR, $value: i32) : () |
[M], [G] |
将32位整数截断为8位并存入内存。 |
| i32.store16 | 0x3b | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR, $value: i32) : () |
[M], [G] |
将32位整数截断为16位并存入内存。 |
| i64.store8 | 0x3c | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR, $value: i64) : () |
[M], [G] |
将64位整数截断为8位并存入内存。 |
| i64.store16 | 0x3d | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR, $value: i64) : () |
[M], [G] |
将64位整数截断为16位并存入内存。 |
| i64.store32 | 0x3e | $flags: [memflags], $offset: [varuPTR] |
($base: iPTR, $value: i64) : () |
[M], [G] |
将64位整数截断为32位并存入内存。 |
其它内存相关指令
| 指令名称 | 操作码 | 立即数 | 签名 | 家族 | 说明 |
|---|---|---|---|---|---|
| memory.grow | 0x40 | $reserved: [varuint1] |
($delta: iPTR) : (iPTR) |
[Z] |
按$delta指定的页数增加内存大小。成功则返回旧大小,失败返回-1。 |
| memory.size | 0x3f | $reserved: [varuint1] |
() : (iPTR) |
[Z] |
获取当前内存的大小(以页为单位)。 |
逆向
静态
基本所有逆向工具的原生或通过插件支持wasm的反汇编和反编译,下面列出一些:
1.WABT:这是官方的二进制工具集,含多个工具,例如wasm2wat能做反汇编,wasm-decompile能做反编译,wasm2c能直接转换为C和相应头文件,而且基本不会有问题!
2.JEB原生支持WASM分析
3.IDA:之前IDA不支持Wasm,有两种方法可以分析,第一种是将其编译为elf,即利用和;而第二种方法是用第三方插件,如wasm2ida,idawasm,不过ida8.5开始支持wasm了
4.Ghidra通过ghidra-wasm-plugin支持wasm
5.radare2也支持,但俺不咋用它
动态
1.chrome本身对Wasm的调试支持很有限,不过类似ida的脚本语言,咱可以直接在DevTools中用JS定义一些脚本,去辅助分析
2.它还可以被编译为C等运行,或者python等都存在它的解释器,可用于运行它,便于黑盒使用
3.可使用Cetus,它利用wail做插桩,实现了很多类似于cheat engine的功能
4.可以反编译再回编译,给它添加dwarf信息,调试起来会更爽
参考
[0] WebAssembly Specifications
[1] What’s in that.wasm? Introducing:wasm-decompile
[4] WebAssembly Reference Manual
[5] WASM汇编入门教程
[6] Hacking WebAssembly Games with Binary Instrumentation
[6] Standing on the Shoulders of Giants: De-Obfuscating WebAssembly Using LLVM