从本章开始讲解Windows内核编程的基础知识。
一、字符串的处理
1.字符串的定义
在驱动开发中,,一般不再用空来表示一个字符串的结束,而是定义了如下的一个结构:
typedef struct _UNICODE_STRING{
USHORT Length; //字符串的长度(字节数)
USHORT MaximumLength; //字符串缓冲区的长度(字节数)
PWSTR Buffer; //字符串缓冲区
}UNICODE_STRING, *PUNICODE_STRING;
以上是Unicode字符串,一个字符为双字节。与之对应的还有一个Ansi字符串。
typedef struct _STRING{
USHORT Length; //字符串的长度(字节数)
USHORT MaximumLength; //字符串缓冲区的长度(字节数)
PSTR Buffer; //字符串缓冲区
}ANSI_STRING, *PANSI_STRING;
Windows内核是使用Unicode编码的。
UNICODE_STRING并不保证Buffer中的字符串是以空结束的。因此,类似下面的做法都是错误的。
UNICODE_STRING str;
…
len = wcslen(str.Buffer); //试图求长度
DbgPrint(“%ws”,str.Buffer); //试图打印str.Buffer
2.字符串的初始化
上面的UNICODE_STRING结构中并不含有字符串缓冲的空间。以下代码是完全错误的:
UNICODE_STRING str;
wcscpy(str.Buffer, L“my first string!”);
str.Length = str.MaximumLength = wcslen(L“my first string!”) * sizeof(WCHAR);
因为str.Buffer只是一个未初始化的指针,它并没有指向有意义的空间。所以下面的方式才是正确的。
UNICODE_STRING str;
str.Buffer = L“my first string!”; //全局的
str.Length = str.MaximumLength = wcslen(L“my first string!”) * sizeof(WCHAR);
实际上,明显的初始化写法如下:
UNICODE_STRING = {
sizeof(L“my first string!”) – sizeof(L“my first string!”)[0],
sizeof(L“my first string!”),
L“my first string!”
};
我们也可以直接使用ntdef.h中的宏来定义:
#include<ntdef.h>
UNICODE_STRING str = RTL_CONSTANT_STRING(L“my first string!”);
为了随机初始化一个字符串,我们可以使用:
UNICODE_STRING str;
RtlInitUnicodeString(&str, L“my first string!”);
以上初始化都不用担心内存释放的问题,因为并没有分配任何内存。
3.字符串的拷贝
我们可以使用RtlCopyUnicodeString来进行拷贝,要注意的是:拷贝目的的字符串的Buffer必须有足够的空间。如果Buffer的空间不足,字符串会拷贝不完全。
例子:
UNICODE_STRING dst; //目标字符串
WCHAR dst_buf[256]; //我们现在还不会分配内存,所以先定义缓冲区
UNICODE_STRING src = RTL_CONST_STRING(L“my first string!”);
//把目标字符串初始化为拥有缓冲区长度为256的UNICODE_STRING空串
RtlInitEmptyString(dst, dst_buf, 256*sizeof(WCHAR));
RtlCopyUnicodedString(&dst, &src); //字符串拷贝
4.字符串的连接
把两个字符串连接在一起,重要的还是保证目标字符串的空间大小。
NTSTATUS status;
UNICODE_STRING dst; //目标字符串
WCHAR dst_buf[256]; //我们现在还不会分配内存,所以先定义缓冲区
UNICODE_STRING src = RTL_CONST_STRING(L“my first string!”);
//把目标字符串初始化为拥有缓冲区长度为256的UNICODE_STRING空串
RtlInitEmptyString(dst, dst_buf, 256*sizeof(WCHAR));
RtlCopyUnicodedString(&dst, &src); //字符串拷贝
status = RtlAppendUnicodeToString(&dst, L“my second string!”);
if(status != STATUS_SUCCESS){…}
RtlAppendUnicodeToString在目标字符串空间不足的时候依然可以连接字符串,但是会返回STSTAUS_BUFFER_TOO_SMALL。
连接两个UNICODE_STRING使用:RtlAppendUnicodeStringToString。
5.字符串的打印
内核建议使用RtlStringCbPrintfW。需要包含头文件ntstrsafe.h.连接的时候还需要连接口ntsafestr.lib
status = RtlStringCbPrintfW(dst->Buffer,512*sizeof(WCHAR),L”file path = %Wz file size = %d”,&file_path,File_size);
打印之前记得先给dst分配足够的空间,一般采取倍增的方法。在不能保证字符串以空结束的时候,避免使用%ws或者%s。
驱动中打印调试信息使用DbgPrint。这个方法在调试版本和发行版本都会编译。所以WDK给我们提供了一个KdPrint函数。
#if DBG
KdPrint(a) DbgPrint##a
#else
KdPrint(a)
#endif
KdPrint((L”file path = %Wz file size = %d”,&file_path,File_size));
二、内存与链表
1.内存的分配与释放
#define MEM_TAG ‘MyTt’
UNICODE_STRING dst = {0};
dst.Buffer = (PWCHAR)ExAllocatePoolWithTag(NonpagePool,src->Length,MEM_TAG);
if(dst.Buffer == NULL){}
dst.Length = dst.MaximumLength = src->Length;
status = RtlCopyUnicodeString(&dst,&src);
NonpagePool表示这些内存永远存在物理内存,不会被交换到硬盘上去。MEM_TAG是内存分配标记,用来检测内存泄
ExAllocatePoolWithTag分配的内存必须使用ExFreePool释放,不然只能重启计算机。
ExFreePool(dst.Buffer);
dst.Buffer = NULL;
dst.Length = dst.MaximumLength = 0;
栈中的字符串不能使用ExFreePool释放,比如:
UNICODE_STRING str = RTL_CONST_STRING(L“my first string!”);
2.使用LIST_ENTRY
在实际的编程中,我们需要自己定义链表的节点,并把节点的第一个成员设置为LIST_ENTRY类型的变量(不一定放在第一位,但通常是这样);此外,我们还需要一个LIST_ENTRY类型的链表头,用InitializeListHead来初始化链表头。
使用InsertHeadList往链表中插入节点。
InsertHeadList(&my_list_head,(PLIST_ENTRY)&my_file_infor);
其他函数:
IsListEmpty,判断链表是否为空
InsertHeadList,从链表头部插入节点
InsertTailList,从链表尾部插入节点
RemoveHeadList,从链表头部删除节点
RemoveTailList,从链表尾部删除节点
WDK还定义了一个宏,CONTAINING_RECORD,作用是通过LIST_ENTRY结构的指针找到这个结构所在的节点的指针。定义如下:
#define CONTAINING_RECORD(address, type, field) ((type *)( \
(PCHAR)(address) – \
(ULONG_PTR)(&((type *)0)->field)))
3.使用长长整型数据
LARGE_INTEGER定义:
typedef union _LARGE_INTEGER {
struct {
DWORD LowPart;
LONG HighPart;
};
struct {
DWORD LowPart;
LONG HighPart;
} u;
LONGLONG QuadPart;
} LARGE_INTEGER, *PLARGE_INTEGER;
这个共用体的方便之处在于,既可以很方便的得到高32位、低32位,也可以方便地得到整个64位。进行运算和比较时,使用QuadPart即可。
4.使用自旋锁
自旋锁是一种轻量级的多处理器间的同步机制。因此,自旋锁对于单处理器是没有实际意义的。它要求持有锁的处理器所占用的时间尽可能短,因为此时别的处理器正在高速运转并等待锁的释放,所以不能长时间占有。
初始化自旋锁:
KSPIN_LOCK my_spin_lock;
KeInitializeSpinLock(&my_spin_lock);
KeInitializeSpinLock这个函数没有返回值。
KIRQL irql;
KeAcquireSpinLock((&my_spin_lock,&irql);
//to do something 这中间应该是原子操作
KeReleaseSpinLock((&my_spin_lock,&irql);
中间只是单线程执行,其他的线程会停留在KeAcquireSpinLock等候,直到KeReleaseSpinLock被执行。KeAcquireSpinLock会提高当前的中断级别。
锁一般不会定义成局部变量。
另外,我们还可以为每个链表都定义并初始化一个锁,在需要向该链表插入或移除节点时不使用前面介绍的普通函数,而是使用如下方法:
ExInterlockedInsertHeadList(&linkListHead, &pData->ListEntry, &spin_lock);
ExInterlockedRemoveHeadList(&linkListHead, &spin_lock);
此时在向链表中插入或移除节点时会自动调用关联的锁进行加锁操作,有效地保证了多线程安全性。