zl程序教程

您现在的位置是:首页 >  硬件

当前栏目

64位内核开发第十讲,日期时间与定时器

内核日期开发 时间 64 定时器 第十
2023-09-27 14:21:04 时间

一丶定时器使用简介

这一篇主要讲一下 时间 日期 以及定时器的相关代码.

1.1 IO定时器

1.1 .1 I/O定时器的使用

在内核下 IO定时器是实现定时器的一种方式, IO定时器可以每隔 1S(一秒)来执行一次由程序员自定义的程序函数.

那么请看下面使用到的DDK函数.

主要是三个方法和一个 程序员自定义的回调例程结构.

官方网址如下: ioInitializeTimer 函数 (wdm.h) - Windows drivers | Microsoft Docs

NTSTATUS IoInitializeTimer(
  [in]           PDEVICE_OBJECT         DeviceObject,  //一个设备对象
  [in]           PIO_TIMER_ROUTINE      TimerRoutine,  //回调例程
  [in, optional] __drv_aliasesMem PVOID Context        //回调例程的参数
);
void IoStartTimer(
  [in] PDEVICE_OBJECT DeviceObject           //启动哪个设备对象的定时器
);
void IoStopTimer(
  [in] PDEVICE_OBJECT DeviceObject          //关闭哪个设备对象的定时器
);

方法只有三个,其中第一个方法只会调用一次,且是跟设备绑定的. 而剩下的方法就是告诉操作系统 启动定时器和停止定时器.

使用很简单.请看下下面的源码即可.

#include <ntifs.h>
#include <wdm.h>

//卸载函数,卸载的时候要停止计时器并且删除设备.
VOID DriverUnLoad(
    PDRIVER_OBJECT pDriverObject)
{
    IoStopTimer(pDriverObject->DeviceObject);
    IoDeleteDevice(pDriverObject->DeviceObject);
    DbgPrint("DriverUnload \r\n");
}

//自己的变量,在定时器函数中要使用.
LONG g_time_count = 0;
VOID MyTimerRoutine(
    __in struct _DEVICE_OBJECT *DeviceObject,
    __in_opt PVOID Context)
{
    InterlockedIncrement(&g_time_count);
    DbgPrint("Timeer Count = %d", g_time_count);
}
extern "C" NTSTATUS DriverEntry(
    PDRIVER_OBJECT pDriverObj,
    PUNICODE_STRING pReg)
{
    NTSTATUS status = STATUS_UNSUCCESSFUL;
    UNREFERENCED_PARAMETER(pReg);
    pDriverObj->DriverUnload = DriverUnLoad;

    //创建一个设备,名字我未指定.可以指定.
    UNICODE_STRING ustr_device_name = RTL_CONSTANT_STRING(L"");
    PDEVICE_OBJECT deviceobj;
    status = IoCreateDevice(
        pDriverObj,
        0, 
        &ustr_device_name,
        FILE_DEVICE_UNKNOWN,
        FILE_DEVICE_SECURE_OPEN,
        FALSE,
        &deviceobj);
    if (!NT_SUCCESS(status))
    {
        return STATUS_UNSUCCESSFUL;
    }
    else
    {
        //在此设备上初始化定时器并且启动定时器.
        IoInitializeTimer(deviceobj, MyTimerRoutine, NULL);
        IoStartTimer(deviceobj);
    }

    return STATUS_SUCCESS;
}

输出结果就是 一秒钟 输出一次内容.

1.1.2 使用的注意事项

使用的注意事项:

1.定时器函数是运行在 IRQL == DISPATCH_LEVEL 下的所以请不要使用非分页内存.
2. 定时器函数不一定运行在由IRP发起的线程中,所以不能直接使用用户程序的内存地址.

1.2 DPC定时器的使用

1.2.1 简介

DPC定时器使用起来更加灵活,其原理就是 在操作系统内部有一个队列,这个队列里面存放着DPC的例程(回调函数)然后操作系统依次执行这个队列里面的DPC例程.

而DPC定时器的原理就是 DPC首先和一个 KTIMER对象绑定,当设置定时器后,则每隔 x秒/毫秒/纳秒(x是我们自定义的)的时间间隔后,则会把我们的 DPC例程 插入到 操作系统的DPC队列中.

1.2.2 函数说明

根据简介所述 我们知道了 DPC 其实是和一个KTIMER绑定的. IO定时器则是跟DEVICE_OBJECT绑定的,且只能每隔1s指定一次例程, 而DPCKTIMER绑定后,其KTIMER可以设置未任意时间.所以相比 IO定时器更加灵活.

