天天看點

9、Windows驅動開發技術詳解筆記(5) 基本文法回顧

5、在驅動中擷取系統時間

1)擷取啟動毫秒數

 在ring3 我們可以通過一個GetTickCount 函數來獲得自系統啟動開始的毫秒數,在ring0也有一個與之對應的KeQueryTickCount 函數。不幸的是,這個函數并不能直接傳回毫秒數,它傳回的是“滴答”數,而一個時鐘“滴答”到底是多久,這在不同的系統中可能是不同的,是以我們還需要另外一個函數的輔助,即KeQueryTimeIncrement 函數。KeQueryTimeIncrement 函數可以傳回一個“滴答”表示多少個100 納秒,注意這裡的機關是100 納秒。

2)擷取系統時間

在ring3 擷取系統時間是非常簡單的,我們直接使用GetLocalTime 就可以通過一個系統時間結構體SYSTEMTIME 來傳回目前時間。到了ring0我們可以使用KeQuerySystemTime來獲得目前時間,但它其實是一個格林威治時間,與ring3得到的LocalTime 不同,是以我們還需要使用ExSystemTimeToLocalTime

函數将這個格林威治時間轉換成當地時間。事情到這裡還沒有結束,現在我們獲得的當地時間不是一個容易閱讀的格式,是以我們還要使用RltTimeToTimeFieldh 函數将其轉換成容易閱讀的格式。

3)兩個小例程

9、Windows驅動開發技術詳解筆記(5) 基本文法回顧
9、Windows驅動開發技術詳解筆記(5) 基本文法回顧

代碼

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;

9、Windows驅動開發技術詳解筆記(5) 基本文法回顧
9、Windows驅動開發技術詳解筆記(5) 基本文法回顧

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)一個例子

9、Windows驅動開發技術詳解筆記(5) 基本文法回顧
9、Windows驅動開發技術詳解筆記(5) 基本文法回顧

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驅動學習筆記,灰狐

繼續閱讀