6

C# 读写文件从用户态切到内核态,到底是个什么流程? - 一线码农

 1 year ago
source link: https://www.cnblogs.com/huangxincheng/p/16392129.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1. 一个很好奇的问题

我们在学习 C# 的过程中,总会听到一个词叫做 内核态 ,比如说用 C# 读写文件,会涉及到代码从 用户态内核态 的切换,用 HttpClient 获取远端的数据,也会涉及到 用户态内核态 的切换,那到底这是个什么样的交互流程?毕竟我们的程序是无法操控 内核态 ,今天我们就一起探索下。

二:探究两态的交互流程

1. 两个态的交界在哪里

我们知道人间和地府的交界处在 鬼门关,同样的道理 用户态内核态 的交界处在 ntdll.dll 层,画个图就像下面这样:

ca12de03e83c49df915b854ed04ce270~tplv-k3u1fbpfcp-zoom-1.image

操作系统为了保护 内核态 的代码,在用户态直接用指针肯定是不行的,毕竟一个在 ring 3,一个在 ring 0,而且 cpu 还做了硬件保护兜底,那怎么进入呢? 为了方便研究,先上一个小例子。

2. 一个简单的文件读取

我们使用 File.ReadAllLines() 实现文件读取,代码如下:


    internal class Program
    {
        public static object lockMe = new object();

        static void Main(string[] args)
        {
            var txt= File.ReadAllLines(@"D:\1.txt");

            Console.WriteLine(txt);

            Console.ReadLine();
        }
    }

在 Windows 平台上,所有内核功能对外的入口就是 Win32 Api ,言外之意,这个文件读取也需要使用它,可以在 WinDbg 中使用 bp ntdll!NtReadFile 在 鬼门关 处进行拦截。


0:000> bp ntdll!NtReadFile
breakpoint 0 redefined
0:000> g
ModLoad: 00007ffe`fdb20000 00007ffe`fdb50000   C:\Windows\System32\IMM32.DLL
ModLoad: 00007ffe`e2660000 00007ffe`e26bf000   C:\Program Files\dotnet\host\fxr\6.0.5\hostfxr.dll
Breakpoint 0 hit
ntdll!NtReadFile:
00007ffe`fe24c060 4c8bd1          mov     r10,rcx

哈哈,很顺利的拦截到了,接下来用 uf ntdll!NtReadFile 把这个方法体的汇编代码给显示出来。


0:000> uf ntdll!NtReadFile
ntdll!NtReadFile:
00007ffe`fe24c060  mov     r10,rcx
00007ffe`fe24c063  mov     eax,6
00007ffe`fe24c068  test    byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
00007ffe`fe24c070  jne     ntdll!NtReadFile+0x15 (00007ffe`fe24c075) 
00007ffe`fe24c072  syscall
00007ffe`fe24c074  ret
00007ffe`fe24c075  int     2Eh
00007ffe`fe24c077  ret

从汇编代码看,逻辑非常简单,就是一个 if 判断,决定到底是走 syscall 还是 int 2Eh,很显然不管走哪条路都可以进入到 内核态,接下来逐一聊一下。

3. int 2Eh 入关走法

相信在调试界没有人不知道 int 是干嘛的,毕竟也看过无数次的 int 3,本质上来说,在内核层维护着一张 中断向量表,每一个数字都映射着一段函数代码,当你打开电脑电源而后被 windows 接管同样借助了 中断向量表 ,好了,接下来简单看看如何寻找 3 对应的函数代码。

windbg 中有一个 !idt 命令就是用来寻找数字对应的函数代码。


lkd> !idt 3

Dumping IDT: fffff804347e1000

03:	fffff80438000f00 nt!KiBreakpointTrap

可以看到,它对应的内核层面的 nt!KiBreakpointTrap 函数,同样的道理我们看下 2E


lkd> !idt 2E

Dumping IDT: fffff804347e1000

2e:	fffff804380065c0 nt!KiSystemService

现在终于搞清楚了,进入内核态的第一个方法就是 KiSystemService,从名字看,它是一个类似的通用方法,接下来就是怎么进去到内核态相关的 读取文件 方法中呢?

要想找到这个答案,可以回头看下刚才的汇编代码 mov eax,6 ,这里的 6 就是内核态需要路由到的方法编号,哈哈,那它对应着哪一个方法呢? 由于 windows 的闭源,我们无法知道,幸好在 github 上有人列了一个清单:https://j00ru.vexillium.org/syscalls/nt/64/ ,对应着我的机器上就是。

777cd7eccd344816b8b322ef98cb832d~tplv-k3u1fbpfcp-zoom-1.image

从图中可以看到其实就是 nt!NtReadFile ,到这里我想应该真相大白了,接下来我们聊下 syscall

4. syscall 的走法

syscall 是 CPU 特别提供的一个功能,叫做 系统快速调用,言外之意,它借助了一组 MSR寄存器 帮助代码快速从 用户态 切到 内核态, 效率远比走 中断路由表 要快得多,这也就是为什么代码会有 if 判断,其实就是判断 cpu 是否支持这个功能。

刚才说到它借助了 MSR寄存器,其中一个寄存器 MSR_LSTAR 存放的是内核态入口函数地址,我们可以用 rdmsr c0000082 来看一下。


lkd> rdmsr c0000082
msr[c0000082] = fffff804`38006cc0

lkd> uf fffff804`38006cc0
nt!KiSystemCall64:
fffff804`38006cc0 0f01f8          swapgs
fffff804`38006cc3 654889242510000000 mov   qword ptr gs:[10h],rsp
fffff804`38006ccc 65488b2425a8010000 mov   rsp,qword ptr gs:[1A8h]
...

从代码中可以看到,它进入的是 nt!KiSystemCall64 函数,然后再执行后续的 6 对应的 nt!NtReadFile 完成业务逻辑,最终也由 nt!KiSystemCall64 完成 内核态 到 用户态 的切换。

知道了这两种方式,接下来可以把图稍微修补一下,增加 syscallint xxx 两种入关途径。

197c3023169b4b67adc78d91b4dee7e5~tplv-k3u1fbpfcp-zoom-1.image

通过汇编代码分析,我们终于知道了 用户态内核态 的切换原理,原来有两种途径,一个是 int 2e,一个是 syscall ,加深了我们对 C# 读取文件 的更深层理解。

图片名称

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK