寒江独钓<2>

共享的内核空间:

用户空间是各个进程隔离的,但是内核空间是共享的。就是说,每个进程看到的高2GB空间范围内的数据,都应该是一样的。如果成功修改了高2GB范围内的代码,让硬盘驱动返回失败,那么所有进程都无法读取硬盘。

内核空间是受到硬件保护的,比如X86下R0层的代码才可以访问内核空间。普通应用程序编译后都允许在Ring3层,R3层代码要调用R0层功能,一般都通过OS提供的一个入口(该入口中调用sysenter指令)来实现。

所以编写的内核模块,是运行在内核空间中,称为OS的一个模块,最终被所有需要该模块提供功能的应用程序或OS本身调用。

内核模块位于内核空间,而内核空间又被所有进程共享。因此,内核模块实际上位于任何一个进程空间中。但是任意一段代码的任意一次执行,一定是位于某个进程空间中的。这个进程是哪一个?这取决于请求的来源,处理的过程等。

PsGetCurrentProcessId获取当前进程ID。

不要误认为所有内核代码都运行在系统进程中。

windows的所谓系统进程是一个名为System的进程,Windows自身生成的一个特殊进程。这个进程在Windows XP下PID始终为4。调用PsGetCurrentProcessId就会发现内核模块中分发函数调用时,当前进程一般都不是System进程。但是DriverEntry函数被调用时,一般都位于系统进程中。这是因为Windows一般都用系统进程来加载内核模块。

数据类型:

unsigned long => ULONG

unsigned char => UCHAR

unsigned int => UINT

void => VOID

unsigned long => PULONG

unsigned char
=> PUCHAR

unsigned int => PUINT

void
=> PVOID

从x86到x64,除了所有的指针从4字节变成8字节之外,上述其他几种类型字节宽度都没有变化

返回状态NTSTATUS

NT_SUCCESS()判断一个返回值是否成功。查看帮助或者ntstatus.h,寻找错误代码。

字符串

UNICODE_STRING

该字符串可以直接打印:DbgPrint("%wZ", &str)

下面介绍几个重要的数据结构:

驱动对象

所谓windows内核对象,并不是一个C++对象。而是windows的内核程序员使用C语言对面向对象编程方式的一种模拟。

一个驱动对象代表了一个驱动程序,或者说一个内核模块。

typedef struct _DRIVER_OBJECT {
    CSHORT Type;    //类型
    CSHORT Size;    //大小

    //
    // The following links all of the devices created by a single driver
    // together on a list, and the Flags word provides an extensible flag
    // location for driver objects.
    //

    PDEVICE_OBJECT DeviceObject;    //设备对象的链表的开始
    ULONG Flags;

    //
    // The following section describes where the driver is loaded.  The count
    // field is used to count the number of times the driver has had its
    // registered reinitialization routine invoked.
    //

    PVOID DriverStart;    //这个内核模块在内核空间中的开始地址
    ULONG DriverSize;    //大小
    PVOID DriverSection;
    PDRIVER_EXTENSION DriverExtension;

    //
    // The driver name field is used by the error log thread
    // determine the name of the driver that an I/O request is/was bound.
    //

    UNICODE_STRING DriverName;    //驱动的名字

    //
    // The following section is for registry support.  Thise is a pointer
    // to the path to the hardware information in the registry
    //

    PUNICODE_STRING HardwareDatabase;

    //
    // The following section contains the optional pointer to an array of
    // alternate entry points to a driver for "fast I/O" support.  Fast I/O
    // is performed by invoking the driver routine directly with separate
    // parameters, rather than using the standard IRP call mechanism.  Note
    // that these functions may only be used for synchronous I/O, and when
    // the file is cached.
    //

    PFAST_IO_DISPATCH FastIoDispatch;    //快速IO分发函数

    //
    // The following section describes the entry points to this particular
    // driver.  Note that the major function dispatch table must be the last
    // field in the object so that it remains extensible.
    //

    PDRIVER_INITIALIZE DriverInit;
    PDRIVER_STARTIO DriverStartIo;
    PDRIVER_UNLOAD DriverUnload;    //驱动卸载函数
    PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];    //普通分发函数

} DRIVER_OBJECT;
typedef struct _DRIVER_OBJECT *PDRIVER_OBJECT;

和编写一个应用程序不同,内核模块并不生成一个进程,只是填写一组回调函数让windows来调用,而且这组回调函数必须符合windows内核规定。

