一:背景 1. 一個很好奇的問題 我們在學習 C# 的過程中,總會聽到一個詞叫做 內核態 ,比如說用 C# 讀寫文件,會涉及到代碼從 用戶態 到 內核態 的切換,用 HttpClient 獲取遠端的數據,也會涉及到 用戶態 到 內核態 的切換,那到底這是個什麼樣的交互流程?畢竟我們的程式是無法操控 ...
一:背景
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# 讀取文件 的更深層理解。