音乐介绍网站怎么做的,做那个网站大全,图片在线制作二维码,嘉兴市城市建设门户网站一#xff1a;背景
1. 讲故事
有朋友在后台留言让我说一下C#的 ThreadStatic 线程本地存储是怎么玩的#xff1f;这么说吧#xff0c;C#的ThreadStatic是假的#xff0c;因为C#完全是由CLR#xff08;C#xff09;承载的#xff0c;言外之意C#的线程本地存储#xff…一背景
1. 讲故事
有朋友在后台留言让我说一下C#的 ThreadStatic 线程本地存储是怎么玩的这么说吧C#的ThreadStatic是假的因为C#完全是由CLRC承载的言外之意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;
#endifstruct ThreadLocalInfo
{Thread* m_pThread;AppDomain* m_pAppDomain; // This field is read only by the SOS plugin to get the AppDomainvoid** m_EETlsData; // ClrTlsInfo::data
};
上面的 m_pThread 就是 C# Thread 在 CLR 层面的承载怎么去验证呢可以把代码跑起来然后用 windbg 验证一下。 0:000 dt coreclr!gCurrentThreadInfo0x000 m_pThread : 0x000001e3506c5fa0 Thread0x008 m_pAppDomain : 0x000001e3506ba9b0 AppDomain0x010 m_EETlsData : 0x000001e3506aa360 - (null) 0:000 !t
ThreadCount: 3
UnstartedThread: 0
BackgroundThread: 2
PendingThread: 0
DeadThread: 0
Hosted Runtime: noLock DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception0 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_pThread0x000001e3506c5fa0 就是我们的主线程最后的 num 就是放在与之关联的 ThreadLocalModule 中这个比较简单关注下汇编代码就好了下面的 rax 就是 ThreadLocalModule。 00007ffb218d2c2c 48b9b07b9921fb7f0000 mov rcx,7FFB21997BB0h
00007ffb218d2c36 ba04000000 mov edx,4
00007ffb218d2c3b e8001fb55f call coreclr!JIT_GetSharedNonGCThreadStaticBase (00007ffb81424b40)
00007ffb218d2c40 8b4820 mov ecx,dword ptr [rax20h]
00007ffb218d2c43 894dfc mov dword ptr [rbp-4],ecx0:000 dp rax0x20 L1
00000294d0539790 abababab0000000a
CLR层面用了太多的高层虚构来玩了一套线程本地存储其实最核心的还要理解再下一层的 __declspec(selectany) 接下来聊聊这玩意是怎么玩的。
2. __declspec(selectany) 是怎么玩的
在Windows层面的术语中有两种 TLS 技术。
动态TLS
借助 Windows 提供的 TlsAlloc, TlsSetValue 之类的方法来实现并且存放在线程 _TEB.TlsSlots 的槽位中参考代码如下 0:000 dt 0x000000f4f0ca6000 ntdll!_TEB0x000 NtTib : _NT_TIB...0x1480 TlsSlots : [64] (null) ...
静态TLS
C#的线程本地存储用的就是静态TLS也就是在编译时就已经声明好的在 PE 文件里面有一个 .tls 节点这个节点的数据会被每个线程在heap堆上copy一份存放在 _TEB.ThreadLocalStoragePointer 来指向的指针数组中参考代码如下 0:000 dt 0x000000f4f0ca6000 ntdll!_TEB0x000 NtTib : _NT_TIB0x058 ThreadLocalStoragePointer : 0x00000294d0536ab0 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);
}
上面的 ij 值在编译时就已经放到了 PE 头的 .tls 节可以用 PPEE 观察下对象头。 从卦中可以看到 .tls 占用了 0x400 字节大小并且用 WinHex 真的观察到了 ij 的值挺有意思。
在内存中TLS区比这个还小一点可以观察一下 DIRECTORY_ENTRY_TLS 节的 StartAddressOfRawData 和 EndAddressOfRawData 字段这也是每个线程copy的原始内存区域可以看到只有 0x20D 大概少了一半截图如下 有了这些前置知识接下来观察内存中的地址在运行之前先把 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 [ecxeax*4]14 004118a4 8b8208010000 mov eax,dword ptr [edx108h]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 [ecxeax*4]15 004118bc 8b8204010000 mov eax,dword ptr [edx104h]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 fsP 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 000004f30:000 dt 0x002bc000 ntdll!_TEB0x000 NtTib : _NT_TIB0x01c EnvironmentPointer : (null) 0x020 ClientId : _CLIENT_ID0x028 ActiveRpcHandle : (null) 0x02c ThreadLocalStoragePointer : 0x00664400 Void...
edx,dword ptr [ecxeax*4]
这句汇编是一个数组操作翻译成 C 就是 ThreadLocalStoragePointer[tls]。 0:000 dp 0x00664400 L1
00664400 00664448
这里要提醒的是上面的 00664448 所在的 heap 位置其实就是 PE 头里的 StartAddressOfRawData~EndAddressOfRawData内存区域的 copy截图如下 eax,dword ptr [edx108h]
这句话的意思就是在 数组元素1 这个结构上偏移108的位置存放着我们的 num 值用 windbg 观察之后果然就是的。 0:000 dp 006644480x108 L1
00664550 7fffffff
三总结
C# 属于一种业务高层抽象的语言它的很多底层被C再次隔离了想要理解本篇的TLS还得需要往下一层一层的击穿作为C#程序员太难了。