这组回调包括上面的“普通分发函数”和“快速IO分发函数”。这些函数用来处理发送给这个内核模块的请求。

winobj可以显示所有的内核对象。(节点“\Driver”)

设备对象

简称DO

在内核中,大部分“消息”都以请求(IRP)的方式传递。而设备对象DEVICE_OBJECT是唯一可以接收请求的实体,任何一个请求(IRP)都是发送给某个设备对象的。(类似于窗口)。

一个DO可以代表很多东西。举个例子:

一个DO可以代表一个实际的硬盘,这个DO将接收读和写两种请求(实际更多)。但是一个DO也可以带代表一个和硬件无关的东西,比如,内核中可能有一个设备,类似“管道”的功能。一个进程写,一个进程读。这个DO与硬件无关。

typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _DEVICE_OBJECT {
    CSHORT Type;
    USHORT Size;
    LONG ReferenceCount;    //引用计数
    struct _DRIVER_OBJECT *DriverObject;    //这个设备所属的驱动对象
    struct _DEVICE_OBJECT *NextDevice;        //下一个设备对象。单向链表
    struct _DEVICE_OBJECT *AttachedDevice;
    struct _IRP *CurrentIrp;
    PIO_TIMER Timer;
    ULONG Flags;                                // See above:  DO_...
    ULONG Characteristics;                      // See ntioapi:  FILE_...
    __volatile PVPB Vpb;
    PVOID DeviceExtension;
    DEVICE_TYPE DeviceType;    //设备类型
    CCHAR StackSize;        //IRP栈大小
    union {
        LIST_ENTRY ListEntry;
        WAIT_CONTEXT_BLOCK Wcb;
    } Queue;
    ULONG AlignmentRequirement;
    KDEVICE_QUEUE DeviceQueue;
    KDPC Dpc;

    //
    //  The following field is for exclusive use by the filesystem to keep
    //  track of the number of Fsp threads currently using the device
    //

    ULONG ActiveThreadCount;
    PSECURITY_DESCRIPTOR SecurityDescriptor;
    KEVENT DeviceLock;

    USHORT SectorSize;
    USHORT Spare1;

    struct _DEVOBJ_EXTENSION  *DeviceObjectExtension;
    PVOID  Reserved;

} DEVICE_OBJECT;

驱动对象,生成多个设备对象。而windows向设备对象发送请求,但是这些请求如何处理呢?实际上,这些请求是被驱动对象的分发函数所捕获。当Windows内核向一个设备发送一个请求时,驱动对象的分发函数中的某一个会被调用。分发函数原型如下:

NTSTATUS MyDispatch(PDEVICE_OBJECT device, PIRP irp)第一个参数是请求的目标设备,第二个是请求的指针。

请求

内核中的请求

比如应用层调用API函数WriteFile写入文件数据,这些操作最终在内核中会被IO管理器翻译成请求(IRP或者其他形式,比如快速IO调用)发送往某个设备对象。

大部分请求以IRP的形式发送。该请求非常复杂。

typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _IRP {
    CSHORT Type;    //类型
    USHORT Size;    //大小

    //
    // Define the common fields used to control the IRP.
    //

    //
    // Define a pointer to the Memory Descriptor List (MDL) for this I/O
    // request.  This field is only used if the I/O is "direct I/O".
    //

    PMDL MdlAddress;    //内存描述符链表指针。实际上,这里用来描述一个缓冲区。可以想象一个内核请求一般都需要一个缓冲区(如读硬盘需要有读出缓冲区)

    //
    // Flags word - used to remember various flags.
    //

    ULONG Flags;

    //
    // The following union is used for one of three purposes:
    //
    //    1. This IRP is an associated IRP.  The field is a pointer to a master
    //       IRP.
    //
    //    2. This is the master IRP.  The field is the count of the number of
    //       IRPs which must complete (associated IRPs) before the master can
    //       complete.
    //
    //    3. This operation is being buffered and the field is the address of
    //       the system space buffer.
    //

    //下面这个共用体中有一个SystemBuffer。这是比MdlAddress稍微简单的表示缓冲区的一种方式。IRP用MdlAddress还是用SystemBuffer取决于这次请求的IO方式。总之二者都有可能。
    union {
        struct _IRP *MasterIrp;
        __volatile LONG IrpCount;
        PVOID SystemBuffer;
    } AssociatedIrp;

    //
    // Thread list entry - allows queueing the IRP to the thread pending I/O
    // request packet list.
    //

    LIST_ENTRY ThreadListEntry;

    //
    // I/O status - final status of operation.
    //

    IO_STATUS_BLOCK IoStatus;    //IO状态,一般请求完成后的返回情况

    //
    // Requestor mode - mode of the original requestor of this operation.
    //

    KPROCESSOR_MODE RequestorMode;

    //
    // Pending returned - TRUE if pending was initially returned as the
    // status for this packet.
    //

    BOOLEAN PendingReturned;

    //
    // Stack state information.
    //

    CHAR StackCount;    //IRP栈空间大小
    CHAR CurrentLocation;    //IRP当前空间

    //
    // Cancel - packet has been canceled.
    //

    BOOLEAN Cancel;

    //
    // Cancel Irql - Irql at which the cancel spinlock was acquired.
    //

    KIRQL CancelIrql;

    //
    // ApcEnvironment - Used to save the APC environment at the time that the
    // packet was initialized.
    //

    CCHAR ApcEnvironment;

    //
    // Allocation control flags.
    //

    UCHAR AllocationFlags;

    //
    // User parameters.
    //

    PIO_STATUS_BLOCK UserIosb;
    PKEVENT UserEvent;
    union {
        struct {
            union {
                PIO_APC_ROUTINE UserApcRoutine;
                PVOID IssuingProcess;
            };
            PVOID UserApcContext;
        } AsynchronousParameters;
        LARGE_INTEGER AllocationSize;
    } Overlay;

    //
    // CancelRoutine - Used to contain the address of a cancel routine supplied
    // by a device driver when the IRP is in a cancelable state.
    //

    __volatile PDRIVER_CANCEL CancelRoutine;    //用来取消一个未决请求的函数

    //
    // Note that the UserBuffer parameter is outside of the stack so that I/O
    // completion can copy data back into the user's address space without
    // having to know exactly which service was being invoked.  The length
    // of the copy is stored in the second half of the I/O status block. If
    // the UserBuffer field is NULL, then no copy is performed.
    //

    //于MdlAddress和SystemBuffer一样表示缓冲区。特性稍有不同
    PVOID UserBuffer;

    //
    // Kernel structures
    //
    // The following section contains kernel structures which the IRP needs
    // in order to place various work information in kernel controller system
    // queues.  Because the size and alignment cannot be controlled, they are
    // placed here at the end so they just hang off and do not affect the
    // alignment of other fields in the IRP.
    //

    union {

        struct {

            union {

                //
                // DeviceQueueEntry - The device queue entry field is used to
                // queue the IRP to the device driver device queue.
                //

                KDEVICE_QUEUE_ENTRY DeviceQueueEntry;

                struct {

                    //
                    // The following are available to the driver to use in
                    // whatever manner is desired, while the driver owns the
                    // packet.
                    //

                    PVOID DriverContext[4];

                } ;

            } ;

            //
            // Thread - pointer to caller's Thread Control Block.
            //

            PETHREAD Thread;    //发出这个请求的线程

            //
            // Auxiliary buffer - pointer to any auxiliary buffer that is
            // required to pass information to a driver that is not contained
            // in a normal buffer.
            //

            PCHAR AuxiliaryBuffer;

            //
            // The following unnamed structure must be exactly identical
            // to the unnamed structure used in the minipacket header used
            // for completion queue entries.
            //

            struct {

                //
                // List entry - used to queue the packet to completion queue, among
                // others.
                //

                LIST_ENTRY ListEntry;

                union {

                    //
                    // Current stack location - contains a pointer to the current
                    // IO_STACK_LOCATION structure in the IRP stack.  This field
                    // should never be directly accessed by drivers.  They should
                    // use the standard functions.
                    //

            //一个IRP栈空间元素
                    struct _IO_STACK_LOCATION *CurrentStackLocation;

                    //
                    // Minipacket type.
                    //

                    ULONG PacketType;
                };
            };

            //
            // Original file object - pointer to the original file object
            // that was used to open the file.  This field is owned by the
            // I/O system and should not be used by any other drivers.
            //

            PFILE_OBJECT OriginalFileObject;

        } Overlay;

        //
        // APC - This APC control block is used for the special kernel APC as
        // well as for the caller's APC, if one was specified in the original
        // argument list.  If so, then the APC is reused for the normal APC for
        // whatever mode the caller was in and the "special" routine that is
        // invoked before the APC gets control simply deallocates the IRP.
        //

        KAPC Apc;

        //
        // CompletionKey - This is the key that is used to distinguish
        // individual I/O operations initiated on a single file handle.
        //

        PVOID CompletionKey;

    } Tail;

} IRP, *PIRP;

