1

聊一聊 C# 的线程本地存储TLS到底是什么

 5 months ago
source link: https://www.cnblogs.com/huangxincheng/p/17940282
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1. 讲故事

有朋友在后台留言让我说一下C#的 ThreadStatic 线程本地存储是怎么玩的?这么说吧,C#的ThreadStatic是假的,因为C#完全是由CLR(C++)承载的,言外之意C#的线程本地存储,用的就是用C++运行时提供的 __declspec(thread)__thread 来虚构的一套玩法,这一篇我们就来简单聊一聊。

二:C# 的线程本地存储

1. 虚构在哪里

在 C# 中使用ThreadStatic就可以将变量和线程进行绑定,参考代码如下:


    internal class Program
    {
        [ThreadStatic]
        public static int num = 10;

        static void Main(string[] args)
        {
            Console.WriteLine($"num={num}");

            Debugger.Break();
        }
    }

在 CLR 中如何将 num 与 Thread 绑定呢?研究过 CLR 源码的朋友应该知道是用 ThreadLocalInfo 的,参考代码如下:


#ifdef _MSC_VER
__declspec(selectany) __declspec(thread) ThreadLocalInfo gCurrentThreadInfo;
#else
EXTERN_C __thread ThreadLocalInfo gCurrentThreadInfo;
#endif

struct ThreadLocalInfo
{
    Thread* m_pThread;
    AppDomain* m_pAppDomain; // This field is read only by the SOS plugin to get the AppDomain
    void** m_EETlsData; // ClrTlsInfo::data
};

上面的 m_pThread 就是 C# Thread 在 CLR 层面的承载,怎么去验证呢?可以把代码跑起来,然后用 windbg 验证一下。


