從首屆 WebAssembly Summit 看 Wasm 未來發展方向

近兩年,Wasm 生態發展得越來越快,舊的技術(Asm.js、Native Client)被其逐漸代替,新的領域(邊緣計算、區塊鏈)開始出現它的身影,甚至連 “Wasm 是否會代替瀏覽器上的 JavaScript?”這類問題也一度成爲開發者討論的焦點。時隔三年,第一次全球性 Wasm 技術大會,究竟有哪些值得我們關注的 Wasm 技術新動向?讓我們一起來看一看。

首屆 WebAssembly Summit 於不久前在美國山景城的 Google 總部順利召開。大會分享者有我們熟悉的科技漫畫作者—— 來自 Mozilla 的 Lin Clark、WebAssembly(下文簡稱 Wasm)的基礎設施工具鏈 Emscripten / Binaryen 作者——來自 Google 的 Alon,以及衆多來自各大知名互聯網公司的 Wasm 研究和開發人員。WebAssembly Summit 是由 Wasm 社區舉辦,專門討論 Wasm 當前和未來發展以及其相關議題的全球性大會。(A one day, single track, conference about all things WebAssembly.)

Wasm 自 2017 年推出其第一個 MVP 版本標準後,Wasm 在應用領域的相關實踐便開始百花齊放。一方面,處在 Post-MVP 的各個新標準提案不斷快速發展(如 Thead,SIMD),並同時被瀏覽器廠商逐步實現。另一方面,在 Web 之外的領域,Wasm 也在 VM / Runtime 基礎設施、編程語言集成、Node.js、Package Management 以及雲等各個領域大顯身手。近兩年,Wasm 生態發展得越來越快,舊的技術(Asm.js、Native Client)被其逐漸代替,新的領域(邊緣計算、區塊鏈)開始出現它的身影,甚至連 “Wasm 是否會代替瀏覽器上的 JavaScript?” 這類問題也一度成爲開發者討論的焦點。時隔三年,第一次全球性 Wasm 技術大會,究竟有哪些值得我們關注的 Wasm 技術新動向?讓我們一起來看一看。

安全性

Lin Clark 作爲開場嘉賓着重介紹了 Wasm 在應用安全性上的設計和考慮。回顧 20 年前,各大互聯網公司在軟件開發過程中,對代碼的複用能力非常低;但 20 年之後,諸如 NPMPip 等代碼包管理工具讓我們不再需要從頭到尾的、完全“獨立”地開發一個應用,模塊化的開發模式讓我們可以大量重用社區中已經成熟的第三方模塊。但在利用這些模塊功能的同時,我們也不得不面對另一個問題,那就是隨第三方代碼而來的代碼安全性問題。

一個應用在運行時會依賴第三方模塊所提供的功能,因此在傳統的軟件開發模式中(譬如 Node.js),第三方代碼同樣共享着“主應用”所擁有的各類系統接口權限(如 Socket、File Operation)和資源訪問(如 Memory)權限。不僅如此,由於模塊化開發模式,使得代碼的整體依賴成樹狀關係,因此整顆依賴樹上的所有模塊代碼都會共享同樣的代碼權限,而這則無疑大大降低了應用整體的安全性,給第三方代碼中所可能包含的惡意代碼或漏洞代碼以可乘之機。

通過對一個真實的、通過第三方模塊惡意代碼竊取用戶數字貨幣的案例進行總結,我們可以發現攻擊者通常會按照以下時間順序來對終端用戶逐步發起攻擊:

  • Day 0:攻擊者創建模塊;
  • Day 2:攻擊者將模塊作爲第三方底層依賴;
  • Day 17:攻擊者爲模塊添加惡意代碼;
  • Day 41-66:目標應用通過升級模塊引入惡意代碼;
  • Day 90:攻擊被用戶發覺。

通常來說,以上述案例爲例,惡意代碼需要同時具備以下幾種權限才能夠對終端用戶成功地發起攻擊:

  1. 操作系統資源訪問權限,包括用於存放 seed 等敏感數據的內存資源、用於發送竊取數據的 Socket 資源;
  2. 操作系統接口調用權限,包括文件的讀寫權限,以及 Socket 的操作權限。

