如何从ml64.exe(MSVC 64位X64汇编程序)访问线程本地存储?

以下C函数尝试使用线程本地存储variables以线程安全的方式防止多核代码中的recursion。 但是,由于原因有些复杂,我需要在X64汇编器(Intel X86 / AMD 64位)中编写此函数,并将其与VC2010中的ml64.exe进行汇编。 我知道如何做到这一点,如果我使用全局variables,但我不知道如何正确使用具有__declspec(线程)的TLSvariables。

__declspec(thread) int tls_VAR = 0; void norecurse( ) { if(0==tls_VAR) { tls_VAR=1; DoWork(); tls_VAR=0; } } 

注意:这是VC2010踢出的function。 但是,MASM(ml64.exe)不支持gs:88OFFSET FLAT:部分代码。

 ; Listing generated by Microsoft (R) Optimizing Compiler Version 16.00.40219.01 include listing.inc INCLUDELIB MSVCRTD INCLUDELIB OLDNAMES PUBLIC norecurse EXTRN DoWork:PROC EXTRN tls_VAR:DWORD EXTRN _tls_index:DWORD pdata SEGMENT $pdata$norecurse DD imagerel $LN4 DD imagerel $LN4+70 DD imagerel $unwind$norecurse pdata ENDS xdata SEGMENT $unwind$norecurse DD 040a01H DD 06340aH DD 07006320aH ; Function compile flags: /Ogtpy xdata ENDS _TEXT SEGMENT norecurse PROC ; File p:\hackytests\64bittest2010\64bittest\64bittest.cpp ; Line 19 $LN4: mov QWORD PTR [rsp+8], rbx push rdi sub rsp, 32 ; 00000020H ; Line 20 mov ecx, DWORD PTR _tls_index mov rax, QWORD PTR gs:88 mov edi, OFFSET FLAT:tls_VAR mov rbx, QWORD PTR [rax+rcx*8] cmp DWORD PTR [rbx+rdi], 0 jne SHORT $LN1@norecurse ; Line 22 mov DWORD PTR [rbx+rdi], 1 ; Line 23 call DoWork ; Line 24 mov DWORD PTR [rbx+rdi], 0 $LN1@norecurse: ; Line 26 mov rbx, QWORD PTR [rsp+48] add rsp, 32 ; 00000020H pop rdi ret 0 norecurse ENDP _TEXT ENDS END 

正如你的答案指出的那样,在微软的C ++编译器生成的程序集列表中找到与以下两行相对应的MASM问题:

 mov rax, QWORD PTR gs:88 mov edi, OFFSET FLAT:tls_VAR 

第一行很简单。 只要用gs:88替换gs:88 gs:[88]

第二行不太明显。 OFFSET FLAT:操作员是一个红鲱鱼。 这意味着使用相对于“FLAT”段开始的偏移量。 对于32位版本的MASM,FLAT段是包含整个4G地址空间的段。 这是用于代码和数据段的段,作为32位平面内存模型的一部分。 MASM的64位版本不支持内存模型,它本质上总是假定平面内存模型的64位版本,因此它不支持FLAT关键字。 因此,普通的OFFSET操作符结束意味着相同的事情。 (事实上​​,对于32位汇编程序,普通的OFFSET也通常意味着相同的事情,因为PECOFF只支持平面内存模型。)

但是在这里使用OFFSET将不起作用。 这是因为它将使用内存中tls_VAR的地址相对于地址0的偏移量。换句话说,它将在内存中使用tls_VAR的绝对地址。 这里需要的是相对于TLS数据部分开始的偏移量。

所以编译器必须在这里做一些特殊的事情。 为了弄清楚,我在编译你的C代码的时候抛弃了生成的目标文件中的重定位:

 > dumpbin /relocations t215a.obj ... RELOCATIONS #4 Symbol Symbol Offset Type Applied To Index Name -------- ---------------- ----------------- -------- ------ 00000008 REL32 00000000 14 _tls_index 00000016 SECREL 00000000 8 tls_VAR 0000002D REL32 00000000 C DoWork ... 

正如您所看到的,它会生成SECREL类型的重定位以引用tls_VAR 。 这使得相对于该符号出现在生成的可执行文件中的部分的底部进行重定位。在这种情况下,这是.tls部分,因此,此重定位会生成相对于用于静态TLS数据的部分开头的偏移量。

所以现在问题变成了如何让MASM产生相同的编译器发出的SECREL重定位。 原来这也有一个简单的解决方案,只是用SECTIONREL替换OFFSET FLAT: SECTIONREL

所以有了这些改变(和一些优化)你的功能变成:

  EXTERN tls_VAR:DWORD EXTERN _tls_index:DWORD EXTERN DoWork:PROC PUBLIC norecurse _TEXT SEGMENT norecurse PROC push rbx sub rsp, 32 mov rax, gs:[88] mov ecx, _tls_index mov rbx, [rax + rcx * 8] cmp DWORD PTR [rbx + SECTIONREL tls_VAR], 0 jne return mov DWORD PTR [rbx + SECTIONREL tls_VAR], 1 call DoWork mov DWORD PTR [rbx + SECTIONREL tls_VAR], 0 return: add rsp, 32 pop rbx ret norecurse ENDP _TEXT ENDS END 

我能够解决这个问题。 我在assember中的实现比C编译器生成的代码效率低,但因为我无法弄清楚如何使用以下两种寻址模式:

  1. mov rax,QWORD PTR gs:88
  2. mov edi,OFFSET FLAT:tls_VAR

对于(1),我必须加载88 rax,并使用gs:[rax]来访问线程的TLS基础。

对于(2),在MASM(ml64.exe)中缺少OFFSET FLAT意味着我必须更聪明。 我通过从可以应用于汇编程序中的TLS变量的线程的TLS基数中减去_tls_start来计算偏移量,以便访问它们的线程本地值。

 PUBLIC norecurse EXTRN _tls_index:DWORD EXTRN _tls_start:DWORD EXTRN tls_VAR:DWORD EXTRN DoWork:PROC _TEXT SEGMENT norecurse PROC ; non-volatile push rbx sub rsp,32 ; The gs segment register refers to the base address of the TEB on x64. ; 88 (0×58) is the offset in the TEB for the ThreadLocalStoragePointer member on x64 mov rax,88 mov edx, DWORD PTR _tls_index mov rax, gs:[rax] mov r11, QWORD PTR [rax+rdx*8] lea r10, _tls_start ; r11 will be the the offset-adjusted TLS-Base sub r11, r10 ; ebx will be the the thread local address of tls_VAR lea rdx, tls_VAR lea rbx,[r11+rdx] cmp DWORD PTR [rbx], 0 jne @F mov DWORD PTR [rbx], 1 call DoWork mov DWORD PTR [rbx], 0 @@: add rsp,32 pop rbx ret norecurse ENDP _TEXT ENDS END 

我很希望看到更有效的方法或指示如何实际使用两种寻址模式,我不能用MASM(ml64.exe)弄清楚。

查看TlsGetValue ,TlsSetvalue和朋友。