那么所使用的方法如下,也很简单.

void KeInitializeTimer(
  [out] PKTIMER Timer  //一个定时器对象指针
);

注意: Timer必须有效的存储在内存中不能被释放,初始化的Timer是没有信号的,需要调用KeSetTimer/Ex才可以工作.

void KeInitializeDpc(
  [out]          __drv_aliasesMem PRKDPC Dpc,
  [in]           PKDEFERRED_ROUTINE      DeferredRoutine,
  [in, optional] __drv_aliasesMem PVOID  DeferredContext
);

参数1: 要初始化的DPC对象的指针

参数2: 满足PKDEFERRED_ROUTINE 这个结构的自定义的例程回调函数.

参数3: 参数2的参数.自定义的我们可以给,如果给定那么当例程被调用的时候其结构中的上下文指针则是我们给定的参数.

BOOLEAN KeSetTimer(
  [in, out]      PKTIMER       Timer,   //定时器对象的指针
  [in]           LARGE_INTEGER DueTime, //时间间隔,每经历多少间隔之后把DPC插入到队列中
  [in, optional] PKDPC         Dpc      //DPC对象.
);

值得注意的是第二个参数 DueTime 如果是正数,那么代表的是绝对时间,这个时间是从1601年的1月1日到触发DPC例程的哪个时刻,单位是100ns,如果是负数,那么代表的就是间隔时间,单位同样是100ns.

使用此函数同样要注意使用此函数之后TIMER会进入操作系统的计数器队列 意思就是调用一次KeSetTimer那么只会将DPC插入一次队列,为了保证实现定时器效果,我们的KeSetTimer在外部调用之后还要在 DPC例程被调用的时候继续调用一次.

返回值 表示是否成功设置了定时器.

BOOLEAN KeCancelTimer(
  [in, out] PKTIMER unnamedParam1 //定时器对象
);

如果设置过定时器,则此函数取消定时器的设置. 这样定时器对象就不会排队了.(队列)

返回值: 如果定时器对象位于系统的计时器队列中,则此函数返回TRUE. 也就是如果设置过那么定时器对象则会在系统的定时器队列中,如果取消成功那么则返回TRUE.

1.2.3 代码实现

#include <ntifs.h>
#include <wdm.h>

KTIMER g_ktimer;
KDPC g_kdpc;
LONG g_time_count = 0;

VOID DriverUnLoad(
    PDRIVER_OBJECT pDriverObject)
{
    KeCancelTimer(&g_ktimer);
    DbgPrint("DriverUnload \r\n");
}

VOID MyTimerRoutine(
    __in struct _KDPC *Dpc,
    __in_opt PVOID DeferredContext,
    __in_opt PVOID SystemArgument1,
    __in_opt PVOID SystemArgument2)
{
    LARGE_INTEGER la_dutime = {0};
    la_dutime.QuadPart = 1000 * 1000 * -10;
    InterlockedIncrement(&g_time_count);
    DbgPrint("Timeer Count = %d", g_time_count);
    KeSetTimer(&g_ktimer, la_dutime, &g_kdpc);
}

extern "C" NTSTATUS DriverEntry(
    PDRIVER_OBJECT pDriverObj,
    PUNICODE_STRING pReg)
{
    NTSTATUS status = STATUS_UNSUCCESSFUL;
    UNREFERENCED_PARAMETER(pReg);
    pDriverObj->DriverUnload = DriverUnLoad;

    UNICODE_STRING ustr_device_name = RTL_CONSTANT_STRING(L"");
    LARGE_INTEGER la_dutime = {0};
    la_dutime.QuadPart = 1000 * 1000 * -10;
    KeInitializeTimer(&g_ktimer);
    KeInitializeDpc(&g_kdpc, MyTimerRoutine, NULL);
    KeSetTimer(&g_ktimer, la_dutime, &g_kdpc);

    return STATUS_SUCCESS;
}

程序会一秒钟 输出一次结果.

1.2.4 使用的注意事项

同1.1.2 小节一样,注意IRQL即可.

二丶等待的使用

2.1 简介

在RING3编程中 我们等待是使用 Sleep()这个API函数 而在内核中也有等待的概念.

而且等待函数有很多. 那么下面一一介绍.

2.1.1 KeWaitForSingleObject 对象等待

在RING3层我们有 WaitForSingleObject()函数和WaitForMultipleObjects()给我们使用那么在内核中一样有同样的函数.

