WebAssembly:线程安全和 C/C++ 局部变量

WebAssembly: thread-safety and C/C++ local variables

我试图理解 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/:

Local variables with unclear static scope (e.g. are used by the address-of operator, or are of type struct and returned by value) are stored in a separate user-addressable stack in linear memory at compile time. This is an isolated memory region with fixed maximum size that is zero initialized by default.

来自https://github.com/WebAssembly/design/blob/main/Rationale.md#locals

C/C++ makes it possible to take the address of a function's local values and pass this pointer to callees or to other threads. Since WebAssembly's local variables are outside the address space, C/C++ compilers implement address-taken variables by creating a separate stack data structure within linear memory. This stack is sometimes called the "aliased" stack, since it is used for variables which may be pointed to by pointers.

换句话说,从__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 ? &param1 : &param2;

    ++*param;

    return param1 / 3 + param2;
}

根据编译器的输出,如果 param1 and/or param2 存储在存在于线性内存,如果共享内存由 --features=atomics,bulk-memory --shared-memory 标志启用,则可以在其他实例之间共享。

那么,编译器在哪些具体情况下可以决定将局部变量存储在别名堆栈上?

编辑: 我做了一些测试来验证,我想知道我在这方面是否正确。我在堆上存储了一个使用16个无符号局部变量的函数的第一个、一半和最后一个局部变量的内存地址,并从javascript打印出来,以及存储的最低值之间的差异__heap_base 的地址是 32*3 bytes + padding,而不是 32*16 + padding,这意味着只有内存地址被占用的三个变量存储在别名堆栈中。当然,这些测试不是线程安全的,因为我将本地地址存储在函数外部,但它说明了这一点:如果在可重入函数上,我临时获取本地地址来实现方便,并且由于其复杂性,编译器不确定我要做什么,它最终决定将局部存储在堆栈上而不是更改其实现,从而使函数线程不安全。

在多线程设置中,每个线程都会将自己的堆栈放入共享内存中。堆栈指针(创建它seems to be done by LLVM createSyntheticSymbols) is placed into a WebAssembly global variable. Currently these globals are used as a thread-local storage。也就是说每个线程都有自己的全局变量。

在WebAssembly实例启动时,主线程会有自己的指向主线程栈的全局变量进入共享内存。如果启动另一个线程,在其启动期间,其全局变量将指向共享内存中的 另一个 位置,该线程的堆栈所在的位置。

栈的分配seems to be done by Emscripten __pthread_create_js if the caller does not supply its own pointer. The allocation of variables into the current stack is done here with stackAlloc 其中:

global.get __stack_pointer

正在获取当前线程堆栈指针,减去所需的字节(堆栈向下增长),将其对齐到 16 个字节,然后将新值记回全局。这都是线程安全的,因为全局只能从线程本身访问。

关于指针,是的,编译器会将指针访问的变量放入显式堆栈中。目前,WebAssembly 堆栈不是“可行走的”,但有一个 proposal 可以做到这一点。许多实现还使用显式堆栈,以获得对堆栈使用(变量、结构等)的更细粒度控制。

所有这些“东西”应该(RFC 2119)对开发人员来说透明。意思是,它似乎只是工作。


根据您的意见:目前的 WebAssembly 标准通过使用原子指令来处理数据竞争。他们的访问顺序是sequentially consistent. In the case of multi-threading, clearly the memory allocator MUST be thread safe. The use of the explicit thread dedicated stack by itself does not have to be (using globals is enough, as written up), because the stack memory is only managed by the thread itself. Check the threads proposal for the atomic instructions and the implementation status。也允许在非共享内存中使用原子指令。


一些实现在进行非原子访问和原子访问时可能会锁定整个内存。这至少是因为规范不禁止更高的内存访问保证。这意味着即使您在某个内存地址创建了一场比赛,您也无法读取 inconsistent/teared 值。但是,这只是一种可能性,不应依赖。

WASM 做出的选择并不罕见。拆分堆栈和多堆栈设计并不新鲜,并且一直与 C 和 C++ 兼容。这是 C 的规范不足的故意结果,它始终允许“堆栈”变量存在于不可寻址的寄存器中。 C栈是抽象的,与底层执行环境的关系有限

当 C++ 采用 C++11 的 Java 内存模型(C 遵循)时,线程安全不是“自动的”,但这仅适用于 C++ 对象.从这个意义上说,“堆”不是一个对象,而是一个概念,实现的责任是保证它的安全。请注意,C++ 标准不要求 性能。技术上允许使用全局锁来保护堆。

在这种情况下,这意味着 WASM 应该将单独的堆栈分开(正如@Nikolay 指出的那样)。这些堆栈占用的内存区域无关紧要,只要各个堆栈的各个片段在任何特定时刻不重叠即可。