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?还是有一种方法可以传递变量,使得它不需要知道就可以工作?
如果您当前的代码对每条指令使用单独的“基本” 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
第一个优先级高,并且与movaps
xmm0,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这样做,虽然少了绝对地址。)
归档时间: |
|
查看次数: |
194 次 |
最近记录: |