聊一聊 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 ~ 0x000000353577e0000xffffd001f8b5f000~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 区间,这也算是加深了自己对操作系统的理解,也帮一些朋友解答了一些困惑!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

groovy XmlParser 递归遍历 xml 文件,修改并保存

使用 groovy.util.XmlParser 解析 xml 文件&#xff0c;对文件进行修改&#xff08;新增标签&#xff09;&#xff0c;然后保存。 是不是 XmlParser 没有提供方法遍历每个节点&#xff0c;难道要自己写&#xff1f; 什么是递归&#xff1f; 不用说&#xff0c;想必都懂得~ …

基于Pixhawk和ROS搭建自主无人车(一):底盘控制篇

参考 ArduPilot Development超维空间科技乐迪MiniPix车船使用说明书 1. 硬件篇 1.1 底盘构成一览 1.2 底盘接线示意 2. 软件篇 2.1 APM 固件下载 pixhawk 是硬件平台&#xff0c;PX4 是 pixhawk 的原生固件&#xff0c;APM&#xff08;Ardupilot Mega&#xff09;是硬件平台…

C++里main函数int main(int argc, char **argv)

C里main函数int main(int argc, char **argv), 这两个参数argc和argv分别是什么

安全帽/反光衣检测AI智能分析网关V4如何查看告警信息并进行处理?

智能分析网关V4属于高性能、低功耗的软硬一体AI边缘计算硬件设备&#xff0c;目前拥有3种型号&#xff08;8路/16路/32路&#xff09;&#xff0c;支持Caffe / DarkNet / TensorFlow / PyTorch / MXNet / ONNX / PaddlePaddle等主流深度学习框架。硬件内部署了近40种AI算法模型…

9个icon图标网站,海量免费矢量图标库!

​划到最后“阅读原文”——领取工具包&#xff08;超过1000工具&#xff0c;免费素材网站分享和行业报告&#xff09; Hi&#xff0c;我是胡猛夫~&#xff0c;专注于分享各类价值网站、高效工具&#xff01; 更多内容&#xff0c;更多资源&#xff0c;欢迎交流&#xff01;公…

MacOS安装Miniforge、Tensorflow、Jupyter Lab等(2024年最新)

大家好&#xff0c;我是邵奈一&#xff0c;一个不务正业的程序猿、正儿八经的斜杠青年。 1、世人称我为&#xff1a;被代码耽误的诗人、没天赋的书法家、五音不全的歌手、专业跑龙套演员、不合格的运动员… 2、这几年&#xff0c;我整理了很多IT技术相关的教程给大家&#xff0…

应用案例 | 基于三维机器视觉的自动化无序分拣解决方案

​ 近年来&#xff0c;电商行业蓬勃发展&#xff0c;订单的海量化、订单类型的碎片化&#xff0c;使物流行业朝着“多品种、无边界、分类广”的方向迅速发展。根据许多研究机构的预测&#xff0c;电子商务销售额预计将以每年两位数的速度增长&#xff0c;推动整个行业的规模不…

【排序】快速排序(C语言实现)

文章目录 前言1. Hoare思想2. 挖坑法3. 前后指针法4. 三路划分5. 快速排序的一些小优化5.1 三数取中常规的三数取中伪随机的三数取中 5.2 小区间优化 6. 非递归版本的快排7. 快速排序的特性总结 前言 快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法&#xff0c;其…

Leetcode 416 分割等和子集

题意理解&#xff1a; 给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集&#xff0c;使得两个子集的元素和相等。 即将数组的元素分成两组&#xff0c;每组数值sum(nums)/2 若能分成这样的两组&#xff0c;则返回true,否则返回false 本质上…

国标28181平台的手机视频监控客户端的电子地图功能对比

目 录 一、手机客户端 1、概述 2、具体功能简述 二、电子地图功能 1、经纬度定位 2、附近设备 3、实时浏览功能 4、录像回放 5、缩放功能 三、手机web客户端和CS客户端上的电子地图功能对比 1、对比表 2、测距&#xff08;PC客户端功能&#xff09; 3…

【分布式技术】rsync远程同步服务