據 NPM 官方調查可知,自 2017 年到 2019 年,NPM 上包含有惡意代碼的模塊數量逐年增加,並且攻擊者的惡意代碼目標逐漸向終端用戶靠近,攻擊實施者更加具有耐心,企圖進行可以暗中實施的、經過精心策劃的攻擊。

拋開第三方模塊主動發起的惡意代碼攻擊,應用自有的代碼漏洞也同樣爲攻擊者提供了可乘之機。比如“經典”的 ZipSlip 任意文件覆蓋漏洞。ZipSlip 沒有對解壓縮文件時的目標地址進行校驗,而是直接進行了拼接,因此當遇到包含惡意代碼文件的壓縮包時,這些文件便可經由此漏洞被解壓到整個文件系統的任意位置,當然前提是隻要應用的執行者擁有文件目錄的寫權限即可。在 NPM 中具有類似漏洞的代碼包只有 59% 被修復,有超過 40% 的代碼包依賴有至少含有一個已知漏洞的 NPM 第三方模塊依賴。實際上,諸如此類的“惡意代碼”或者“代碼漏洞”問題無法被完全避免,因此我們需要考慮其他的方式來保證應用的運行安全。

究其根源,發生類似安全性問題主要是由於:惡意代碼擁有了本不該擁有的系統資源和系統接口權限。我們不能相信代碼本身的行爲方式是否能完全滿足我們對安全性的要求。那回過頭來,我們看一看 Wasm 是怎樣做到能夠保證應用的運行安全性的。

類比於操作系統上每個應用在運行時的獨立進程(Process),每一個 Wasm 模塊在實例化運行時也都有自己獨立的運行時沙盒環境(Sandbox),對應着獨立的可用內存資源以及調用棧。但與傳統的操作系統“進程”所不同的是,每一個實例化的模塊都只能使用在實例化時被主動分配的系統資源(內存)與接口能力(系統調用)。不僅如此,相對於傳統進程需要通過“序列化”與“反序列化”才能夠在進程間傳遞信息(IPC)也有所不同,Wasm 實例之間的消息傳遞可以更輕量地完成。

Interface Types 提案

通常情況下,Wasm 模塊之間只能夠傳遞最基本的數值類型數據,如整型(Integer)以及浮點型(Float)。但藉助於 “Interface Types” 提案(Phase 1),模塊實例之間便可以通過最小成本來直接且安全地與更多複雜的數據類型進行交互,比如常用的字符串類型。在進行復雜類型數據交互時,Wasm 引擎會將複雜數據從被調用模塊實例的內存空間直接拷貝到調用者的內存空間,而非數據共享。這降低了數據共享時被惡意代碼篡改的風險,同時相較於傳統“進程”模型而言,也減少了數據傳遞時序列化與反序列化的性能開銷。

Interface Types 提案設計細節

WASI

Wasm 模塊除了會對其所擁有的數據進行“隔離”以外,對於應用在 Web 瀏覽器以外的 Wasm 模塊來說,對模塊實例所能夠擁有的系統接口權限進行“隔離”也是勢在必行的安全性措施,而 WASI(WebAssembly System Interface)的出現便解決了這個問題。通過該提案,我們可以有針對性地爲每一個獨立的模塊實例提供不同的操作系統接口 / 資源權限。這些操作系統接口或資源權限可以在每個模塊進行實例化時被調用者主動指定。而這種安全策略便是我們所熟知的 “Capability-based Security”,一種基於給定資源的安全性控制策略。

WebAssembly Nanoprocess

基於上述我們提出的 Wasm 模塊在數據及權限方面所使用的安全性策略,我們提出了一種新的 Wasm 應用構建模式 — “Wasm Nanoprocess”。一般來說,一個完整的大型 Wasm 應用可能同時包含有多個相互依賴的底層 Wasm 模塊,由於每一個模塊實例都擁有自己獨立的數據、資源以及權限控制,因此我們可以稱每一個實例化模塊爲一個獨立的 “nanoprocess”,翻譯過來即“納米進程”。

