一:背景
1. 一個很好奇的問題
我們在學習 C# 的程序中,總會聽到一個詞叫做 內核態 ,比如說用 C# 讀寫檔案,會涉及到代碼從 用戶態 到 內核態 的切換,用 HttpClient 獲取遠端的資料,也會涉及到 用戶態 到 內核態 的切換,那到底這是個什么樣的互動流程?畢竟我們的程式是無法操控 內核態 ,今天我們就一起探索下,
二:探究兩態的互動流程
1. 兩個態的交界在哪里
我們知道人間和地府的交界處在 鬼門關,同樣的道理 用戶態 和 內核態 的交界處在 ntdll.dll 層,畫個圖就像下面這樣:
作業系統為了保護 內核態 的代碼,在用戶態直接用指標肯定是不行的,畢竟一個在 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/ ,對應著我的機器上就是,
從圖中可以看到其實就是 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 完成 內核態 到 用戶態 的切換,
知道了這兩種方式,接下來可以把圖稍微修補一下,增加 syscall 和 int xxx 兩種入關途徑,
三:總結
通過匯編代碼分析,我們終于知道了 用戶態 到 內核態 的切換原理,原來有兩種途徑,一個是 int 2e,一個是 syscall ,加深了我們對 C# 讀取檔案 的更深層理解,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/493671.html
標籤:.NET技术
上一篇:async和await詳解
