├── README.md
├── WebAssembly专栏
├── 1.浅述WebAssembly历史.md
├── 2.Emscripten使用入门.md
├── 3. 打造基于WASM的高性能安全沙盒.md
└── images
│ ├── asmjs.benchmark.png
│ ├── asmjs.png
│ ├── benchmarks.png
│ ├── c2j.png
│ ├── debugging.png
│ ├── emcc.png
│ ├── emscripten.png
│ ├── nacl.png
│ ├── opcode.webp
│ └── wasm.webp
├── 深入浅出V8优化
├── 1. Smi和HeapNumber.md
├── 2. 字符串的优化.md
└── images
│ ├── smi.bool.png
│ ├── smi.heapnumber.png
│ ├── smi.heapobject.png
│ ├── smi.iee754.impl.png
│ ├── smi.iee754.png
│ ├── smi.jsobject.png
│ ├── smi.smi.png
│ ├── string.cons.jpg
│ ├── string.gc.png
│ ├── string.gc2.jpg
│ ├── string.layout.png
│ └── string.seq.jpg
├── 深入浅出动态化SSR服务
├── 深入浅出动态化SSR服务(一).md
├── 深入浅出动态化SSR服务(三).md
└── 深入浅出动态化SSR服务(二).md
└── 高性能WASM播放器实现
├── images
├── pic1.gif
├── pic2.png
├── pic3.png
├── pic4.png
└── pic5.png
└── 高性能WASM播放器实现.md
/README.md:
--------------------------------------------------------------------------------
1 | ### 目录
2 | * [高性能WASM播放器实现](https://github.com/ErosZy/md/blob/master/%E9%AB%98%E6%80%A7%E8%83%BDWASM%E6%92%AD%E6%94%BE%E5%99%A8%E5%AE%9E%E7%8E%B0/%E9%AB%98%E6%80%A7%E8%83%BDWASM%E6%92%AD%E6%94%BE%E5%99%A8%E5%AE%9E%E7%8E%B0.md)(2020-07-26)
3 | * [InfoQ - 深入浅出动态化SSR服务](https://github.com/ErosZy/md/tree/master/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E5%8A%A8%E6%80%81%E5%8C%96SSR%E6%9C%8D%E5%8A%A1)(2020-03-11)
4 | * [深入浅出动态化SSR服务(一)](https://github.com/ErosZy/md/blob/master/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E5%8A%A8%E6%80%81%E5%8C%96SSR%E6%9C%8D%E5%8A%A1/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E5%8A%A8%E6%80%81%E5%8C%96SSR%E6%9C%8D%E5%8A%A1%EF%BC%88%E4%B8%80%EF%BC%89.md)
5 | * [深入浅出动态化SSR服务(二)](https://github.com/ErosZy/md/blob/master/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E5%8A%A8%E6%80%81%E5%8C%96SSR%E6%9C%8D%E5%8A%A1/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E5%8A%A8%E6%80%81%E5%8C%96SSR%E6%9C%8D%E5%8A%A1%EF%BC%88%E4%BA%8C%EF%BC%89.md)
6 | * [深入浅出动态化SSR服务(三)](https://github.com/ErosZy/md/blob/master/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E5%8A%A8%E6%80%81%E5%8C%96SSR%E6%9C%8D%E5%8A%A1/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E5%8A%A8%E6%80%81%E5%8C%96SSR%E6%9C%8D%E5%8A%A1%EF%BC%88%E4%B8%89%EF%BC%89.md)
7 | * [InfoQ - WebAssembly专栏](https://github.com/ErosZy/md/tree/master/WebAssembly%E4%B8%93%E6%A0%8F)(2020-11-16)
8 | * [1. 浅述 WebAssembly 历史](https://github.com/ErosZy/md/blob/master/WebAssembly%E4%B8%93%E6%A0%8F/1.%E6%B5%85%E8%BF%B0WebAssembly%E5%8E%86%E5%8F%B2.md)
9 | * [2. Emscripten使用入门](https://github.com/ErosZy/md/blob/master/WebAssembly%E4%B8%93%E6%A0%8F/2.Emscripten%E4%BD%BF%E7%94%A8%E5%85%A5%E9%97%A8.md)
10 | * [3. 打造基于 WASM 的高性能安全沙盒](https://github.com/ErosZy/md/blob/master/WebAssembly%E4%B8%93%E6%A0%8F/3.%20%E6%89%93%E9%80%A0%E5%9F%BA%E4%BA%8EWASM%E7%9A%84%E9%AB%98%E6%80%A7%E8%83%BD%E5%AE%89%E5%85%A8%E6%B2%99%E7%9B%92.md)
11 | * [前端之巅公众号 - 深入浅出V8优化](https://github.com/ErosZy/md/blob/master/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BAV8%E4%BC%98%E5%8C%96)(2022-06-18)
12 | * [1. Smi和HeapNumber](https://github.com/ErosZy/md/blob/master/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BAV8%E4%BC%98%E5%8C%96/1.%20Smi%E5%92%8CHeapNumber.md)
13 | * [2. 字符串的优化](https://github.com/ErosZy/md/blob/master/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BAV8%E4%BC%98%E5%8C%96/2.%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E4%BC%98%E5%8C%96.md)
14 |
15 |
16 | ### 文章协议
17 | 本作品采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/WebAssembly专栏/1.浅述WebAssembly历史.md:
--------------------------------------------------------------------------------
1 | 浅谈WebAssembly历史
2 | ---
3 |
4 | > 感谢Emscripten核心作者Alon Zakai在我写作此篇文章时对邮件所提问题的耐心解答和帮助,如此才使得我能够更全面、详细及正确地还原有关WebAssembly的技术演变历程。
5 |
6 | WebAssembly无疑是近年来让人最为兴奋的新技术之一,它虽始于浏览器但已经开始不断地被各个语言及平台所集成。在实际的工业化落地上,区块链、边缘计算、游戏及图像视频等多个领域依靠WebAssembly都创造了让人称赞的产品。WebAssembly技术本身具有非常多的优点,其中最为被人所熟知的三点有:
7 | * 二进制格式
8 | * Low-Level的编译目标
9 | * 接近Native的执行效率
10 |
11 | 那么WebAssembly是从何演变而来,它为什么具有这些优点与特性并且是如何被标准化的,更重要的是作为普通开发者我们应如何更好的入手开发及实践WebAssembly呢?本专题将会将围绕WebAssembly及Emscripten工具链,通过一系列的文章依次介绍WebAssembly的演变历程、工具链使用、实践案例、最新应用场景及使用技巧,帮助普通开发者正确理解WebAssembly的使用场景,并能够正确且顺利地使用Emscripten工具链完成自己的WebAssembly相关项目。
12 |
13 | 本文作为专题的第一篇,我们将会较为详细地介绍WebAssembly的相关演变历程,以使得读者能够深入理解WebAssembly这门技术的使用场景,从而更好的学习和使用WebAssembly技术。
14 |
15 | ### JavaScript的弊端
16 | JavaScript毫无疑问是技术领域的佼佼者。自Brendan Eich于1995年花费10天时间为Netscape开发出JavaScript为始,到现在已经走过了20多个年头。随着技术的蓬勃发展,不管是NPM与GitHub上丰富的JavaScript库与框架,还是React Native、Node.js、Electron、QuickJS等领域技术的出现,无一不彰显着JavaScript生态的繁荣,JavaScript这门语言也变得越来越流行和重要。
17 |
18 | 但与此同时,随着各类应用功能的复杂化,受限于JavaScript语言本身动态类型和解释执行的设计,其性能问题也逐渐凸现。我们厄待需要新的技术帮助我们解决JavaScript的性能问题。在2008年底,Google、Apple、Mozilla为JavaScript引入了JIT(Just-In-Time)引擎,试图解决JavaScript的性能问题,并取得了非常好的效果。其中的佼佼者非Google的V8莫属,其大举提升了JavaScript的性能,并拉开了JavaScript引擎竞速的序幕。
19 |
20 | 那JIT(Just-In-Time)引擎是如何提升JavaScript的性能的呢?我们知道,由于JavaScript是解释型语言,因此JavaScript引擎需要逐行对JavaScript代码进行翻译变为可执行的代码。可执行代码有多种形式,其中较为常见的是基于AST的直接执行以及ByteCode的执行方式。显而易见,这些做法相比于直接运行机器码而言都并不高效,那么如果我们能根据代码的执行频次将部分代码实时编译为机器码,则会获得更大的性能提升,这就是JIT(Just-In-Time)的基本思路。
21 |
22 | 在实际生产中,JIT(Just-In-Time)引擎一般会引入多层次的决策来优化我们的代码:
23 | * warm阶段(解释执行的代码被执行多次): 将解释执行的代码发送给JIT(Just-In-Time)引擎,并创建出编译为机器码的执行代码,但此处并不进行替换;
24 | * hot阶段(解释执行的代码被执行得十分频繁): 解释执行代码被替换为warm阶段的机器码执行代码;
25 | * very hot阶段:将解释执行的代码发送给优化编译器(Optimising Compiler),创建和编译出更高效的机器码的执行代码并进行替换;
26 |
27 | 假设我们的JavaScript代码中有部分代码被执行了多次,此时这部分代码会被标记为warm,同时被送往JIT(Just-In-Time)引擎进行优化。JIT(Just-In-Time)引擎此时会针对这些代码进行逐行的机器码编译,然后存储在一张表的单元中(实际上表单元仅指向了被编译的机器码)。当解释执行的代码被执行得非常频繁时变会进入hot阶段,JIT(Just-In-Time)引擎会将解释执行的代码直接替换为编译的机器码版本。
28 |
29 | 需要注意的是,表单元的引用依据实际上会依赖于行号以及参数类型,假设我们有如下的代码:
30 | ```javascript
31 | function doSomething(value){
32 | // performing some operations
33 | }
34 |
35 | const arr = [0, "String"];
36 | for (let i = 0; i < arr.length; i++) {
37 | doSomething(arr[i])
38 | }
39 | ```
40 | 由于数组arr中存在两种数据类型(Number/String),当我们多次执行相关代码时,`doSomething`函数会被JIT(Just-In-Time)引擎创建并编译出两个不同类型的机器码执行代码版本,并且使用不同的表单元进行引用。当然,由于机器码执行代码的创建和编译是存在代价的,因此不同的JIT(Just-In-Time)引擎会有不同的优化策略。
41 |
42 | 如果部分代码执行得异乎频繁,那么自然的这部分解释执行的代码会被发送给优化编译器(Optimising Compiler)进行更高程度的优化,从而创建并编译出相比warm阶段更高效的机器码执行代码版本。与此同时,在创建这些高度优化的机器码执行代码期间,编译器将会严格限制执行代码的适用类型(比如仅适用于Number/String或某些特定类型参数),并且在每次调用执行前都会进行参数类型的检查,如果匹配则会使用这些高度优化的机器码执行代码,否则将会回退到warm阶段生成的机器码执行代码或是直接解释执行。
43 |
44 | 当JavaScript有了JIT(Just-In-Time)后就能高枕无忧了么?不尽然。从上面的介绍中我们可以看到,JIT(Just-In-Time)引擎的优化并非是完全无代价的。同时由于JavaScript自身的灵活性,如果我们编写JavaScript代码时并没有将数据类型严格固定,那么JIT(Just-In-Time)的效果将会被大打折扣。在Google V8团队的[《JIT-less V8》](https://v8.dev/blog/jitless)文章中我们可以看到,使用JIT-less模式的V8在运行Youtube的Living Room页面时,其测试成绩与使用JIT的V8实际差距仅为6%。这个测试侧面反应了JIT在生产中并不是完全的“性能银弹”。
45 |
46 |
47 |

48 |
JIT-less模式下V8与基线的对比
49 |
50 |
51 | 那么JavaScript能变得更快么,还是说我们需要其他技术来解决JavaScript的性能问题?此时NaCl和PNaCl应运而生。
52 |
53 | ### NaCl与PNaCl
54 |
55 | 尽管JavaScript由于JIT的加入在性能上有了很大的提升,但在许多性能敏感的领域,JavaScript仍旧无法满足我们的需求,因此在2008年,Google的Brad Chen、Bennet Yee以及David Sehr开源了NaCl技术,并在2009年正式达到生产可用的状态。NaCl全称为"Native Client",其由C/C++语言编写并定义了一套Native Code的安全子集(SFI技术),并执行于自己独立的沙盒环境之中,以防止安全性未知的C/C++代码对操作系统本身产生危害。
56 |
57 | NaCl应用及其模块在性能上与原生应用的差距非常小,但由于NaCl与CPU架构强关联不具有可移植性,因此需要针对不同的平台进行开发以及编译,这导致了我们无法自由分发NaCl应用及其模块。为了解决这个问题,NaCl改进技术PNaCl出现了。
58 |
59 |
60 |

61 |
NaCl的性能损耗极小
62 |
63 |
64 | PNaCl的全称为"Portable Native Client",其通过替换Native Code为LLVM IR子集并在客户端编译为NaCl的方式解决了NaCl的分发问题。PNaCl不依赖于特定的CPU架构,更易于被部署和使用,“一次编译,到处运行”在PNaCl上得到了实现。但同样的,PNaCl也是运行在自己的独立沙盒之中,其无法直接的访问Web APIs,而是需要通过一个名为"PPAPI"的接口来与JavaScript进行通信。
65 |
66 | PNaCl技术在当时看起来是一个非常理想的方案,其兼具高性能和易于分发的特点,但实际上其在当时并没有受到非常强的支持。PPAPI出现的时代正好是处于人们尽可能试图摆脱Flash、Java Applet等插件的时代,尽管当时Chrome已经将NaCl与PNaCl直接集成,但其运行在独立沙盒环境与使用独立API的方式与Flash、Java Applet等插件非常类似。同时,其开发难度和成本以及糟糕的兼容性问题(2011年开始Firefox及Opera正式支持PPAPI及NaCl)都成为了NaCl/PNaCl普及的最大障碍。
67 |
68 | ### 让人惊艳的asm.js
69 |
70 | 谈到asm.js和WebAssembly就不得不提到其中关键人物Alon Zakai。在2010年,Alon Zakai结束了两年的创业项目,加入了Mozilla负责Android版Firefox的开发。在Mozilla的工作之余,Alon Zakai继续编写着自己的C/C++游戏引擎,在项目临近尾声之时,Alon Zakai突发奇想,想将自己的C/C++游戏引擎运行在浏览器上。在2010年,NaCl还是一门非常新的技术,而PNaCl才刚刚开始开发,此时并没有一个非常好的技术方案能够将Alon的C/C++游戏引擎跑在浏览器上。但好在C/C++是强类型语言,而JavaScript是弱类型语言,将C/C++代码编译为JavaScript代码在技术实现上是完全可行的。于是Alon Zakai自此开始编写相关的Compiler实现,`Emscripten(LLVM into JavaScript)`由此诞生了!
71 |
72 |
73 |

74 |
75 |
76 | 到2011年,Emscripten已经具备编译像Python以及DOOM等中大型项目的能力,与此同时Emscripten也在JSConfEU会议上首次亮相,并取得了一定的影响力。Mozilla看到了Emscripten项目的巨大潜力(相较于NaCl而言更加的Web友好),Brendan及Andreas邀请Alon加入Mozilla的Research团队全职负责Emscripten项目的开发,Alon Zakai欣然接受并将工作的重心放在了如何提升Emscripten编译的JavaScript代码执行速度上。
77 |
78 | 在`JavaScript的弊端`章节中我们可以看到,尽管JavaScript拥有JIT(Just-In-Time),但由于JavaScript本身的语言特性,导致JIT(Just-In-Time)难以被预测,在实际的生产环境当中JIT(Just-In-Time)的效果往往并没有那么显著。为了使得JavaScript运行得更快,我们应该要更充分的利用JIT(Just-In-Time),因此在2013年,Alon Zakai联合Luke Wagner,David Herman发布了asm.js。
79 |
80 | asm.js的思想很简单,就是尽可能的明确对应的类型,以便JIT(Just-In-Time)能够被充分利用。如下图示例所示:
81 |
82 |
83 |

84 |
85 |
86 | 我们可以看到,对于`add`函数而言,由于传入参数`x`,`y`以及返回值进行了`|0`的操作,那么其能够很明确为JIT(Just-In-Time)指明对应的类型(i32),因此能被JIT(Just-In-Time)充分优化(不考虑后期AOT的情况)。通过添加类似的类型注解,Emscripten编译的asm.js在运行速度上相比普通JavaScript有了质的飞跃,在Benchmark中,asm.js能达到Native性能的50%左右,相比于普通的JavaScript代码而言取得了极大的性能提升,这无疑是让人兴奋的成果,但是asm.js自身也存在一些无法忽视的问题,其总体而言并不是一个非常理想的技术方案。
87 |
88 |
89 |

90 |
91 |
92 | 最显而易见的就是asm.js代码的"慢启动"问题,由于asm.js还是和JavaScript一样的文本格式,因此对于大中型项目而言,其解析所花费的时间会非常长,无法与高效的二进制格式相提并论。其次,asm.js实质上是一种较为hack的实现方式,类似`|0`的类型标注不具有可读性,同时拓展asm.js也变得越来越复杂且不可靠:随着asm.js想要更加接近于Native的执行性能,不免会对诸多Math函数(例如Math.imul及Math.fround等)进行拓展和改写,从长远来看这对TC39标准的制定是不友好的,同时asm.js自身的相关实现(例如memory growth等)也遭遇了非常多的问题而被迫不断修订asm.js标准。"The hacks had a cost",因此我们需要一个全新的技术来解决asm.js所遇到的这些问题。
93 |
94 | ### 合作共赢 - WebAssembly
95 |
96 | 在2013年,NaCl/PNaCl与asm.js/Emscripten形成了不同路线发展的竞争态势,但与此同时,Google及Mozilla也在工具及虚拟机层面加强了许多合作,其中包括:
97 | * 由Google的JF Bastien牵头的每月Google和Mozilla工具团队之间的交流会;
98 | * Emscripten和PNaCl开始互相共享部分代码,包括Legalization Passes、le32 triple等;
99 | * 尝试将NaCl应用通过Emscripten进行编译,并开源Pepper.js;
100 | * Google及Mozilla共同就asm.js进行代码贡献并规划未来Native Code在Web上的合理方案;
101 | * 就WebAssembly前身"WebAsm"进行标准和方案的讨论;
102 |
103 | 最终在2015年的4月1号,"WebAssembly"击败了"WebAsm"、"WebMachine"以及其它名称在Google和Mozilla的团队交流邮件中被确定使用。至2015年6月17号,两方就WebAssembly的标准化工作进行了确定,并搭建了WebAssembly官网同时进行了对外的宣传工作。WebAssembly的设计汲取了NaCl与asm.js两者的优点:
104 | * WebAssembly并不依赖于JavaScript,与NaCl/PNaCl一样,它基于二进制格式,能够被快速的解析;
105 | * 与asm.js一样,依靠Emscripten等工具链提供的API,它以非常自然的方式直接操作Web APIs,而不用像PNaCl一样需要处理与JavaScript之间的通信;
106 | * WebAssembly依赖于LLVM IR并使用独立的VM环境,因此其它语言/平台能够以较低成本进行接入,并且能够且易于被持续优化至接近Native的性能;
107 |
108 | 目前各大主流浏览器已经完全实现了WebAssembly的MVP版本,并已被接纳为"浏览器的第二语言"。依靠优秀的设计,WebAssembly也从浏览器平台走向更多的平台,WASI(WebAssembly System Interface)将为WebAssembly提供更多的可能性。随着WebAssembly的相关标准的逐渐确定和完善,WebAssembly技术的应用领域将会越来越广。
109 |
110 | ### 最后
111 | 本文我们从JavaScript开始,介绍了NaCl/PNaCl以及asm.js技术方案的优缺点。通过简单回顾WebAssembly的相关历史背景,我们能更好地够理解WebAssembly技术的演变过程及其适用场景。在后面的文章中,我们将基于Emscripten工具链继续探讨WebAssembly,通过具体的实例介绍WebAssembly应用的基本方法和相关实现。
112 |
--------------------------------------------------------------------------------
/WebAssembly专栏/2.Emscripten使用入门.md:
--------------------------------------------------------------------------------
1 | Emscripten使用入门
2 | ----
3 |
4 | 在上一篇中我们较为详细的讲述了WebAssembly的演变历程,通过WebAssembly的演变历程我们对WebAssembly的三个优点(二进制格式、Low-Level的编译目标、接近Native的执行效率)也有了比较深刻的理解。在本章中我们将选取Emscripten及C/C++语言来简要讲述WebAssembly相关工具链的使用,通过较为简单的例子帮助大家能够更为快速上手WebAssembly相关的应用开发。请放心,在本章中我们将避免复杂难懂的C/C++语言技巧,力求相关示例简单、直接、易懂。如果你有Rust、Golang等支持WebAssembly的相关语言背景,那么可以将本章相关内容作为参考,与对应官方工具链进行结合学习。
5 |
6 | ### 关于Emscripten
7 |
8 | Emscripten是WebAssembly工具链里重要的组成部分。从最为简单的理解来说,Emscripten能够帮助我们将C/C++代码编译为ASM.js以及WebAssembly代码,同时帮助我们生成部分所需的JavaScript胶水代码。但实质上Emscripten与LLVM工具链相当接近,其包含了各种我们开发所需的C/C++头文件、宏参数以及相关命令行工具。通过这些C/C++头文件及宏参数,其可以指示Emscripten为源代码提供合适的编译流程并完成数据转换,如下图所示:
9 |
10 |
11 |

12 |
Emscripten编译流程(来自官网)
13 |
14 |
15 | emcc是整个工具链的编译器入口,其能够将C/C++代码转换为所需要的LLVM-IR代码,Clang/LLVM(Fastcomp)能够将通过emcc生成的LLVM-IR代码转换为ASM.js及WebAssembly代码,而emsdk及.emscripten文件主要是用来帮助我们管理工具链内部的不同版本的子集工具及依赖关系以及相关的用户编译设置。
16 |
17 | 在我们的日常业务开发过程中,实际上并不需要太过关心Emscripten内部的实现细节,Emscripten已经非常成熟且易于使用。但相关读者若想知道Emscripten内部的更多细节,可以访问Emscripten官网以及Github阅读相关WIKI进行了解。
18 |
19 | ### 下载、安装与配置
20 |
21 | 在进行相关操作之前,请先确保已经安装git工具并能够使用基本的git命令,接下来我们以Linux系统下的操作作为示例演示如何下载、安装及配置Emscripten。若你的操作系统为Windows或是OSX等其他系统,请参考官方文档中的相关章节进行操作。
22 |
23 | * 安装
24 |
25 | 进入你自己的安装目录,执行如下命令获取到Emscripten SDK Manager(emsdk):
26 | ```shell
27 | > git clone https://github.com/emscripten-core/emsdk.git
28 | ```
29 |
30 | * 下载
31 | 进入emsdk目录,并执行如下的命令进行安装操作:
32 | ```shell
33 | > cd emsdk
34 | > git pull
35 | > ./emsdk install latest
36 | ```
37 | 需要注意的是,install命令可以安装特定版本的Emscripten开发包及其依赖的所有自己工具,例如:
38 | ```shell
39 | > ./emsdk install 1.38.45
40 | ```
41 |
42 | * 激活及配置
43 | 当安装完成后,我们可以通过如下命令进行Emscripten的激活和配置:
44 | ```shell
45 | > ./emsdk activate latest # or ./emsdk activate 1.38.45
46 | > source ./emsdk_env.sh
47 | ```
48 |
49 | 现在让我们执行 `emcc -v` 命令查看相关的信息,若正确输出如下类似信息则说明Emscripten安装及配置成功。
50 |
51 |
52 |

53 |
emcc -v的相关信息输出
54 |
55 |
56 | ### 小试身手
57 | 终于进入有趣的部分了,按照惯例,我们先以打印"Hello World!"作为我们学习WebAssembly的第一个程序吧!让我们先快速编写一个C/C++的打印"Hello World!"代码,如下所示:
58 | ```C++
59 | #include
60 |
61 | int main() {
62 | printf("Hello World!\n");
63 | return 0;
64 | }
65 | ```
66 |
67 | 这个程序很简单,使用相关的GCC等相关编译器能够很正确得到对应的输出。那么如何产出WebAssembly的程序呢?依靠Emscripten整个操作也非常简单:
68 | ```shell
69 | > emcc main.c -o hello.html
70 | ```
71 | 执行完毕后你将得到三个文件代码,分别是:
72 | * hello.html
73 | * hello.js:相关的胶水代码,包括加载WASM文件并执行调用等相关逻辑
74 | * hello.wasm:编译得到的核心WebAssembly执行文件
75 |
76 | 接着我们在当前目录启动一个静态服务器程序(例如NPM中的static-server),然后访问hello.html后我们就能看到"Hello World!"在页面上正确输出了!当然,实际上hello.html文件并不是一定需要的,如果我们想要让NodeJS使用我们代码,那么直接执行:
77 | ```shell
78 | > emcc main.c
79 | ```
80 | 即可得到 `a.out.js` 及 `a.out.wasm` 两个文件,然后我们使用NodeJS执行:
81 | ```shell
82 | > node a.out.js
83 | ```
84 | 也能正确的得到对应的输出(你可以自行创建html文件并引入 `a.out.js`进行浏览器环境的执行 )。
85 |
86 | 当然,在我们的日常的业务开发中相关程序是不可能如此简单的。除了我们自己的操作逻辑外,我们还会依赖于非常多商用或开源的第三方库及框架。比如在数据通信及交换中我们往往会使用到JSON这种轻量的数据格式。在C/C++中有非常多相关的开源库能解决JSON解析的问题,例如`cJSON`等,那么接下来我们就增加一点点复杂度,结合 `cJSON` 库编一个简单的JSON解析的程序。
87 |
88 | 首先我们从Github中找到 `cJSON` 的主页,然后下载相关的源码放置在我们项目的vendor文件夹中。接着我们在当前项目的根目录下创建一个`CMakeList.txt`文件,并填入如下内容:
89 |
90 | ```CMake
91 | cmake_minimum_required(VERSION 3.15) # 根据你的需求进行修改
92 | project(sample C)
93 |
94 | set(CMAKE_C_STANDARD 11) # 根据你的C编译器支持情况进行修改
95 | set(CMAKE_EXECUTABLE_SUFFIX ".html") # 编译生成.html
96 |
97 | include_directories(vendor) # 使得我们能引用第三方库的头文件
98 | add_subdirectory(vendor/cJSON)
99 |
100 | add_executable(sample main.c)
101 |
102 | # 设置Emscripten的编译链接参数,我们等等会讲到一些常用参数
103 | set_target_properties(sample PROPERTIES LINK_FLAGS "-s EXIT_RUNTIME=1")
104 | target_link_libraries(sample cjson) # 将第三方库与主程序进行链接
105 | ```
106 |
107 | 那什么是 `CMakeList.txt` 呢?简单来说,`CMakeList.txt` 是 `CMake` 的“配置文件”,`CMake` 会根据 `CMakeList.txt` 的内容帮助我们生成跨平台的编译命令。在我们现在及之后的文章中,不会涉及非常复杂的 `CMake` 的使用,你完全可以把 `CMakeList.txt` 里的相关内容当成固定配置提供给多个项目的复用,如若需要更深入的了解 `CMake` 的使用,可以参考 `CMake` 的官网教程及文档。好了,现在让我们在代码中引入 `cJSON` 然后并使用它进行JSON的解析操作,代码如下:
108 |
109 | ```C++
110 | #include
111 | #include "cJSON/cJSON.h"
112 |
113 | int main() {
114 | const char jsonstr[] = "{\"data\":\"Hello World!\"}";
115 | cJSON *json = cJSON_Parse(jsonstr);
116 |
117 | const cJSON *data = cJSON_GetObjectItem(json, "data");
118 | printf("%s\n", cJSON_GetStringValue(data));
119 |
120 | cJSON_Delete(json);
121 | return 0;
122 | }
123 | ```
124 |
125 | 代码的整体逻辑非常简单易懂,在这里就不再赘述。由于我们使用了 `CMake`,因此Emscripten的编译命令需要有一点点修改,我们将不使用emcc而是使用emcmake及emmake来创建我们的相关WebAssembly代码,命令如下:
126 | ```shell
127 | > mkdir build
128 | > cd build
129 | > emcmake cmake ..
130 | > emmake make
131 | ```
132 |
133 | 我们创建了一个build文件夹用来存放cmake相关的生成文件及信息,接着进入build文件夹并使用emcmake及emmake命令生成对应的WebAssembly代码sample.html、sample.js、sample.wasm,最后我们执行访问sample.html后可以看到其正确的输出了JSON的data内容。
134 |
135 | > 如若你从未使用过CMake,请不要为CMake的相关内容因不理解而产生沮丧或者畏难情绪。在我的日常的WebAssembly开发中,基本都是沿用一套 `CMakeList.txt` 并进行增删改,与此同时编译流程基本与上诉内容一致,你完全可以将这些内容复制在你的备忘录里,下次需要用到时直接修改即可。
136 |
137 | ### WebAssembly的调试
138 |
139 | 对于开发的WebAssembly代码而言,我们对于调试可以使用两种方式,一种方式是通过日志的方式进行输出,另一种方式使用单步调试。使用日志的方式输出调试信息非常容易,Emscripten能很好的支持C/C++里面的相关IO库。而对于单步调试而言,目前最新版本的Firefox及Chrome浏览器都已经有了一定的支持,例如我们有如下代码:
140 |
141 | ```C++
142 | #include
143 |
144 | int main() {
145 | printf("Hello World!");
146 | return 0;
147 | }
148 | ```
149 |
150 | 然后我们使用emcc进行编译得到相关的文件:
151 | ```shell
152 | > emcc -g4 main.c -o main.wasm # -g4可生成对应的sourcemap信息
153 | ```
154 | 接着打开Chrome及其开发者工具,我们就可以看到对应的main.c文件并进行单步调试了。
155 |
156 |
157 |

158 |
使用Chrome进行单步调试
159 |
160 |
161 | 但值得注意的是,目前emcmake对于soucemap的生成支持并不是很好,并且浏览器的单步调试支持也仅仅支持了代码层面的映射关系,对于比较复杂的应用来说目前的单步调试能力还比较不可用,因此建议开发时还是以日志调试为主要手段。
162 |
163 | ### JavaScript调用WebAssembly
164 |
165 | 对于WebAssembly项目而言,我们经常会需要接收外部JavaScript传递的相关数据,难免就会涉及到互操作的问题。回到最开始的JSON解析例子,我们一般情况而言是需要从外部JavaScript中获取到JSON字符串,然后在WebAssembly代码中进行解析后做对应的业务逻辑处理,并返回对应的结果给外部JavaScript。接下来,我们会增强JSON解析的相关代码,实现如下:
166 | ```C++
167 | #include
168 | #include "cJSON/cJSON.h"
169 |
170 | int json_parse(const char *jsonstr) {
171 | cJSON *json = cJSON_Parse(jsonstr);
172 | const cJSON *data = cJSON_GetObjectItem(json, "data");
173 | printf("%s\n", cJSON_GetStringValue(data));
174 | cJSON_Delete(json);
175 | return 0;
176 | }
177 | ```
178 |
179 | 在如上代码中,我们将相关逻辑封装在 `json_parse` 的函数之中,以便外部JavaScript能够顺利的调用得到此方法,接着我们修改一下 `CMakeList.txt` 的编译链接参数:
180 | ```CMake
181 | #....
182 | set_target_properties(sample PROPERTIES LINK_FLAGS "\
183 | -s EXIT_RUNTIME=1 \
184 | -s EXPORTED_FUNCTIONS=\"['_json_parse']\"\
185 | ")
186 | ```
187 | EXPORTED_FUNCTIONS配置用于设置需要暴露的执行函数,其接受一个数组。这里我们需要将 `json_parse` 进行暴露,因此只需要填写 `_json_parse`即可。需要注意的是,这里暴露的函数方法名前面以下划线(_)开头。然后我们执行emcmake编译即可得到对应的生成文件。
188 |
189 | 接着我们访问sample.html,并在控制台执行如下代码完成JavaScript到WebAssembly的调用:
190 | ```JavaScript
191 | let jsonstr = JSON.stringify({data:"Hello World!"});
192 | jsonstr = intArrayFromString(jsonstr).concat(0);
193 |
194 | const ptr = Module._malloc(jsonstr.length);
195 | Module.HEAPU8.set(jsonstr, ptr);
196 | Module._json_parse(ptr);
197 | ```
198 | 在这里,`intArrayFromString`、`Module._malloc` 以及 `Module.HEAPU8` 等都是Emscripten提供给我们的方法。 `intArrayFromString` 会将字符串转化成UTF8的字符串数组,由于我们知道C/C++中的字符串是需要 `\0` 结尾的,因此我们在末尾concat了一个0作为字符串的结尾符。接着,我们使用 `Module._malloc` 创建了一块堆内存并使用 `Module.HEAPU8.set` 方法将字符串数组赋值给这块内存,最后我们调用 `_json_parse` 函数即可完成WebAssembly的调用。
199 |
200 | 需要注意的是,由于WebAssembly端的C/C++代码接收的是指针,因此你是不能够将JavaScript的字符串直接传给WebAssembly的。但如果你传递的是int、float等基本类型,那么就可以直接进行传递操作。当然,上面的代码我们还可以进一步简化为:
201 | ```javascript
202 | const jsonstr = JSON.stringify({data:"Hello World!"});
203 | const ptr = allocate(intArrayFromString(jsonstr), 'i8', ALLOC_NORMAL);
204 | Module._json_parse(ptr);
205 | ```
206 |
207 | 总而言之,如果是基本类型,例如int、float、double等,那么你可以直接进行传值调用,但如果是数组、指针等类型,你就需要依靠 `Module` 对象的方法开辟内存空间,传递地址信息给WebAssembly进行调用。
208 |
209 | ### WebAssembly调用JavaScript
210 |
211 | WebAssembly在执行完成之后可能会需要返回部分返回值,针对这个场景其也分为两种情况:
212 | * 如果返回int、float、double等基础类型,那么直接函数声明返回类型后返回即可;
213 | * 如果需要返回数组、指针等类型,则可以通过 `EM_ASM` 或是 `Memory Copy` 的方式进行处理;
214 |
215 | 例如我们在WebAssembly端接收并解析JSON字符串后,判断对应数值然后返回修改后的JSON字符串,这个需求我们采用 `EM_ASM` 方式的代码如下:
216 | ```C++
217 | #include
218 | #include "cJSON/cJSON.h"
219 | #ifdef __EMSCRIPTEN__
220 | #include
221 | #endif
222 |
223 | int json_parse(const char *jsonstr) {
224 | cJSON *json = cJSON_Parse(jsonstr);
225 | cJSON *data = cJSON_GetObjectItem(json, "data");
226 | cJSON_SetValuestring(data, "Hi!");
227 |
228 | const char *result = cJSON_Print(json);
229 | #ifdef __EMSCRIPTEN__
230 | EM_ASM({
231 | if(typeof window.onRspHandler == "function"){
232 | window.onRspHandler(UTF8ToString($0))
233 | }
234 | }, result);
235 | #endif
236 |
237 | cJSON_Delete(json);
238 | return 0;
239 | }
240 | ```
241 | 首先我们引入emscripten.h头文件,接着我们使用 `EM_ASM` 调用外部的 `window.onRspHandler` 回调方法即可完成对应需求。`EM_ASM` 大括号内可以书写任意的JavaScript代码,并且可以对其进行传参操作。在本例中,我们将result传递给 `EM_ASM` 方法,其 `$0` 为传参的等价替换,若还有更多参数则可以写为 `$1`、`$2`等。接着,我们编译对应代码,然后访问sample.html,并在控制台执行如下代码完成JavaScript到WebAssembly的调用:
242 |
243 | ```javascript
244 | window.onRspHandler = (result) => {
245 | console.log(result); // output: {"data":"Hi!"}
246 | };
247 |
248 | const jsonstr = JSON.stringify({data:"Hello World!"});
249 | const ptr = allocate(intArrayFromString(jsonstr), 'i8', ALLOC_NORMAL);
250 | Module._json_parse(ptr);
251 | ```
252 |
253 | 可以看到,`window.onRspHandler` 函数被调用并正确的进行了结果输出。实际上Emscripten给我们提供了非常多的JavaScript调用函数及宏,包括:
254 | * EM_ASM
255 | * EM_ASM_INT
256 | * emscripten_run_script
257 | * emscripten_run_script_int
258 | * emscripten_run_script_string
259 | * emscripten_async_run_script
260 | * .......
261 |
262 | 在实际使用中我们推荐使用 `EM_ASM` 相关宏,若需要了解其中的差异可以参考Emscripten官网相关章节。那么如果我们使用 `Memory Copy` 的话,代码如下:
263 |
264 | ```C++
265 | #include
266 | #include
267 | #include
268 | #include "cJSON/cJSON.h"
269 |
270 | int json_parse(const char *jsonstr, char *output) {
271 | cJSON *json = cJSON_Parse(jsonstr);
272 | cJSON *data = cJSON_GetObjectItem(json, "data");
273 | cJSON_SetValuestring(data, "Hi!");
274 |
275 | const char *string = cJSON_Print(json);
276 | memcpy(output, string, strlen(string));
277 |
278 | cJSON_Delete(json);
279 | return 0;
280 | }
281 | ```
282 |
283 | 我们相比之前的实现多传递了一个参数output,在WebAssembly端解析、改写JSON完成后,使用memcpy将对应结果复制到output当中。接着,我们编译对应代码,然后访问sample.html,并在控制台执行如下代码完成JavaScript到WebAssembly的调用:
284 | ```javascript
285 | const jsonstr = JSON.stringify({data:"Hello World!"});
286 | const ptr = allocate(intArrayFromString(jsonstr), 'i8', ALLOC_NORMAL);
287 |
288 | const output = Module._malloc(1024);
289 | Module._json_parse(ptr, output);
290 | console.log(UTF8ToString(output)); // output: {"data":"Hi!"}
291 | ```
292 |
293 | 如上所示,我们使用 `Malloc._malloc` 创建了一块堆内存,并传递给 `_json_parse` 函数,同时使用 `UTF8ToString` 方法将对应JSON字符串结果输出。
294 |
295 | ### 使用更多的Emscripten的API
296 |
297 | 实际上Emscripten为了方便我们在C/C++中编写代码,其提供了非常多的API供我们使用,其中包括:Fetch、File System、VR、HTML5、WebSocket等诸多实现。例如我们以Fetch为例:
298 | ```C++
299 | #include
300 | #include
301 |
302 | #ifdef __EMSCRIPTEN__
303 | #include
304 | void downloadSucceeded(emscripten_fetch_t *fetch) {
305 | printf("%llu %s.\n", fetch->numBytes, fetch->url);
306 | emscripten_fetch_close(fetch);
307 | }
308 |
309 | void downloadFailed(emscripten_fetch_t *fetch) {
310 | emscripten_fetch_close(fetch);
311 | }
312 | #endif
313 |
314 | int main() {
315 | #ifdef __EMSCRIPTEN__
316 | emscripten_fetch_attr_t attr;
317 | emscripten_fetch_attr_init(&attr);
318 | strcpy(attr.requestMethod, "GET");
319 | attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
320 | attr.onsuccess = downloadSucceeded;
321 | attr.onerror = downloadFailed;
322 | emscripten_fetch(&attr, "http://myip.ipip.net/");
323 | #endif
324 | }
325 | ```
326 | 在上面的代码中我们使用了 `emscripten_fetch` 相关函数来进行浏览器宿主环境fetch方法的调用。为了启用Emscripten中的Fetch能力,我们还需要修改编译链接参数,为其增加-s FETCH=1:
327 | ```CMake
328 | #....
329 | set_target_properties(sample PROPERTIES LINK_FLAGS "\
330 | -s NO_EXIT_RUNTIME=1 \
331 | -s FETCH=1 \
332 | ")
333 | ```
334 | 想要了解更多的可用API及细节,你可以访问Emscripten官网阅读API Reference相关章节。
335 |
336 | ### 编译链接参数
337 |
338 | 在上面实践中我们使用了一些编译连接的参数,包括:
339 | * -g
340 | * -s EXIT_RUNTIME
341 | * -s EXPORTED_FUNCTIONS
342 | * -s FETCH
343 | * -s NO_EXIT_RUNTIME
344 |
345 | 实际上,Emscripten包含了非常丰富的相关设置参数帮助我们在编译和链接时优化我们的代码。其中部分常用的参数包括:
346 |
347 | * -O1、-O2、-O3、-Oz、-Os、-g等:编译优化,具体可参考Emscripten官网相关章节;
348 | * -s ENVIRONMENT:设定编译代码的可执行环境,默认值为"web,work,node";
349 | * -s SINGLE_FILE:是否将ASM.js或WebAssembly代码以Base64的方式嵌入到JavaScript胶水代码中,可取值0/1;
350 | * -s WASM:是否编译为WebAssembly代码,0编译为ASM.js,1编译为WebAssembly;
351 | * -s FETCH:是否启用Fetch模块,可取值0/1;
352 | * -s DISABLE_EXCEPTION_CATCHING:禁止生成异常捕获代码,可取值0/1;
353 | * -s ERROR_ON_UNDEFINED_SYMBOLS:编译时出现Undefined Symbols后是否退出,可取值0/1;
354 | * -s EXIT_RUNTIME: 执行完毕 `main` 函数后是否退出,可取值0/1;
355 | * -s FILESYSTEM:是否启用File System模块,可取值0/1;
356 | * -s INVOKE_RUN:是否执行C/C++的`main`函数,可取值0/1;
357 | * -s ASSERTIONS:是否给运行时增加断言,可取值0/1;
358 | * -s TOTAL_MEMORY:总的可用内存使用数,可取以16777216为基数的整数值;
359 | * -s ALLOW_MEMORY_GROWTH:当可用内存不足时,是否自动增长,可取值0/1;
360 | * -s EXPORTED_FUNCTIONS:暴露的函数列表名称;
361 | * -s LEGACY_VM_SUPPORT:是否增加部分兼容函数以兼容低版本浏览器(iOS9、老版本Chrome等),可取值0/1;
362 | * -s MEM_INIT_METHOD:是否将.mem文件以Base64的方式嵌入到JavaScript胶水代码中,可取值0/1;
363 | * -s ELIMINATE_DUPLICATE_FUNCTIONS:将重复函数进行自动剔除,可取值0/1;
364 | * --closure: 是否使用Google Closure进行最终代码的压缩,可取值0/1;
365 | * --llvm-lto:是否进行LLVM的链接时优化,可取值0-3;
366 | * --memory-init-file:同-s MEM_INIT_METHOD;
367 | * ......
368 |
369 | 更多编译链接参数设置可以参考 `src/settings.js` 文件。
370 |
371 | ### 总结
372 | 在本章中我们较为详细的介绍了Emscripten的入门使用,关于Emscripten的更多内容(代码性能及体积优化、API使用等)可以参考Emscripten官网或Github的WIKI。在接下来的文章中,我们会以具体需求实例为入口,帮助大家能够更好的学习Emscripten在实际生产中的使用。
373 |
--------------------------------------------------------------------------------
/WebAssembly专栏/3. 打造基于WASM的高性能安全沙盒.md:
--------------------------------------------------------------------------------
1 | 打造基于WASM的高性能安全沙盒
2 | ----
3 |
4 | 通过上一篇文章对 Emscripten 的使用介绍,大家应该具备一定的使用 Emscripten 工具链开发相关 WebAssembly 项目的基础能力了。在本文中,我们将继续通过具体案例更深入地了解 Emscripten 的使用技巧,同时加强对 WebAssembly 二进制格式、Low-Level 编译目标及接近 Native 执行效率的理解。
5 |
6 | ### 前端核心数据加密
7 |
8 | Web 技术的开放以及便捷带来了其极高速的发展,但自作者从事 Web 前端相关开发工作以来,并没有听到太多关于前端核心数据加密的方案,因此“前端数据无加密”慢慢的也变成了业界的一个共识。但在某些日常开发的场景中,我们又会涉及到相当强度的前端核心数据加密的问题,特别是需要在于后端的数据通信上面(包括 HTTP、HTTPS 以及 WebSocket 的数据交换),例如前端日志数据加密就是一个典型的场景。
9 |
10 |
11 | 针对于此类需求,在以往的实践中我们通常使用纯 JavaScript 实现的 Crypto 库来对相关数据加密,同时使用 UglifyJS、Google Closure Compiler 等混淆器进行相关加密代码的混淆,以增加对应的破译难度。但在目前的实践之中由于浏览器自身的调试工具逐渐方便且强大,因此此类方法的安全性变得越来越脆弱。
12 |
13 |
14 | 考虑到这种情况,越来越多的公司开始将前端核心数据加密的相关逻辑迁移至 WebAssembly,依靠 WebAssembly 在整个过程中的编译及二进制特性来提高对应数据加密逻辑的安全性,以此保障业务安全。对于这种方案而言,由于我们可以将 WebAssembly 视作一个全新的 VM,其他语言通过相关工具链(例如 Emscripten)来产出此 VM 可执行的代码,其安全性相比于单纯的 Crypto 库加 JavaScript 混淆器而言具有比较明显的优势。
15 |
16 |
17 | 现在让我们就着手实现一个非常简单的 WebAssembly 签名加密模块来进行一下对应的体验。首先,创建相关的项目,同时下载 Github 上的某 MD5 实现到本项目的 vendor 文件夹中,整体目录结构如下所示:
18 |
19 | ```shell
20 | ├main.c
21 | ├CMakeList.txt
22 | ├vendor
23 | ├─base64
24 | │ └─CMakeList.txt
25 | │ └─base64.c
26 | │ └─base64.h
27 | └─md5
28 | └─CMakeList.txt
29 | └─md5.c
30 | └─md5.h
31 | ```
32 |
33 | 接着我们依照上一章的 CMake 文件进行稍加修改,增加对此 MD5 库的编译链接,如下所示:
34 |
35 | ```cmake
36 | cmake_minimum_required(VERSION 3.15) # 根据你的需求进行修改
37 | project(sample C)
38 |
39 | set(CMAKE_C_STANDARD 11) # 根据你的C编译器支持情况进行修改
40 | set(CMAKE_EXECUTABLE_SUFFIX ".html") # 编译生成.html
41 |
42 | include_directories(${PROJECT_SOURCE_DIR}/vendor) # 使得我们能引用第三方库的头文件
43 | add_subdirectory(${PROJECT_SOURCE_DIR}/vendor/md5)
44 | add_subdirectory(${PROJECT_SOURCE_DIR}/vendor/base64)
45 |
46 | add_executable(sample main.c)
47 |
48 | # 设置Emscripten的编译链接参数,我们等等会讲到一些常用参数
49 | set_target_properties(sample PROPERTIES LINK_FLAGS "\
50 | -s EXIT_RUNTIME=1 \
51 | -s EXPORTED_FUNCTIONS=\"['_sign']\"
52 | ")
53 |
54 | target_link_libraries(sample md5 base64) # 将第三方库与主程序进行链接
55 | ```
56 |
57 | 最后实现我们的数据签名逻辑即可,代码如下:
58 |
59 | ```c++
60 | #include
61 | #include "md5/md5.h"
62 | #include "base64/base64.h"
63 |
64 | const char* salt_key = "md5 salt key";
65 |
66 | #ifdef __EMSCRIPTEN__
67 | int sign(const char* msg, int len, char *output){
68 | int data_len = strlen(salt_key) + len + 1;
69 | char *data = malloc(data_len);
70 | memset(data, 0, data_len);
71 | memcpy(data, msg, len);
72 | memcpy(data + len, salt_key, strlen(salt_key));
73 |
74 | uint8_t result[16] = {0};
75 | md5((uint8_t *)data, strlen(data), result);
76 |
77 | char *encode_out = malloc(BASE64_ENCODE_OUT_SIZE(16));
78 | base64_encode(result, 16, encode_out);
79 | memcpy(output, encode_out, BASE64_ENCODE_OUT_SIZE(16));
80 | free(encode_out);
81 |
82 | return 0;
83 | }
84 | #endif
85 | ```
86 |
87 | 在这里我们使用了上一章介绍的Memory Copy的方式来进行结果数据的传递,因此我们的 JavaScript 代码调用应如下所示:
88 |
89 | ```javascript
90 | const str = intArrayFromString("your data here");
91 | const ptr = allocate(str, "i8", ALLOC_NORMAL);
92 | const output = Module._malloc(1024);
93 | Module._sign(ptr, str.length, output);
94 | console.log(UTF8ToString(output)); // O6VFgoqQiF52FYyH4VmpPQ==
95 | ```
96 |
97 | 在sign的具体实现中我们可以看到,为了增加整体的安全性,我们对其中的内容进行了“加盐”处理。这种操作看起来万无一失,但是实际上我们稍加分析是可以拿到对应的“盐”值的。现在我们将生成的 sample.wasm 文件拖入到编辑器中,然后搜索md5 salt key,我们可以很顺利的得到对应的“盐”值。
98 |
99 |
100 |

101 |
WebAssembly 无法隐藏所有信息
102 |
103 |
104 | 针对于这种情况,我们可以进一步使用异或等算法对“盐”值进行相关的处理来达到更深度的处理,但对于需要更高强度的核心数据保护的应用而言这也不过是叠加的障眼法,在这里我们需要更可靠的方式来达到我们的需求。
105 |
106 | ### 沙盒保护
107 |
108 | 沙盒保护的思路比较直接:为了更好的达到代码整体保护的目的,我们会自行创建一个完全独立的代码执行环境。在这个独立环境中,我们根据我们的需求限制内部代码使用的 built-in API 从而达到完全可控的状态。与此同时,只要我们符合执行代码的相关规范,那么我们可以自行设计相关的 Opcode 及 IR,来达到防逆向的目的。针对于前端环境而言,其整体流程如下所示:
109 |
110 |
111 |

112 |
沙盒保护的编译/运行过程
113 |
114 |
115 | 由于整个前端环境大部分都依靠于 JavaScript,因此在沙盒环境上我们有非常多的选择,在本章中我们采用 QuickJS 来进行相关的介绍和演示。如果有其他的沙盒环境需求,可以参考本章的相关思路来进行适当的修改。
116 |
117 |
118 | QuickJS 想必大家都不会陌生,其作者 Fabrice Bellard 编写了 TinyCC/TinyGL/FFmpeg/JSLinux 等多个知名项目。QuickJS 实现精小,几乎完整支持 ECMA2019,同时具有极低的启动速度有优秀的解释执行性能。在使用 QuickJS 完成我们的沙盒之前,我们先简单的了解一下 QuickJS 的相关 API,然后再使用 Emscripten 来对其进行编译完成我们的整个示例。
119 |
120 |
121 | 首先我们对 CMake 进行相关修改,增加对 QuickJS 的编译链接支持,如下所示:
122 |
123 | ```cmake
124 | cmake_minimum_required(VERSION 3.15)
125 | project(sample C)
126 |
127 | set(CMAKE_C_STANDARD 11)
128 | include_directories(${PROJECT_SOURCE_DIR}/vendor)
129 | add_subdirectory(${PROJECT_SOURCE_DIR}/vendor/quickjs)
130 |
131 | add_executable(sample main.c)
132 |
133 | target_link_libraries(sample quickjs)
134 | ```
135 |
136 | 然后我们先通过"Hello World"的输出示例来完成整个项目的初始化,代码如下:
137 |
138 | ```c++
139 | #include "quickjs/quickjs.h"
140 |
141 | int main() {
142 | JSRuntime *rt = JS_NewRuntime();
143 | JSContext *ctx = JS_NewContext(rt);
144 | JSValue value = JS_Eval(ctx, "'Hello World!'", 12, "", 0);
145 | printf("%s\n", JS_ToCString(ctx, value)); // Hello World!
146 | JS_FreeContext(ctx);
147 | JS_FreeRuntime(rt);
148 | return 0;
149 | }
150 | ```
151 |
152 | 现在,对于沙盒内部的 JavaScript 代码而言,我们将会暴露出一个名为 crypto 的全局函数,此函数会对传入的字符串进行 MD5 的相关加密,并且返回给外部环境,其内部 JavaScript 代码如下所示:
153 |
154 | ```javascript
155 | const MD5_SALT_KEY = 'md5 salt key';
156 | function md5(str) {
157 | // MD5的相关算法实现
158 | // 此处逻辑你可以引入npm上的相关库
159 | // 然后使用Webpack/Parcel等工具进行编译
160 | }
161 |
162 | function crypto(str) {
163 | str = `${str}${MD5_SALT_KEY}`;
164 | return md5(str);
165 | }
166 | ```
167 |
168 | QuickJS 要调用对应的内部全局 crypto 函数很简单,我们使用 JS_Eval 即可完成:
169 |
170 | ```c++
171 | #include
172 | #include "quickjs/quickjs.h"
173 |
174 | const char* JS_CODE = "const MD5_SALT_KEY = 'md5 salt key';\n"
175 | "function md5(str) { return str; }\n"
176 | "function crypto(str) {\n"
177 | " return md5(`${str} ${MD5_SALT_KEY}`);\n"
178 | "};";
179 |
180 | int main() {
181 | JSRuntime *rt = JS_NewRuntime();
182 | JSContext *ctx = JS_NewContext(rt);
183 | JS_Eval(ctx, JS_CODE, strlen(JS_CODE), "", 0);
184 | JSValue value = JS_Eval(ctx, "crypto('data')", 14, "", 0);
185 |
186 | printf("%s\n", JS_ToCString(ctx, value)); // data md5 salt key
187 | JS_FreeContext(ctx);
188 | JS_FreeRuntime(rt);
189 | return 0;
190 | }
191 | ```
192 |
193 | 运行我们的代码,我们可以看到,当调用执行结束后其正确的输出了相关内容。接下来我们对目前的实现加入部分 Emscripten 的胶水代码并进行 WebAssembly 的编译,从而使得我们能从 Web 端或 NodeJS 进行相关的执行,调整后的 CMake 如下所示:
194 |
195 | ```cmake
196 | cmake_minimum_required(VERSION 3.15)
197 | project(sample C)
198 |
199 | set(CMAKE_C_STANDARD 11)
200 | set(CMAKE_EXECUTABLE_SUFFIX ".html") # 编译生成.html
201 |
202 | include_directories(${PROJECT_SOURCE_DIR}/vendor)
203 | add_subdirectory(${PROJECT_SOURCE_DIR}/vendor/quickjs)
204 |
205 | add_executable(sample main.c)
206 |
207 | set_target_properties(sample PROPERTIES LINK_FLAGS "\
208 | -s EXIT_RUNTIME=0 \
209 | -s EXPORTED_FUNCTIONS=\"['_init', '_eval', '_dispose']\"
210 | ")
211 |
212 | target_link_libraries(sample quickjs)
213 | ```
214 |
215 | 接着,我们调整我们的 C++部分代码如下:
216 |
217 | ```c++
218 | #include
219 | #include "quickjs/quickjs.h"
220 |
221 | static JSRuntime *rt;
222 | static JSContext *ctx;
223 |
224 | const char* JS_CODE = "const MD5_SALT_KEY = 'md5 salt key';\n"
225 | "function md5(str) { return str; }\n"
226 | "function crypto(str) {\n"
227 | " return md5(`${str} ${MD5_SALT_KEY}`);\n"
228 | "};";
229 |
230 | int init(){
231 | rt = JS_NewRuntime();
232 | ctx = JS_NewContext(rt);
233 | JS_Eval(ctx, JS_CODE, strlen(JS_CODE), "", 0);
234 | return 0;
235 | }
236 |
237 | int eval(const char *str, int len, char *output){
238 | JSValue value = JS_Eval(ctx, str, len, "", 0);
239 | const char *retstr = JS_ToCString(ctx, value);
240 | memcpy(output, retstr, strlen(retstr));
241 | JS_FreeValue(ctx, value);
242 | return 0;
243 | }
244 |
245 | int dispose(){
246 | JS_FreeContext(ctx);
247 | JS_FreeRuntime(rt);
248 | return 0;
249 | }
250 | ```
251 |
252 | 最后我们进行相关编译,然后使用如下代码进行相关的调用:
253 |
254 | ```javascript
255 | const datastr = "data";
256 | const buffer = intArrayFromString(datastr);
257 | const ptr = allocate(buffer, 'i8', ALLOC_NORMAL);
258 |
259 | const output = Module._malloc(1024);
260 | Module._init();
261 | Module._eval(ptr, buffer.length, output);
262 | Module._dispose();
263 | console.log(UTF8ToString(output)); // output: data md5 salt key
264 | ```
265 |
266 | 根据上一章的实践我们知道,由于我们的内部 JavaScript 代码是以字符串的方式直接进行呈现和编译的,因此如果我们对 Emscripten 编译生成 WASM 文件进行二进制查看的话,我们仍然能还原出我们的实际相关实现。同时根据我们的流程来看,我们首先需要将其 JavaScript 内容编译为 Opcode,然后再进行相关的嵌入才较为合理。要想得到 QuickJS 的 Opcode ByteStream 比较简单,直接通过 qjc 即可获得:
267 |
268 | ```shell
269 | > cd quickjs
270 | > make all
271 | > ./qjsc -c index.js
272 | > cat out.c
273 | ```
274 |
275 | 通过查找 out.c 我们可以看到 qjsc 为我们生成了如下的代码:
276 |
277 | ```c++
278 | /* File generated automatically by the QuickJS compiler. */
279 |
280 | #include
281 |
282 | const uint32_t qjsc_index_size = 209;
283 |
284 | const uint8_t qjsc_index[209] = {
285 | 0x02, 0x06, 0x18, 0x4d, 0x44, 0x35, 0x5f, 0x53,
286 | 0x41, 0x4c, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x06,
287 | 0x6d, 0x64, 0x35, 0x0c, 0x63, 0x72, 0x79, 0x70,
288 | // ......
289 | };
290 | ```
291 |
292 | 其中的 qjsc_index 数组就是我们 JavaScript 代码对应的 Opcode ByteStream。接着我们修改我们的代码,然后调整执行的方法,其代码如下:
293 |
294 | ```c++
295 | const uint8_t JS_CODE[209] = {
296 | 0x02, 0x06, 0x18, 0x4d, 0x44, 0x35, 0x5f, 0x53,
297 | 0x41, 0x4c, 0x54, 0x5f, 0x4b, 0x45, 0x59, 0x06,
298 | 0x6d, 0x64, 0x35, 0x0c, 0x63, 0x72, 0x79, 0x70,
299 | // ......
300 | };
301 |
302 | int init(){
303 | rt = JS_NewRuntime();
304 | ctx = JS_NewContext(rt);
305 | JS_Eval(ctx, JS_CODE, 209, "", JS_EVAL_TYPE_MODULE);
306 | return 0;
307 | }
308 | ```
309 |
310 | 最后我们同样进行整体编译,然后尝试相同调用,可以看到其执行正常。同时查看 WASM 文件我们已经无法顺利查看到内部执行 JavaScript 代码内容了(已变为 QuickJS 的 Opcode ByteStream)。
311 |
312 | ### 总结
313 |
314 | 在本章我们较为详细的介绍了前端加密及代码保护的困境,以及如何使用 WebAssembly 并结合 QuickJS 打造高性能安全沙盒的相关介绍,关于 QuickJS 的更进一步的内容可以参考其官方网站。在下一章中,我们将使用 WebAssembly 结合 WebGL 来进行一些图形相关的实践,同时对比其在此领域相比 JavaScript 的优劣势。
--------------------------------------------------------------------------------
/WebAssembly专栏/images/asmjs.benchmark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ErosZy/md/f2e55a54d030737f78fa5ee11c4e4db10fe7eeb2/WebAssembly专栏/images/asmjs.benchmark.png
--------------------------------------------------------------------------------
/WebAssembly专栏/images/asmjs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ErosZy/md/f2e55a54d030737f78fa5ee11c4e4db10fe7eeb2/WebAssembly专栏/images/asmjs.png
--------------------------------------------------------------------------------
/WebAssembly专栏/images/benchmarks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ErosZy/md/f2e55a54d030737f78fa5ee11c4e4db10fe7eeb2/WebAssembly专栏/images/benchmarks.png
--------------------------------------------------------------------------------
/WebAssembly专栏/images/c2j.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ErosZy/md/f2e55a54d030737f78fa5ee11c4e4db10fe7eeb2/WebAssembly专栏/images/c2j.png
--------------------------------------------------------------------------------
/WebAssembly专栏/images/debugging.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ErosZy/md/f2e55a54d030737f78fa5ee11c4e4db10fe7eeb2/WebAssembly专栏/images/debugging.png
--------------------------------------------------------------------------------
/WebAssembly专栏/images/emcc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ErosZy/md/f2e55a54d030737f78fa5ee11c4e4db10fe7eeb2/WebAssembly专栏/images/emcc.png
--------------------------------------------------------------------------------
/WebAssembly专栏/images/emscripten.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ErosZy/md/f2e55a54d030737f78fa5ee11c4e4db10fe7eeb2/WebAssembly专栏/images/emscripten.png
--------------------------------------------------------------------------------
/WebAssembly专栏/images/nacl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ErosZy/md/f2e55a54d030737f78fa5ee11c4e4db10fe7eeb2/WebAssembly专栏/images/nacl.png
--------------------------------------------------------------------------------
/WebAssembly专栏/images/opcode.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ErosZy/md/f2e55a54d030737f78fa5ee11c4e4db10fe7eeb2/WebAssembly专栏/images/opcode.webp
--------------------------------------------------------------------------------
/WebAssembly专栏/images/wasm.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ErosZy/md/f2e55a54d030737f78fa5ee11c4e4db10fe7eeb2/WebAssembly专栏/images/wasm.webp
--------------------------------------------------------------------------------
/深入浅出V8优化/1. Smi和HeapNumber.md:
--------------------------------------------------------------------------------
1 | Smi和HeapNumber
2 | ---
3 |
4 | > 本文所述实现仅基于V8 2.0.2.1版本,若后续版本有差异请以后续版本为主
5 |
6 | 在标准中,ECMAScript的Number类型都是Boxed Object(尽管有字面量和Number构造函数之分),如果完全按照规范无脑实现,那么类似于下面的伪代码:
7 |
8 | ```c++
9 | class Number: public Object {
10 | double value;
11 | }
12 | ```
13 |
14 | 但这样的实现会存在一些问题:
15 | 1. 所有Number对象均需要分配在堆上,持有一个Number对象需要4字节 + 8字节的大小(32位);
16 | 2. 分配更多的对象,浪费了更多的CPU缓存,对应的存取性能相比纯数值的存取更慢;
17 |
18 | 但现实的开发过程中,我们的程序会被V8基于两个假设进行优化:
19 | 1. 更常使用较小数值的整数,而不是更大的整数值或浮点数;
20 | 2. 我们访问过的数值会在后续频繁地存取,程序的局部性特点很强;
21 |
22 | 因此V8针对Number类型进行了拆分:Smi 和 HeapNumber
23 |
24 | ```javascript
25 | // 在32位系统下,Smi的取值范围为 -2^30 ~ 2**30 - 1
26 | -Infinity // HeapNumber
27 | -(2**30)-1 // HeapNumber
28 | -(2**30) // Smi
29 | -42 // Smi
30 | -0 // HeapNumber
31 | 0 // Smi
32 | 4.2 // HeapNumber
33 | 42 // Smi
34 | 2**30-1 // Smi
35 | 2**30 // HeapNumber
36 | Infinity // HeapNumber
37 | NaN // HeapNumber
38 | ```
39 |
40 |
41 |

42 |
43 |
44 | ### Smi如何实现的
45 |
46 | Smi的基本想法很简单,即:指针是合法的整形。因此我们可以通过指针转整形来存储一定范围的整数值。
47 |
48 |
49 |

50 |
51 |
52 | 如果我们通过C++简要来实现相关逻辑则代码如下:
53 |
54 | ```c++
55 | #include
56 |
57 | class Object {
58 | // ......
59 | }
60 |
61 | class Smi: public Object {
62 | public:
63 | inline int32_t value(){
64 | return static_cast(reinterpret_cast(this)) >> 1;
65 | }
66 |
67 | static Smi* fromInt(int32_t value){
68 | intptr_t tagged_value = (static_cast(value) << 1) | 0;
69 | return reinterpret_cast(tagged_value);
70 | }
71 |
72 | static bool isSmi(Smi* ptr){
73 | return (reinterpret_cast(ptr) & 1) == 0;
74 | }
75 | };
76 |
77 | int main() {
78 | int32_t value = 23333;
79 | Smi* smi = Smi::fromInt(value);
80 | std::cout << "is smi: " << Smi::isSmi(smi) << std::endl;
81 | std::cout << "is equal: " << (value == smi->value()) << std::endl;
82 | return 0;
83 | }
84 | ```
85 |
86 | ### HeapNumber如何实现的
87 |
88 | 在V8中,HeapNumber的继承关系如图所示:
89 |
90 | ```c++
91 | class Object {
92 | public:
93 | static const int32_t kHeaderSize = 0;
94 | }
95 |
96 | class HeapObject: public Object {
97 | public:
98 | static const int32_t kHeaderSize = Object::kHeaderSize + 4; // 必须的指针长度
99 | }
100 |
101 | class HeapNumber: public HeapObject {
102 | public:
103 | static const int32_t kSize = HeapObject::kHeaderSize + 8; // 指针(4) + double(8)
104 | }
105 | ```
106 |
107 | 其一个HeapNumber对象自身占用12字节内存(在32位系统中),由于GC的原因(Alloc的返回对象均为Object指针),HeapNumber的内存布局直接采用偏移量处理的方式(这里又涉及一个Trick):
108 |
109 |
110 |

111 |
112 |
113 | 因此HeapNumber的存取需要自行手工的对内存偏移进行处理,简要实现的代码如下:
114 |
115 | ```c++
116 | #include
117 |
118 | const int32_t kHeapObjectTag = 1;
119 |
120 | class Object {
121 | public:
122 | static const int32_t kHeaderSize = 0;
123 |
124 | bool isHeapObject() {
125 | return (reinterpret_cast(this) & 3) == kHeapObjectTag;
126 | }
127 | };
128 |
129 | class HeapObject:public Object {
130 | public:
131 | static const int32_t kHeaderSize = Object::kHeaderSize + 4;
132 |
133 | void set_map(void* map) {
134 | uint8_t* ptr = reinterpret_cast(this) - kHeapObjectTag;
135 | *reinterpret_cast(ptr) = reinterpret_cast(map);
136 | }
137 |
138 | static HeapObject* cast(Object* ptr){
139 | return reinterpret_cast(ptr);
140 | }
141 | };
142 |
143 | class HeapNumber:public HeapObject {
144 | public:
145 | static const int32_t kSize = HeapObject::kHeaderSize + 8;
146 | static const int32_t kValueOffset = HeapObject::kHeaderSize;
147 |
148 | void set_value(double value) {
149 | uint8_t *ptr = reinterpret_cast(this) + kValueOffset - kHeapObjectTag;
150 | *reinterpret_cast(ptr) = value;
151 | }
152 |
153 | double value() {
154 | uint8_t *ptr = reinterpret_cast(this) + kValueOffset - kHeapObjectTag;
155 | return *reinterpret_cast(ptr);
156 | }
157 |
158 | static HeapNumber* cast(Object* ptr) {
159 | return reinterpret_cast(ptr);
160 | }
161 | };
162 |
163 | Object* AllocateRaw(size_t size) {
164 | return reinterpret_cast