WebAssembly内存结构学习记录

2024-09-05 05:52

本文主要是介绍WebAssembly内存结构学习记录,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

参考:

大文件上传深入研究:https://juejin.cn/post/6870837414852886542

Worker+Wasm切片上传:https://juejin.cn/post/7221003401996091429

Wasm实现MD5文件编码:https://juejin.cn/post/7319541565318398003

SharedArrayBuffer与幽灵漏洞:https://juejin.cn/post/7300106933745909771

Wasm多线程:https://42yeah.github.io/webassembly/2019/01/14/wasm-threading.html

WebAssembly学习记录:https://blog.csdn.net/qq_37464878/article/details/138202363?spm=1001.2014.3001.5501

阿里巴巴基于Wasm的WebC平台技术会议:https://www.bilibili.com/video/BV1vy411q7Lm/?spm_id_from=333.999.0.0

1.内存结构

1.1 JavaScript内存结构

Text: 代码段

Heap: 堆区

Stack: 栈区

1.2 C内存结构

Text: 代码段

Data: 已初始化全局变量数据段(int a = 1;

Bss: 未初始化全局变量数据段(int a;

Heap: 堆区

Stack: 栈区

在这里插入图片描述

1.3 WASM内存结构

Local:

  • 存储: 函数作用域局部变量。

  • 解释: Wasm汇编语言可访问,C语言中无法直接访问。

  • 举例: 下图的 add函数是C语言提供,里面用local声明了几个局部变量。

Global:

  • 存储: 全局变量。

  • 解释: 引入WebAssembly时从JavaScript注入全局变量。Wasm汇编语言可访问,C语言中无法直接访问。

  • 举例: 下图的 import "wasi_snapshot_preview1"等等表示需要从JavaScript导入全局变量与之对应。fd_xxx等内容是引入了头文件 #include <iostream>导致必须配置的。

在这里插入图片描述

Linear Memory:

  • 存储: 是缓冲区ArrayBuffer或SharedArrayBuffer。
  • 解释: 是线性内存区域,内存单元地址连续。
    • Stack:
      • 存储: 操作栈。
      • 解释: 存储函数局部变量。局部变量和Local的局部变量一一对应,地址是Local的逻辑地址。
      • 原因: Local地址不希望被访问,没有开放C语言访问权限,因此创建Stack区域。
    • Data:
      • 存储: 全局变量。
      • 解释: c语言定义的全局变量存储在Data。
      • 举例: add函数是c语言提供,实现 a + b + 全局变量效果。断点上面两行表示从线性内存1024位置取出值放入栈顶。右侧可以看到栈顶的值已经变成全局变量123。

Table:

  • 存储: 函数的指针
  • 解释: func表示将函数放入table表中
  • 举例: 下图的代码新建了一个函数的指针的表 (table $__indirect_function_table (;0;) (export "__indirect_function_table") 2 2 funcref)

在这里插入图片描述

注:下图提到的“StackPointer”和“MemoryBase”是LLVM编译器的产物,如果使用EMSCRIPTEN不一定有这两个内容

在这里插入图片描述

2.内存交换

2.1 JavaScript和Wasm的内存交换

回顾&类比JavaScript和Worker交换内存

拷贝

ArrayBuffer+MessageChannel

移动

ArrayBuffer+MessageChannel+transferable(postMessage配置)

共享

SharedArrayBuffer+Atomics(上锁)

JavaScript访问Wasm内存

加载完WebAssembly后Wasm默认导出自己的线性内存,JavaScript通过memory对象访问内存 (最大大概16MB)

一般是JavaScript通过调用Wasm函数返回指针或变量配合memory对象访问内存。

c++

// 导出:申请内存函数
// JavaScript可以申请Wasm内存
int* createIntArray(int length) {return new int[length];
}// 导出:写入内存函数
// JavaScript可以写入Wasm内存
void writeToArray(int* arr, int index, int value) {arr[index] = value;
} // 导出:快排函数
// 基于上述操作JavaScript可以实现和Wasm交互的快速排序
int* qSort(int* arr, int length) {		std::sort(arr, arr + length);return arr;
}

JavaScript

// 加载WebAssembly
const importObject = {wasi_snapshot_preview1: {proc_exit: function (code) {console.log(`Process exited with code ${code}`);},},
};
const result = await WebAssembly.instantiateStreaming(fetch(fileName), importObject);const data = new Int32Array(result.instance.exports.memory.buffer,// wasm函数返回的指针pointer,length
);

Wasm访问Javascript内存

ArrayBuffer:

不能访问。JavaScript传来的数据统一被拷贝到ArrayBuffer缓冲区中,进入Wasm的内存区域。

在这里插入图片描述

SharedArrayBuffer:

不能访问。但是可以利用SharedArrayBuffer共享缓冲区,JavaScript调用Wasm导出的函数时把SharedArrayBuffer传给Wasm。此时JavaScript和Wasm可以同时读写同一个缓冲区。

注:SharedArrayBuffer目前默认不支持使用,需要特殊配置响应头,因为共享缓冲区可能导致安全问题

2.2 SharedArrayBuffer安全性

2.2.1 幽灵漏洞

内存工作原理

  • 结构: CPU——高速缓存——内存

预测执行

  • 预测: Intel芯片在2000年左右对CPU执行速度进行优化。对于条件语句,CPU会预测执行结果,会直接忽视条件尝试执行某个分支,并忽视数组越界,内存权限等限制条件,原因是条件计算完后会统一判断。等到条件计算完毕后再决定是否回滚刚才的操作。

  • 回滚: CPU的执行结果会尽可能存入缓存,但是回滚不会消除缓存中的执行结果。

旁信道攻击

  • 遍历: 对数据进行暴力破解,根据程序响应时间来判断破解是否正确。
  • 举例: 比如破解密码,1234567,猜测密码为199999和猜测密码为99999程序反应时间有细微不同,因为1是正确的,一旦访问到会被尽可能放入高速缓存。这样程序响应这两个密码都是错误的时候,199999的响应速度会稍微快一点。

幽灵漏洞举例:

利用预测执行和旁信道攻击来访问没有权限的内存,利用系统的响应速度来判断是否获取成功,获取成功可以直接去高速缓存中获取目标值

2.2.2 高精度计时器

背景

幽灵漏洞的攻击思路是根据读取数据的时间差来判断是否访问到目标内存。但是很早之前浏览器已经降低了 setTimeoutperformance.now可以获取的时间戳的精度,这样来避免攻击者获取到微小的CPU时间差。

制作高精度计时器

创建worker,和主线程共享sharedArrayBuffer区域。worker中通过for循环每次给sharedArrayBuffer的某个区域递增微量的值。

主线程中进行幽灵漏洞攻击,在访问某个内存值前后分别获取sharedArrayBuffer指定位置的值,用差值来表示访问时间,获取高精度时间差。

注意:这个操作非sharedArrayBuffer不可,如果主线程和worker没有共享内存区域,那么必须通过postMessage通信,时间有损耗

高精度计时器举例

Worker

self.onmessage = (event) => {const view = new Int8Array(event?.data)let time = 0while(true) {view[0] = timetime += 0.00001}
}

Main

const sharedArrayBuffer = new SharedArrayBuffer(1)
const worker = new Worker('你的worker')
worker.postMessage(sharedArrayBuffer)setTimeout(() => {const startTime = sharedArrayBuffer[0] // 你的幽灵攻击代码 0.0003const endTime = sharedArrayBuffer[0]// 利用高精度时间来分析你的幽灵攻击过程
}, 
// 等待Worker启动的时间
time) 
2.2.3 安全限制

在ES6开始浏览器就支持SharedArrayBuffer,但是由于上述安全问题会默认禁用该能力。想启用该能力必须配置HTML页面的响应头。

跨源嵌入策略:COEP

Cross-Origin-Embedder-Policy: require-corp

限制嵌入的非同源请求。例如嵌入的资源如果没有配置CORS或CORP跨域,那么请求直接会被拒绝

跨源开放策略:COOP

Cross-Origin-Opener-Policy: same-origin

限制打开的非同源资源交互。例如window.open新打开的页面或使用的worker不允许和原页面通信,除非同源。

副作用举例:

如果background背景图使用了非同源资源,配置上述响应头会导致资源无法加载。

2.3 内存交换问题

拷贝限制:

  • 解释: JavaScript不能直接向wasm传递引用或指针,需要拷贝数据。

  • 举例:

    • 场景: 在处理文件的场景下,JavaScript向wasm传递数据一般通过缓冲区(ArrayBuffer)的视图(TypedArray)进行传递。
    • 内存交换: 缓冲区和视图都不会把缓冲区内容读入内存,但是传递给wasm处理时必须将数据读入内存(拷贝过程)。因为wasm的隔离性,wasm只能读取自己的内存。
    • 副作用: 但是好消息是wasm的内存建立在缓冲区,读写速度不慢,也不占用浏览器内存。

大小限制:

  • 解释: wasm的内存有限制,容易溢栈。
  • 举例:
    • 场景1: 上面1.3中给出的图例中展示了emscripten编译的wasm,C语言和JavaScript都是默认操作,内存上限为16MB。已经达到了上限。
    • 场景2: 在npm发布的一些基于wasm的包(比如hash-wasm),wasm的内存上限可能是64KB,仅允许一次扩容,到128KB。

安全限制:

  • 解释: wasm的内存一般不通过SharedArrayBuffer交换,有安全问题,使用需要配置html页面的响应头。

3.多线程能力

3.1 Wasm多线程

在wasm内部启用多线程需要额外配置编译时的emscripten指令,并且浏览器必须配置开启SharedArrayBuffer。原因是底层依托于浏览器环境,多线程还是需要借助Worker实现。

#include <iostream>
#include <thread>void count(void) {for (int i = 1; i <= 3; i++) {std::cout << "Counting " << i << std::endl;}
}int main(void) {for (int i = 0; i < 3; i++) {std::thread worker(count);worker.detach();}return 0;
}

3.2 Worker+Wasm多线程

不启用SharedArrayBuffer,通过JavaScript实现多线程。每个Web Worker中都引入Wasm进行计算,替换Worker中的JavaScript计算。

Worker+Wasm对文件MD5编码:

下面是一个worker的代码。worker引入hash-wasm,里面引用wasm进行md5运算。

hash-wasm本身不支持在wasm中开启多线程,因此借助worker传入大文件切片来让wasm做编码实现多线程能力。

import SparkMD5 from 'spark-md5'
import { createMD5 } from 'hash-wasm'/** @constant {string} WebAssembly编码模式 */
const WASM = 'wasm'
/** @constant {string} Javascript编码模式 */
const ORIGIN = 'origin'// eslint-disable-next-line
self.onmessage = (event) => {
encodeFile(event?.data?.mode, event?.data?.buffer)
}/*** @function encodeByWasm* @param {ArrayBuffer} 文件切片内容* @returns {Promise} 文件切片编码结果* @description 通过WebAssembly对文件MD5编码*/
const encodeByWasm = async (arrayBuffer) => {return createMD5().then((encoder) => {// 创建:操作缓冲区的视图const buffer = arrayBuffer instanceof ArrayBuffer ? arrayBuffer : new ArrayBuffer()const view = new Uint8Array(buffer)// 编码:依据当前片段编码encoder?.update?.(view)// 保存:当前片段的编码结果const state = encoder?.save()return state}).catch((err) => {console.warn('EncodeByWasm Error', err)return null})
}/*** @function encodeByOrigin* @param {ArrayBuffer} 文件切片内容* @returns {Promise} 文件切片编码结果* @description 通过Javascript对文件MD5编码*/
const encodeByOrigin = (arrayBuffer) => {}/*** @param {string} mode 编码模式* @param {ArrayBuffer} arrayBuffer 文件切片*/
const encodeFile = (mode, arrayBuffer) => {const codeFunction =mode === WASM ? encodeByWasm : mode === ORIGIN ? encodeByOrigin : () => Promise.resolve()codeFunction(arrayBuffer).then((result) => {// eslint-disable-next-lineself.postMessage(result)})
}

4.浏览器上运行环境

4.1 运行环境综述

在这里插入图片描述

上述的效果是在浏览器端从不同层面模拟一个本不属于浏览器的运行环境。

模拟器类

解释:

只模拟硬件,模拟CPU,寄存器,磁盘,网卡等功能。在该环境上运行的内容,必须是操作系统镜像。

举例:

运行windows2000镜像。https://bellard.org/jslinux/vm.html?url=https://bellard.org/jslinux/win2k.cfg&mem=192&graphic=1&w=1024&h=768

系统接口类

解释:

在模拟了硬件的基础上再次模拟了操作系统,具有文件系统,进程调度,网络管理等操作系统功能。在该环境上运行的内容,必须实现系统接口(调用操作系统功能的函数)。

举例:

运行底层的高级程序语言,C/C++等。https://browsix.org/

WebContainer类

解释:

模拟了硬件,操作系统和系统接口。在该环境上运行的内容,可以是偏高层的高级程序设计语言,不需要实现系统接口和操作系统交互。

举例:

运行JavaScript,Python等等。https://stackblitz.com/

BrowserNode类

解释:

是更高层的模拟。是WebContainer的一个具体方向,只能运行NodeJS相关代码。

举例:

专用的NodeJS运行环境。https://stackblitz.com/

这篇关于WebAssembly内存结构学习记录的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1138128

相关文章

Vite 打包目录结构自定义配置小结

《Vite打包目录结构自定义配置小结》在Vite工程开发中,默认打包后的dist目录资源常集中在asset目录下,不利于资源管理,本文基于Rollup配置原理,本文就来介绍一下通过Vite配置自定义... 目录一、实现原理二、具体配置步骤1. 基础配置文件2. 配置说明(1)js 资源分离(2)非 JS 资

Redis实现高效内存管理的示例代码

《Redis实现高效内存管理的示例代码》Redis内存管理是其核心功能之一,为了高效地利用内存,Redis采用了多种技术和策略,如优化的数据结构、内存分配策略、内存回收、数据压缩等,下面就来详细的介绍... 目录1. 内存分配策略jemalloc 的使用2. 数据压缩和编码ziplist示例代码3. 优化的

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

Python内存优化的实战技巧分享

《Python内存优化的实战技巧分享》Python作为一门解释型语言,虽然在开发效率上有着显著优势,但在执行效率方面往往被诟病,然而,通过合理的内存优化策略,我们可以让Python程序的运行速度提升3... 目录前言python内存管理机制引用计数机制垃圾回收机制内存泄漏的常见原因1. 循环引用2. 全局变

Unity新手入门学习殿堂级知识详细讲解(图文)

《Unity新手入门学习殿堂级知识详细讲解(图文)》Unity是一款跨平台游戏引擎,支持2D/3D及VR/AR开发,核心功能模块包括图形、音频、物理等,通过可视化编辑器与脚本扩展实现开发,项目结构含A... 目录入门概述什么是 UnityUnity引擎基础认知编辑器核心操作Unity 编辑器项目模式分类工程

Java集合中的链表与结构详解

《Java集合中的链表与结构详解》链表是一种物理存储结构上非连续的存储结构,数据元素的逻辑顺序的通过链表中的引用链接次序实现,文章对比ArrayList与LinkedList的结构差异,详细讲解了链表... 目录一、链表概念与结构二、当向单链表的实现2.1 准备工作2.2 初始化链表2.3 打印数据、链表长

Python学习笔记之getattr和hasattr用法示例详解

《Python学习笔记之getattr和hasattr用法示例详解》在Python中,hasattr()、getattr()和setattr()是一组内置函数,用于对对象的属性进行操作和查询,这篇文章... 目录1.getattr用法详解1.1 基本作用1.2 示例1.3 原理2.hasattr用法详解2.

创建springBoot模块没有目录结构的解决方案

《创建springBoot模块没有目录结构的解决方案》2023版IntelliJIDEA创建模块时可能出现目录结构识别错误,导致文件显示异常,解决方法为选择模块后点击确认,重新校准项目结构设置,确保源... 目录创建spChina编程ringBoot模块没有目录结构解决方案总结创建springBoot模块没有目录

基于Spring Boot 的小区人脸识别与出入记录管理系统功能

《基于SpringBoot的小区人脸识别与出入记录管理系统功能》文章介绍基于SpringBoot框架与百度AI人脸识别API的小区出入管理系统,实现自动识别、记录及查询功能,涵盖技术选型、数据模型... 目录系统功能概述技术栈选择核心依赖配置数据模型设计出入记录实体类出入记录查询表单出入记录 VO 类(用于

SpringBoot利用树形结构优化查询速度

《SpringBoot利用树形结构优化查询速度》这篇文章主要为大家详细介绍了SpringBoot利用树形结构优化查询速度,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一个真实的性能灾难传统方案为什么这么慢N+1查询灾难性能测试数据对比核心解决方案:一次查询 + O(n)算法解决