聊一聊 C# 线程切换后上下文都去了哪里
一:背景
1. 讲故事
总会有一些朋友是不是问一个问题,在 Windows 中线程做了上下文切换,请问被切的线程他的寄存器上下文都去了哪里?能不能给我挖出来?这个问题其实比较底层,如果对操作系统没有个体系层面的理解以及做过源码分析,其实很难说明白,这篇我们就从.NET高级调试的角度试着分析一下吧。
二:寄存器上下文去哪了
1. 用户线程的两态空间
用C#代码创建的线程在操作系统层面上来说属于 用户态线程,这种线程拥有两个线程栈,哈哈,是不是打破了一些朋友的三观。分别为 用户态栈 和 内核态栈。
为了方便讲解,写一段简单的测试代码,不断的调用 Sleep(1) 让代码在用户态和内核态不断的切换,也就能观察得到这两套栈空间,参考代码如下:
static void Main(string[] args)
{
for (int i = 0; i < int.MaxValue; i++)
{
Thread.Sleep(1);
Console.WriteLine($"i={i}");
}
}
将程序跑起来后我们用 windbg 附加,观察这个程序的上下文,参考如下:
0: kd> !process 0 2 ConsoleApp7.exe
PROCESS ffffe00185e33440
SessionId: 2 Cid: 0f4c Peb: 7ff73b7a8000 ParentCid: 15f4
DirBase: 1573c1000 ObjectTable: ffffc00165357840 HandleCount: <Data Not Accessible>
Image: ConsoleApp7.exe
THREAD ffffe0018917a080 Cid 0f4c.0f50 Teb: 00007ff73b7ae000 Win32Thread: ffffe00185e3db20 WAIT: (DelayExecution) UserMode Alertable
ffffffffffffffff NotificationEvent
...
2: kd> dt nt!_KTHREAD ffffe0018917a080
+0x028 InitialStack : 0xffffd001`f8b64c90 Void
+0x030 StackLimit : 0xffffd001`f8b5f000 Void
+0x038 StackBase : 0xffffd001`f8b65000 Void
...
+0x058 KernelStack : 0xffffd001`f8b63c80 Void
...
+0x0f0 Teb : 0x00007ff7`3b7ae000 Void
...
2: kd> dt ntdll!_NT_TIB 0x00007ff7`3b7ae000
+0x000 ExceptionList : (null)
+0x008 StackBase : 0x00000035`35790000 Void
+0x010 StackLimit : 0x00000035`3577e000 Void
+0x018 SubSystemTib : (null)
+0x020 FiberData : 0x00000000`00001e00 Void
+0x020 Version : 0x1e00
+0x028 ArbitraryUserPointer : (null)
+0x030 Self : 0x00007ff7`3b7ae000 _NT_TIB
...
上面的信息非常清晰,两套栈空间 StackBase ~ StackLimit,分别为 0x0000003535790000 ~ 0x000000353577e000 和 0xffffd001f8b5f000~0xffffd001f8b65000。
2. 理解系统调用
理解了线程的两套栈空间之后,接下来说的就是系统调用,简单来说就是C#线程从 用户态 进入到 内核态 时,他的用户态寄存器上下文会存放到 _KTRAP_FRAME 结构体中,而这个结构体会放在内核态的线程栈上,有些朋友可能有点懵,画个图如下:

接下来的问题是如何验证呢?非常简单,第一种是通过 !thread 观察线程栈上的 TrapFrame 标记,第二种是提取内核线程的 _KTHREAD.TrapFrame 字段,为了方便测试,直接在 Sleep 的内核函数 NtDelayExecution 处下一个进程级别的断点,输出如下:
1: kd> bp /p ffffe00185e33440 nt!NtDelayExecution
breakpoint 0 redefined
1: kd> g
Breakpoint 0 hit
nt!NtDelayExecution:
fffff802`e4e8dfb0 4883ec28 sub rsp,28h
3: kd> !thread ffffe0018917a080
THREAD ffffe0018917a080 Cid 0f4c.0f50 Teb: 00007ff73b7ae000 Win32Thread: ffffe00185e3db20 RUNNING on processor 3
IRP List:
ffffe00187633ca0: (0006,0358) Flags: 00060800 Mdl: 00000000
Not impersonating
DeviceMap ffffc0015d587160
Owning Process ffffe00185e33440 Image: ConsoleApp7.exe
Attached Process N/A Image: N/A
Wait Start TickCount 21032 Ticks: 1 (0:00:00:00.015)
Context Switch Count 8187 IdealProcessor: 3
UserTime 00:00:00.015
KernelTime 00:00:00.125
Win32 Start Address ConsoleApp7_exe!wmainCRTStartup (0x00007ff73beb3c60)
Stack Init ffffd001f8b64c90 Current ffffd001f8b64550
Base ffffd001f8b65000 Limit ffffd001f8b5f000 Call 0000000000000000
Priority 10 BasePriority 8 PriorityDecrement 2 IoPriority 2 PagePriority 5
Child-SP RetAddr : Args to Child : Call Site
ffffd001`f8b64af8 fffff802`e4be9b63 : ffffe001`8917a080 00000000`00000014 ffffffff`ffffd8f0 ffffe001`886c3fe0 : nt!NtDelayExecution
ffffd001`f8b64b00 00007ff8`cf383b6a : 00007ff8`cc0d3777 00000035`3578e198 00000000`00000001 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @ ffffd001`f8b64b00)
00000035`3578e0d8 00007ff8`cc0d3777 : 00000035`3578e198 00000000`00000001 00000000`00000000 00000000`00000000 : ntdll!NtDelayExecution+0xa
00000035`3578e0e0 00007ff8`aec355f2 : 00000035`35977a40 00000000`00000001 00000035`00000000 00000000`00000000 : KERNELBASE!SleepEx+0xa7
(Inline Function) --------`-------- : --------`-------- --------`-------- --------`-------- --------`-------- : coreclr!ClrSleepEx+0xd (Inline Function @ 00007ff8`aec355f2)
00000035`3578e180 00007ff8`aec354eb : 06000000`00000001 00007ff8`aec35450 04000000`00000001 00000000`00000000 : coreclr!Thread::UserSleep+0xb2
00000035`3578e1d0 00007ff8`4f1ea095 : 00000035`3578e3c0 00000035`3578e4b8 00000000`00000001 00000000`00000001 : coreclr!ThreadNative::Sleep+0x9b
3: kd> dt nt!_KTRAP_FRAME ffffd001`f8b64b00
...
+0x030 Rax : 0x00007ff7`3b770002
+0x038 Rcx : 0x00000035`358d33a0
+0x040 Rdx : 0x00000035`37b5c9b8
+0x048 R8 : 0x00000035`37b5c9c8
+0x050 R9 : 0x00000035`3578dd70
+0x058 R10 : 0x00007ff7`3b780022
+0x060 R11 : 0x00000035`3578e170
+0x068 GsBase : 0x00007ff7`3b7ae000
+0x068 GsSwap : 0x00007ff7`3b7ae000
...
+0x0d0 FaultAddress : 0x00000035`37b7b000
...
+0x140 Rbx : 1
+0x148 Rdi : 0
+0x150 Rsi : 1
+0x158 Rbp : 0x503b1
+0x168 Rip : 0x7ff8cf383b6a [Type: unsigned __int64]
+0x180 Rsp : 0x353578e0d8 [Type: unsigned __int64]
...
仔细观察上面的 RIP 和 RSP 值,都能看到它是在 Ring3 上的现场,分别对应着用户态的 ret 和 ntdll!NtDelayExecution,输出如下:
3: kd> uf 0x7ff8cf383b6a
ntdll!NtDelayExecution:
00007ff8`cf383b60 4c8bd1 mov r10,rcx
00007ff8`cf383b63 b834000000 mov eax,34h
00007ff8`cf383b68 0f05 syscall
00007ff8`cf383b6a c3 ret
3: kd> k
# Child-SP RetAddr Call Site
00 ffffd001`f8b64af8 fffff802`e4be9b63 nt!NtDelayExecution
01 ffffd001`f8b64b00 00007ff8`cf383b6a nt!KiSystemServiceCopyEnd+0x13
02 00000035`3578e0d8 00007ff8`cc0d3777 ntdll!NtDelayExecution+0xa
03 00000035`3578e0e0 00007ff8`aec355f2 KERNELBASE!SleepEx+0xa7
04 (Inline Function) --------`-------- coreclr!ClrSleepEx+0xd
05 00000035`3578e180 00007ff8`aec354eb coreclr!Thread::UserSleep+0xb2
06 00000035`3578e1d0 00007ff8`4f1ea095 coreclr!ThreadNative::Sleep+0x9b
07 00000035`3578e320 00000035`3578e3c0 0x00007ff8`4f1ea095
3. 内核态线程上下文切换
上一节的_KTRAP_FRAME结构只是保存了 Ring3 -> Ring0 的现场,其实还有一个现场,很显然是调用线程执行 Sleep(1) 后让自己暂停并出让cpu核,为了让自己下一次得到完美的调度,此次必须要保存现场,那这个保存现场的逻辑在哪里的?其实是通过内核的 nt!KiSwapContext 函数实现的。
本来想在 nt!KiSwapContext 处下个断点,发现命中不了我的 Sleep 函数的 SwapContext,怀疑有cli之类的屏蔽外部中断导致的,这里只能反汇编源码了,参考如下:
3: kd> uf nt!KiSwapContext
nt!KiSwapContext:
fffff802`e4be3f30 4881ec38010000 sub rsp,138h
fffff802`e4be3f37 488d842400010000 lea rax,[rsp+100h]
fffff802`e4be3f3f 0f29742430 movaps xmmword ptr [rsp+30h],xmm6
fffff802`e4be3f44 0f297c2440 movaps xmmword ptr [rsp+40h],xmm7
fffff802`e4be3f49 440f29442450 movaps xmmword ptr [rsp+50h],xmm8
fffff802`e4be3f4f 440f294c2460 movaps xmmword ptr [rsp+60h],xmm9
fffff802`e4be3f55 440f29542470 movaps xmmword ptr [rsp+70h],xmm10
fffff802`e4be3f5b 440f295880 movaps xmmword ptr [rax-80h],xmm11
fffff802`e4be3f60 440f296090 movaps xmmword ptr [rax-70h],xmm12
fffff802`e4be3f65 440f2968a0 movaps xmmword ptr [rax-60h],xmm13
fffff802`e4be3f6a 440f2970b0 movaps xmmword ptr [rax-50h],xmm14
fffff802`e4be3f6f 440f2978c0 movaps xmmword ptr [rax-40h],xmm15
fffff802`e4be3f74 488918 mov qword ptr [rax],rbx
fffff802`e4be3f77 48897808 mov qword ptr [rax+8],rdi
fffff802`e4be3f7b 48897010 mov qword ptr [rax+10h],rsi
fffff802`e4be3f7f 4c896018 mov qword ptr [rax+18h],r12
fffff802`e4be3f83 4c896820 mov qword ptr [rax+20h],r13
fffff802`e4be3f87 4c897028 mov qword ptr [rax+28h],r14
fffff802`e4be3f8b 4c897830 mov qword ptr [rax+30h],r15
fffff802`e4be3f8f 65488b1c2520000000 mov rbx,qword ptr gs:[20h]
fffff802`e4be3f98 488bf9 mov rdi,rcx
fffff802`e4be3f9b 488bf2 mov rsi,rdx
fffff802`e4be3f9e 418bc8 mov ecx,r8d
fffff802`e4be3fa1 e8ba020000 call nt!SwapContext (fffff802`e4be4260)
fffff802`e4be3fa6 488d8c2400010000 lea rcx,[rsp+100h]
fffff802`e4be3fae 0f28742430 movaps xmm6,xmmword ptr [rsp+30h]
fffff802`e4be3fb3 0f287c2440 movaps xmm7,xmmword ptr [rsp+40h]
fffff802`e4be3fb8 440f28442450 movaps xmm8,xmmword ptr [rsp+50h]
fffff802`e4be3fbe 440f284c2460 movaps xmm9,xmmword ptr [rsp+60h]
fffff802`e4be3fc4 440f28542470 movaps xmm10,xmmword ptr [rsp+70h]
fffff802`e4be3fca 440f285980 movaps xmm11,xmmword ptr [rcx-80h]
fffff802`e4be3fcf 440f286190 movaps xmm12,xmmword ptr [rcx-70h]
fffff802`e4be3fd4 440f2869a0 movaps xmm13,xmmword ptr [rcx-60h]
fffff802`e4be3fd9 440f2871b0 movaps xmm14,xmmword ptr [rcx-50h]
fffff802`e4be3fde 440f2879c0 movaps xmm15,xmmword ptr [rcx-40h]
fffff802`e4be3fe3 488b19 mov rbx,qword ptr [rcx]
fffff802`e4be3fe6 488b7908 mov rdi,qword ptr [rcx+8]
fffff802`e4be3fea 488b7110 mov rsi,qword ptr [rcx+10h]
fffff802`e4be3fee 4c8b6118 mov r12,qword ptr [rcx+18h]
fffff802`e4be3ff2 4c8b6920 mov r13,qword ptr [rcx+20h]
fffff802`e4be3ff6 4c8b7128 mov r14,qword ptr [rcx+28h]
fffff802`e4be3ffa 4c8b7930 mov r15,qword ptr [rcx+30h]
fffff802`e4be3ffe 4881c438010000 add rsp,138h
fffff802`e4be4005 c3 ret
1: kd> uf nt!SwapContext
nt!SwapContext:
...
nt!SwapContext+0xc9:
fffff802`1a9df329 0fae5918 stmxcsr dword ptr [rcx+18h]
fffff802`1a9df32d 48896758 mov qword ptr [rdi+58h],rsp
fffff802`1a9df331 488b6658 mov rsp,qword ptr [rsi+58h]
fffff802`1a9df335 f6470380 test byte ptr [rdi+3],80h
fffff802`1a9df339 741c je nt!SwapContext+0xf7 (fffff802`1a9df357) Branch
...
上面有一句非常重要的汇编代码 rsp,qword ptr [rsi+58h],翻译过来就是 esp=newThread.KernelStack,其实就是切换到新线程的内核态栈,并且在执行 nt!SwapContext 之前会进行现场保存,比如上面的 xmm 之类的寄存器,在切换完之后在新线程的同等位置上pop出这些现场。
最后一个问题是这个上下文保存在哪里呢?通过观察是还是在 InitialStack ~ KernelStack 之间,并且比 _KTRAP_FRAME 的位置要低,画个模型图如下:

感兴趣的朋友可以在那些能被 int 3 的 KiSwapContext 处下断点,比较下大小即可,截图如下:

三:总结
哈哈,是不是非常有意思,一个简单的 Sleep(1) 涉及到两块的寄存器上下文,并都保存在内核线程栈的 InitialStack ~ KernelStack 区间,这也算是加深了自己对操作系统的理解,也帮一些朋友解答了一些困惑!

聊一聊 C# 线程切换后上下文都去了哪里的更多相关文章
- linux线程切换和进程切换
进程切换分两步: 1.切换页目录以使用新的地址空间 2.切换内核栈和硬件上下文 对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的. ...
- Flask--偏函数, 线程安全, 请求上下文
一 . 偏函数 from functools import partial def func(a, b): return a + b new_func = partial(func, 3, 4) # ...
- EventBus 线程切换原理
主要问题其实只有两个,其一:如何判断当前发送事件的线程是否是主线程:其二:如何在接收事件时指定线程并执行: 一个一个来看. 1.如何判断是否在主线程发送 EventBus在初始化的时候会初始化一个Ma ...
- Day035--Python--管道, Manager, 进程池, 线程切换
管道 #创建管道的类: Pipe([duplex]):在进程之间创建一条管道,并返回元组(conn1,conn2),其中conn1,conn2表示管道两端的连接对象,强调一点:必须在产生Process ...
- RxJava 操作符 on和doOn 线程切换 调度 Schedulers 线程池 MD
Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...
- Java线程切换(一)
(本文由言念小文原创,转载请注明出处) 一 前言有Android开发经验的同学都清楚,UI的更新必须在主线程中进行,且主线程不能被阻塞,否则系统ANR异常.我们往往做一些数据处理是耗时操作,必须要在 ...
- 将 C++/WinRT 中的线程切换体验带到 C# 中来(WPF 版本)
原文:将 C++/WinRT 中的线程切换体验带到 C# 中来(WPF 版本) 如果你要在 WPF 程序中使用线程池完成一个特殊的任务,那么使用 .NET 的 API Task.Run 并传入一个 L ...
- MHA的在线切换后的一些总结(mha方案来自网络)
mha方案来自:http://www.cnblogs.com/xuanzhi201111/p/4231412.html MHA的在线切换 192.168.2.131 [root bin]$ maste ...
- 浅谈linux线程切换问题
http://www.jb51.net/article/102059.htm 处理器总处于以下状态中的一种: 1.内核态,运行于进程上下文,内核代表进程运行于内核空间 2.内核态,运行于中断上下文,内 ...
- linux线程切换问题
处理器总处于以下状态中的一种: 1.内核态,运行于进程上下文,内核代表进程运行于内核空间: 2.内核态,运行于中断上下文,内核代表硬件运行于内核空间: 3.用户态,运行于用户空间: 一个进程的上下 ...
随机推荐
- api接口对接如何实现,php如何对接api
API接口对接是现代软件开发中不可或缺的一部分,它允许不同的应用程序之间进行数据交换和服务调用.在PHP中,可以使用多种方式实现API接口的对接,包括基于HTTP协议的传统方法以及现代的API客户端库 ...
- CodeForces 1187E Tree Painting
题意:给定一棵\(n\)个点的树 初始全是白点 要求你做\(n\)步操作,每一次选定一个与一个黑点相隔一条边的白点,将它染成黑点,然后获得该白点被染色前所在的白色联通块大小的权值. 第一次操作可以任意 ...
- 从软件工程师角度聊聊 Kubernetes
作为软件工程师,我们应该熟悉 K8s,尽管它有点像 DevOps,但它能让我们更好地了解幕后发生的事情,让我们与部署工作更密切相关,更有责任感.本文将从软件工程师的角度探讨 Kubernetes (K ...
- 后浪搞的在线版 Windows 12「GitHub 热点速览」
本周比较火的莫过于 3 位初中生开源的 Windows 12 网页版,虽然项目完成度不如在线版的 Windows 11,但是不妨一看.除了后生可畏的 win12 之外,开源不到一周的 open-int ...
- Unity 游戏开发、02 基础篇 | 知识补充、简单使用动画、动画状态机
前置笔记(由浅入深) Unity 游戏开发.01 基础篇 2 场景操作 3D场景 Q 手型工具(鼠标中键):上下左右移动场景 ALT + 鼠标左键:以视图为中心旋转 鼠标右键:以观察者为中心旋转 SH ...
- Django框架——路由控制、视图层
文章目录 1 路由控制 一 Django中路由的作用 二 简单的路由配置 三 有名分组 四 路由分发 五 反向解析 六 名称空间 七 django2.0版的path 基本示例 path转化器 注册自定 ...
- Blackmail
Blackmail Arthur Hailey The chief house officer, Ogilvie, who had declared he would appear at the Cr ...
- Linux: Authentication token is no longer valid
遇见问题: [oracle@sxty-jkdb-184:/u01/rman]crontab -l Authentication token is no longer valid; new one re ...
- Go方法特性详解:简单性和高效性的充分体现
本文深入探讨了Go语言中方法的各个方面,包括基础概念.定义与声明.特性.实战应用以及性能考量.文章充满技术深度,通过实例和代码演示,力图帮助读者全面理解Go方法的设计哲学和最佳实践. 关注[TechL ...
- Go 函数的健壮性、panic异常处理、defer 机制
Go 函数的健壮性.panic异常处理.defer 机制 目录 Go 函数的健壮性.panic异常处理.defer 机制 一.函数健壮性的"三不要"原则 1.1 原则一:不要相信任 ...