聊一聊 C# 线程切换后上下文都去了哪里
一:背景
1. 讲故事
总会有一些朋友问一个问题,在 Windows 中线程做了上下文切换,请问被切的线程他的寄存器上下文都去了哪里?能不能给我挖出来?这个问题其实比较底层,如果对操作系统没有个体系层面的理解以及做过源码分析,其实很难说明白,这篇我们就从.NET高级调试
的角度试着分析一下吧。
二:寄存器上下文去哪了
1. 用户线程的两态空间
用 C#代码创建的线程在操作系统层面上来说属于 用户态线程
,这种线程拥有两个线程栈,哈哈,是不是打破了一些朋友的三观。分别为 用户态栈
和 内核态栈
。
为了方便讲解,写一段简单的测试代码,不断的调用 Sleep(1)
让代码在用户态和内核态不断的切换,也就能观察得到这两套栈空间,参考代码如下:
将程序跑起来后我们用 windbg 附加,观察这个程序的上下文,参考如下:
上面的信息非常清晰,两套栈空间 StackBase ~ StackLimit
,分别为 0x0000003535790000 ~ 0x000000353577e000
和 0xffffd001f8b5f000~0xffffd001f8b65000
。
2. 理解系统调用
理解了线程的两套栈空间之后,接下来说的就是系统调用
,简单来说就是 C#线程从 用户态
进入到 内核态
时,他的用户态寄存器上下文会存放到 _KTRAP_FRAME
结构体中,而这个结构体会放在内核态的线程栈上,有些朋友可能有点懵,画个图如下:
接下来的问题是如何验证呢?非常简单,第一种是通过 !thread
观察线程栈上的 TrapFrame 标记,第二种是提取内核线程的 _KTHREAD.TrapFrame
字段,为了方便测试,直接在 Sleep
的内核函数 NtDelayExecution
处下一个进程级别的断点,输出如下:
仔细观察上面的 RIP 和 RSP 值,都能看到它是在 Ring3 上的现场,分别对应着用户态的 ret 和 ntdll!NtDelayExecution,输出如下:
3. 内核态线程上下文切换
上一节的_KTRAP_FRAME
结构只是保存了 Ring3 -> Ring0
的现场,其实还有一个现场,很显然是调用线程执行 Sleep(1)
后让自己暂停并出让 cpu 核,为了让自己下一次得到完美的调度,此次必须要保存现场,那这个保存现场的逻辑在哪里的?其实是通过内核的 nt!KiSwapContext
函数实现的。
本来想在 nt!KiSwapContext
处下个断点,发现命中不了我的 Sleep 函数的 SwapContext,怀疑有 cli 之类的屏蔽外部中断导致的,这里只能反汇编源码了,参考如下:
上面有一句非常重要的汇编代码 rsp,qword ptr [rsi+58h]
,翻译过来就是 esp=newThread.KernelStack
,其实就是切换到新线程的内核态栈,并且在执行 nt!SwapContext 之前会进行现场保存,比如上面的 xmm 之类的寄存器,在切换完之后在新线程的同等位置上 pop 出这些现场。
最后一个问题是这个上下文保存在哪里呢?通过观察是还是在 InitialStack ~ KernelStack
之间,并且比 _KTRAP_FRAME
的位置要低,画个模型图如下:
感兴趣的朋友可以在那些能被 int 3
的 KiSwapContext 处下断点,比较下大小即可,截图如下:
三:总结
哈哈,是不是非常有意思,一个简单的 Sleep(1) 涉及到两块的寄存器上下文,并都保存在内核线程栈的 InitialStack ~ KernelStack
区间,这也算是加深了自己对操作系统的理解,也帮一些朋友解答了一些困惑!
文章转载自:一线码农
评论