目录 一、rsync&#xff08;远程同步&#xff09; 二、实操rsync远程文件同步 准备一个服务端192.168.20.18以及一个客户端192.168.20.30 1、服务端搭建&#xff1a;先完成服务端配置&#xff0c;启动服务 rsync拓展 1、关于rsyncd服务的端口号 2、rsync和scp的区别 2、测…

在qemu虚拟机环境下,使用kgdb调试kernel

enable kgdb的情况下&#xff0c;使用qemu启动kernel 1&#xff0c;需要先在内核配置中增加kgdb的支持 2&#xff0c;启动qemu虚拟机时&#xff0c;增加参数-s -S&#xff0c;这两个参数会使得kernel在启动之后遇到的第一个指令等待gdb连接 例子&#xff1a; /qemu-project…

爬虫之牛刀小试(三):爬取中国天气网全国天气

天气网&#xff1a; import requests from bs4 import BeautifulSoup import time from pyecharts.charts import Bar from pyecharts import options as optsurl_hb http://www.weather.com.cn/textFC/hb.shtml url_db http://www.weather.com.cn/textFC/db.shtml url_hd …

RocketMQ Dashboard可视化工具

RocketMQ Dashboard 将 RocketMQ的相关指标展示在web页面 &#xff0c;支持以可视化工具代替 Topic 配置、Broker 管理等命令行操作。 官方文档地址&#xff1a;RocketMQ Dashboard | RocketMQ 目录 1.下载安装 1.1 系统要求&#xff1a; 1.2 源码安装 1.3 访问页面 2.功…

微信小程序地图展示区轮廓+展示指定地区标点气泡

需求&#xff1a;显示当前地区所有的学校列表&#xff1a;名称。区域显示区域名称下面所属学校数量 根据用户缩小放大当前区域&#xff08;大于12显示区&#xff0c;小于12显示当前区学校列表&#xff09;&#xff0c;获取&#xff1a;regionchange的type&#xff1a;end数据&…

中央处理器CPU(1)----指令周期和微程序

前言&#xff1a;由于期末复习计算机组成效率太慢所以抽时间写一下文章总结一下思路&#xff0c;理解不是很深&#xff0c;欢迎各位不吝赐教。 由于时间不是很充分&#xff0c;所以有些考点由于我们不考试&#xff0c;一笔带过了。 我这是期末复习总结&#xff0c;不是考研知识…

【BIAI】Lecture 7 - EEG data analysis

EEG data analysis 专业术语 EEG 脑电图 excitatory postsynaptic potential(EPSP)兴奋性突触后电位 inhibitory postsynaptic potential(IPSP) 抑制性突触后电位 action potential 动作电位 dipoles 偶极子 Pyramidal neurons 椎体细胞 Axon 轴突 Dendrite 树突 Synapse 突触…

python 识别验证码图片

描述&#xff1a;利用cv2和百度的aipocr 识别有噪点的验证码图&#xff0c;百度aip需要自己注册&#xff0c;把密钥替换为自己的即可 百度智能云 测试用的图片 # encoding: utf-8 from PIL import Image # 调用百度OCR接口识别验证码 from aip import AipOcr class preserv…

R语言下载安装及VScode配置

文章目录 1. R 下载和安装1.1 下载1.2 安装 2. VSCODE 配置2.1 安装R拓展2.2 安装R语言辅助功能包2.3 DEBUG 1. R 下载和安装 1.1 下载 网址&#xff1a;https://www.r-project.org/ 选择一个镜像地址下载 选择对应的版本 一般选择base即可 1.2 安装 下载安装包后按提示安装…

2.【CPP】入门(宏||内联函数||拷贝构造||析构函数||构造函数)

0x01.引言 1.实现一个宏函数ADD #define ADD(x,y) ((x)(y))//宏是预编译阶段完成替换&#xff0c;注意括号2.宏的优缺点 优点&#xff1a; 1.增强代码的复用性 2.宏函数不用建立栈帧&#xff0c;提高性能 缺点&#xff1a; 1.不方便调试 2.没有安全检查 0x02.内联函数 1.以空…