内联汇编中访问thread_local变量

Mal*_*eod 3 c++ x86 assembly gcc thread-local-storage

我正在处理一些具有使用内联汇编的优化版本的C ++代码。优化版本显示的行为不是线程安全的,可以追溯到3个全局变量,可以从程序集内部进行广泛访问。

__attribute__ ((aligned (16))) unsigned int SHAVITE_MESS[16];
__attribute__ ((aligned (16))) thread_local unsigned char SHAVITE_PTXT[8*4];
__attribute__ ((aligned (16))) unsigned int SHAVITE_CNTS[4] = {0,0,0,0};
Run Code Online (Sandbox Code Playgroud)

...

asm ("movaps xmm0, SHAVITE_PTXT[rip]");
asm ("movaps xmm1, SHAVITE_PTXT[rip+16]");
asm ("movaps xmm3, SHAVITE_CNTS[rip]");
asm ("movaps xmm4, SHAVITE256_XOR2[rip]");
asm ("pxor   xmm2,  xmm2");
Run Code Online (Sandbox Code Playgroud)

我天真地认为解决此问题的最简单方法是使变量成为thread_local,但这会导致程序集中出现段错误-似乎程序集不知道变量是否是线程局部的?

我在一个小thread_local测试用例的汇编中进行了研究,以查看gcc如何处理它们,mov eax, DWORD PTR fs:num1@tpoff并尝试修改代码以执行相同的操作:

asm ("movaps xmm0, fs:SHAVITE_PTXT@tpoff");
asm ("movaps xmm1, fs:SHAVITE_PTXT@tpoff+16");
asm ("movaps xmm3, fs:SHAVITE_CNTS@tpoff");
asm ("movaps xmm4, fs:SHAVITE256_XOR2@tpoff");
asm ("pxor   xmm2,  xmm2");
Run Code Online (Sandbox Code Playgroud)

如果所有变量也都是thread_local,则该方法有效,它也与参考实现(非汇编)匹配,因此看起来可以成功工作。但是,这似乎是特定于CPU的,如果我看一下-m32我用get 进行编译的输出mov eax, DWORD PTR gs:num1@ntpoff

由于代码无论如何都是“ x86”特定的(使用aes-ni),我想我可以反编译并实现所有可能的变体。

但是,我不太喜欢这种解决方案,感觉有点像猜测编程。进一步这样做并没有真正帮助我在将来的任何情况下学到任何东西,这些情况可能对一种体系结构而言不太具体。

有没有更通用/正确的方式来解决这个问题?如何以一种更通用的方式告诉程序集变量是thread_local?还是有一种方法可以传递变量,使得它不需要知道就可以工作?

Pet*_*des 5

如果您当前的代码对每条指令使用单独的“基本” asm语句,则该代码编写不正确,并且会通过破坏XMM寄存器而不告知您而对编译器说谎。 那不是您使用GNU C内联汇编的方式。

你应该把它改写AES-NI和SIMD内部函数_mm_aesdec_si128这样编译器会发出的一切正确的寻址方式。 https://gcc.gnu.org/wiki/DontUseInlineAsm


或者,如果您确实仍然希望使用GNU C内联汇编,请对输入/输出操作数使用扩展汇编"+m",可以是局部var或所需的任何C变量,包括静态或局部线程。另请参阅https://stackoverflow.com/tags/inline-assembly/info,以获取有关inlien asm的指南的链接。

但是希望您可以使它们自动存储在函数内部,或者让调用者分配并传递指向上下文的指针,而不用完全使用静态或线程本地存储。线程局部访问的访问速度稍慢,因为非零段基减慢了加载执行单元中的地址计算。我认为,当地址提早准备好时,问题可能不大,但是请确保您确实需要TLS,而不仅仅是堆栈或调用者提供的临时空间。这也会损害代码大小。

当GCC在模板中为操作数约束填充%0%[named]操作"m"数时,它将使用适当的寻址模式。 无论是fs:SHAVITE_PTXT@tpoff+16还是XMMWORD PTR [rsp-24]XMMWORD PTR _ZZ3foovE15SHAVITE256_XOR2[rip](对于函数局部静态变量),它都可以正常工作。(只要您不会遇到与Intel语法不匹配的操作数大小,在这种情况下编译器将使用内存操作数来填充该操作数,而不是像AT&T语法模式那样将其保留给助记符后缀。)

像这样,使用全局,TLS全局,局部自动和局部静态变量只是为了证明它们都相同。

// compile with -masm=intel

//#include <stdalign.h>  // for C11
alignas(16) unsigned int SHAVITE_MESS[16];                 // global (static storage)
alignas(16) thread_local unsigned char SHAVITE_PTXT[8*4];  // TLS global