函数如下:

NTSTATUS
KeWaitForSingleObject (
    PVOID Object,        //要等待的对象(EVENT MUTEX 信号 线程 或者计数)
    KWAIT_REASON WaitReason,//等待的原因,驱动设置为Executive,如果是用户则设置为UserRequest
    KPROCESSOR_MODE WaitMode,//等待方,调用方是内核则使用KernelMode
    BOOLEAN Alertable,//等待是可警报的,那么就设置为TRUE,否则相反.
    PLARGE_INTEGER Timeout//超时时间,为NULL则是无限等待
    );

和ring3相似,唯一不同就是 多了几个参数. 其中第一个参数就是要等待的对象

举例:

#include <ntifs.h>
#include <wdm.h>

VOID DriverUnLoad(
    PDRIVER_OBJECT pDriverObject)
{

    DbgPrint("DriverUnload \r\n");
}

extern "C" NTSTATUS DriverEntry(
    PDRIVER_OBJECT pDriverObj,
    PUNICODE_STRING pReg)
{
    NTSTATUS status = STATUS_UNSUCCESSFUL;
    UNREFERENCED_PARAMETER(pReg);
    pDriverObj->DriverUnload = DriverUnLoad;

    KEVENT kevent;
    KeInitializeEvent(&kevent, SynchronizationEvent, FALSE);
    LARGE_INTEGER ladutime = {0};
    ladutime.QuadPart = 1000 * 1000 * -10;
    KeWaitForSingleObject(&kevent, Executive, KernelMode, FALSE, &ladutime);

    return STATUS_SUCCESS;
}

2.1.2 KeDelayExecutionThread 线程睡眠

KeDelayExecutionThread 函数和 KeWaitforsingleObject一样,都是强制让线程进入睡眠状态,经过指定的时间之后线程恢复.

NTSTATUS KeDelayExecutionThread(
  [in] KPROCESSOR_MODE WaitMode, //调用方等待的模式,一般是KernelMode
  [in] BOOLEAN         Alertable,//是否是可警告的,一般是FALSE
  [in] PLARGE_INTEGER  Interval  //等待的时间,同2.1.1一样
);

KeDelayExecutionThread的返回值代表了 延迟是如何完成的.

返回代码 描述
STATUS_SUCCESS 延迟已完成,因为已用指定的时间间隔。
STATUS_ALERTED 延迟已完成,因为线程已发出警报。
STATUS_USER_APC 用户模式 APC 在指定的间隔过期之前交付。

使用:

  LARGE_INTEGER ladutime = {0};
  ladutime.QuadPart = 1000 * 1000 * -10;
  KeDelayExecutionThread(KernelMode, FALSE, &ladutime);

上面代码强制线程等待一秒

2.1.3 KeStallExecutionProcessor 忙等待

这个函数是让CPU处于忙等待状态,而不是睡眠状态.

这个方法是让CPU一直不停的等待,类似于自旋锁.这种方法浪费CPU的时间. DDK规定这个函数不宜等待时间超过50us .

使用:

KeStallExecutionProcessor(1000);

2.1.4 KTIMER WaitFor 定时器等待

定时器对象可以被KeWaitForSingleObject所等待,利用这个特性我们可以直接使用定时器来完成一个等待.

代码如下:

VOID WaitForTimer(ULONG ulMircoSecond)
{
    KTIMER timer;
    KeInitializeTimer(&timer);

    //设置定时器
    LARGE_INTEGER ladutime = {0};
    ladutime.QuadPart = ulMircoSecond * 1000 * -10;
    KeSetTimer(&timer, ladutime, NULL);

    KeWaitForSingleObject(&timer, Executive, KernelMode, FALSE, NULL);
}

值得注意得是并没有绑定 DPC,所以只会将 Timer时间插入到 计时器队列中. 我们下面只需要等待即可. 如果有DPC那么计时器队列执行得时候,则会将附加得DPC插入到 DPC队列中.

在ring3层中.我们会使用 ** GetTickCount** 这个函数,返回系统自启动到现在所经历的毫秒数.在驱动中也有一个对应的函数 KeQueryTickCount

三丶 获取系统滴答数,并进行转换.

2.1 获取滴答数与毫秒数

上面说了有对应函数获取. 但是 这个函数返回的 TickCount 并不是简单的毫秒数,所以必须结合 ** KeQueryTimeinCrement **函数来求得具体的纳秒数.

如下代码.求得实际的毫秒数. 两个函数结合使用.