WebAssembly Nanoprocess 架構模式 (圖片來自 Lin Clark)

當一個含有惡意代碼的 Wasm 模塊被“鏈接”到整個應用的依賴樹中時,由於應用的各依賴模塊所能夠使用的資源以及系統接口權限均來自於最上層的調用者,即需要在應用運行的入口模塊中被指定,然後再由該模塊向下層依賴模塊進行分發,因此當惡意模塊的內部代碼需要使用某種未經授權的額外資源時,整個模塊依賴樹的 “import” 段簽名便會發生錯誤,並會在運行時向上層用戶拋出該異常,以提示某個模塊的某些特定資源或者權限沒有被導入。而在這種情況下,特殊的權限調用需求便會引起人們的注意。

而即便惡意代碼獲得了特定操作系統接口的執行權限,但想要從其他依賴模塊的實例中獲取對應內存段中的敏感信息,也並非易事。由於我們之前提到的 Wasm 模塊內存數據隔離特性,只有在模塊主動向外部暴露(通過 “export” 段)特定數據,或者直接調用(動態鏈接)目標模塊內的方法時才能夠將自身內存段中的數據傳遞過去。因此,只有通過限制惡意代碼對數據以及系統接口權限的訪問和使用,“Wasm Nanoprocess” 這種構建模式纔可以在最大程度上保證 Wasm 應用及其所依賴第三方模塊的安全性。

Bytecode Alliance

技術社區的繁榮離不開一個健康、可持續發展的生態環境。爲了能讓 Wasm 離用戶更近,而不單只是面對生硬的標準,“Bytecode Alliance” 字節碼聯盟成立了。聯盟的宗旨在於希望能夠爲開發者提供健全的、成熟的、基於各類安全策略(包括上述的 Nanoprocess 策略)構建的開發工具鏈(虛擬機、編譯器以及底層庫)生態,讓開發者可以在各類環境下快速構建 Wasm 應用而不用考慮安全性等基本問題,可以更多地專注應用本身的設計與開發。

性能

如果說安全性是一個 Wasm 應用需要考慮的第一要務,那麼性能便是除此之外的另一個“重中之重”了。Alon 爲我們帶來了關於 Wasm 應用性能優化技巧的分享,相較傳統基於 JavaScript 構建的 Web 應用來說,Wasm 由於使用了二進制格式(Binary Format),因此同樣大小的模塊文件可以存儲更多的代碼信息。不僅如此,由於 Wasm 模塊一般是從 C / C++ / Rust 等強類型語言代碼編譯而來的,因此我們也能夠藉助編譯器來對原始代碼進行諸如 DCE(Dead Code Elimination)等的代碼優化處理。需要注意的是,從強類型語言到 Wasm 的轉換並沒有特意爲 Web 平臺進行優化,因此諸如 C++ 中對 STL、模板等特性和庫的大量使用則可能會使最終生成的 Wasm 模塊文件大小出乎我們的意料。

下面我們將以一個使用 C++ 開發,且基於 Emscripten 構建的 Wasm 項目爲例,來介紹整個開發流程中可以使用的性能優化技巧。

壓縮代碼

在服務器上啓用 gzip 壓縮,或者使用 Brotli(由 Google 開發的一種通用無損壓縮算法),同時對 Emscripten 生成的 JavaScript 膠水文件進行壓縮。

使用優化器

使用 wasm-opt 對 Wasm 二進制文件進行優化處理。wasm-opt 是一個通用的 Wasm 二進制代碼優化器,它被默認附帶在 Binaryen 的工具鏈集合中。Alon 根據以往的測試案例統計,對於由 LLVM-Wasm 後端直接生成的 Wasm 二進制文件,wasm-opt 可以有效優化其約 20% 的文件體積。在 wasm-opt 內部,優化器會對 Wasm 二進制代碼進行諸如 DCE,“常量傳播”“內聯” 等一系列常規的優化操作。除此之外,基於 Wasm 標準虛擬指令集的結構,優化器還會進行如 “Local Optimization” “Memory Segment Optimization” “Structured Control Flow” 等其他共 68 項特殊優化流程(Pass),下面我們介紹其中一個經典的優化流程。

