Windows內核開發-6-內核機制 Kernel Mechanisms
- 2021 年 8 月 22 日
- 筆記
- Windows內核安全與驅動開發
Windows內核開發-6-內核機制 Kernel Mechanisms
一部分Windows的內核機制對於驅動開發很有幫助,還有一部分對於內核理解和調試也很有幫助。
Interrupt Request Level | 中斷請求級別 |
---|---|
Deferred Procedure Calls(DPC) | 延遲調用 |
Asynchronous Procedure Calls(APC) | 非同步調用 |
Structured Exception Handling | 異常處理 |
System Crash | 系統崩潰 |
Thread Synchronization | 執行緒同步 |
High IRQL Synchronization | 高級IRQL(中斷請求級別)同步 |
Work Items |
Interrupt Request Level(IRQL) 中斷請求級別
ISR:當要處理的執行緒多於了可用處理器的數量時,會考慮到執行緒的優先順序。同時硬體設備需要去通知系統來讓系統進程調度。比如:由磁碟驅動器執行的I/O操作,操作完後,磁碟驅動器會通過請求中斷來通知系統操作已經完成。然後該請求中斷連接到中斷控制器硬體設備,然後把請求發送到處理器進行處理。有一個問題是,哪一共執行緒來執行中斷服務程式(ISR Interrupt Service Routine)呢?
每個物理硬體中斷都有一個優先順序,叫做IRQL(Interrupt Request Level)中斷請求等級。由HAL(硬體抽象層,Windows內核中的一個東西)來決定IRQL為多少。每個處理器以及處理器的上下文都有自己的IRQL對應就像暫存器一樣,每條指令都有暫存器的值對應。可以像對待CPU的暫存器一樣來對待IRQL。
對於IRQL來說基本規則就是:處理器會執行IRQL級別高的對應的程式碼。例如:當前處理器的IRQL為0,這時有一個IRQL為5的中斷關聯進來,處理器就會在當前執行緒的內核堆棧中保存狀態,然後將處理器的IRQL提升為5,然後執行中斷服務程式(ISR Interrupt Service Routine)和執行中斷相關的程式碼。一旦執行結束,IRQL就會回到原來的環境。另一方面如果在中斷的IRQL==5的時候又有新中斷來了也是一樣的,先判斷IRQL的大小,如果大就調用新中斷如果小就等待。
發生中斷時的操作
中斷嵌套
對於以上兩張圖,有一個很明顯的情況,就是所有的ISR(Interrupt Service Routine中斷服務程式)都在首先被中斷的執行緒中完成的。Windows沒有專門的執行緒來處理中斷而是由當前在中斷處理器上運行的執行緒來處理。
當User態的程式碼執行時,IRQL總是等於0,所以在用戶態開發的時候經常也沒文檔記錄這個IRQL這個東西。大部分的內核程式碼也是伴隨IRQL==0來運行但是在內核kernel態時可以提高IRQL。
一些比較重要的IRQL:
IRQL | 作用 |
---|---|
PASSIVE_LEVEL in WDK (0) | 這是最常用的IRQL,用戶態的程式碼的IRQL就一直是一個值,由執行緒調度來進行工作。 |
APC_LEVEL (1) | 專門用於內核的APC,執行緒調度正常。 |
DISPATCH_LEVEL (2) | 分發派遣層。DPC和更低的中斷被屏蔽,不能訪問分頁記憶體,因為缺頁中斷也是在這個層。執行緒調度器也在此層,調度時只考慮優先順序。因此APC_LEVEL上的執行緒被阻塞後,可以調度執行PASSIVE_LEVEL執行緒。 |
Device IRQL | 用於硬體中斷的一系列級別(x64/ARM/ARM64 上為 3 到 11,x86 上為 3 到 26).來自 IRQL 2 的所有規則也適用於此。 |
Highest level (HIGH_LEVEL) | 這是最高的 IRQL,屏蔽所有中斷。 被一些人使用 處理鏈表操作的 API。 實際值15(x64/ARM/ARM64) 和 31 (x86)。 |
當處理器的IRQL提升到二及二以上時,執行程式碼就會有很多限制:
1:訪問不存在物理記憶體的記憶體會導致系統崩潰,這意味著從非分頁池訪問數據總是安全的,而從分頁池或用戶提供的緩衝區訪問數據是不安全的,應該避免。
2: 等待任何調度程式內核對象(例如互斥鎖或事件)會導致系統崩潰,除非等待超時為零,這仍然是允許的。
產生限制的原因:由於調度程式是在IRQL(2)上運行,因此如果處理器的IRQL大於等於2那麼就無法在處理器上運行因此就不會發生上下執行緒環境切換(用該CPU上的另一個執行緒替換該執行緒)。只有更高級別的中斷才能臨時將程式碼轉移到關聯的ISR,但是它仍然是同一個執行緒。
TIPS:在WinDbg中使用!irql可以查看當前處理器的IRQL,也可以查看指定處理器的IRQL。還可以在WinDbg中使用 !idt debugger 命令查看系統上註冊的中斷。
Raising and Lowering IRQL 提高和降低IRQL
在用戶態是不能修改IRQL的,只有內核態可以。IRQL可以被KeRaiseIrql函數提升和被KeLowerIrql函數降低。這裡提供一個程式碼片段來方便理解:
//假設當前IRQL <=DISPATCH_LEVEL 也就是IRQL(2)
KIRQL oldIrql;//KIRQL是對UCHAR的一種typedef重命名
KeRaiseIrql(DISPATCH_LEVEL,&oldIrql);
NT_ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);
KeLowerIrql(oldIrql);
如果提高了IRQL,請確保在相同的函數中降低它,從函數中只提升了原來的卻不降低是非常危險的。用了KeRaiseIrql來提高請務必用KeLowerIrql來降低
Thread Priorities vs. IRQLs 執行緒優先順序和IRQL的異同
IRQL是處理器的一個屬性,執行緒優先順序是執行緒的一個屬性,執行緒優先順序只有在IRQL<2時才有意義。但是也不能一直在IRQL>=2的狀態下,不然用戶態的程式碼無法運行。
任務管理器用一個叫做System interrupt的偽進程來描述CPU在IRQL>=2的情況下花費的時候,而Process Explorer用interrupt來描述:
Deferred Procedure Calls(DPC)延遲調用
該圖顯示了客戶端調用I/O操作時的經典表達:User下的執行緒打開文件句柄,然後調用ReadFile來讀內容。由於執行緒可以非同步調用,它幾乎馬上就可以重新獲得控制權並可以做其他工作。收到ReadFile的讀取請求的驅動程式會調用文件系統驅動程式(例如 NTFS),它可能會一直往下調用知道磁碟驅動程式,最後磁碟驅動程式對磁碟進行操作。
當硬體完成讀操作時,會發出一個中斷。該中斷會引起與之關聯的中斷服務程式(ISR Interrupt Service Routine)在設備的IRQL處執行。在該設備IRQL處執行。一個經典的ISR訪問設備硬體的獲取的結果應該是初始請求想要的結構。
完成一個請求通常是通過調用IoCompleteRequest函數來完成的,但是該函數的文檔說只能在IRQL<=DISPATCH_LEVEL(2)時才能有用。
允許ISR調用IoCompleteRequest(和類似的函數)的機制被稱為DPC(Derferred Procedure Call)
註冊ISR的驅動程式通過從非分頁池記憶體中分配KDPC結構體,並用KeInitializeDpc來初始化給後面DPC做調用準備。當ISR被調用時,就在快要退出函數時,ISR通過KeInsertQueueDpc函數來對其進行排隊來請求DPC儘快執行,當DPC函數執行時,就會調用IoCompleteRequest函數了。這是一種調用DPC的折中方案。它在IRQL=DISPATCH_LEVEL狀態上運行,這表示它也不能進行調度和訪問分頁記憶體。但是也不英雄。
每一個處理器都有自己的DPC隊列,在默認的情況下KeInsertQueueDpc函數將DPC插入當前處理器的DPC隊列里。當ISR(interrupt service routine中斷服務程式)調用完成即將返回前,在降低IRQL等級為之前的等級時,會檢測處理器的隊列裡面是否還有PDC,如果有處理器降低IRQL等級為DISPATH_LEVEL(2)然後以先進先出(隊列的方式)來處理隊列里的DPC直到隊列為空。處理器的IRQL等級才降為0,並回復中斷時的環境。
也可以自己訂製DPC:通過這兩個函數KeSetImportantceDpc KeSetTargetProcessorDpc.
Using DPC with a Timer 使用帶定時器的DPC
DPC最初是為了給ISR使用而創建的,但是也有別的機制。DPC可以和計時器綁定一起使用。
KTIMER結構體表示內核定時器(kernel timer)允許通過相對或者絕對時間來設置一個定時器。定時器(timer)是一個調度對象,可以用KeWaitForSingleObject等函數來等待,但是不太方便。更簡單常用的辦法是在計時器(kernel timer)中使用回調函數。
用一個例子程式來方便理解:
KTIMER Timer;
KDPC TimerDpc;
void OnTimerExpired(KDPC* Dpc, PVOID context, PVOID,PVOID)
{
UNREFERENCED_PARAMETER(Dpc);
UNREFERENCED_PARAMETER(context);
NT_ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);//處理計時器到了的情況
}
void InitializeAndStartTimer(ULONG msec)
{
KeInitializeTimer(&Timer);
KeInitializeDpc(&TimerDpc,
OnTimerExpired,
nullptr
); //把函數作為一種參數傳進去,就是回調函數
LARGE_INTEGER interval;
//相對間隔以 100 納秒為單位(並且必須為負)通過乘以 10000 轉換為毫秒
interval.QuadPart = -10000LL * msec;
KeSetTimer(&Timer, interval, &TimerDpc);
}
這段程式碼表示當計時器到期時,DPC會被插入到CPU中的DPC隊列中並儘快執行。使用DPC比普通基於IRQL(0)的回調更厲害,因為它級別比較高,保證在User太程式碼和大多數內核程式碼之前執行。
Asynchronouts Procedure Calls(APC)非同步調用
DPC被封裝成函數在IRQL==DISPATCH_LEVEL的時候被調用。
非同步調用APC也是被封裝成函數來調用。但是和DPC不同,APC是專門給特定執行緒使用,而DPC和執行緒無關。這意味著每個執行緒都有一個APC隊列,每個處理器有DPC隊列。
APC有三種:
類型 | 詳情 |
---|---|
User mode APCs | 僅當執行緒進入警報狀態時,它們才會在IRQL==PASSIVE_LEVEL的用戶模式下執行。通常通過API調用來實現,比如:SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectEx等類型API。這些API的最後一個參數可以設置為true來講執行緒處於警報狀態(alertable state)。在警報狀態下,執行緒會查看它的APC隊列,執行APC隊列里的內容,直到APC隊列為空。 |
Normal kernel mode APCs | 在內核模式下以IRQL==PASSIVE_LEVEL執行並搶佔用戶模式下的程式碼和DPC時間。 |
Special kernel APCs | 在內核模式下以IRQL==APC_LEVEL(1)執行並搶佔User下的程式碼和APC以及普通內核里的APC時間。這些APC被I/O系統來完成I/O操作。 |
APC的API在內核模式下沒有文檔記錄,所以驅動程式一般不用APC。
用戶模式可以通過調用某些 API 來使用(用戶模式)APC。例如,調用 ReadFileEx 或 WriteFileEx 啟動非同步 I/O 操作。操作完成後,用戶模式 APC 會附加到調用執行緒。如前所述,當執行緒進入可警報狀態時,該 APC 將執行。在用戶模式下顯式生成 APC 的另一個有用函數是QueueUserAPC。查看 Windows API 文檔以獲取更多資訊。
Critical Regions and Guarded Regions 關鍵區域和保護區域
關鍵區域禁止用戶態和普通內核APC執行。執行緒使用KeEnterCriticalRegion函數來進入臨界區,使用KeLeaveCriticalRegion來離開臨界區。內核編程中的某些功能需要位於關鍵區(Critical Regions)內。尤其是在使用執行資源(executive resources)時。保護區域阻止所有APC執行。KeEnterGuardedRegion和KeleaveGuardedRegion必須成套出現不然很危險。
Structured Exception Handling 異常處理
異常是由於某條指令執行某些導致處理器引發錯誤的操作而發生的事件。異常的例子包括:除數為0,斷點,頁錯誤,堆棧溢出和無效指令等。如果發生異常內核會捕獲它並在可能的情況下運行程式碼來處理異常,這種機制稱為異常處理結構(Structured Exception Handling SEH),可以用於User和Kernel。異常也是斷點實現的原理。
異常:異常和中斷概念類似,當程式中出現了某種異常,作業系統會尋找該異常的處理函數,如果沒有處理函數,就會由作業系統的默認錯誤處理函數處理,內核模式下就是直接藍屏。
回卷:程式執行到某個地方出現異常錯誤時,系統會尋找出錯點是否處於一個try{}塊中,並進入try塊中提供的異常處理函數,如果當前try中沒有就會向更外一層的try中找直到最外層try也沒有就交給作業系統處理。
內核異常處理程式由IDT(Interrupt Dispatch Table 中斷調度列表)來調用,IDT與中斷和ISR之間的映射相同。一一對應。對於Windbg來說,可以使用!idt命令來查看所有的映射。編號較低的中斷向量實際上就是異常處理程式,比如:
lkd> !idt
Dumping IDT: fffff8011d941000
00: fffff8011dd6c100 nt!KiDivideErrorFaultShadow
01: fffff8011dd6c180 nt!KiDebugTrapOrFaultShadow Stack = 0xFFFFF8011D9459D0
02: fffff8011dd6c200 nt!KiNmiInterruptShadow Stack = 0xFFFFF8011D9457D0
03: fffff8011dd6c280 nt!KiBreakpointTrapShadow
04: fffff8011dd6c300 nt!KiOverflowTrapShadow
05: fffff8011dd6c380 nt!KiBoundFaultShadow
06: fffff8011dd6c400 nt!KiInvalidOpcodeFaultShadow
07: fffff8011dd6c480 nt!KiNpxNotAvailableFaultShadow
08: fffff8011dd6c500 nt!KiDoubleFaultAbortShadow Stack = 0xFFFFF8011D9453D0
09: fffff8011dd6c580 nt!KiNpxSegmentOverrunAbortShadow
0a: fffff8011dd6c600 nt!KiInvalidTssFaultShadow
0b: fffff8011dd6c680 nt!KiSegmentNotPresentFaultShadow
0c: fffff8011dd6c700 nt!KiStackFaultShadow
0d: fffff8011dd6c780 nt!KiGeneralProtectionFaultShadow
0e: fffff8011dd6c800 nt!KiPageFaultShadow
10: fffff8011dd6c880 nt!KiFloatingErrorFaultShadow
11: fffff8011dd6c900 nt!KiAlignmentFaultShadow
一些常見的異常:
exception | description |
---|---|
Division by zero(0) 除0 | 就是除0了 |
BreakPoint(3) 斷點 | 內核將控制傳送給調試器 |
Invalid opcode(6) 無效 | CPU遇到了未知指令 |
page fault(14) 段錯誤 | 如果用於將虛擬地址轉換為物理地址的頁表條目的 Valid 位設置為零,則 CPU 會引發此錯誤,這表明(就 CPU 而言)該頁面未駐留在物理記憶體中。簡單來說就是訪問了不可訪問的地址。 |
一旦引發了異常,內核會在發生異常的函數中搜索處理程式(除了一些透明處理的異常,例如斷點BreakPoint(3)),如果沒有找到就會向上搜索調用堆棧,直到找到異常處理程式,如果堆棧耗盡,那麼系統崩潰。
Windows在C語言中提供了四個關鍵字來讓開發者完成異常處理:
關鍵字 | 描述 |
---|---|
_try | 一段可能出現問題的程式碼 |
_except | 如果程式碼出現了問題的解決辦法 |
_finally | 和異常無關,提供無論_try程式碼塊是正常退出還是異常退出都可以保證執行的程式碼。 |
_leave | 提供一種優化的機制來從 __try 塊內的某處跳轉到 _finally 塊。 |
關鍵字的有效組合是 _try/except和 _try/finally.這些關鍵字在User下和Kernel下一樣。
_try/except
這一章里,實現了一個驅動程式,其中有程式碼訪問用戶模式緩衝區的內容,這個是非常危險的。因為如果用戶模式的程式碼開闢了一個執行緒在驅動程式訪問緩衝區前就釋放掉了這個緩衝區。這個時候就會導致系統崩潰。所有永遠不能信任User下的數據,包含user下的數據就應該在_try/except塊中來處理,以確保錯誤的緩衝區不會使得驅動崩潰。
__try
{}
__except(filter_value)
{}
try裡面的函數如果出現了異常就會根據filter_value中的數值來判斷是否需要再__except裡面處理。
System Crash 系統崩潰
也就是之前經常看到網上吐槽的 Windows又又又又藍屏啦。藍屏就是Windows系統的一種系統崩潰,也叫BSOD。
這裡將會討論系統崩潰時會發生什麼以及如何應對它:
大家身為Windows內核的學習人員,千萬不要把系統藍屏當作壞東西來處理。系統藍屏其實是一種保護機制,如果再往下執行就有可能有毀滅性打擊,就直接藍屏不讓系統繼續執行了。
如果崩潰的系統連接到了一個內核的調試器的話,會在調試器中產生一個中斷,可以讓你在調試器裡面對系統的狀態進行檢查。
可以在Windows裡面進行配置使得當出現藍屏時保存一個dump文件,這個dump文件會保存系統藍屏的環境。
這裡的設置就很明顯了,是要設置成什麼樣子的,保存的目標文件地址。
轉存的類型決定了會寫入多少資訊:
類型 | 描述 |
---|---|
小記憶體轉儲 | 非常小沒啥用 |
內核記憶體轉儲 | 捕獲所有的內核記憶體,一般來說這個是夠的,因為用戶程式碼也不會搞出藍屏。 |
完整記憶體轉儲 | 提供了所有的資訊,包括User和Kernel的,可以獲得完整資訊。但是這個文件太大了。 |
自動記憶體轉儲(Windows8+) | 等同與內核記憶體轉儲,在啟動時自動調整頁面文件大小,來保證有一個合適的大小來存儲內核記憶體轉儲文件。 |
活躍記憶體轉儲(Windows10+) | 類似與完整記憶體轉儲,除了崩潰的系統有文件,否則是不會有的。有助於減小伺服器系統的轉儲文件大小。 |
崩潰轉儲資訊
一旦有了dump文件,就可以直接在WinDbg中選擇文件/打開轉儲文件來指向這個dump文件了,比如:
Microsoft (R) Windows Debugger Version 10.0.18317.1001 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
Loading Dump File [C:\Temp\MEMORY.DMP]
Kernel Bitmap Dump File: Kernel address space is available, User address space may n\
ot be available.
************* Path validation summary **************
Response Time (ms) Location
Deferred SRV*c:\Symbols*//msdl.microsoft.\
com/download/symbols
Symbol search path is: SRV*c:\Symbols*//msdl.microsoft.com/download/symbols
Executable search path is:
Windows 10 Kernel Version 18362 MP (4 procs) Free x64
Product: WinNt, suite: TerminalServer SingleUserTS
Built by: 18362.1.amd64fre.19h1_release.190318-1202
Machine Name:
Kernel base = 0xfffff803`70abc000 PsLoadedModuleList = 0xfffff803`70eff2d0
Debug session time: Wed Apr 24 15:36:55.613 2019 (UTC + 3:00)
System Uptime: 0 days 0:05:38.923
Loading Kernel Symbols
Microsoft (R) Windows Debugger Version 10.0.18317.1001 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
Loading Dump File [C:\Temp\MEMORY.DMP]
Kernel Bitmap Dump File: Kernel address space is available, User address space may n\
ot be available.
************* Path validation summary **************
Response Time (ms) Location
Deferred SRV*c:\Symbols*//msdl.microsoft.\
com/download/symbols
Symbol search path is: SRV*c:\Symbols*//msdl.microsoft.com/download/symbols
Executable search path is:
Windows 10 Kernel Version 18362 MP (4 procs) Free x64
Product: WinNt, suite: TerminalServer SingleUserTS
Built by: 18362.1.amd64fre.19h1_release.190318-1202
Machine Name:
Kernel base = 0xfffff803`70abc000 PsLoadedModuleList = 0xfffff803`70eff2d0
Debug session time: Wed Apr 24 15:36:55.613 2019 (UTC + 3:00)
System Uptime: 0 days 0:05:38.923
Loading Kernel Symbols
WinDbg調試器建議執行 !analyze -v命令來初步分析dump文件:
2: kd> !analyze -v
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************
DRIVER_IRQL_NOT_LESS_OR_EQUAL (d1)
An attempt was made to access a pageable (or completely invalid) address at an
interrupt request level (IRQL) that is too high. This is usually
caused by drivers using improper addresses.
If kernel debugger is available get stack backtrace.
Arguments:
Arg1: ffffd907b0dc7660, memory referenced
Arg2: 0000000000000002, IRQL
Arg3: 0000000000000000, value 0 = read operation, 1 = write operation
Arg4: fffff80375261530, address which referenced memory
Debugging Details:
------------------
(truncated)
DUMP_TYPE: 1
BUGCHECK_P1: ffffd907b0dc7660
BUGCHECK_P2: 2
BUGCHECK_P3: 0
BUGCHECK_P4: fffff80375261530
READ_ADDRESS: Unable to get offset of nt!_MI_VISIBLE_STATE.SpecialPool
Unable to get value of nt!_MI_VISIBLE_STATE.SessionSpecialPool
ffffd907b0dc7660 Paged pool
CURRENT_IRQL: 2
FAULTING_IP:
myfault+1530
fffff803`75261530 8b03 mov eax,dword ptr [rbx]
(truncated)
ANALYSIS_VERSION: 10.0.18317.1001 amd64fre
TRAP_FRAME: fffff98853b0f7f0 -- (.trap 0xfffff98853b0f7f0)
NOTE: The trap frame does not contain all registers.
Some register values may be zeroed or incorrect.
rax=0000000000000000 rbx=0000000000000000 rcx=ffffd90797400340
rdx=0000000000000880 rsi=0000000000000000 rdi=0000000000000000
rip=fffff80375261530 rsp=fffff98853b0f980 rbp=0000000000000002
r8=ffffd9079c5cec10 r9=0000000000000000 r10=ffffd907974002c0
r11=ffffd907b0dc1650 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei ng nz na po nc
myfault+0x1530:
fffff803`75261530 8b03 mov eax,dword ptr [rbx] ds:00000000`00000000=?\
???????
Resetting default scope
LAST_CONTROL_TRANSFER: from fffff80370c8a469 to fffff80370c78810
STACK_TEXT:
fffff988`53b0f6a8 fffff803`70c8a469 : 00000000`0000000a ffffd907`b0dc7660 00000000`0\
0000002 00000000`00000000 : nt!KeBugCheckEx
fffff988`53b0f6b0 fffff803`70c867a5 : ffff8788`e4604080 ffffff4c`c66c7010 00000000`0\
0000003 00000000`00000880 : nt!KiBugCheckDispatch+0x69
fffff988`53b0f7f0 fffff803`75261530 : ffffff4c`c66c7000 00000000`00000000 fffff988`5\
3b0f9e0 00000000`00000000 : nt!KiPageFault+0x465
fffff988`53b0f980 fffff803`75261e2d : fffff988`00000000 00000000`00000000 ffff8788`e\
c7cf520 00000000`00000000 : myfault+0x1530
fffff988`53b0f9b0 fffff803`75261f88 : ffffff4c`c66c7010 00000000`000000f0 00000000`0\
0000001 ffffff30`21ea80aa : myfault+0x1e2d
fffff988`53b0fb00 fffff803`70ae3da9 : ffff8788`e6d8e400 00000000`00000001 00000000`8\
3360018 00000000`00000001 : myfault+0x1f88
fffff988`53b0fb40 fffff803`710d1dd5 : fffff988`53b0fec0 ffff8788`e6d8e400 00000000`0\
0000001 ffff8788`ecdb6690 : nt!IofCallDriver+0x59
fffff988`53b0fb80 fffff803`710d172a : ffff8788`00000000 00000000`83360018 00000000`0\
0000000 fffff988`53b0fec0 : nt!IopSynchronousServiceTail+0x1a5
fffff988`53b0fc20 fffff803`710d1146 : 00000054`344feb28 00000000`00000000 00000000`0\
0000000 00000000`00000000 : nt!IopXxxControlFile+0x5ca
fffff988`53b0fd60 fffff803`70c89e95 : ffff8788`e4604080 fffff988`53b0fec0 00000054`3\
44feb28 fffff988`569fd630 : nt!NtDeviceIoControlFile+0x56
fffff988`53b0fdd0 00007ff8`ba39c147 : 00000000`00000000 00000000`00000000 00000000`0\
0000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x25
00000054`344feb48 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`0\
0000000 00000000`00000000 : 0x00007ff8`ba39c147
(truncated)
FOLLOWUP_IP:
myfault+1530
fffff803`75261530 8b03 mov eax,dword ptr [rbx]
FAULT_INSTR_CODE: 8d48038b
SYMBOL_STACK_INDEX: 3
SYMBOL_NAME: myfault+1530
FOLLOWUP_NAME: MachineOwner
MODULE_NAME: myfault
IMAGE_NAME: myfault.sys
(truncated)
每個dump文件有四個關鍵線索可以分析:
Arguments:
Arg1: ffffd907b0dc7660, memory referenced
Arg2: 0000000000000002, IRQL
Arg3: 0000000000000000, value 0 = read operation, 1 = write operation
Arg4: fffff80375261530, address which referenced memory
Arg1:代表被引用的記憶體地址,2:IRQL等級,3:讀操作還是寫操作,4:正在訪問的地址。
一旦獲得dump文件,可以查看WinDbg文檔:「錯誤檢查程式碼參考」(Bugcheck Code Reference)來分析。
分析轉儲文件
轉儲文件(dump文件)就是對系統的一個快照,除了無法設置斷點和執行指令以外其他的和內核調試是一樣的,比如:
指令 | 作用 |
---|---|
~ns | 切換cpu索引值,類似於切換執行緒 |
!running -t | !running命令列出崩潰時所有在處理器上運行的執行緒,-t更是顯示每個執行緒的棧 |
!stacks | 列出所有執行緒的棧 |
系統掛起
不僅僅只有系統崩潰需要導出轉儲文件來分析,還有情況也需要,比如系統卡死,死鎖了,無法響應了這種情況也需要生成dump文件來分析。
如果說系統卡死了,但是還可以響應一小部分,可以採用一個工具來將系統崩潰生成一個dump文件來分析:
//docs.microsoft.com/en-us/sysinternals/downloads/(下載地址),裡面的NotMyFault程式:
這裡選擇你要生成系統崩潰的原因,然後選擇Crash就可以了。
如果說系統完全無法響應,可以將WinDbg連接上去,來正常調試或者使用.dump指令來生成dumo文件。
如果說系統無法響應有連接不了就可以採用註冊表來手動生成dump文件。參考:
Thread Synchronization 執行緒同步
一個驅動程式可以被多個user程式調用,所以就難免會出現執行緒調度的問題,比如說一個正在改一個正在訪問,這樣就很不安全了,這也被稱為數據競爭。這種情況下,最簡單安全的辦法就是當一個執行緒訪問某個內容時,其他執行緒都不能訪問,只能等待。這樣就不會導致不安全的情況了。
Windows提供了一些原語辦法來實現執行緒同步。
互鎖Interlocked
互鎖函數提供了執行原子操作的方便方式,利用硬體特徵。
一個簡單的例子:如果有兩個執行緒同時訪問一個地址執行加1操作,沒有使用一些操作的話是有可能會導致最終結果只增加了1,而不是加2:
一些驅動程式能夠使用的互鎖函數:
函數 | 描述 |
---|---|
InterlockedIncrement/InterlockedIncrement16/InterlockedIncrement64 | 對32/16/64位的整數原子化加1 |
InterlockedDecrement/16/64 | 對32/16/64位的整數原子化減1 |
InterlockedAdd/InterlockedAdd64 | 原子化的將32/64位數加到一個變數上 |
InterlockedExchange8/16/64 | 原子化地交換32/8/16/64位整數 |
InterlockedCompareExchange/64/128 | 原子化地比較一個變數與一個值,如果相等則將提供的值交換到變數中並返回TRUE;否則,將當前的值放入變數中並返回FALSE |
這些函數在User下也可以用的。因為他們其實並不是函數,而是CPU的內聯函數–CPU的一種特殊指令。
分發器對象 Dispatcher Objects
分發器對象也叫可等待對象。這些對象有著有訊號和無訊號兩種狀態,之所以被稱為可等待對象是因為執行緒可以等待該對象從無訊號到有訊號然後再使用。這個在User態下被稱為訊號對象。
用於等待的主要函數是KeWaitForSingleObject和KeWaitForMultipleObject函數:
NTSTATUS
KeWaitForSingleObject (
PVOID Object,
KWAIT_REASON WaitReason,
KPROCESSOR_MODE WaitMode,
BOOLEAN Alertable,
PLARGE_INTEGER Timeout
);
NTSTATUS
KeWaitForMultipleObjects (
ULONG Count,
PVOID Object[],
WaitType,
KWAIT_REASON WaitReason,
KPROCESSOR_MODE WaitMode,
BOOLEAN Alertable,
PLARGE_INTEGER Timeout,
PKWAIT_BLOCK WaitBlockArray
);
參數 | 描述 |
---|---|
Object | 等待的對象而不是句柄,如果是句柄可以採用ObReferenceObjectByHandle來獲取對象指針 |
WaitReason | 等待的原因,這個列表很長,不過驅動程式通常設置為Executive,如果是用戶請求就應該設置為UserRequest |
WaitMode | 等待模式,可以是KernelMode也可以是UserMode,多數內核驅動設置為KernelMode |
Altertable | 等待過程中執行緒是否該處於警報狀態,警報狀態允許傳遞用戶模式的非同步過程調用(APC),用戶模式的APC在等待模式WaitMode設置為UserMode時可以傳遞。多數驅動程式將其設置為FALSE |
Timeout | 等待超時時間,如果為NULL表示一直等待。單位是100納秒 |
Count | 等待對象數目 |
Object[] | 等待對象的指針數組 |
WaitType | 指明要等待所有對象有訊號(WaitAll)還是只要有一個對象有訊號(WaitAny) |
WaitBlockArray | 結構數組,用於等待操作的內部管理。 |
返回值有兩種:STATUS_SUCCESS 等待完成有訊號了,STATUS_TIMEOUT:等待完成,超時。
注意返回值都是真,不能直接用返回值為真來判斷是否等待成功。
KeWaitForSingleObject和KeWaitForMultipleObject都一樣,如果指定了WaitAll只有所有都等待了才返回真。對於WaitAny如果有一個有訊號了,就會返回該有訊號的對象在對象數組中的索引。
互斥量Mutex
很經典的一個東西,用來解決多執行緒中的某個執行緒在任何時刻訪問共享資源的標準問題。
互斥量Mutex在自由的時候是有訊號的,一旦被調用這個互斥量就變成沒訊號的了,別的執行緒就無法調用它了。調用的執行緒就被稱為擁有者。對於Mutex來說,擁有關係很重要。因為:
1 如果某個執行緒擁有了它,該執行緒就是唯一可以釋放該互斥量的執行緒
2 一個互斥量能多次被統一執行緒獲取,需要注意的是使用完之後必須釋放掉,不然別的執行緒無法獲取。
要使用互斥量Mutex,需要從非分頁池(notPaged)中分配一個KMUTEX結構。互斥量的API包含了如下與KMUTEX一起工作的函數:
KeInitializeMutex:必須被調用一次來初始化互斥量。
某一個等待函數需要將分配的KMUTEX結構體的地址作為參數傳遞給它
在某個執行緒是互斥量的擁有者時需要調用KeReleaseMutex釋放互斥量
示例程式碼:
#include<ntddk.h>
KMUTEX MyMutex;
void Init()
{
KeInitializeMutex(&MyMutex, 0);
}
void DoWork()
{
//等待互斥量有訊號
KeWaitForSingleObject(&MyMutex, Executive, KernelMode, FALSE, NULL);
//自由處理
//使用完之後釋放互斥量
KeReleaseMutex(&MyMutex, FALSE);
}
這裡有一個優化,就是不管怎麼樣都需要釋放掉互斥量,所有這裡可以採用_try/__finally來使用:
#include<ntddk.h>
KMUTEX MyMutex;
void Init()
{
KeInitializeMutex(&MyMutex, 0);
}
void DoWork()
{
//等待互斥量有訊號
KeWaitForSingleObject(&MyMutex, Executive, KernelMode, FALSE, NULL);
__try
{
//自由處理
}
__finally
{
//使用完之後釋放互斥量
KeReleaseMutex(&MyMutex, FALSE);
}
}
再用C++來給它包裝成一個容器來更方便使用:
#include<ntddk.h>
class Mutex
{
public:
void Init()
{
KeInitializeMutex(&_mutex, 0);
}
void Lock()
{
KeWaitForSingleObject(&_mutex, Executive, KernelMode, FALSE, NULL);
}
void Unlock()
{
KeReleaseMutex(&_mutex, FALSE);
}
private:
KMUTEX _mutex;
};
template<typename Tlock>
class AutoLock
{
public:
AutoLock(Tlock& lock) :_lock(lock)
{
lock.Lock();
}
~AutoLock
{
_lock.Unlock();
}
private:
Tlock& _lock;
};
Mutex MyMutex;
void Init()
{
MyMutex.Init();
}
void DoWork()
{
AutoLock<Mutex> lock(MyMutex);
//自由使用
}
快速互斥量
快速互斥量是傳統互斥量的升級版。有自己的API。
特性:
1 不能遞歸獲取,不然會死鎖
2 被獲取後,CPU的IRQL會提高到APC_LEVEL(1),會阻止執行緒上的APC傳遞
3 只能無限等待,不能限制等待時間
4 User下沒有
使用流程:
從非分頁池中分配FAST_MUTEX結構並調用ExInitializeFastMutex初始化。使用ExAcquireFastMutex或ExAcquireFastMutexUnsafe來獲取,使用ExReleaseFastMutex或ExReleaseFastMutexUnsafe來釋放。
class FastMutex
{
public:
void Init();
void Lock();
void Unlock();
private:
FAST_MUTEX _mutex;
};
void FastMutex::Init()
{
ExInitializeFastMutex(&_mutex);
}
void FastMutex::Lock()
{
ExAcquireFastMutex(&_mutex);
}
void FastMutex::Unlock()
{
ExReleaseFastMutex(&_mutex);
}
訊號量
訊號量一般來限制某些東西,比如隊列的長度。訊號量的最大值和初始值(一般初始值==最大值)又KeInitalizeSemaphore來確定,當訊號量內部值大於1,此時為有訊號,等於0為無訊號。調用KeWaitForSingleObject使用時當訊號值大於一會表示等待成功然後訊號值減1。KeReleaseSemaphore會釋放訊號讓訊號值加1.
事件(event)
是一個BOOL值,真為有訊號,假為無訊號。主要目的是在某事發生時釋放訊號。來提供同步。
事件有兩種類型:
1 通知事件N(手動重置):該事件被觸發後會釋放所有正在等待的執行緒,並且狀態一直保持為有訊號,除非被顯示重置。
2: 同步事件(自動重置):被觸發後最後釋放一個執行緒。觸發後回到無訊號狀態。
創建方法:從非分頁池裡創建一個KEVENT結構,指明類型和初始狀態,然後調用KeInitalizeEvent初始化,調用KeResetEvent或KeClearEvent重置。
執行體資源(Executive Resource)
內核提供了一種單寫多讀的執行緒同步原語,就是執行體資源。
流程:非分頁池中創建ERESOURCE結構調用ExInitializeResourceLite初始化。執行緒就可以調用ExAcquireResourceExclusiveLite來獲取寫操作,調用ExAcquireResourceSharedLite來獲取共享鎖(讀),調用完之後不管什麼操作都得用ExReleaseResouceList釋放。但是必須要禁用APC。可以通過臨界區來實現:
ERESOURCE resource;
void WriteData()
{
KeEnterCriticalRegion();
ExAcquireResourceExclusiveLite(&resource, TRUE);
//隨意
ExReleaseResource(&resource);
KeLeaveCriticalRegion();
}
void DeleteResource()
{
ExDeleteResource(&resource);
}