一:背景

1. 讲故事

前几天公号里有一位朋友留言说,你windbg玩的溜,能帮我分析下被 ThreadStatic 修饰的变量到底存放在哪里吗?能不能帮我挖出来,其实这个问题问的挺深的,玩高级语言的朋友相信很少有接触到这个的,虽然很多朋友都知道这个特性怎么用,当然我也没特别研究这个,既然要回答这个问题,我得研究研究回答之!为了更好的普适性,先从简单的说起!

二:ThreadStatic 的用法

1. 普通的 static 变量

相信很多朋友在代码中都使用过 static 变量,它的好处多多,比如说我经常会用 static 去做一个进程级缓存,从而提高程序的性能,当然你也可以作为一个非常好的一级缓存,如下代码:


public class Test
{
public static Dictionary<int, string> cachedDict = new Dictionary<int, string>();
}

刚才我也说到了,这是一个进程级的缓存,多个线程都看得见,所以在多线程的环境下,你需要特别注意同步的问题。要么使用锁,要么使用 ConcurrentDictionary,我觉得这也是一个思维定式,很多时候思维总是在现有基础上去修补,去亡羊补牢,而没有跳出这个思维从根基上去处理,说这么多是什么意思呢?我举一个例子:

在市面上常见的链式跟踪框架中,比如说: Zikpin,SkyWalking,会使用一些集合去存储跟踪当前线程的一些链路信息,比如说 A -> B -> C -> D -> B -> A,常规的思维就像上面说的一样,定义一个全局 cachedDict,然后使用各种同步机制,其实你也可以降低 cachedDict 的访问作用域,将 全局访问 改成 Thread级访问,这难道不是更好的解决思路吗?

2. 用 ThreadStatic 标记 static 变量

要想做到 Thread级作用域,实现起来非常简单,在 cachedDict 上打一个 ThreadStatic 特性即可,修改代码如下:


public class Test
{
[ThreadStatic]
public static Dictionary<int, string> cachedDict = new Dictionary<int, string>();
}

接下来可以开多个线程给 cachedDict 灌数据,看看 dict 是不是 Thread级作用域,实现代码如下:


class Program
{
static void Main(string[] args)
{
var task1 = Task.Run(() =>
{
if (Test.cachedDict == null) Test.cachedDict = new Dictionary<int, string>();
Test.cachedDict.Add(1, "mary");
Test.cachedDict.Add(2, "john"); Console.WriteLine($"thread={Thread.CurrentThread.ManagedThreadId} 的 dict 有记录: {Test.cachedDict.Count}");
}); var task2 = Task.Run(() =>
{
if (Test.cachedDict == null) Test.cachedDict = new Dictionary<int, string>();
Test.cachedDict.Add(3, "python");
Test.cachedDict.Add(4, "jaskson");
Test.cachedDict.Add(5, "elen"); Console.WriteLine($"thread={Thread.CurrentThread.ManagedThreadId} 的 dict 有记录: {Test.cachedDict.Count}");
}); Console.ReadLine();
}
} public class Test
{
[ThreadStatic]
public static Dictionary<int, string> cachedDict = new Dictionary<int, string>();
}

从结果来看,确实是一个 Thread 级,而且还避免了线程间同步开销,哈哈,这么神奇的东西,难怪有读者想看看底层到底是怎么实现的。

三:用 Windbg 挖 ThreadStatic

1. 对 TEB 和 TLS 的认识

  • TEB (Thread Environment Block)

每一个线程都有一份属于自己专属的私有数据,这些数据就放在 Thread 的 TEB 中,如果你想看的话,可以在 windbg 中打印出来。


0:000> !teb
TEB at 0000001e1cdd3000
ExceptionList: 0000000000000000
StackBase: 0000001e1cf80000
StackLimit: 0000001e1cf6e000
SubSystemTib: 0000000000000000
FiberData: 0000000000001e00
ArbitraryUserPointer: 0000000000000000
Self: 0000001e1cdd3000
EnvironmentPointer: 0000000000000000
ClientId: 0000000000005980 . 0000000000005aa8
RpcHandle: 0000000000000000
Tls Storage: 000001b599d06db0
PEB Address: 0000001e1cdd2000
LastErrorValue: 0
LastStatusValue: c0000139
Count Owned Locks: 0
HardErrorMode: 0

从 teb 的结构中可以看出,既有 线程本地存储(TLS),也有异常相关信息的存储 (ExceptionList) 等等相关信息。

  • TLS (Thread Local Storage)

