首页 > 编程语言 >聊一聊 C# 的线程本地存储TLS到底是什么

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

时间:2024-01-05 14:58:49浏览次数:27  
标签:TLS C# mov 聊一聊 线程 dword ptr

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

 

一:背景

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 观察下对象头。

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

在内存中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 [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,截图如下:

  • eax,dword ptr [edx+108h]

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


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

三:总结

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

标签:TLS,C#,mov,聊一聊,线程,dword,ptr
From: https://www.cnblogs.com/sexintercourse/p/17947235

相关文章

  • Handler创建Dispatcher请求调用执行Utility
    文章目录应用场景Handler创建Dispatcher请求创建响应1、创建响应工程2、书写响应代码3、打成jar包4、将导出的jar包放入DispatcherClient\lib下。5、修改DispatcherClient\conf下Service.properties文件6、修改Module\conf下translator.xml文件7、书写bomchange.bat,并......
  • 马某 说c# 不开源,他是蠢还是坏?
    马某说c#不开源,他是蠢还是坏? 马某在视频 计算机主流开发语言的现状和未来3-5年的发展前景——Java、Golang、Python、C\C#\C++、JS、前端、AI、大数据、测试、运维、网络安全 点评各种语言,其中说到C#的时候,居然说C#是不开源的,而且还说局限于微软平台。一个不蠢的人深......
  • pycharm配置sqlite3数据库 可视化插件
    1.首先你需要创建一个sqlite3数据库,创建好数据库以后会生成一个.db后缀名或者.sqlite后缀的数据库文件。如果你不知道window如何安装sqlite3数据库,请参考我的这篇文章:如果你不知道如何使用sqlite3,请参考我的这篇文章: 3.要在pycharm中打开.sqlite后缀的数据库文件和打开.db......
  • 【Amadeus原创】cas3.5配置LDAP域控
    一、安装cas3.5点击下载CAS3.5.2:CAS解压缩下载的cas-server-3.5.2-release.zip,在%CAS%\modules文件夹中找到cas-server-webapp-3.5.2.war将其重命名为cas.war将cas.war拷贝到%TOMCAT_HOME%\webapps文件夹下,启动Tomcat,就会看到Tomcat自动将cas.war解压到ca......
  • echarts选项变化后,图表不更新
    echarts做地图下钻,全国地图切换省份地图使用chart.setOption(option)后地图不刷新,原因如下:setOption有三个参数chart.setOption(option,notMerge,lazyUpdate)option——图表的配置项和数据notMerge——可选,是否不跟之前设置的option进行合并,默认为false,即合并。lazy......
  • 如何修改Centos7的IP地址
    目录一、打开网卡配置文件二、修改参数三、设置IP和掩码等四、重启服务一、打开网卡配置文件vi/etc/sysconfig/network-scripts/ifcfg-ens33二、修改参数(1)BOOTPROTO=static #dhcp:自动分配ip,static:静态ip(2)ONBOOT=yes#开启启动必须是yes三、设置IP和掩码等IPADDR=19......
  • nested exception is java.lang.IllegalArgumentException异常问题解决
    项目启动报错如下:nestedexceptionisjava.lang.IllegalArgumentException:Couldnotresolveplaceholder'xxx'invalue"${xxx}"问题解决比较简单,只说我所遇到的情况,原因就是字母拼写问题仔细看还是能看到大写的K和小写的k有一些细微的区别,将nacos中的k和代码中修改一致后启......
  • linux如何排查cpu持续飙高原因
    一、检查CPU使用率首先在Linux系统中检查CPU使用率。可以通过在命令行中输入top或htop命令来查看当前系统中各个进程的CPU使用率。如果CPU使用率大于80%,则可以考虑进行排查。$top二、检查系统负载另外可以使用uptime命令来查看系统的平均负载情况。$uptime如果系统负载超过CPU内......
  • 数据反序列化参数TypeReference
    使用jackson或者fastjson将json字符串反序列化成对象(以fastjson为例)TypeReference<AjaxResult<List<Person>>>typeReference=newTypeReference<AjaxResult<List<Person>>>(){};Stringjson="{\"code\":200,\"success\......
  • MySQL三大日志,mvcc、DateTime 类型等
    1、MySQL事务隔离级别详解解决幻读的方法解决幻读的方式有很多,但是它们的核心思想就是一个事务在操作某张表数据的时候,另外一个事务不允许新增或者删除这张表中的数据了。解决幻读的方式主要有以下几种:将事务隔离级别调整为 SERIALIZABLE 。在可重复读的事务级别下,给事务操......