RemoveUnusedBrs

該優化階段主要用於對如下情況進行字節碼上的 精簡替換

RemoveUnusedBrs 優化

在上圖的左側給出的 WAT 代碼中我們可以看到,一個由 block 和 br_if 組成的“條件中斷”結構。當 X 爲“真”時,執行流程會跳過整個 block 結構;反之當 X 爲“假”時,則會執行 Y 內的代碼邏輯。這裏我們將用右側,使用 if 實現的、具有完全相同邏輯的“條件判斷”結構來代替左側部分的代碼。這裏我們將兩段 WAT 代碼分別對應的字節碼展示出來,即:

  • 左側:… 02 40 (X) 0d 00 (Y) 0b …
  • 右側:… (X) 45 04 40 (Y) 0b …

可以看到,相較於左側的原始字節碼,新改寫後的右側代碼可以少生成 1byte 對應的二進制字節碼,降低了整個二進制模塊文件的體積。而如果把 wasm-opt 中所有類似的精細優化過程都加起來,平均下來則可以優化目標 Wasm 模塊文件多達 20% 的體積。wasm-opt 命令行工具一般跟隨 Binaryen 工具套件一同發行,當然其也有對應的 JavaScript 版本以及線上開箱即用版本:http://wasm-shr.ink

審視字節碼

我們可以通過對 Wasm 二進制文件的字節碼進行 Profiling 來審視對應的源代碼是否還有優化改進的空間,這些常用的性能分析工具有如下幾種:

  • Bloaty McBloatface — Google 自研的二進制文件分析器,支持 Wasm 格式;
  • Twiggy — 來自 Rust 社區的 Wasm 文件分析器;
  • wasm-opt’s --func-metrics。

優化源語言

除了可以直接對 Wasm 的二進制字節碼進行分析優化以外,我們還可以從源語言以及對應的編譯器工具鏈入手來進行優化,這裏以 C++ 爲例。爲了能夠減小生成的 Wasm 模塊文件體積,如果在代碼中沒有使用 C++ Exception 相關的特性,建議爲編譯器指定 “-fno-exceptions” 參數以關閉編譯器對異常相關特性的支持。類似的還有:RTTI 相關特性的參數 “-fno-rtti”。在實際編碼時需要注意,使用模板(Templates)可能會產生重複的代碼段,進而增加最終產物的體積。虛函數(virtual)的調用可能會阻止編譯器 DCE 的進行,進而弱化了優化結果。更進一步,儘量使用 C-like Array 來代替 C++ STL 中的 std::vector、使用 printf 代替 std::iostream 等等。由於 STL 中的數據容器和對象一般都經過了高度的抽象和封裝,因此,對應最終生成的 Wasm 字節碼體積通常會比真正使用到的部分大很多。

除了關閉上述不需要的編譯器功能、使用低抽象層次語言特性之外,我們在某種程度上也可以直接使用 Web 平臺上的 API 來提供應用的運行效率。比如,在下面這段代碼中,我們藉助 Emscripten 提供的 EM_JS 宏在 C++ 代碼中直接調用瀏覽器的 API “console.log” 來向宿主環境打印消息。相比調用 printf 函數,這種直接使用宿主環境 API 的方式可以讓我們省略掉很多並不需要的、在膠水代碼中實現的抽象環節。通常來說,這些抽象環節主要用來彌補不同平臺之間的函數調用差異。

使用 EM_JS 宏直接在 C++ 代碼中 “調用” Web API

優化編譯鏈路

