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

相关文章

将sqlserver数据迁移到mysql的详细步骤记录

《将sqlserver数据迁移到mysql的详细步骤记录》:本文主要介绍将SQLServer数据迁移到MySQL的步骤,包括导出数据、转换数据格式和导入数据,通过示例和工具说明,帮助大家顺利完成... 目录前言一、导出SQL Server 数据二、转换数据格式为mysql兼容格式三、导入数据到MySQL数据

Java循环创建对象内存溢出的解决方法

《Java循环创建对象内存溢出的解决方法》在Java中,如果在循环中不当地创建大量对象而不及时释放内存,很容易导致内存溢出(OutOfMemoryError),所以本文给大家介绍了Java循环创建对象... 目录问题1. 解决方案2. 示例代码2.1 原始版本(可能导致内存溢出)2.2 修改后的版本问题在

关于rpc长连接与短连接的思考记录

《关于rpc长连接与短连接的思考记录》文章总结了RPC项目中长连接和短连接的处理方式,包括RPC和HTTP的长连接与短连接的区别、TCP的保活机制、客户端与服务器的连接模式及其利弊分析,文章强调了在实... 目录rpc项目中的长连接与短连接的思考什么是rpc项目中的长连接和短连接与tcp和http的长连接短

大数据小内存排序问题如何巧妙解决

《大数据小内存排序问题如何巧妙解决》文章介绍了大数据小内存排序的三种方法:数据库排序、分治法和位图法,数据库排序简单但速度慢,对设备要求高;分治法高效但实现复杂;位图法可读性差,但存储空间受限... 目录三种方法:方法概要数据库排序(http://www.chinasem.cn对数据库设备要求较高)分治法(常

Redis多种内存淘汰策略及配置技巧分享

《Redis多种内存淘汰策略及配置技巧分享》本文介绍了Redis内存满时的淘汰机制,包括内存淘汰机制的概念,Redis提供的8种淘汰策略(如noeviction、volatile-lru等)及其适用场... 目录前言一、什么是 Redis 的内存淘汰机制?二、Redis 内存淘汰策略1. pythonnoe

Oracle查询优化之高效实现仅查询前10条记录的方法与实践

《Oracle查询优化之高效实现仅查询前10条记录的方法与实践》:本文主要介绍Oracle查询优化之高效实现仅查询前10条记录的相关资料,包括使用ROWNUM、ROW_NUMBER()函数、FET... 目录1. 使用 ROWNUM 查询2. 使用 ROW_NUMBER() 函数3. 使用 FETCH FI

Java内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

Python MySQL如何通过Binlog获取变更记录恢复数据

《PythonMySQL如何通过Binlog获取变更记录恢复数据》本文介绍了如何使用Python和pymysqlreplication库通过MySQL的二进制日志(Binlog)获取数据库的变更记录... 目录python mysql通过Binlog获取变更记录恢复数据1.安装pymysqlreplicat

Java中switch-case结构的使用方法举例详解

《Java中switch-case结构的使用方法举例详解》:本文主要介绍Java中switch-case结构使用的相关资料,switch-case结构是Java中处理多个分支条件的一种有效方式,它... 目录前言一、switch-case结构的基本语法二、使用示例三、注意事项四、总结前言对于Java初学者

关于Java内存访问重排序的研究

《关于Java内存访问重排序的研究》文章主要介绍了重排序现象及其在多线程编程中的影响,包括内存可见性问题和Java内存模型中对重排序的规则... 目录什么是重排序重排序图解重排序实验as-if-serial语义内存访问重排序与内存可见性内存访问重排序与Java内存模型重排序示意表内存屏障内存屏障示意表Int