簡單聊聊C#的線程本地存儲TLS到底是什么
一:背景
1. 講故事
有朋友在后臺留言讓我說一下C#的 ThreadStatic
線程本地存儲是怎么玩的?這么說吧,C#的ThreadStatic是假的,因為C#完全是由CLR(C++)承載的,言外之意C#的線程本地存儲,用的就是用C++運(yùn)行時提供的 __declspec(thread)
或 __thread
來虛構(gòu)的一套玩法,這一篇我們就來簡單聊一聊。
二:C# 的線程本地存儲
1. 虛構(gòu)在哪里
在 C# 中使用ThreadStatic就可以將變量和線程進(jìn)行綁定,參考代碼如下:
internal class Program { [ThreadStatic] public static int num = 10; static void Main(string[] args) { Console.WriteLine($"num={num}"); Debugger.Break(); } }
在 CLR 中如何將 num 與 Thread 綁定呢?研究過 CLR 源碼的朋友應(yīng)該知道是用 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 就是放在與之關(guān)聯(lián)的 ThreadLocalModule 中,這個比較簡單,關(guān)注下匯編代碼就好了,下面的 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層面用了太多的高層虛構(gòu)來玩了一套線程本地存儲,其實(shí)最核心的還要理解再下一層的 __declspec(selectany)
,接下來聊聊這玩意是怎么玩的。
2. __declspec(selectany) 是怎么玩的
在Windows層面的術(shù)語中,有兩種 TLS 技術(shù)。
動態(tài)TLS
借助 Windows 提供的 TlsAlloc, TlsSetValue 之類的方法來實(shí)現(xiàn),并且存放在線程 _TEB.TlsSlots
的槽位中,參考代碼如下:
0:000> dt 0x000000f4f0ca6000 ntdll!_TEB
+0x000 NtTib : _NT_TIB
...
+0x1480 TlsSlots : [64] (null)
...
靜態(tài)TLS
C#的線程本地存儲用的就是靜態(tài)TLS,也就是在編譯時就已經(jīng)聲明好的,在 PE 文件里面有一個 .tls 節(jié)點(diǎn),這個節(jié)點(diǎn)的數(shù)據(jù)會被每個線程在heap堆上copy一份,存放在 _TEB.ThreadLocalStoragePointer
來指向的指針數(shù)組中,參考代碼如下:
0:000> dt 0x000000f4f0ca6000 ntdll!_TEB
+0x000 NtTib : _NT_TIB
+0x058 ThreadLocalStoragePointer : 0x00000294`d0536ab0 Void
...
動態(tài)的TLS我就不介紹了,這里著重說一下靜態(tài)的TLS。
3. 靜態(tài)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 值在編譯時就已經(jīng)放到了 PE 頭的 .tls 節(jié),可以用 PPEE 觀察下對象頭。
從卦中可以看到 .tls 占用了 0x400 字節(jié)大小,并且用 WinHex 真的觀察到了 i,j 的值,挺有意思。
在內(nèi)存中TLS區(qū)比這個還小一點(diǎn),可以觀察一下 DIRECTORY_ENTRY_TLS
節(jié)的 StartAddressOfRawData 和 EndAddressOfRawData 字段,這也是每個線程copy的原始內(nèi)存區(qū)域,可以看到只有 0x20D ,大概少了一半,截圖如下:
有了這些前置知識,接下來觀察內(nèi)存中的地址,在運(yùn)行之前先把 ASLR
關(guān)掉,匯編代碼參考如下:
//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 索引,當(dāng)前是 0 ,參考如下:
0:000> dp 0041a1b4 L1
0041a1b4 00000000
fs:[2Ch]
在用戶態(tài)層面上 fs 指向的是當(dāng)前線程的 TEB 結(jié)構(gòu),其中的 2C 偏移指的就是 ThreadLocalStoragePointer 結(jié)構(gòu),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]
這句匯編是一個數(shù)組操作,翻譯成 C 就是 ThreadLocalStoragePointer[tls]
。
0:000> dp 0x00664400 L1
00664400 00664448
這里要提醒的是:上面的 00664448 所在的 heap 位置其實(shí)就是 PE 頭里的 StartAddressOfRawData~EndAddressOfRawData
內(nèi)存區(qū)域的 copy,截圖如下:
eax,dword ptr [edx+108h]
這句話的意思就是在 數(shù)組元素1
這個結(jié)構(gòu)上偏移108的位置存放著我們的 num 值,用 windbg 觀察之后果然就是的。
0:000> dp 00664448+0x108 L1
00664550 7fffffff
三:總結(jié)
C# 屬于一種業(yè)務(wù)高層抽象的語言,它的很多底層被C++再次隔離了,想要理解本篇的TLS,還得需要往下一層一層的擊穿,作為C#程序員太難了。
到此這篇關(guān)于簡單聊聊C#的線程本地存儲TLS到底是什么的文章就介紹到這了,更多相關(guān)C#線程本地存儲TLS內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C# IQueryable及IEnumerable區(qū)別解析
這篇文章主要介紹了C# IQueryable及IEnumerable區(qū)別解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-09-09C#基于SerialPort類實(shí)現(xiàn)串口通訊詳解
這篇文章主要為大家詳細(xì)介紹了C#基于SerialPort類實(shí)現(xiàn)串口通訊,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-01-01C#讀取XML的CDATA節(jié)點(diǎn)內(nèi)容實(shí)例詳解
在本篇文章里小編給大家整理了關(guān)于C# 讀取XML的CDATA節(jié)點(diǎn)內(nèi)容的相關(guān)知識點(diǎn)內(nèi)容,有需要的朋友們參考學(xué)習(xí)下。2019-09-09C#操作SQLite數(shù)據(jù)庫方法小結(jié)
這篇文章介紹了C#操作SQLite數(shù)據(jù)庫的方法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-06-06