进程会在启动后给 TLS 分配总共 1088 个槽位,每个线程都会分配一个专属的 tlsindex 索引,并且拥有一组 slots 槽位,可以用 windbg 去验证一下。


0:000> !tls
Usage:
tls <slot> [teb]
slot: -1 to dump all allocated slots
{0-0n1088} to dump specific slot
teb: <empty> for current thread
0 for all threads in this process
<teb address> (not threadid) to dump for specific thread.
0:000> !tls -1
TLS slots on thread: 5980.5aa8
0x0000 : 0000000000000000
0x0001 : 0000000000000000
0x0002 : 0000000000000000
0x0003 : 0000000000000000
0x0004 : 0000000000000000
...
0x0019 : 0000000000000000
0x0040 : 0000000000000000 0:000> !t Lock
DBG ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
0 1 5aa8 000001B599CEED90 2a020 Preemptive 000001B59B9042F8:000001B59B905358 000001b599cdb130 1 MTA
5 2 90c 000001B599CF4930 2b220 Preemptive 0000000000000000:0000000000000000 000001b599cdb130 0 MTA (Finalizer)
7 3 74 000001B59B7272A0 102a220 Preemptive 0000000000000000:0000000000000000 000001b599cdb130 0 MTA (Threadpool Worker)
9 4 2058 000001B59B7BAFF0 1029220 Preemptive 0000000000000000:0000000000000000 000001b599cdb130 0 MTA (Threadpool Worker)

从上面的 {0-0n1088} to dump specific slot 中可以看出,进程中总会有 1088 个槽位,而且当前主线程 5aa8 拥有 27 个 slot 槽位。

好了,基本概念介绍完了,接下来准备分析一下汇编代码了。

2. 从汇编代码中寻找答案

为了更好的用 windbg 去挖,我就定义一个简单的 ThreadStatic int 变量,代码如下:


class Program
{
[ThreadStatic]
public static int i = 0; static void Main(string[] args)
{
i = 10; // 12 line var num = i; Console.ReadLine();
}
}

接下来用 !U 反汇编一下 Main 函数的代码,着重看一下第 12 行代码的 i = 10;


0:000> !U /d 00007ffbe0ae0ffb
E:\net5\ConsoleApp5\ConsoleApp5\Program.cs @ 12:
00007ffb`e0ae0fd6 48b9b0fbb7e0fb7f0000 mov rcx,7FFBE0B7FBB0h
00007ffb`e0ae0fe0 ba01000000 mov edx,1
00007ffb`e0ae0fe5 e89657a95f call coreclr!JIT_GetSharedNonGCThreadStaticBase (00007ffc`40576780)
00007ffb`e0ae0fea c7401c0a000000 mov dword ptr [rax+1Ch],0Ah

从汇编指令上来看,最后的 10 赋给了 rax+1Ch 的低32位,那 rax 的地址从哪里来的呢?可以看出核心逻辑在 JIT_GetSharedNonGCThreadStaticBase 方法内,接下来就得研究一下这个方法都干嘛了。

3. 调试核心函数 JIT_GetSharedNonGCThreadStaticBase

接下来在第 12 处设置一个断点 !bpmd Program.cs:12 处,方法的简化汇编代码如下:


coreclr!JIT_GetSharedNonGCThreadStaticBase:
00007ffc`2c38679a 448b0dd7894300 mov r9d, dword ptr [coreclr!_tls_index (00007ffc`2c7bf178)]
00007ffc`2c3867a1 654c8b042558000000 mov r8, qword ptr gs:[58h]
00007ffc`2c3867aa b908000000 mov ecx, 8
00007ffc`2c3867af 4f8b04c8 mov r8, qword ptr [r8+r9*8]
00007ffc`2c3867b3 4e8b0401 mov r8, qword ptr [rcx+r8]
00007ffc`2c3867b7 493b8060040000 cmp rax, qword ptr [r8+460h]
00007ffc`2c3867be 732b jae coreclr!JIT_GetSharedNonGCThreadStaticBase+0x6b (00007ffc`2c3867eb)
00007ffc`2c3867c0 4d8b8058040000 mov r8, qword ptr [r8+458h]
00007ffc`2c3867c7 498b04c0 mov rax, qword ptr [r8+rax*8]
00007ffc`2c3867cb 4885c0 test rax, rax
00007ffc`2c3867ce 741b je coreclr!JIT_GetSharedNonGCThreadStaticBase+0x6b (00007ffc`2c3867eb)
00007ffc`2c3867d0 8bca mov ecx, edx
00007ffc`2c3867d2 f644011801 test byte ptr [rcx+rax+18h], 1
00007ffc`2c3867d7 7412 je coreclr!JIT_GetSharedNonGCThreadStaticBase+0x6b (00007ffc`2c3867eb)
00007ffc`2c3867d9 488b4c2420 mov rcx, qword ptr [rsp+20h]
00007ffc`2c3867de 4833cc xor rcx, rsp
00007ffc`2c3867e1 e89a170600 call coreclr!__security_check_cookie (00007ffc`2c3e7f80)
00007ffc`2c3867e6 4883c438 add rsp, 38h
00007ffc`2c3867ea c3 ret

接下来我仔细分析下这里的 mov 操作。

1) dword ptr [coreclr!_tls_index (00007ffc`2c7bf178)]

这个很简单,获取该线程专属的 tls_index 索引

2) qword ptr gs:[58h]

这里的 gs:[58h] 是什么意思呢? 应该有朋友知道,gs寄存器 是专门用于存放当前线程的 teb 地址,后面的 58 表示在 teb 地址上的偏移量,那问题来了,这个地址到底指向谁了呢? 其实你可以把 teb 的数据结构给打印出来就明白了。


0:000> dt teb
coreclr!TEB
+0x000 NtTib : _NT_TIB
+0x038 EnvironmentPointer : Ptr64 Void
+0x040 ClientId : _CLIENT_ID
+0x050 ActiveRpcHandle : Ptr64 Void
+0x058 ThreadLocalStoragePointer : Ptr64 Void
+0x060 ProcessEnvironmentBlock : Ptr64 _PEB
...

上面这句 +0x058 ThreadLocalStoragePointer : Ptr64 Void 可以看出,其实就是指向 ThreadLocalStoragePointer 。

3) qword ptr [r8+r9*8]