这里注意所谓的IRP栈空间,这是因为一个IRP往往要传递n个设备才能得以完成。在传递过程中,有可能会有一些“中间变换”,导致请求的参数变化。为了保护这种参数变化,我们给每次中转都留下一个“栈空间”,用来保存中间参数。所以一个请求并非简单的一个输入,并等待一个输出,而是经过许多中转才得以完成。而且在中转的每个步骤,输入都可以改变,所以可变部分的输入信息保存在一个栈似的结构中。每中转一次,都使用其中一个位置。域CurrentLocation表示当前使用了哪一个。

函数调用

WDK自带的help,基本没有应用程序使用的API。几乎可以保证这里查到的所有函数都可以在内核中使用。

函数的分类

Ex-, Io-, Rtl, Ke-, Zw-, Nt-, Ps-

IoCreateFile 比ZwCreateFile更底层

有一些C运行库如:sprintf,strlen,strcpy,wcslen,wcscpy,memcpy,memset都是可以的。malloc,free,strdup不行。

基本上如果只涉及字符串和内存数据而不涉及内存管理,比如分配和释放,则可以使用。但是MS不提倡。

驱动开发模型

windows 9x时期的VXD,windowsNT时期的KDM,windows98-2000时期的WDM

书中简单区分,将一切Windows2000-Vista下未调用WDF API的驱动称为传统驱动,如果调用了WDF API的称为WDF驱动。

当然WDF驱动,是可以调用传统型驱动所调用的内核API的。

调用源

内核编程的一个显著特点是,任意一个函数往往可能有多个调用源。

主要可追溯的如下:

1. 入口函数DriverEntry和卸载函数DriverUnload

2. 各种分发函数(普通,快速IO)

3. 处理请求时设置的完成函数

4. 其他回调函数

还可能包括其他调用源,了解调用源对于以下两个问题有很大好处:

1. 处理函数可重入性

2. 考虑运行中断级

多线程安全性

P.60

调用源DriverEntry/ DriverUnload运行环境为单线程。

其他分发,回调,完成函数都是多线程运行环境。

中断级

实际编程中,比较复杂功能的内核API都要求必须在Passive级执行,比如IoCreateFile,只有简单的函数能在Dispatch级执行。Dispatch比Passive高

判断中断级:

1. 如果没有中断级的提高和降低,则函数执行时的中断级于调用源相同。

2. 如果获取自选锁则提高,释放则下降。

DriverEntry, DriverUnload, 各种分发函数 ===> Passive级

完成函数,NDIS回调 ===> Dispatch

如果当前代码确实运行在Dispatch级,但是又必须调用一个只能在Passive级的内核API,无法使用API强制降低当前中断级。可行的方法,比如生成一个线程专门去执行要执行的Passive代码。等等。

一些宏

_in_bcount(StatusBufferSize) IN PVOID StatusBuffer

说明参数是一个输入参数,而且说明StatusBuffer作为一个缓冲区,字节长度被另一个参数StatusBufferSize所指定。

pragma alloc_text

指定某个函数的可执行代码在编译后在sys文件中的位置。内核编译出来之后是一个PE格式的sys文件,这个文件的代码段(text)段中有不同的节(section),不同的节被加载到内存中后处理情况不同。

需要关心的是三个节

1. INIT节的特点是初始化完毕后就被释放。不再占用内存空间

2. PAGE节的特点是位于可以进行分页交换的内存空间,这些空间在内存紧张时可以被交换到硬盘以节省内存。

3. 如果未用上述预编译指令,则代码默认位于PAGELK节,加载后位于不可分页交换的内存空间中。


为了节省内存,可以把很多函数放在PAGE节中。但是放在PAGE节中的函数不可以在Dispatch级调用,因为这种函数的调用可能诱发缺页中断。但是缺页中断处理不能在Dispatch级完成。为此,一般都用一个宏PAGED_CODE进行测试。
原文链接: https://www.cnblogs.com/huangyong9527/archive/2012/07/26/2610398.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/56928

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月9日 上午7:45
下一篇 2023年2月9日 上午7:48

相关推荐