void foo() {
    alignas(16) unsigned int SHAVITE_CNTS[4] = {0,0,0,0};   // automatic storage (initialized)
    alignas(16) static unsigned int SHAVITE256_XOR2[4];     // local static

    asm (
        "movaps xmm0, xmmword ptr %[PTXT]     \n\t"
        "movaps xmm1, xmmword ptr %[PTXT]+16  \n\t"   // x86 addressing modes are always offsetable
        "pxor   xmm2,  xmm2       \n\t"          // mix shorter insns with longer insns to help decode and uop-cache packing
        "movaps xmm3, xmmword ptr %[CNTS]+0     \n\t"
        "movaps xmm4, xmmword ptr %[XOR2_256]"

       : [CNTS] "+m" (SHAVITE_CNTS),    // outputs and read/write operands
         [PTXT] "+m" (SHAVITE_PTXT),
         [XOR2_256] "+m" (SHAVITE256_XOR2)

       : [MESS] "m" (SHAVITE_MESS)      // read-only inputs

       : "xmm0", "xmm1", "xmm2", "xmm3", "xmm4"  // clobbers: list all you use
    );
}
Run Code Online (Sandbox Code Playgroud)

如果避免使用xmm8..15或通过以下方式保护它,则可以使其在32位和64位模式之间可移植: #ifdef __x86_64__

注意,[PTXT] "+m" (SHAVITE_PTXT)作为一个操作数指整个阵列是一个输入/输出端,当SHAVITE_PTXT是一个真正的阵列,一个char*

它当然会扩展为对象开始的寻址模式,但您可以使用常量来抵消它+16。汇编器接受的[rsp-24]+16等效项是,[rsp-8]因此它只适用于基址寄存器或静态地址。

告诉编译器输入和/或输出中的整个数组意味着即使在内联之后,它也可以安全地围绕asm语句进行优化。例如,编译器知道对更高数组元素的写入也与asm的输入/输出相关,而不仅仅是第一个字节。它不能将以后的元素保留在整个asm的寄存器中,也不能将加载/存储重新排序到这些数组。


如果您使用过SHAVITE_PTXT[0](即使使用指针也可以使用),则编译器将在操作数中使用Intel语法byte ptr foobar。但是幸运的是,xmmword ptr byte ptr第一个优先级高,并且与movapsxmm0,xmmword ptr%[foo]` 的操作数大小匹配。(AT&T语法没有这个问题,其中助记符在必要时通过后缀携带操作数大小;编译器不填充任何内容。)

您的某些数组恰巧是16字节大小,因此编译器已经填写xmmword ptr,但是冗余也可以。

如果仅使用指针而不是数组,请参阅如何指示可以使用内联ASM参数“指向”的内存?对于"m" (*(unsigned (*)[16]) SHAVITE_MESS)语法。您可以将其用作实际输入操作数,也可以将其用作“虚拟”输入以及"+r"操作数中的指针。

也许更好,请索取SIMD寄存器的输入,输出或类似的读/写操作数[PTXT16] "+x"( *(__m128i)&array[16] )。它可以选择您未声明破坏者的任何XMM寄存器。使用#include <immintrin.h>来定义__m128i,或者自己与GNU C原始向量语法做到这一点。 __m128i使用,__attribute__((may_alias))以便指针广播不会创建严格混淆的UB。

如果编译器可以内联此代码并在跨asm语句的XMM寄存器中保留局部变量,而不是由您的手写asm进行存储/重装以将其保存在内存中,则这特别好。


以上源代码的编译器输出

来自Godbolt编译器,带有gcc9.2。填写%[stuff]模板后,这只是编译器的asm文本输出。

# g++ -O3 -masm=intel
foo():
        pxor    xmm0, xmm0
        movaps  XMMWORD PTR [rsp-24], xmm0      # compiler-generated zero-init array

        movaps xmm0, xmmword ptr fs:SHAVITE_PTXT@tpoff     
        movaps xmm1, xmmword ptr fs:SHAVITE_PTXT@tpoff+16  
        pxor   xmm2,  xmm2       
        movaps xmm3, xmmword ptr XMMWORD PTR [rsp-24]+0     
        movaps xmm4, xmmword ptr XMMWORD PTR foo()::SHAVITE256_XOR2[rip]
        ret
Run Code Online (Sandbox Code Playgroud)

这是汇编二进制输出的反汇编:

foo():
 pxor   xmm0,xmm0
 movaps XMMWORD PTR [rsp-0x18],xmm0   # compiler-generated

 movaps xmm0,XMMWORD PTR fs:0xffffffffffffffe0
 movaps xmm1,XMMWORD PTR fs:0xfffffffffffffff0    # note the +16 worked
 pxor   xmm2,xmm2
 movaps xmm3,XMMWORD PTR [rsp-0x18]               # note the +0 assembled without syntax error
 movaps xmm4,XMMWORD PTR [rip+0x200ae5]        # 601080 <foo()::SHAVITE256_XOR2>
 ret
Run Code Online (Sandbox Code Playgroud)

还要注意,非TLS全局变量使用的是RIP相对寻址模式,而TLS则没有,而是使用符号扩展的[disp32]绝对寻址模式。

(在位置- 相关的代码,你可以在理论上使用RIP相对寻址模式下产生这样的相对TLS基地。我不认为GCC这样做,虽然少了绝对地址。)