一、函數多線程的安全問題
函數多線程安全指的是,當一個函數在被調用但尚未返回時,如果被其他線程再次調用,其執行結果仍然是可靠的。
在用戶層編寫多線程程序時,我們通常會關注同步問題,以確保線程安全。同樣地,在內核中也需要注意以下幾點:
-
可能在多線程環境中運行的函數必須保證線程安全。如果運行在單線程環境中,則不需要考慮線程安全性,因為沒有其他線程操作。
-
如果函數A調用函數B,函數B調用函數C,且A和B都運行在同一單線程中,那么C也應保證運行在單線程中。
-
如果函數A調用函數B,函數B調用函數C,且B和C可能在多線程環境中運行,那么A也可能在多線程環境中運行。因為A會調用B和C,所以B和C在多線程環境中運行。
-
如果函數A調用函數B,函數B調用函數C,且B運行在多線程環境中,那么需要采取多線程序列化為單線程的強制措施,使得函數B在單線程中運行。
內核中的“多線程序列化為單線程的強制措施”包括互斥體和自旋鎖。
二、中斷級別
這篇博客主要討論了理論和需要注意的問題。在內核中,Kernel API有中斷級別之分。
在用戶層,代碼都是同級別運行,因此不需要關心中斷級別,但在Kernel內核中,必須關注中斷級別,否則可能會導致持續的問題。
目前,中斷級別有兩個:
- Passive級別。
- Dispatch級別。
關于這兩種級別,內核博客的前兩課有簡單介紹其本質。我們只需注意這兩種級別的區別即可。
簡單的函數運行在Dispatch級別中。因此,在調用任何Kernel API之前,都需要查詢一下中斷級別。
Dispatch級別比Passive級別高。
如何判斷中斷級別?
- 調用路徑:A -> B -> C,那么C的調用路徑就是A和B,因為是一條線。
- 調用源:A -> B -> C,那么一次遞推到A,那么C的調用源就是A。
判斷規則:
- 如果調用路徑上沒有特殊情況(導致中斷級別提高或降低的情況),那么函數執行時的中斷級別與其調用源級別相同。
- 如果調用路徑上有獲取自旋鎖,則中斷級別隨之提高;如果有釋放自旋鎖,則中斷級別降低。
內核中的中斷級別:
調用源 | 級別 |
---|---|
DriverEntry | Passive級別 |
DriverUnload | Passive級別 |
各種分發函數 | Passive級別 |
完成函數 | Dispatch級別 |
各種NDIS回調函數 | Dispatch級別 |
查詢Kernel API時,會發現有說明該API在什么級別使用。
例如:
可以看到中斷級別是…
疑問?
如果當前代碼運行在DISPATCH級別,但必須調用PASSIVE級別的API,該怎么辦?可以利用內核API降低當前中斷級別嗎?
答:
不可以。Windows代碼都是在規范的級別上運行的,任意降低或提高都會影響代碼的執行。因此,在需要調用這種API時,可以創建一個專門的線程來執行PASSIVE級別的代碼。
解決方法很多,可以在網絡上搜索相關資料。博主也是自學,所以暫時還沒有接觸到更多方法。
三、內核中宏代碼代表的意思
在內核中查看API時,可以看到許多宏,這些宏都是空宏,用于說明。
例如:
- IN:表示這個參數是傳遞進去的。
- OUT:表示這個參數是傳出參數。
例如:
__in_bcount(StatusBuffsize) IN PVOID statusBuffer;
這里的參數表示statusBuffer的大小依賴于StatusBuffsize這個參數來指定。
四、指定函數位置的預編譯指令
這個小主題本來可以放在第三個問題中,但單獨說明更重要。
#pragma alloc_text()
這個宏表示我們的函數的可執行代碼編譯出來之后在.sys文件中的位置。
.sys文件本質上是PE文件,PE文件有段和節,不同的節加載到內存中會有不同的處理情況。我們需要關注的有三種:
- INIT節:這個節的特點是初始化完畢后就會釋放,不再占用內存空間。
- PAGE節:這個節的特點是可以進行分頁交換的內存空間,即內存不夠時可以臨時放到磁盤上。
- PAGELK節:這是默認的節,如果不指定代碼放在哪里,就會放在這個節中。特點是不能進行內存交換,即不可以和磁盤交互。
我們可以指定我們的代碼放到哪個節中。
例如,入口函數只會調用一次,可以放到INIT節中,這樣初始化完后就不占用內核內存,因為內核內存是共享的,用完就沒了。
例如:
#pragma alloc_text(INIT,DriverEntry)
注意的問題:
如果將函數放入PAGE節中,代表該函數運行中可以放到磁盤上,這樣會產生缺頁中斷。因此,放到PAGE節中的函數不能調用DISPATCH級別的函數。