WebAssembly取代JavaScript時代來臨?
前言
V8除了解釋和編譯JS之外,其它的功能如還可以編譯WebAssembly。屬于雙引擎性質的組件,功能天然強大,有谷歌的支撐微軟協(xié)助,背景也夠硬。
WebAssembly(后面簡稱:Wasm)這幾年的火爆程序不亞于AI,它以短小,精悍,緊湊的二進制格式。在游戲,音視頻,云,區(qū)塊鏈等方面幾乎全方位的秒殺了JS那笨重和簡陋的設計。甚至一些久經考驗的,聲名遠揚的桌面軟件比如AutoCAD、Photoshop通過Wasm部分移植到了web端。nodejs也用到了v8組件進行js解析。
同時可以把Rust,C++,Go的代碼編譯成Wasm,然后運行在瀏覽器上。因Wasm字節(jié)碼是一個偽C形式的代碼,但更接近匯編,它天然在對抗逆向方面有強效作用。
本篇來看下它的核心所在。
詳情
取代JS是否可行?看例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WASM Demo</title>
</head>
<body>
<h1>WASM</h1>
<script>
(async () => {
const Wasm = "AGFzbQEAAAABBwFgAn9/AX8DAgEABwcBA2FkZAAACgkBBwAgACABags=";
const bytes = Uint8Array.from(atob(Wasm), c => c.charCodeAt(0));
const { instance } = await WebAssembly.instantiate(bytes.buffer);
const result = instance.exports.add(60, 60);
const p = document.createElement('p');
p.textContent = `60 + 60 = ${result}`;
document.body.appendChild(p);
})();
</script>
</body>
</html>給Script腳本進行了Wasm的操作,沒有用JS。我們看下純Wasm。
const Wasm = "AGFzbQEAAAABBwFgAn9/AX8DAgEABwcBA2FkZAAACgkBBwAgACABags=";
function base64ToArrayBuffer(base64) {
const binary_string = (function() {
const b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
let s = "";
let i = 0;
while (i < base64.length) {
const e1 = b.indexOf(base64.charAt(i++));
const e2 = b.indexOf(base64.charAt(i++));
const e3 = b.indexOf(base64.charAt(i++));
const e4 = b.indexOf(base64.charAt(i++));
const c1 = (e1 << 2) | (e2 >> 4);
const c2 = ((e2 & 15) << 4) | (e3 >> 2);
const c3 = ((e3 & 3) << 6) | e4;
s += String.fromCharCode(c1);
if (e3 != 64) s += String.fromCharCode(c2);
if (e4 != 64) s += String.fromCharCode(c3);
}
return s;
})();
const len = binary_string.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
(async () => {
const buffer = base64ToArrayBuffer(Wasm);
const { instance } = await WebAssembly.instantiate(buffer);
const result = instance.exports.add(60, 60);
print(`60 + 60 = ${result}`);
})();看Wasm變量;
const Wasm = "AGFzbQEAAAABBwFgAn9/AX8DAgEABwcBA2FkZAAACgkBBwAgACABags=";這一串代碼的Wasm如下,實際上就實現了一個加法運算;
(module
(func (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add)
(export "add" (func 0)))我們后面通過:
awaitWebAssembly.instantiate(buffer)實例化了一個句柄,然后通過句柄計算兩個整數:instance.exports.add(60, 60)。當然這些表面上的代碼不是我們的重點,我們需要知道的是Add這個函數代碼在內存,在V8(Chromium)引擎里面是怎么被執(zhí)行的。
先看下它初次的匯編:
D:\chromium\src\out\Debug>d8 --allow-natives-syntax --print-wasm-code "C:\Users\Administrator\Desktop\wasm.js"
--- WebAssembly code ---
name: wasm-function[0]
index: 0
kind: wasm function
compiler: Liftoff
Body (size = 256 = 196 + 60 padding)
Instructions (size = 180)
0xa448f51900 0 4531e4 xorl r12,r12
0xa448f51903 3 e868f8ffff call 000000A448F51170 (jump table)
0xa448f51908 8 4881ec08000000 REX.W subq rsp,0x8
0xa448f5190f f 8bc0 movl rax,rax
0xa448f51911 11 8bd2 movl rdx,rdx
0xa448f51913 13 8b4eff movl rcx,[rsi-0x1]
0xa448f51916 16 4903ce REX.W addq rcx,r14
0xa448f51919 19 0fb74907 movzxwl rcx,[rcx+0x7]
0xa448f5191d 1d 81f9be000000 cmpl rcx,0xbe
0xa448f51923 23 0f8421000000 jz 000000A448F5194A <+0x4a>
0xa448f51929 29 b94c000000 movl rcx,000000000000004C
0xa448f5192e 2e 4989e2 REX.W movq r10,rsp
0xa448f51931 31 4883ec28 REX.W subq rsp,0x28
0xa448f51935 35 4883e4f0 REX.W andq rsp,0xf0
0xa448f51939 39 4c89542420 REX.W movq [rsp+0x20],r10
0xa448f5193e 3e 48b86022bcc3fc7f0000 REX.W movq rax,00007FFCC3BC2260
0xa448f51948 48 ffd0 call rax
0xa448f5194a 4a 493b65a0 REX.W cmpq rsp,[r13-0x60]
0xa448f5194e 4e 0f8640000000 jna 000000A448F51994 <+0x94>
//這里即是相加的地方
0xa448f51954 54 8d0c10 leal rcx,[rax+rdx*1]
//中間省略,便于觀看
0xa448f519b2 b2 ebb4 jmp 000000A448F51968 <+0x68>
Source positions:
pc offset position
96 0 statement
a6 6 statement
Safepoints (stack slots = 9, entries = 1, byte size = 15)
0xa448f5199b 9b slots (sp->fp): 000000000
RelocInfo (size = 0)
--- End code ---
60 + 60 = 120對于add函數,它用的匯編不是add指令,也不是簡單的相加,而是lea指令
0xa448f51954 54 8d0c10 leal rcx,[rax+rdx*1]用lea的好處,可以在不影響標志位的情況下把寄存器+寄存器的地址求出來。它生成此段匯編的地方如下:
//src\v8\src\wasm\baseline\x64\liftoff-assembler-x64-inl.h
void LiftoffAssembler::emit_i32_add(Register dst, Register lhs, Register rhs) {
if (lhs != dst) {
leal(dst, Operand(lhs, rhs, times_1, 0));
} else {
addl(dst, rhs);
}
}為了定位到emit_i32_add這個函數,我們需要在以下函數下斷點;
void Decode() {
//省略便于觀看
DecodeFunctionBody();
}以及;
//src\v8\src\wasm\function-body-decoder-impl.h
void DecodeFunctionBody() {
TRACE("wasm-decode %p...%p (module+%u, %d bytes)\n", this->start(),
this->end(), this->pc_offset(),
static_cast<int>(this->end() - this->start()));
{
if (V8_LIKELY(this->current_inst_trace_->first == 0)) {
// Decode the function body.
while (this->pc_ < this->end_) {
// Most operations only grow the stack by at least one element (unary
// and binary operations, local.get, constants, ...). Thus check that
// there is enough space for those operations centrally, and avoid any
// bounds checks in those operations.
stack_.EnsureMoreCapacity(1, this->zone_);
uint8_t first_byte = *this->pc_;
WasmOpcode opcode = static_cast<WasmOpcode>(first_byte);
CALL_INTERFACE_IF_OK_AND_REACHABLE(NextInstruction, opcode);
int len;
// Allowing two of the most common decoding functions to get inlined
// appears to be the sweet spot.
// Handling _all_ opcodes via a giant switch-statement has been tried
// and found to be slower than calling through the handler table.
if (opcode == kExprLocalGet) {
len = WasmFullDecoder::DecodeLocalGet(this, opcode);
} else if (opcode == kExprI32Const) {
len = WasmFullDecoder::DecodeI32Const(this, opcode);
} else {
//這里下斷點
OpcodeHandler handler = GetOpcodeHandler(first_byte);
len = (*handler)(this, opcode);
}
this->pc_ += len;
}
}最終會把生成的機器碼賦值給下面;
jit_allocation的address變量進行執(zhí)行。這個地方如果是.NET它會直接用生成機器碼地址進行跳轉執(zhí)行,而不會再次賦值或者拷貝到其它地址。
//src\v8\src\wasm\wasm-code-manager.cc
WritableJitAllocation jit_allocation = ThreadIsolation::LookupJitAllocation(
reinterpret_cast<Address>(dst_code_bytes.begin()),
dst_code_bytes.size(), ThreadIsolation::JitAllocationType::kWasmCode,
true);
jit_allocation.CopyCode(0, desc.buffer, desc.instr_size);由于以上代碼都在RUNTIME_FUNCTION宏;
RUNTIME_FUNCTION(Runtime_WasmCompileLazy) {
DCHECK_EQ(2, args.length());
Tagged<WasmTrustedInstanceData> trusted_instance_data =
TrustedCast<WasmTrustedInstanceData>(args[0]);
int func_index = args.smi_value_at(1);
TRACE_EVENT1("v8.wasm", "wasm.CompileLazy", "func_index", func_index);
DisallowHeapAllocation no_gc;
SealHandleScope scope(isolate);
DCHECK(isolate->context().is_null());
if (trusted_instance_data->has_native_context()) {
isolate->set_context(trusted_instance_data->native_context());
}
bool success = wasm::CompileLazy(isolate, trusted_instance_data, func_index);
if (!success) {
DCHECK(v8_flags.wasm_lazy_validation);
AllowHeapAllocation throwing_unwinds_the_stack;
wasm::ThrowLazyCompilationError(
isolate, trusted_instance_data->native_module(), func_index);
DCHECK(isolate->has_exception());
return ReadOnlyRoots{isolate}.exception();
}
return Smi::FromInt(
wasm::JumpTableOffset(trusted_instance_data->module(), func_index));
}最終是由v8.dll!Builtins_XXXX函數調比如下面的Builtins_WasmCEntry堆棧;
> v8.dll!Builtins_WasmCEntry() C++
v8.dll!Builtins_WasmCompileLazy() C++
v8.dll!Builtins_JSToWasmWrapperAsm() C++
v8.dll!Builtins_JSToWasmWrapper() C++
v8.dll!Builtins_InterpreterEntryTrampoline() C++
v8.dll!Builtins_AsyncFunctionAwaitResolveClosure() C++
v8.dll!Builtins_PromiseFulfillReactionJob() C++
v8.dll!Builtins_RunMicrotasks() C++
v8.dll!Builtins_JSRunMicrotasksEntry() C++
[內聯(lián)框架] v8.dll!v8::internal::GeneratedCode<unsigned long long,unsigned long long,v8::internal::MicrotaskQueue *>::Call(unsigned __int64 args, v8::internal::MicrotaskQueue * args) 行 212 C++
v8.dll!v8::internal::`anonymous namespace'::Invoke(v8::internal::Isolate * isolate, const v8::internal::`anonymous namespace'::InvokeParams & params) 行 556 C++v8.dll!Builtins_WasmCEntry()調用
RUNTIME_FUNCTION宏的地方和返回的地方
00007FFCAC47D6DC FF D3 call rbx //調用的地方
//調用之后返回的地方
00007FFCAC47D6DE 49 3B 85 C8 03 00 00 cmp rax,qword ptr [r13+3C8h]當然最終的Wasm代碼執(zhí)行在
Builtins_WasmCompileLazy
00007FFCAC4449A0 41 FF E7 jmp r15經過兩個jmp的跳轉終于來到了wasm代碼匯編地方
0000078B48441900 45 31 E4 xor r12d,r12d
0000078B48441903 E8 68 F8 FF FF call 0000078B48441170
0000078B48441908 48 81 EC 08 00 00 00 sub rsp,8
0000078B4844190F 8B C0 mov eax,eax
0000078B48441911 8B D2 mov edx,edx
//此處省略結尾
總結就是,V8通過樁入口進入到Builtins相對應的實現預定好的執(zhí)行函數,進行執(zhí)行,其中的解釋,編譯,以及執(zhí)行,跟其它語言比如Rust/Java等沒有什么分別。
無論是V8的JS引擎和Wasm引擎,本質上都屬于編譯范疇,并不難。難點在于其龐大的外圍組件和技術。
比如(以下參考網路)
Browser 進程 (瀏覽器主進程) | 所有其它進程的“調度者”。負責窗口管理、地址欄/書簽/下載管理、網絡堆棧(大部分 HTTP 請求)、磁盤緩存、權限控制、插件/子進程生命周期管理等。UI(標簽欄、工具欄)也在這里渲染。 |
Renderer 進程 (渲染進程) | 每個標簽頁(或 iframe group / 站點隔離單元)對應一個渲染進程。負責解析 HTML/CSS/JS,排版布局、執(zhí)行 JavaScript、構建 DOM 樹和 Render Tree,合成圖層(Compositing)等。崩潰時只影響該標簽頁。 |
GPU 進程 | 把各個 Renderer 的圖層交給 GPU 進行加速合成、WebGL、Canvas、視頻解碼等工作。減少 CPU 占用,提高流暢度。 |
Network Service 進程 (較新版本) | 原本在 Browser 進程里的網絡堆?,F在拆出來單獨進程,用于安全隔離和更高性能的網絡請求、DNS 解析、緩存處理。 |
Extension/插件進程 | 每個擴展/插件(非 PPAPI 的也可能共用)有自己的進程,執(zhí)行擴展 JS、后臺頁面、content script 與 UI 通信等。 |
Utility 進程 | 輔助工作進程,如音視頻解碼、文件讀寫、打印、索引等獨立模塊。崩潰也不會影響主瀏覽器。 |
Crashpad / 報錯進程 | 負責崩潰報告、日志收集,保證即便崩潰也能上傳 dump。 |
Sandboxed Service Worker / Isolated Origin 進程 | 在站點隔離、service worker、shared worker 下可能單獨拉起的子進程,進一步隔離不同域或 worker。 |
這些組件導致了Chromium異常的龐大,一個level-symbols one和two的compile結果高達200多個G。




