除了可以對源語言代碼進行優化以外,我們還可以對編譯器鏈路進行優化。除了我們上述提到的幾個 C / C++ 編譯器可以直接使用的優化標記以外,Emscripten 也爲我們提供了衆多可選的優化參數,一些常用的優化標記如下所示。

  • -O3 / -Os / -Oz:它們是與 clang / gcc 類似的全局優化標記;-O3 重在提高優化速度,-Os 重在優化產物體積,而 -Oz 則處在兩者之間。Emscripten 會自動調用 wasm-opt 對生成的 Wasm 二進制文件和 JavaScript 膠水文件進行優化。如下圖所示,將 Wasm 模塊 “import” 段中的複雜符號名替換成簡單符號,便可以同時壓縮 JavaScript 文件(縮短實例化時的導入對象名稱)和 Wasm 二進制文件的體積;

meta-DCE 優化細節

  • -s MALLOC-emmalloc: Emscripten 默認使用 dllmalloc 內存分配器來作爲 malloc 的實現。而在不需要分配大量“可變大小”內存塊的情況下,可以使用 emmalloc 來作爲 malloc 的實現。相較於 dllmalloc 而言,emmalloc 實現的代碼量僅爲前者的三分之一,可以在保持相同內存分配效率的同時大大減小 Wasm 模塊的體積;
  • –closure 1:通過該標記,Emscripten 會自動調用 Google Closure Compiler 來對最後生成的 JavaScript 膠水文件進行壓縮;
  • -s ENVIRONMENT=web:如果生成的 Wasm 應用僅需運行在 Web 平臺,那麼可以通過指定該參數來讓 Emscripten 去掉產物中爲支持 Nodejs 環境而加入的一些代碼。

關於更多其他高級標記 / 參數的具體用法,可以查閱 Emscripten 的官網文檔。

未來

在不久的將來,我們將藉助 Wasm 正在發展中的多項提案來解決我們現階段遇到的一些問題,比如藉助 Wasm 的 “Exception Handling” 提案,我們可以更好地在 Wasm 中對接 C++ 或其他強類型語言中的 Exception 特性。同樣的,藉助於即將到來的 “Multiple Tables” 提案以及 LLVM 的幫助,我們可以更好地在 Wasm 中處理面向對象強類型語言中的諸如“虛函數調用” “轉型”等涉及虛函數表的相關特性。

瀏覽器引擎

Tadeu 爲我們介紹了 JSC ( JavaScriptCore)上爲 Wasm 新增的解釋器 — LLInt(Low Level Interpreter)的一些基本情況。JSC 上有專門爲執行 Wasm 字節碼增設的兩個 JIT 編譯器,它們分別是:BBQ(Building Bytecode Quickly)以及 OMG(Optimized Machine Code Generator)。對比 V8,這裏的 BBQ 類似 Liftoff,主要用於快速編譯並生成機器碼,降低應用啓動時間;而 OMG 則類似於 TurboFan,主要用於優化熱代碼,並生成經過優化的機器碼,提高運行效率。但實際上,由於 BBQ 的性能並沒有那麼令人滿意,因此 LLInt 的出現便是用來解決應用冷啓動速度(start-up time)慢的問題。LLInt 希望能夠在加快 Wasm 應用冷啓動速度的同時,並遵守 BBQ 在調用規範(Calling Conventions)以及堆棧、寄存器使用上的一些約定,使得後續的 JIT 過程可以直接依賴 LLInt 在此之前得到的成果。

優化策略

其中比較有意思的一項優化策略是 “Constant & Local Propagation”,聽起來跟普通編譯器使用的“常量傳播”優化十分類似。可以參考下圖,這裏左邊的 WAT 經過平展(flatten)處理後可以對應到 Wasm 字節碼的四個虛擬指令,四個虛擬指令又對應到如下的四個實際的寄存器操作。可以看到對於前兩個寄存器操作,由於這裏 “loc2” 寄存器內的常量內容是已知的,因此可以直接將其帶入並替換到 “i32_add” 操作中,以減少整體的指令執行次數。

Constant & Local Propagation 優化(圖一)

經過指令參數替換後的結果:

Constant & Local Propagation 優化(圖二)