0:000> dt coreclr!gCurrentThreadInfo
   +0x000 m_pThread        : 0x000001e3`506c5fa0 Thread
   +0x008 m_pAppDomain     : 0x000001e3`506ba9b0 AppDomain
   +0x010 m_EETlsData      : 0x000001e3`506aa360  -> (null) 

0:000> !t
ThreadCount:      3
UnstartedThread:  0
BackgroundThread: 2
PendingThread:    0
DeadThread:       0
Hosted Runtime:   no
                                                                                                            Lock  
 DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
   0    1     2e04 000001E3506C5FA0    2a020 Preemptive  000001E3521DCE80:000001E3521DD4A8 000001e3506ba9b0 -00001 MTA 
   6    2     4ef8 000001E3506F1A30    21220 Preemptive  0000000000000000:0000000000000000 000001e3506ba9b0 -00001 Ukn (Finalizer) 
   7    3     3550 000001E3726A0AE0    2b220 Preemptive  0000000000000000:0000000000000000 000001e3506ba9b0 -00001 MTA 

从卦中可以清楚的看到 m_pThread=0x000001e3506c5fa0 就是我们的主线程,最后的 num 就是放在与之关联的 ThreadLocalModule 中,这个比较简单,关注下汇编代码就好了,下面的 rax 就是 ThreadLocalModule。


00007ffb`218d2c2c 48b9b07b9921fb7f0000 mov rcx,7FFB21997BB0h
00007ffb`218d2c36 ba04000000      mov     edx,4
00007ffb`218d2c3b e8001fb55f      call    coreclr!JIT_GetSharedNonGCThreadStaticBase (00007ffb`81424b40)
00007ffb`218d2c40 8b4820          mov     ecx,dword ptr [rax+20h]
00007ffb`218d2c43 894dfc          mov     dword ptr [rbp-4],ecx

0:000> dp rax+0x20 L1
00000294`d0539790  abababab`0000000a

CLR层面用了太多的高层虚构来玩了一套线程本地存储,其实最核心的还要理解再下一层的 __declspec(selectany) ,接下来聊聊这玩意是怎么玩的。

2. __declspec(selectany) 是怎么玩的

在Windows层面的术语中,有两种 TLS 技术。

  • 动态TLS

借助 Windows 提供的 TlsAlloc, TlsSetValue 之类的方法来实现,并且存放在线程 _TEB.TlsSlots 的槽位中,参考代码如下:


0:000> dt 0x000000f4f0ca6000 ntdll!_TEB
   +0x000 NtTib            : _NT_TIB
   ...
   +0x1480 TlsSlots         : [64] (null) 
   ...

  • 静态TLS

C#的线程本地存储用的就是静态TLS,也就是在编译时就已经声明好的,在 PE 文件里面有一个 .tls 节点,这个节点的数据会被每个线程在heap堆上copy一份,存放在 _TEB.ThreadLocalStoragePointer 来指向的指针数组中,参考代码如下:


0:000> dt 0x000000f4f0ca6000 ntdll!_TEB
   +0x000 NtTib            : _NT_TIB
   +0x058 ThreadLocalStoragePointer : 0x00000294`d0536ab0 Void
   ...

动态的TLS我就不介绍了,这里着重说一下静态的TLS。

3. 静态TLS详解

为了方便讲解,先上一段测试代码。


#include <windows.h>
#include <stdio.h>
#include <limits.h>


__declspec(thread) int i = INT_MAX;
__declspec(thread) int j = INT_MAX;

int main() {
	int num1 = i;
	int num2 = j;
	printf("i=%d,j=%d", num1, num2);
}

上面的 i,j 值在编译时就已经放到了 PE 头的 .tls 节,可以用 PPEE 观察下对象头。

214741-20240102132306920-198456071.png

从卦中可以看到 .tls 占用了 0x400 字节大小,并且用 WinHex 真的观察到了 i,j 的值,挺有意思。

在内存中TLS区比这个还小一点,可以观察一下 DIRECTORY_ENTRY_TLS 节的 StartAddressOfRawData 和 EndAddressOfRawData 字段,这也是每个线程copy的原始内存区域,可以看到只有 0x20D ,大概少了一半,截图如下:

214741-20240102132306912-2099667198.png

有了这些前置知识,接下来观察内存中的地址,在运行之前先把 ASLR 关掉,汇编代码参考如下:

   //int num1 = i;
   14 00411895 a1b4a14100      mov     eax,dword ptr [ConsoleApplication2!_tls_index (0041a1b4)]
   14 0041189a 648b0d2c000000  mov     ecx,dword ptr fs:[2Ch]
   14 004118a1 8b1481          mov     edx,dword ptr [ecx+eax*4]
   14 004118a4 8b8208010000    mov     eax,dword ptr [edx+108h]
   14 004118aa 8945f8          mov     dword ptr [ebp-8],eax

   //int num2 = j;
   15 004118ad a1b4a14100      mov     eax,dword ptr [ConsoleApplication2!_tls_index (0041a1b4)]
   15 004118b2 648b0d2c000000  mov     ecx,dword ptr fs:[2Ch]
   15 004118b9 8b1481          mov     edx,dword ptr [ecx+eax*4]
   15 004118bc 8b8204010000    mov     eax,dword ptr [edx+104h]
   15 004118c2 8945ec          mov     dword ptr [ebp-14h],eax

可以看到每一句大概会生成 5 行汇编代码,我们简单分析下。

  • ConsoleApplication2!_tls_index (0041a1b4)

这个值就是 PE 头的 AddressOfIndex 值,可以再回头观察下,里面存的就是 tls 索引,当前是 0 ,参考如下:


0:000> dp 0041a1b4 L1
0041a1b4  00000000

  • fs:[2Ch]

在用户态层面上 fs 指向的是当前线程的 TEB 结构,其中的 2C 偏移指的就是 ThreadLocalStoragePointer 结构,windbg 观察如下:


0:000> dg fs
                                  P Si Gr Pr Lo
Sel    Base     Limit     Type    l ze an es ng Flags
---- -------- -------- ---------- - -- -- -- -- --------
0053 002bc000 00000fff Data RW Ac 3 Bg By P  Nl 000004f3

0:000> dt 0x002bc000 ntdll!_TEB
   +0x000 NtTib            : _NT_TIB
   +0x01c EnvironmentPointer : (null) 
   +0x020 ClientId         : _CLIENT_ID
   +0x028 ActiveRpcHandle  : (null) 
   +0x02c ThreadLocalStoragePointer : 0x00664400 Void
   ...

  • edx,dword ptr [ecx+eax*4]

这句汇编是一个数组操作,翻译成 C 就是 ThreadLocalStoragePointer[tls]


0:000> dp 0x00664400 L1
00664400  00664448

这里要提醒的是:上面的 00664448 所在的 heap 位置其实就是 PE 头里的 StartAddressOfRawData~EndAddressOfRawData内存区域的 copy,截图如下:

214741-20240102132306985-1596149722.png
  • eax,dword ptr [edx+108h]

这句话的意思就是在 数组元素1 这个结构上偏移108的位置存放着我们的 num 值,用 windbg 观察之后果然就是的。


0:000> dp 00664448+0x108 L1
00664550  7fffffff

C# 属于一种业务高层抽象的语言,它的很多底层被C++再次隔离了,想要理解本篇的TLS,还得需要往下一层一层的击穿,作为C#程序员太难了。

图片名称

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK