我试图理解WebAssembly内存模型,特别是从这样的角度出发:在WebAssembly实例之间共享线性内存时,我面临什么样的风险?所有C/C++ =>提供的基本内存模型如下(堆栈从__heap_base - 1开始,向下增长):
+-----------------------------------------------+
| ? | static data | stack | heap |
+-----------------------------------------------+
^ ^ ^ ^ ^
| | | | |
0 __global_base __data_end __heap_base MAX_MEMORY但以下事实让我感到惊讶。来自https://webassembly.org/docs/security/
具有不明确的静态范围的局部变量(例如,由address-of运算符使用,或类型为struct,并由值返回)在编译时存储在线性内存中的单独的用户可寻址堆栈中。这是一个独立的内存区域,具有固定的最大大小,默认为零初始化。
来自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和/或param2存储在存在于线性内存上的别名堆栈中,则--features=atomics,bulk-memory --shared-memory将不再是线程安全的,如果通过--features=atomics,bulk-memory --shared-memory标志启用共享内存,则可以在其他实例之间共享。
因此,编译器可以在哪种情况下决定将局部变量存储在别名堆栈?上。
编辑:我做了一些测试来验证,我想知道我是否正确。我在堆中存储了使用16个无符号局部变量的函数的第一个局部变量、第二个局部变量和最后一个局部变量的内存地址,然后从javascript中打印出它们,最低存储地址到__heap_base的区别是32*3 bytes + padding,而不是32*16 + padding,这意味着只有内存地址被存储在别名堆栈上的三个变量。当然,这些测试并不是线程安全的,因为我将局部变量的地址存储在函数之外,但它说明了一点:如果在可重入函数上,为了实现方便,我暂时取一个本地地址,而且由于它的复杂性,编译器不确定我要做什么,它最终可以决定将本地存储在堆栈上,而不是改变它的实现,使函数线程变得不安全。
发布于 2021-11-01 07:49:05
在多线程设置中,每个线程都会将自己的堆栈放入共享内存。堆栈指针(它的创建是LLVM createSyntheticSymbols)放置在一个WebAssembly 全局变量中。目前,这些全局标记被用作线程本地存储器。这意味着每个线程都有自己的全局变量。
在WebAssembly实例开始时,主线程将有自己的全局变量指向主线程堆栈到共享内存中。如果在启动时启动另一个线程,则其全局变量将指向共享内存中的另一个位置,该线程的堆栈位于共享内存中。
如果调用方不提供自己的指针,则分配堆栈Emscripten __pthread_create_js。将变量分配到当前堆栈中的工作是通过stackAlloc完成的,其中:
global.get __stack_pointer获取当前线程堆栈指针,减去所需的字节(堆栈向下增长),将其对齐为16个字节,然后将新值记忆回全局。这都是线程安全的,因为全局只能从线程本身访问。
关于指针,是的,编译器将把指针访问的变量放置到显式堆栈中。目前,WebAssembly堆栈并不是“可移动的”,但是有一个提案来实现它。许多实现都使用显式堆栈,以获得对堆栈使用(变量、结构等)的更细粒度的控制。
对于开发人员来说,所有这些“东西”都应该是(RFC 2119)是透明的。意思是,它似乎只是起作用。
基于您的注释: WebAssembly标准目前通过使用原子指令来处理数据竞赛。它们的访问顺序是顺序一致。在多线程情况下,显然内存分配程序必须是线程安全的.不用自己使用显式线程专用堆栈(正如所写的那样,使用全局就足够了),因为堆栈内存仅由线程本身管理。检查线程方案中的原子指令和执行情况。它还允许在未共享内存中使用原子指令。
一些实现在进行非原子访问以及原子访问时可能锁定整个内存。这至少是因为规范没有禁止更高的内存访问保证。这意味着,即使您在某个内存地址上创建了一个竞赛,也无法读取不一致/撕裂的值。然而,这只是一种不应依赖的可能性。
发布于 2021-11-01 09:46:52
WASM做出的选择并不是那么罕见。分栈和多栈设计并不新鲜,而且始终与C和C++兼容.这是C规范不规范的结果,它总是允许“堆栈”变量存在于不可寻址寄存器中。C堆栈是抽象的,与底层执行环境只有有限的关系。
当C++采用用于C++11的Java模型(C遵循该模型)时,线程安全并不是“自动的”,而是只适用于C++ objects。“堆”不是这个意义上的对象,而是一个概念,它是实现的责任,以确保安全。请注意,C++标准不要求性能。技术上允许使用全局锁来保护堆。
在这种情况下,这意味着WASM应该保持单独的堆栈(如@Nikolay所指出的)。这些堆栈占据的内存区域并不重要,只要各个堆栈的各个片段在任何特定的时刻都不重叠。
https://stackoverflow.com/questions/69789570
复制相似问题