有了前两步的基础,这句汇编就很简单了,它做了一个索引操作: ThreadLocalStoragePointer[tls_index] ,对不对,从而获取属于该线程的 tls 内容,这个 ThreadStatic 的变量就会存放在这个数组的某一个内存块中。

后续还有一些计算偏移的逻辑运算都基于这个 ThreadLocalStoragePointer[tls_index] 之上,方法调用绕来绕去,汇编没法看哈

四:总结

总的来说,可以确定 ThreadStatic 变量 确实是存放在 TEB 的 ThreadLocalStoragePointer 数组中,这几天 NET5 的 CoreCLR 没有编译成功,大家如果感兴趣,可以 调试 CoreCLR + 汇编 做更深入的挖掘!

本文由博客一文多发平台 OpenWrite 发布!

最新文章

  1. 架构师养成记--15.Disruptor并发框架
  2. 【BZOJ 1911】【APIO 2010】特别行动队
  3. paip.2013年技术趋势以及热点 v3.0 cao
  4. Go语言的类型转化
  5. [Firefly引擎][学习笔记一][已完结]带用户验证的聊天室
  6. 移动硬盘安装linux系统小记
  7. Windows server 2008下开启telnet功能
  8. C#实现在线更新系统
  9. swift:打造你自己的折线图
  10. Linux下通过rm -f删除大量文件时提示&quot;-bash: /bin/rm: Argument list too long&quot;的解决方法
  11. hdu 4715 Difference Between Primes 2013年ICPC热身赛A题 素数水题
  12. apk反编译工具-apktool
  13. 删除 mysql 日志文件后 ,启动出错
  14. 教程:安装禅道zentao项目管理软件github上的开发版
  15. ThreadLocal深入剖析
  16. Python爬虫入门教程 35-100 知乎网全站用户爬虫 scrapy
  17. nginx的The page you are looking for is temporarily unavailable错误解决办法
  18. vss使用笔记
  19. (四)创建ROS程序包(就是软件包)
  20. 使用log4jdbc记录SQL信息

热门文章

  1. centos7启用EPEL Repository
  2. 第三章 MySQL的多实例
  3. Java编程思想 笔记
  4. js中!!的运用
  5. python 作业 日报模板输出
  6. SpringApplication.run(xxx.class, args)背后的东东——整体脉络
  7. P2937 [USACO09JAN]Laserphones S
  8. 【CF1443F】Identify the Operations 题解
  9. Azure DevOps 扩展之 Hub 插件的菜单权限控制配置
  10. Spring 自调用事务失效,你是怎么解决的?