5、在驅動中擷取系統時間
1)擷取啟動毫秒數
在ring3 我們可以通過一個GetTickCount 函數來獲得自系統啟動開始的毫秒數,在ring0也有一個與之對應的KeQueryTickCount 函數。不幸的是,這個函數并不能直接傳回毫秒數,它傳回的是“滴答”數,而一個時鐘“滴答”到底是多久,這在不同的系統中可能是不同的,是以我們還需要另外一個函數的輔助,即KeQueryTimeIncrement 函數。KeQueryTimeIncrement 函數可以傳回一個“滴答”表示多少個100 納秒,注意這裡的機關是100 納秒。
2)擷取系統時間
在ring3 擷取系統時間是非常簡單的,我們直接使用GetLocalTime 就可以通過一個系統時間結構體SYSTEMTIME 來傳回目前時間。到了ring0我們可以使用KeQuerySystemTime來獲得目前時間,但它其實是一個格林威治時間,與ring3得到的LocalTime 不同,是以我們還需要使用ExSystemTimeToLocalTime
函數将這個格林威治時間轉換成當地時間。事情到這裡還沒有結束,現在我們獲得的當地時間不是一個容易閱讀的格式,是以我們還要使用RltTimeToTimeFieldh 函數将其轉換成容易閱讀的格式。
3)兩個小例程
代碼
1 /************************************************************************
2
3 * 函數名稱:MyGetTickCount
4
5 * 功能描述:擷取tick數目
6
7 * 參數清單:
8
9 * 傳回 值:傳回狀态
10
11 *************************************************************************/
12
13 VOID
14
15 MyGetTickCount()
16
17 {
18
19 LARGE_INTEGER tick_count;
20
21 ULONG inc;
22
23 inc = KeQueryTimeIncrement();
24
25 KeQueryTickCount(&tick_count);
26
27 // 因為1 毫秒等于1000000 納秒,而inc 的機關是100 納秒
28
29 // 是以除以10000 即得到目前毫秒數
30
31 tick_count.QuadPart *= inc;
32
33 tick_count.QuadPart /= 10000;
34
35 KdPrint(("[Test] TickCount : %d", tick_count.QuadPart));
36
37 }
38
39 /************************************************************************
40
41 * 函數名稱:MyGetCurrentTime
42
43 * 功能描述:擷取目前系統時間
44
45 * 參數清單:
46
47 * 傳回 值:傳回狀态
48
49 *************************************************************************/
50
51 VOID
52
53 MyGetCurrentTime()
54
55 {
56
57 LARGE_INTEGER CurrentTime;
58
59 LARGE_INTEGER LocalTime;
60
61 TIME_FIELDS TimeFiled;
62
63 static WCHAR Time_String[32] = {0};
64
65 // 這裡得到的其實是格林威治時間
66
67 KeQuerySystemTime(&CurrentTime);
68
69 // 轉換成本地時間
70
71 ExSystemTimeToLocalTime(&CurrentTime, &LocalTime);
72
73 // 把時間轉換為容易了解的形式
74
75 RtlTimeToTimeFields(&LocalTime, &TimeFiled);
76
77 KdPrint(("[Test] NowTime : %4d-%2d-%2d %2d:%2d:%2d",
78
79 TimeFiled.Year, TimeFiled.Month, TimeFiled.Day,
80
81 TimeFiled.Hour, TimeFiled.Minute, TimeFiled.Second));
82
83 }
4)定時器
使用定時器
KeSetTimer()
http://msdn.microsoft.com/en-us/library/ff553286%28VS.85%29.aspx
這個函數的原型如下:
BOOLEAN
KeSetTimer(
IN PKTIMER Timer, // 定時器
IN LARGE_INTEGER DueTime, // 延後執行的時間
IN PKDPC Dpc OPTIONAL // 要執行的回調函數結構
);
這是因為需要提供一個回調函數。初始化Dpc的函數原型如下:
VOID
KeInitializeDpc(
IN PRKDPC Dpc,
IN PKDEFERRED_ROUTINE DeferredRoutine,
IN PVOID DeferredContext
這是一個“延時執行”的過程。每次執行了後,下次就不會再被調用了。如果想要定時反複執行,就必須在每次CustomDpc(DeferredRoutine)函數被調用的時候,再次調用KeSetTimer,來保證下次還可以執行。
注意的是,CustomDpc将運作在APC中斷級。是以并不是所有的事情都可以做(在調用任何核心系統函數的時候,請注意WDK說明文檔中标明的中斷級要求。)是以要完全實作定時器的功能,需要自己封裝一些東西。下面的結構封裝了全部需要的資訊:
// 内部時鐘結構
typedef struct MY_TIMER_
{
KDPC dpc;
KTIMER timer;
PKDEFERRED_ROUTINE func;
PVOID private_context;
} MY_TIMER,*PMY_TIMER;
1 // 初始化這個結構:
3 void MyTimerInit(PMY_TIMER timer, PKDEFERRED_ROUTINE func)
5 {
7 // 請注意,我把回調函數的上下文參數設定為timer,為什麼要
9 // 這樣做呢?
11 KeInitializeDpc(&timer->dpc,sf_my_dpc_routine,timer);
13 timer->func = func;
15 KeInitializeTimer(&timer->timer);
17 return (wd_timer_h)timer;
19 }
21 // 讓這個結構中的回調函數在n毫秒之後開始運作:
23 BOOLEAN MyTimerSet(PMY_TIMER timer,ULONG msec,PVOID context)
25 {
27 LARGE_INTEGER due;
29 // 注意時間機關的轉換。這裡msec是毫秒。
30
31 due.QuadPart = -10000*msec;
33 // 使用者私有上下文。
35 timer->private_context = context;
37 return KeSetTimer(&timer->timer,due,&mytimer->dpc);
39 };
41 // 停止執行
43 VOID MyTimerDestroy(PMY_TIMER timer)
45 {
47 KeCancelTimer(&mytimer->timer);
49 };
使用結構PMY_TIMER已經比結合使用KDPC和KTIMER簡便許多。但是還是有一些要注意的地方。真正的OnTimer回調函數中,要獲得上下文,必須要從timer->private_context中獲得。此外,OnTimer中還有必要再次調用MyTimerSet(),來保證下次依然得到執行。
MyOnTimer (
IN struct _KDPC *Dpc,
IN PVOID DeferredContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
)
// 這裡傳入的上下文是timer結構,用來下次再啟動延時調用
PMY_TIMER timer = (PMY_TIMER)DeferredContext;
// 獲得使用者上下文
PVOID my_context = timer->private_context;
// 在這裡做OnTimer中要做的事情
……
// 再次調用。這裡假設每1秒執行一次
MyTimerSet(timer,1000,my_context);
};
6、在驅動中建立核心線程
1)建立
在ring3 我們可以使用CreateThread這個Win32 API 建立線程,在ring0也有與之對應的核心函數PsCreateSystemThread。
這個函數與CreateThread的使用很相似,它可以通過第一個參數傳回線程的句柄,最後兩個參數分别指定線程函數的位址和參數,在ring3我們就是這麼做的。
我們使用CreateThread建立的線程隻屬于目前程序(不過CreateRemoteThread函數可以在指定程序中建立線程),而PsCreateSystemThread 函數預設情況下建立的卻是一個系統程序,它屬于程序名為“system”,PID=4的這個程序。不過PsCreateSystemThread也是可以建立使用者線程的,這取決于它的第四個參數ProcessHandle,如果它為空,則建立的即系統線程;如果它是一個程序句柄,則建立的就是屬于該指定程序的使用者線程。
線程函數是一個非常重要的部分,它決定了該線程具有什麼樣的功能。線程函數必須按照如下規範聲明:
VOID ThreadProc(IN PVOID context);
這個VOID指針參數通過強制轉換可以達到很多特殊效果,給予了我們很大的自由度。我們還需要注意的一點,在核心裡建立的線程必須自己調用PsTerminateSystemThread 來結束自身,它不能像ring3 的線程那樣可以在執行完畢後自動結束。
NTSTATUS
PsCreateSystemThread(
OUT PHANDLE ThreadHandle,
IN ULONG DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle OPTIONAL,
OUT PCLIENT_ID ClientId OPTIONAL,
IN PKSTART_ROUTINE StartRoutine,
IN PVOID StartContext);
這個函數的參數也很多。經驗如下:ThreadHandle用來傳回句柄。放入一個句柄指針即可。DesiredAccess總是填寫0。後面三個參數都填寫NULL。最後的兩個參數一個用于改線程啟動的時候執行的函數。一個用于傳入該函數的參數。
2)線程同步
雖然多線程并不是真正的并發運作,但由于CPU配置設定的時間片很短,看起來它們就像是并發運作的一樣。
此前我們曾經介紹過自旋鎖,它就是一種典型的同步方案,不過線上程同步的時候通常不使用它,而是使用事件通知,此外還有類似ring3的臨界區、信号燈等方法。
下面我們介紹使用KEVENT事件對象進行同步的方法。
在使用KEVENT 事件對象前,需要首先調用核心函數KeInitialize Event 對其初始化,這
個函數的原型如下所示:
KeInitializeEvent(
IN PRKEVENT Event,
IN EVENT_TYPE Type,
IN BOOLEAN State);
第一個參數Event 是初始化事件對象的指針;第二個參數Type表明事件的類型。事件分兩種類型:一類是“通知事件”,對應參數為NotificationEvent,另一類是“同步事件”,對應參數為SynchronizationEvent;第三個參數State 如果為TRUE,則事件對象的初始化狀态為激發狀态,否則為未激發狀态。
如果建立的事件對象是“通知事件”,當事件對象變為激發态時,需要我們手動将其改回未激發态。如果建立的事件對象是“同步事件”,當事件對象為激發态時,如果遇到相應的KeWaitForXXXX 等核心函數,事件對象會自動變回到未激發态。設定事件的函數是KeSetEvent,可通過該函數修改事件對象的狀态。
3)一個例子
1 KEVENT kEvent;
3 VOID
5 CreateThreadTest()
7 {
9 HANDLE hThread;
11 NTSTATUS status;
13 UNICODE_STRING ustrTest;
15 // 初始化
17 KeInitializeEvent(&kEvent, SynchronizationEvent, TRUE);
19 RtlInitUnicodeString(&ustrTest, L"This is a string for test!");
21 // 建立系統線程
23 status = PsCreateSystemThread(&hThread, 0, NULL, NULL, NULL, MyThreadFunc,
25 (PVOID)(&ustrTest));
27 if (!NT_SUCCESS(status))
29 {
31 KdPrint(("[Test] CreateThread Test Failed!"));
33 }
35 ZwClose(hThread);
37 // 等待事件
39 KeWaitForSingleObject(&kEvent, Executive, KernelMode, FALSE, 0);
41 }
43 // 線程函數
45 VOID
47 MyThreadFunc( IN PVOID context )
49 {
51 PUNICODE_STRING str = (PUNICODE_STRING)context;
53 KdPrint(("[Test] %d : %wZ", (int)PsGetCurrentProcessId(), str));
55 // 設定事件對象
57 KeSetEvent(&kEvent, 0, FALSE);
59 // 結束線程
61 PsTerminateSystemThread(STATUS_SUCCESS);
63 }
參考
【1】Windows 驅動開發技術詳解
【2】http://msdn.microsoft.com/en-us/library/ff565757%28VS.85%29.aspx
【3】Windows驅動學習筆記,灰狐