代码如下;

void MyGetTickCount(PULONG  msec) //进行传出
{
    LARGE_INTEGER la;
    ULONG MyInc;
    MyInc = KeQueryTimeIncrement(); //返回滴答数

    //下方 KeQueryTickCount 的宏的原型.

    KeQueryTickCount(&la);

    la.QuadPart *= MyInc;
    la.QuadPart /= 10000;

    *msec = la.LowPart;

}

KeQueryTickCount 返回的是一个滴答数.而这个滴答数在每个系统中是不同的. 所以我们需要使用
KeQueryTimeIncrement 来进行辅助. 这个函数可以标识返回的一个 滴答是表示多少个 100纳秒 单位是100纳秒
所以上面的代码详解为如下:
1.首先获取了inc = 多少个100纳秒
2.而后KeQueryTickCount获取多少个滴答数
3.滴答数 * 实际的多少个100纳秒 = 实际的纳秒数
4.要转换为毫秒 1秒= 1000毫秒 1毫秒 = 1000000(百万)纳秒. 所以 实际的纳秒/100 = (1000000)/100 = 10000(个) 滴答.
5.直接 /= 10000 = 实际的毫秒

在内核中获得毫秒等是远远不够的.还要获取当前系统的时间.

2.2 获取年月日

上面的获取是远远不够的. 如果向进一步获取详细信息. 那么驱动中提供了一个结构

** TIME_FIELDS ** 这个结构里面提供了相信的信息.

结构如下:

typedef struct _TIME_FIELDS {
    CSHORT Year;        // range [1601...]
    CSHORT Month;       // range [1..12]
    CSHORT Day;         // range [1..31]
    CSHORT Hour;        // range [0..23]
    CSHORT Minute;      // range [0..59]
    CSHORT Second;      // range [0..59]
    CSHORT Milliseconds;// range [0..999]
    CSHORT Weekday;     // range [0..6] == [Sunday..Saturday]
} TIME_FIELDS;

想要进行获取.需要三个API函数

  1. ** KeQuerySystemTime ** 得到当前的格林威治时间
  2. ** ExSystemTimeToLocalTime** 将格林威治事件转化为本地时间
  3. ** RtlTimerToTimeFields ** 转化为人们可以阅读的 Time_File类型的事件.

函数原型:

Ps 64为下跟32位的函数是一样的.但是64位下会替换为宏.但是不影响你使用.

VOID
KeQuerySystemTime (
    _Out_ PLARGE_INTEGER CurrentTime
    );                                   //64位下会替换为宏,没有此函数类型.


NTKERNELAPI
VOID
ExSystemTimeToLocalTime (
    _In_ PLARGE_INTEGER SystemTime,
    _Out_ PLARGE_INTEGER LocalTime
    );


NTSYSAPI
VOID
NTAPI
RtlTimeToTimeFields (
    _In_ PLARGE_INTEGER Time,
    _Out_ PTIME_FIELDS TimeFields
    );

前两个函数的转化得到的都是 PLARGE_INTEGER 类型.并不是人们所能直观看到的.所以利用最后一个函数进行转化即可.

PTCHAR GetTimeYMS()
{
    //获取年月日.
    LARGE_INTEGER SystemTime;
    LARGE_INTEGER LocalTime;
    TIME_FIELDS TimeFiled;
    TCHAR *time_str = ExAllocatePoolWithTag(PagedPool, 32, 0);

    KeQuerySystemTime(&SystemTime);
    ExSystemTimeToLocalTime(&SystemTime,&LocalTime);
    RtlTimeToTimeFields(&LocalTime,&TimeFiled);
#ifdef UNICODE
#define RtlStringCchPrintf RtlStringCchPrintfW
#else
#define RtlStringCchPrintf RtlStringCchPrintfA
#endif // UNICODE

    RtlStringCchPrintf(
        time_str,
        32,
        TEXT("%4d-%2d-%2d %2d-%2d-%2d"),
        TimeFiled.Year,
        TimeFiled.Month,
        TimeFiled.Day,                //年月日时分秒
        TimeFiled.Hour,
        TimeFiled.Minute,
        TimeFiled.Second);
    return time_str;
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObj, PUNICODE_STRING pRegPath)
{





    PTCHAR pTime = NULL;
    pDriverObj->DriverUnload = DriverUnLoad;

    pTime = GetTimeYMS();


    DbgPrint("%Ls \r\n", pTime);
    return STATUS_SUCCESS;
}