在使用插User APC注入DLL時,經常面臨一個問題,那就是線程必須是處於Alertable模式才能注入成功。但一直對這個Alertable的含義不甚清楚,今天總算是把這個梗消化了。
微軟對Alertable與APC的執行關系有詳細的描述:
https://msdn.microsoft.com/en-us/library/ms810047.aspx
其中有一段是這樣說的:
也就是說,正常情況下,用戶模式的APC是不會打斷用戶態程序的執行流的。除非,線程是Alertable——可喚醒的。
So, what is Alertable?
想象一個應用場景:
客戶端程序每隔5分鍾就和服務端進行一次通信,實現“心跳”,最簡單的就是使用Sleep(5*60*1000)。那么這樣一來,這5分鍾內,線程就沉睡了,如果這個時候有比較緊急的網絡IO事件發生怎么辦呢?線程還在沉睡中,因為5分鍾時間還未到,所以無法及時處理這些事件。如何解決這個問題呢?那就是使用SleepEx替換Sleep。這個函數比起Sleep就多了一個參數Alertable,表示該線程是“可喚醒的”,就是說,線程雖然等待時間未到,但如果發生一些事件,線程也會及時去處理。這些事件就是:IO完成例程需要執行或者線程有APC需要交付。
內核中線程數據結構KTHREAD中的Alertable成員就是表示該線程是不是可喚醒的。這個成員會在什么時候被賦值呢?參見WRK有三個宏會設置該值:
InitializeDelayExecution()
InitializeWaitSingle()
InitializeWaitMultiple()
這三個宏分別被
SleepEx()---->KeDelayExecutionThread()
WaitForSingleObject()---->KeWaitForSingleObject()
WaitForMultipleObjects()---->KeWaitForMultipleObjects()
調用。
當上述調用發生時,線程Alertable被置為TRUE。同時,還會通過宏TestForAlertPending設置KTHREAD的另外一個成員:UserApcPending,當Alertable為TRUE,並且User APC隊列不為空,那么該值將被置為TRUE。
當從內核模式返回時,DISPATCH_USER_APC在交付用戶模式APC前會判斷這個標志,如果為FALSE,則不會交付User APC。
這也就是為什么當線程為Alertale的時候,插入的User APC才會得到執行。
還有一個問題:在進程啟動時使用驅動進行APC注入DLL的時候,並沒有去考慮這個UserApcPending標記,那為什么APC就一定能得到執行呢?
這是因為,在XP中,進程初始化重要工作LdrInitializeThunk本身就是使用APC進行執行的,所以在PspUserThreadStartup中對UserApcPending設置為了TRUE,這樣保證了初始的時候User APC能成功交付。
即使在Win7中,LdrInitializeThunk不再使用APC進行派遣,LdrInitializeThunk在執行完成后會使用NtTestAlert,同樣會設置UserApcPending為TRUE。從而在返回用戶模式時,User APC同樣能交付。
但每次交付用戶模式的時候,UserApcPending會被重置,所以線程啟動之后就不再能保證插APC能得到執行了。除非使用前面說到的那些Alertable為TRUE的等待函數,再次設置了UserApcPending。