当前位置:首页 > 科技  > 软件

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

来源: 责编: 时间:2024-01-02 17:28:07 355观看
导读一:背景1. 讲故事有朋友在后台留言让我说一下C#的 ThreadStatic 线程本地存储是怎么玩的?这么说吧,C#的ThreadStatic是假的,因为C#完全是由CLR(C++)承载的,言外之意C#的线程本地存储,用的就是用C++运行时提供的 __declspec(th

一:背景

1. 讲故事

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

二:C# 的线程本地存储

1. 虚构在哪里

在 C# 中使用ThreadStatic就可以将变量和线程进行绑定,参考代码如下:L9328资讯网——每日最新资讯28at.com

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 的,参考代码如下:L9328资讯网——每日最新资讯28at.com

#ifdef _MSC_VER__declspec(selectany) __declspec(thread) ThreadLocalInfo gCurrentThreadInfo;#elseEXTERN_C __thread ThreadLocalInfo gCurrentThreadInfo;#endifstruct 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 验证一下。L9328资讯网——每日最新资讯28at.com

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> !tThreadCount:      3UnstartedThread:  0BackgroundThread: 2PendingThread:    0DeadThread:       0Hosted 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。L9328资讯网——每日最新资讯28at.com

00007ffb`218d2c2c 48b9b07b9921fb7f0000 mov rcx,7FFB21997BB0h00007ffb`218d2c36 ba04000000      mov     edx,400007ffb`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],ecx0:000> dp rax+0x20 L100000294`d0539790  abababab`0000000a

CLR层面用了太多的高层虚构来玩了一套线程本地存储,其实最核心的还要理解再下一层的 __declspec(selectany) ,接下来聊聊这玩意是怎么玩的。L9328资讯网——每日最新资讯28at.com

2. __declspec(selectany) 是怎么玩的

在Windows层面的术语中,有两种 TLS 技术。L9328资讯网——每日最新资讯28at.com

  • 动态TLS

借助 Windows 提供的 TlsAlloc, TlsSetValue 之类的方法来实现,并且存放在线程 _TEB.TlsSlots 的槽位中,参考代码如下:L9328资讯网——每日最新资讯28at.com

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

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

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

动态的TLS我就不介绍了,这里着重说一下静态的TLS。L9328资讯网——每日最新资讯28at.com

3. 静态TLS详解

为了方便讲解,先上一段测试代码。L9328资讯网——每日最新资讯28at.com

#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 观察下对象头。L9328资讯网——每日最新资讯28at.com

图片L9328资讯网——每日最新资讯28at.com

从卦中可以看到 .tls 占用了 0x400 字节大小,并且用 WinHex 真的观察到了 i,j 的值,挺有意思。L9328资讯网——每日最新资讯28at.com

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

图片L9328资讯网——每日最新资讯28at.com

有了这些前置知识,接下来观察内存中的地址,在运行之前先把 ASLR 关掉,汇编代码参考如下:L9328资讯网——每日最新资讯28at.com

//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 行汇编代码,我们简单分析下。L9328资讯网——每日最新资讯28at.com

  • ConsoleApplication2!_tls_index (0041a1b4)

这个值就是 PE 头的 AddressOfIndex 值,可以再回头观察下,里面存的就是 tls 索引,当前是 0 ,参考如下:L9328资讯网——每日最新资讯28at.com

0:000> dp 0041a1b4 L10041a1b4  00000000
  • fs:[2Ch]

在用户态层面上 fs 指向的是当前线程的 TEB 结构,其中的 2C 偏移指的就是 ThreadLocalStoragePointer 结构,windbg 观察如下:L9328资讯网——每日最新资讯28at.com

0:000> dg fs                                  P Si Gr Pr LoSel    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!_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]。L9328资讯网——每日最新资讯28at.com

0:000> dp 0x00664400 L100664400  00664448

这里要提醒的是:上面的 00664448 所在的 heap 位置其实就是 PE 头里的 StartAddressOfRawData~EndAddressOfRawData内存区域的 copy,截图如下:L9328资讯网——每日最新资讯28at.com

图片L9328资讯网——每日最新资讯28at.com

  • eax,dword ptr [edx+108h]

这句话的意思就是在 数组元素1 这个结构上偏移108的位置存放着我们的 num 值,用 windbg 观察之后果然就是的。L9328资讯网——每日最新资讯28at.com

0:000> dp 00664448+0x108 L100664550  7fffffff

三:总结

C# 属于一种业务高层抽象的语言,它的很多底层被C++再次隔离了,想要理解本篇的TLS,还得需要往下一层一层的击穿,作为C#程序员太难了。L9328资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-56404-0.html聊一聊 C# 的线程本地存储TLS到底是什么

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 用Go实现一个带缓存的REST API服务端

下一篇: 利用Go传统RPC和gRPC框架分别实现一个RPC服务端

标签:
  • 热门焦点
  • 石头智能洗地机A10 Plus体验:双向自清洁治好了我的懒癌

    一、前言和介绍专为家庭请假懒人而生的石头科技在近日又带来了自己的全新旗舰新品,石头智能洗地机A10 Plus。从这个产品名上就不难看出,这次石头推出的并不是常见的扫地机器
  • 7月安卓手机性能榜:红魔8S Pro再夺榜首

    7月份的手机市场风平浪静,除了红魔和努比亚带来了两款搭载骁龙8Gen2领先版处理器的新机之外,别的也想不到有什么新品了,这也正常,通常6月7月都是手机厂商修整的时间,进入8月份之
  • 多线程开发带来的问题与解决方法

    使用多线程主要会带来以下几个问题:(一)线程安全问题  线程安全问题指的是在某一线程从开始访问到结束访问某一数据期间,该数据被其他的线程所修改,那么对于当前线程而言,该线程
  • 自动化在DevOps中的力量:简化软件开发和交付

    自动化在DevOps中扮演着重要角色,它提升了DevOps的效能。通过自动化工具和方法,DevOps团队可以实现以下目标:消除手动和重复性任务。简化流程。在整个软件开发生命周期中实现更
  • 一个注解实现接口幂等,这样才优雅!

    场景码猿慢病云管理系统中其实高并发的场景不是很多,没有必要每个接口都去考虑并发高的场景,比如添加住院患者的这个接口,具体的业务代码就不贴了,业务伪代码如下:图片上述代码有
  • 每天一道面试题-CPU伪共享

    前言:了不起:又到了每天一到面试题的时候了!学弟,最近学习的怎么样啊 了不起学弟:最近学习的还不错,每天都在学习,每天都在进步! 了不起:那你最近学习的什么呢? 了不起学弟:最近在学习C
  • 小红书1周涨粉49W+,我总结了小白可以用的N条涨粉笔记

    作者:黄河懂运营一条性教育视频,被54万人&ldquo;珍藏&rdquo;是什么体验?最近,情感博主@公主是用鲜花做的,火了!仅仅凭借一条视频,光小红书就有超过128万人,为她疯狂点赞!更疯狂的是,这
  • 阿里大调整

    来源:产品刘有媒体报道称,近期淘宝天猫集团启动了近年来最大的人力制度改革,涉及员工绩效、层级体系等多个核心事项,目前已形成一个初步的&ldquo;征求意见版&rdquo;:1、取消P序列
  • 小米公益基金会捐赠2500万元驰援北京、河北暴雨救灾

    8月2日消息,今日小米科技创始人雷军在其微博上发布消息称,小米公益基金会宣布捐赠2500万元驰援北京、河北暴雨救灾。携手抗灾,京冀安康!以下为公告原文
Top