环境安装
环境安装需要极其谨慎, 因为稍有不慎, 就需要重装系统.
驱动开发环境依赖WDK
(Windows driver kit),也就是微软的驱动开发工具包. 这个工具包里面包含了驱动程序的编译工具 , 头文件, 库等. 缺了这个, 无法编译驱动程序.
WDK有很多的版本, 版本的选择很重要. 一般最好选择最新版的.
除了WDK还需要和WDK同一版本的SDK .
如果所安装的WDK,SDK或者VS错误了, 可以使用卸载工具将它们全部卸载干净再配置. 普通的卸载工具是卸载不干净的, 但以下工具可以卸天卸地卸空气: Total Uninstall(旗舰版)
项目配置
编译出来的驱动其版本必须和加载该驱动的操作系统版本一致, 例如 : 加载驱动的是win7 32位系统, 则必须将驱动项目的目标系统设置为win7 , 且驱动必须是x86体系的.
源码编写
驱动程序可以使用C
语言项目, 也可以使用C++
项目, 但是, 通常使用C项目, 因为C++的项目有名称粉碎机制, 在定义函数的时候, 需要加入extern "C"
. 此外, C++编译器生成的代码不如C语言生成的代码高效, 而内核的代码会被高频繁的调用, 越高效越好.
驱动调试
编写出来的驱动实则属于系统内核的一个插件, Windows的系统内核文件为:ntoskrnl.exe
, 所有的内核模块都是加载到这个exe的一个DLL
, 因此, 内核驱动(.sys
)就像DLL
一样, 无法单独运行, 需要加载到主模块中.
调试一个内核驱动, 需要调试整个操作系统, 因此需要建立双机调试环境, 使用windbg来调试.
API使用规则
Windows的API分为用户层API和内核层API, 在用户层中,无法直接调用内核层API, 实际上, 绝大部分的用户层的API在最底层都会通过ntdll.dll
切换到内核层,间接调用内核层的API .
在内核层编程中, 不能调用用户层的API , 也就是说, 像是创建文件, 就不能使用CreateFile
, 创建进程, 不能使用CreateProcess
, 在内核层中会提供内核版本的创建文件,创建进程等等API.
内核API命名规则
window操作系统的内核使用C语言编写, 但是使用了面向对象的思想, 在系统内核中, 它将不同的API分成了不同的组件, 每个组件的API名称等会带有一个组件的名称:
函数前缀
所属的组件或函数说明
Cc
缓存管理器
Cm
配置管理器(即注册表)
Dbg/Kd
调试支持函数
Ex
执行体函数
FsRtl
文件系统驱动程序
Fstub
文件系统引导接口函数
Hal
HAL提供的接口函数
Io
I/O管理器
Ke
内核函数
Lpc
本地过程调用(LPC)函数
Mm
内存管理器
Nt
windwos系统服务
Ob
对象管理器
Perf
日志记录函数
Po
电源管理器
Pp
即插即用管理器
Ps
进程/线程
Raw
RAW文件系统函数
Rtl
内存运行时库函数
Se
安全函数
Vf
驱动程序检验器函数
Wmi
Windows管理器规范
Zw
和Nt前缀同名的一套函数,省去了参数验证的步骤,其它逻辑相同,可以认为以Nt前缀为名称的函数针对用户模式的调用者,以Zw前缀为名称的函数针对内核模式调用者
数据类型, 字符串和内存操作
Windows内核开发中, 采用的是和用户层编程所不一样的数据类型:
数据类型
长度
基本型
类型名称
UINT8
8 bit
unsigned char
无符号字符
UCHAR
8 bit
unsigned char
无符号字符
PUCHAR
32 bit
unsigned char*
无符号字符指针
UINT16
16 bit
unsigned short
无符号短整形
USHORT
16 bit
unsigned short
无符号短整形
PUSHORT
32 bit
unsigned short*
无符号短整形指针
UINT32
32 bit
unsigned int
无符号整形
UINT
32 bit
unsigned int
无符号整形
ULONG
32 bit
unsigned long
无符号长整形
PUNIT
32 bit
unsigned int*
无符号整形指针
UINT64
64 bit
unsigned __int64
无符号64位整形
ULONG64
64 bit
unsigned __int64
无符号64位整形
PULONG64
32 bit
unsigned __int64*
无符号64位整形指针
这些数据类型和用户层的数据类型大同小异.
字符串操作
字符串的操作是截然不同的.
内核中同一采用UNICODE_STRING
结构体来存取字符串. 这样做是为了更安全. Windows内核默认使用Unicode编码.
下面是结构体的说明:
typedef struct _UNICODE_STRING {
USHORT Length; // 字符串的长度, 单位是字节数
USHORT MaximumLength; // 最大字节数
PWCH Buffer;
#endif // MIDL_PASS
} UNICODE_STRING;
在这个结构体中, length保存的是当前字符串的字符长度(字节数), MaximumLength保存的是字符串缓冲区的最大长度(字节数).
使用这个结构体的时候, 将字符串赋值给Buffer
字段, 但不能只给这个字段赋值, 其它字段也必须一起初始化, 下面是操作这个结构体的函数:
函数名
功能
RtlInitUnicodeString
初始化字符串 ,注意,此函数不会分配空间.
RtlFreeUnicodeString
销毁字符串
RtlCopyUnicodeString
拷贝字符串
RtlAppendUnicodeStringToString
追加字符串
RtlCmpareUnicodeString
比较字符串
RtlUnicodeStringToInteger
字符串转数字
RtlIntegerToUnicodeString
数字转字符串
RtlAppendUnicodeStringToString
将UNICODE字符串结构转换为ANSI
Kdprint
输出调试信息
内存操作
ExAllocatePool
- 内存分配函数ExFreePool
- 内存释放函数
内核链表
在内核中, 有很多链表:
进程内核对象链表
线程内核对象链表
驱动对象链表
模块链表
...
在Windows内核中, 无论是什么链表, 都是使用如下结构的双向链表:
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink; // 指向下一个节点,如果没有下一个,则指向链表的头结点(循环链表)
struct _LIST_ENTRY *Blink; // 指向上一个节点.(双向链表)
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
内核中提供了以下函数来操作链表:
InitializeListHead
- 初始化链表InsertHeadList
- 将节点插入到链表头部InsertTailList
- 将节点插入到链表尾部RemoveTailList
- 删除该节点前一个节点.