而從整個編譯器的執行鏈路上來看,LLInt 會作爲所有方法體的啓動執行單元。隨着方法內部調用計數器的不斷增加,BBQ 及 OMG 會相應隨之啓動,並執行進一步的優化編譯。這裏可以看到,當在主函數內部遇到高計算量代碼時,編譯鏈路會直接啓動 OMG 進行深度的優化編譯,並通過 OSR(On-Stack Replacement)來對不同優化層級上的調用棧數據進行替換。

JavaScriptCore WebAssembly 優化鏈路

暫停與思考

Ashley 爲我們帶來了一些對 WebAssembly 這項技術的思考。首先,她強調了一個事實,即 “WebAssembly 的出現並不是爲了取代 JavaScript”。但是 WebAssembly 從出現至今,仍然有很多地方並沒有做到那麼令人滿意,其中第一個也是讓很多人感同身受的地方,那就是 WebAssembly 並沒有成功地將這項好用的技術推廣給最需要它的人。如下圖所示,當一個滿懷期待,對這項新技術充滿渴望的開發者打開 Wasm 官網時,映入眼簾的是如下用各種抽象的專業術語“堆砌”而成的,對這項新技術的枯燥的介紹。因此,“not human-readable” 的官網和文檔成爲了初期阻礙 Wasm 快速發展的一個重要因素。

WebAssembly 官網部分截圖

Ashley 想要告訴我們的是,我們應該花更多的精力在如何能夠讓 Wasm 成爲用戶的強大武器上,而不是單純只關注於如何把這個“武器”變得更加強大。從現階段來看,Wasm 這項技術已經從 Web 走到了非 Web 領域,並且除 MVP 標準外還有多達 24+ 個新的提案正在並行向前推進。但通過對身邊各企業在 Wasm 上的實踐觀察可發現,在 Wasm 騰空出世的這三年裏,有超過半數的應用場景都僅侷限於區塊鏈和虛擬貨幣,類似 “AutoCAD 移植”這類標杆性的應用場景也只是曇花一現,並且也很難被普通開發者所學習效仿。如今的 Wasm 社區裏,可能有太多的人只關注在 Wasm 的技術標準上,他們都變成了單純的技術追求者,而並沒有思考如果從普通開發者的角度來看,如何才能夠快速地瞭解這項技術,並且可以上手用它來做點什麼。

拿《The Rise of Worse Is Better》這篇經典計算機科學論文的中心思想作爲總結:簡潔(Worse,功能少、簡單以及實現容易)的編程語言或者軟件系統往往會比那些大而全、功能複雜的要更好(Better)。類比到 Wasm,一次性把它做的盡善盡美,然後再讓用戶去使用(指通過更“友善”的方式接近用戶);或者,在只有一部分核心功能的時候就讓用戶提前使用,然後再根據他們的反饋不斷完善。你覺得哪種方式更好呢?

WebAssembly Summit 議題深度解析分爲上下兩部分,本篇我們介紹了 WebAssembly Summit 各位嘉賓在上半場帶來的 Wasm 在標準制定、編譯優化以及瀏覽器引擎上的一些精彩分享。下篇我們將介紹,Wasm 在現階段物聯網、雲等各類工程領域中的一些精彩實踐。

作者介紹

於航,曾在阿里巴巴本地生活、Tapatalk 等國內外企業工作,現在 PayPal 上海負責 GRC 相關的技術研發工作;FCC (FreeCodeCamp China) 上海技術社區負責人;多次 QCon、GMTC 大會講師;WebAssembly 技術佈道者;2018 年出版名爲《深入淺出 WebAssembly》的國內第一本 Wasm 技術書籍;2019 年開發名爲 TWVM 的輕量級 Wasm 虛擬機。主要技術研究領域爲:前端基礎技術架構、 Serverless、WebAssembly、LLVM 及編譯器等相關方向。

活動推薦:
GMTC全球大前端技術大會(北京站)2020,於航老師擔任“前端前沿技術”出品人,也將邀請行業內知名技術專家現場爲大家分享更多新興技術,瞭解前端技術的發展趨勢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章