聊一聊 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;
#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!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:   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_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],ecx0: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 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!_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#程序员太难了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/417930.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

长风破浪会有时

工作半年了&#xff0c;实际的工作体验跟当初想象的好像是不太一样。感觉还是在学校读书好啊 方向&#xff1f; 半年没更了&#xff0c;偶尔也有些网友私信问我DSP相关的问题&#xff0c;但是我现在也没有继续做DSP了&#xff0c;很多也都忘了&#xff0c;所以可能帮不上什么…

linux第一个小程序 --- 进度条【简洁】

行缓冲区的概念 结果&#xff1a;先输入hello world然后休眠三秒后结束 当去掉’\n“ 后&#xff0c;结果就变成了先休眠三秒&#xff0c;然后打印hello world后结束。 该现象就证明了缓冲区的存在。 当缓冲区中遇到’‘\n’或者缓冲区被写满后才会被打印出来&#xff0c;在第…

Rust、Go、C ,哪个才是“内存管理大师”?

编程语言各有各的“大能”&#xff0c;但如果谈到内存管理&#xff0c;Rust的话语权不是一般的高。GC&#xff08;垃圾回收&#xff09;&#xff1f;手动分配&#xff1f;对于掌握了Rust奥义的开发者而言&#xff0c;这些词汇简直弱爆了。众所周知&#xff0c;Rust编程语言的主…

nginx基本优化

1、安装nginx隐藏版本号 1.1、查看百度web服务器 [rootcjq11 ~]# curl -I http://www.baidu.com 1.2、隐藏nginx服务器版本号 [rootcjq11 ~]# cd /usr/local/src/nginx-1.22.0/ [rootcjq11 nginx-1.22.0]# vim src/core/nginx.h第13、14行修改版本号和服务器名称 [rootcj…

洛谷P5732 【深基5.习7】杨辉三角(C语言)

入门递推题&#xff0c;就算你不是OIer也该知道的杨辉三角 同时这也是组合数的公式&#xff0c;很重要&#xff0c;因为常规组合数公式是阶乘运算会爆&#xff0c;而这个就不怎么会了 赋 arr[i][j]初值1&#xff0c;接下来就可以递推了 #include<stdio.h> int main() …

LeetCode 热题 100 | 双指针(下)

目录 42. 接雨水 1 方法一&#xff1a;我的方法 2 方法二&#xff1a;动态规划 3 方法三&#xff1a;双指针 菜鸟做题第一周&#xff0c;语言是 C 42. 接雨水 1 方法一&#xff1a;我的方法 Warning&#xff1a;这是我的智障做法&#xff0c;请勿模仿 我只能说它教会…

Oracle 12CR2 RAC部署翻车,bug避坑经历

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&am…

R 语言学习 case3:柱状图(ggchart)

主要涉及到对图的优化&#xff0c;使用ggchart工具包 ggchart 链接&#xff1a;https://thomas-neitmann.github.io/ggcharts/index.html step1: 安装工具包 install.packages("ggcharts") install.packages("tidytext")step2: 导入工具包 library(dplyr…

web渗透安全学习笔记:2、HTML基础知识

目录 前言 HTML的标题 段落链接与插入图片 HTML元素 HTML属性 HTML头部 HTML与CSS HTML与JavaScript 表格与列表 HTML区块 布局 HTML表单 HTML与数据库 音频与视频 HTML事件 前言 HTML的标题 <!DOCTYPE html> <html> <head> <meta chars…

Appium 环境配置

Appium 是一个开源的、跨平台的测试框架&#xff0c;可以用来测试 Native App、混合应用、移动 Web 应用&#xff08;H5 应用&#xff09;等&#xff0c;也是当下互联网企业实现移动自动化测试的重要工具。Appium 坚持的测试理念&#xff1a; •无需用户对 App 进行任何修改或…

[嵌入式软件][入门篇][仿真平台] STM32F103实现LED、按键

上一篇&#xff1a;[嵌入式软件][入门篇] 搭建在线仿真平台(STM32) 文章目录 一、点亮LED灯(1) 简介(2) 示例代码(3) 仿真效果&#xff08;闪烁&#xff09; 二、按键检测(1) 简介1. 按键原理2. 检测按键端口3. 消抖 (2) 示例代码1 &#xff08;按下点亮&#xff0c;松开熄灭&a…

网络安全等级保护测评规划与设计

笔者单位网络结构日益复杂&#xff0c;应用不断增多&#xff0c;使信息系统面临更多的风险。同时&#xff0c;网络攻防技术发展迅速&#xff0c;攻击的技术门槛随着自动化攻击工具的应用也在不断降低&#xff0c;勒索病毒等未知威胁也开始泛滥。基于此&#xff0c;笔者单位拟进…