【发布时间】:2021-12-15 18:00:15
【问题描述】:
我试图理解 WebAssembly 内存模型,特别是从以下角度:在 WebAssembly 实例之间共享线性内存时我会面临什么样的风险?所有 C/C++ => wasm 教程给我们的基本内存模型如下(堆栈以__heap_base - 1 开头并向下增长):
+-----------------------------------------------+
| ? | static data | stack | heap |
+-----------------------------------------------+
^ ^ ^ ^ ^
| | | | |
0 __global_base __data_end __heap_base MAX_MEMORY
但以下事实让我感到惊讶。来自https://webassembly.org/docs/security/:
具有不明确静态范围的局部变量(例如,由地址运算符使用,或者是结构类型并按值返回)在编译时存储在线性内存中单独的用户可寻址堆栈中。这是一个独立的内存区域,具有固定的最大大小,默认初始化为零。
来自https://github.com/WebAssembly/design/blob/main/Rationale.md#locals:
C/C++ 可以获取函数的本地值的地址并将此指针传递给被调用者或其他线程。由于 WebAssembly 的局部变量位于地址空间之外,因此 C/C++ 编译器通过在线性内存中创建单独的堆栈数据结构来实现地址获取变量。这个堆栈有时被称为“别名”堆栈,因为它用于可能被指针指向的变量。
换句话说,从__heap_base - 1 到__data_end 定义的堆栈是C/C++ 编译模块的实现工件。 “WASM 堆栈”位于线性内存之外。碰巧的是,当您获取本地地址(例如)时,编译器将其存储在“别名堆栈”中,因此需要获取地址。
在使用共享内存的情况下,这种行为是否会引发新的非常危险的数据竞争?
想象这样一段代码:
int calculation(int param1, int param2)
{
if (param1 == param2 * 2)
++param1;
else
++param2;
return param1 / 3 + param2;
}
这里,calculation 是线程安全的。但是,如果我用这种等效形式替换 calculation:
int calculation(int param1, int param2)
{
int* param = param1 == param2 * 2 ? ¶m1 : ¶m2;
++*param;
return param1 / 3 + param2;
}
根据编译器的输出,calculation 可能不再是线程安全的,以防param1 和/或param2 存储在别名堆栈上,该别名堆栈位于线性内存中,可以与其他内存共享--features=atomics,bulk-memory --shared-memory 标志启用共享内存的实例。
那么,在哪些具体情况下编译器可以决定将局部变量存储在别名堆栈上?
编辑:我做了一些测试来验证,我想知道我是否正确。我在堆上存储了使用 16 个无符号局部变量的函数的第一个、一半和最后一个局部变量的内存地址,我从 javascript 中将它们打印出来,以及最低存储地址与 @987654335 之间的差异@ 是32*3 bytes + padding,而不是32*16 + padding,这意味着只有内存地址被占用的三个变量存储在别名堆栈中。当然,这些测试不是线程安全的,因为我将本地人的地址存储在函数之外,但它说明了一点:如果在可重入函数上,我暂时获取本地人的地址来实现方便,并且由于其复杂性,编译器不确定我要做什么,它最终可以决定将本地存储在堆栈上而不是更改其实现,从而使函数线程不安全。
【问题讨论】:
-
我对 WASM 了解不多,但我认为 WASM 堆栈毕竟是一个堆栈。对
calculation的每次调用都会为param1和param2分配自己的内存,因此无论是来自不同线程还是递归调用,都应该获得独立的内存用于其param1和param2。 -
我不知道 Web Assembly 内存是如何工作的,但您当然可以假设他们不是白痴,并且从多个线程调用相同的简单函数不会导致数据竞争。只需尝试通过共享线性内存来更好地了解它们的含义。
-
我还假设“别名堆栈”及其堆栈指针是线程本地的,即每个线程都有自己的。这样就解决了线程安全方面的问题,并且线程内的递归调用仍然有效,因为它是一个堆栈。
-
@NateEldredge 据我所知,WebAssembly 还不支持多线程,所以我无法从 C++ 启动线程。必须在 Javascript 级别实现并行性:我创建一个共享线性内存并启动不同的 WebWorker,每个 WebWorker 都有自己的 wasm 实例,但都共享相同的导入内存。别名堆栈将在所有实例之间共享,没有一个实例可以知道它与其他多少兄弟共享内存。
-
@ALX23z 我编辑了我的问题以进一步澄清这一点。
标签: c++ multithreading webassembly