第一章:内核上级指导
1、如果没有设置DriverUnload函数指针,则一个内核模块一旦被加载就不能卸载了。
2、makefile文件内容永远也不需要改动。
3、设置断点之前系统必须已经中断。
4、驱动加载之前,设置断点不方便,手工断点如下:
#if DBG
_asm int 3
#endif
如果不是调试状态执行会直接蓝屏,断点弹出之后可以设置新的断点
5、WinDbg为双机调试,Softice可以进行单机调试但已经不再更新,吴岩峰等人开发的Syser也可以进行单机调试,100%国产
第二章:内核编程环境及其特殊性
1、在可以容纳4GB内存控件的32位Windows系统上,低2G是用户空间,高2G是内核空间。
2、用户空间是各个进程隔离的,但是内核空间是共享的。也就是说,每个进程看见的高2G控件范围内的数据都应该是一样的。
3、内核空间受硬件保护。x86架构下R0层代码才可以访问内核空间,R3层的代码要调用R0层功能时,一般通过操作系统提供的一个入口(该入口中调用sysenter指令)来实现。
4、内核模块已经位于内核空间,作为R0代码执行,所以不受任何限制,可以任意修改内核。
5、内核模块实际上位于任何一个进程空间中,但是任意一段代码的任意一次执行,一定是位于某个进程空间中的,这个进程是哪一个?取决于请求的来源、处理的过程等,PsGetCurrentProcessId函数能得到当前进程的进程号,函数返回的Handle实际上是一个进程ID
6、并不是所以代码都运行在系统进程内,Windows所谓系统进程是一个名为“System”的进程,是Windows自身生成的一个特殊进程,DriverEntry函数被调用时,一般都位于系统进程内,这是因为Windows一般都用系统进程来加载内核模块,并不说明内核代码始终运行在System进程里。
7、使用宏NT_SUCCESS()可以判断一个返回值是否成功,NTSTATUS的值可以在WDK头文件(如:ntstatus.h)中查找到
NTSTATUS MyFunction()
{
NTSTATUS status;
status = ZwCreateFile(...);
if(!NT_SUCCESS(status))
{
return status;
}
...
}
8、字符串
typedef struct _UNICODE_STRING{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
}UNICODE_STRING *PUNICODE_STRING;
--------------------------------------------------------------------------------------
UNICODE_STRING str = RTL_CONSTANT_STRING(L"first: Hello,my salary!");
DbgPrint("%wZ", &str);
--------------------------------------------------------------------------------------
UNICODE_STRING str = RTL_CONSTANT_STRING(L"Hello");
KdPrint(("Buffer:%ws\nMaxinumLength:%d\nLength:%d", str.Buffer, str.MaximumLength, str.Length));
9、内核模块并不是生成一个进程,只是填写一组回调函数让Windows来调用,而且这组回调函数必须符合Windows内核规定。
10、一个内核模块的所有功能都由普通分发函数和快速IO分发函数提供给windows。
11、大部分“消息”都以请求(IRP)的方式传递。而设备对象是唯一可以接收请求的实体,任何一个“请求”都是发送给某个设备对象的。
12、因为我们总是在内核程序中生成一个DO,而一个内核程序是用一个驱动对象表示的,所以一个设备对象总是属于一个驱动对象。一个驱动对象中有n个设备,这些设备用这个指针连接起来作为一个单向的链表。
13、驱动对象生成多个设备对象,而windows向设备对象发送请求,但是这些请求如何处理呢?实际上,这些请求是被驱动对象的分发函数所捕获的。当windows内核向一个设备发送一个请求时,驱动对象的分发函数中的某一个会被调用。
14、如WriteFile这些操作最终在内核中会被IO管理器翻译成请求(IRP或者与之等效的其它形式,比如快速IO调用)发送往某个设备对象。
15、一个IRP往往要传递N个设备才能得以完成,在传递过程中有可能会有一些中间变换,导致请求的参数变化。为了保存这种参数变化,我们给每次中转都留一个栈空间,用来保存中间参数。所以一个请求并非简单的一个输入并等待一个输出,而是经过许多中转才得以完成
16、IO管理器就是将用户调用的API函数翻译成IRP或者将等价请求发送到内核各个不同的设备的关键组件。
17、常用的C运行时库中的函数,如果只涉及字符串和内存数据(而不涉及内存管理,比如内存的分配和释放),则是可以在内核程序里调用的。
18、任意一个函数可能有多个调用源,主要可以追溯到的调用源如下:
(1、入口函数DriverEntry和卸载函数DriverUnload。
(2、各种分发函数(包括普通分发函数和快速IO分发函数)
(3、处理请求时设置的完成函数。也就是说,该请求完成后会被系统调用的回调函数
(4、其它回调函数(如各类NDIS驱动程序的特征函数)
19、了解这段代码可能的调用源应该在哪里对处理函数可重入性和考虑运行中断级有很大的好处
20、何时需要保证函数的多线程安全性可以通过下面几条规则来简单判断:
(1、可能运行于多线程环境的函数,必须是多线程安全的,只运行于单线程环境的函数,则不需要多线程安全性。
(2、如果函数A的所有调用源只运行于同一单线程环境,则函数A也是只运行在单线程环境的。
(3、如果函数A的其中一个调用源是可能运行在多线程环境的,或者多个调用源可能运行于不同的可并发的多个线程环境,而且调用路径上没有采取多线程序列化成单线程的强制措施,则函数A也是可能运行在多线程环境的。
(4、如果在函数A所有的可能运行于多线程环境的调用路径上,都有多线程程序序列化成单线程的强制措施,则函数A是运行在单线程环境的。
(5、所谓的多线程序列化成单线程的强制措施是指如互斥体、自旋锁等同步手段。
(6、只使用函数内部资源,完全不实用全局变量、静态变量或者其它全局性资源的函数是多线程安全的。
(7、如果对某个全局变量或者静态变量的所有访问都被强制的同步手段限制为同一时刻只有一个线程访问则即是使用了这些全局变量和静态变量,岁函数的多线程安全性也是没有影响的。
21内核代码的主要调用源的运行环境
调用源 | 运行环境 | 原因 |
DriverEntry,DriverUnload | 单线程 | 这两个函数由系统进程的单一线程调用。不会出现多线程同时调用的情况。 |
各种分发函数 | 多线程 |
没有任何文档保证分发函数是不会被多线程同时调用的。此外,分发函数不会和DriverEntry并发,但可能和DriverUnload并发 |
完成函数 | 多线程 | 完成函数随时可能被位置的线程调用 |
各种NDIS回调函数 | 多线程 | 和完成函数相同 |
22、中断级:Passive级和Dispatch级,Dispatch级比Passive级高,实际编程中,许多具有比较复杂功能的内核API都要求必须在Passive级执行,只有比较简单的函数能在Dispatch级执行。调用任何一个内核API之前,必须查看WDK文档,了解这个内核API的中断级要求
23、判断正在编写的代码可能的中断级:
(1、如果调用路径上更没有特殊的情况(导致中断级的提高或者降低),则一个函数执行时的中断级和它的调用源的中断级相同。
(2、如果在调用路径上有获取自旋锁,则中断级随之升高;如果调用路径上有释放自旋锁,则中断级随之下降
24、内核代码主要调用源的运行中断级
调用源 | 一般的运行中断级 |
DriverEntry,DriverUnload | Passive级 |
各种分发函数 | Passive级 |
完成函数 | Dispatch级 |
各种NDIS回调函数 | Dispatch级 |
25、如果当前代码确实运行在Dispatch级,但是又必须调用一个只能运行在Passive级的内核API,任意的降低中断级都会导致系统产生不可预料的后果。
26、指定函数位置的预编译指令:
#pragma alloc_text(INIT, DriverEntry)
#pragma alloc_text(PAGE, NdisProtUnload)
#pragma alloc_text(PAGE, NdisProtUnload)
#pragma alloc_text(PAGE, NdisProtClose)
#pragma alloc_text这个宏仅仅用来指定某个函数的可执行代码在编译出来后在sys文件中的位置。内核模块编译出来之后是一个PE格式的sys文件,这个文件的代码段(text段)中有不同的节(Section),不同的节被加载到内存中之后处理情况不同。读者需要关心的主要是3种节,INIT节的特点是在初始化完毕之后就被释放,也就是说就不再占用内存空间了,PAGE节的特点是位于可以进行分页交换的内存空间,这些控件在内存紧张时可以被交换到硬盘上以节省内存。如果未用上述的预编译指令处理,则代码默认位于PAGELK节,加载后位于不可分页交换的内存空间中。
函数DriverEntry显然只需要在初始化阶段执行一次,因此这个函数一般都用#pragma alloc_text(INIT, DriverEntry)使之位于初始化后立刻释放的空间内。为了节约内存,可以把很多函数放在PAGE节中。但是要注意:放在PAGE节中的函数不可以在Dispatch级调用,因为这种函数的调用可能诱发缺页中断。但是缺页中断处理不能在Dispatch级完成。为此,一般都用一个宏PAGED_CODE()进行测试。如果发现当前中断级为Dispatch级,则程序直接报异常让程序员及早发现。
#pragma alloc_text(PAGE, SfAttachToMountedDevice)
....
NTSTATUS
SfAttachToMountedDevice(
IN PDEVICE_OBJECT DeviceObject,
IN PDEVICE_OBJECT SFilterDeviceObject
)
{
PSFILTER_DEVICE_EXTENSION newDevExt =
SFilterDeviceObject->DeviceExtension;
NTSTATUS status;
ULONG i;
PAGED_CODE();
...
}