《Windows内核安全与驱动编程》-第三章学习

3 字符串与链表

3.1 字符串操作

3.1.1 字符串结构

在驱动开发中,一般不再使用空来表示一个字符串的结束,而是定义了一个如下的数据结构:

typedef struct _UNICODE_STRING{
    USHORT Length;				//字符串的长度(字节数)
    USHORT MaximumLength;		//字符串缓冲区的长度(字节数)
    PWSTR Buffer;				//字符串缓冲区
}UNICODE_STRING,*PUNICODE_STRING;

在Windows的内核中一般使用Unicode编码。

3.1.2 字符串初始化

较为简单的是使用API进行初始化。函数定义如下:

VOID  (
IN OUT PUNICODE_STRING DestinationString,
IN PCWSTR SourceString
);

DestnationString表示需要初始化的字符串结构,SourceString表示字符串,举例:

UNICODE_STRING str = {0};
RtlInitUnicodeString(&str, L"my first string!");	//字符串左边加L表示为宽字符,即UNICODE字符。
3.1.3 字符串的拷贝

使用RtlCopyUnicodeString来进行拷贝。在拷贝时需要注意,如果拷贝目标字符串的Buffer指向的缓冲区没有足够的空间,那么字符串会被截断,但是不会报错。这是一个比较隐蔽的错误。

举例:

UNICODE_STRING dst;			//目标字符串
WCHAR dst_buf[256];			//定义缓冲区
UNICODE_STRING src = RTL_CONSTANT_STRING(L"My source string!");

//把目标字符串初始化为拥有缓冲区长度为256的UNICODE_STRING空串
RtlInitEmptyUnicodeString(&dst,dst_buf,256*sizeof(WCHAR));
RtlCopyUnicodeString(&dst,&src);

其中 RTL_CONSTANT_STRING 是初始化一个常数字符串常用的宏。该拷贝之所以会成功,是因为256比 L“My source string!” 的长度要长。

3.1.4 字符串的连接

使用 RtlAppendUnicodeToString 完成,需要考虑的依然是保证目标字符串的空间大小。

NTSTATUS status;
UNICODE_STRING dst;				//目标字符串
WCHAR dst_buf[256];				//定义缓冲区
UNICODE_STRING src = RTL_CONSTANT_STRING(L"My source string");

//奖目标字符串初始化为拥有缓冲区长度256的空串。
RtlInitEmptyUnicodeString(&dst,dst_buf,256*sizeof(WCHAR));
//拷贝进第一个字符串
RtlInitCopyUnicodeString(&dst,&src);
//连接字符串
status = RtlAppendUnicodeToString(&dst,L"My secend string!");

根据status来判断是否连接成功。如果成功会返回STATUS_SUCCESS;否则会返回相应的错误码。

3.1.5 字符串的打印

利用 RtlStringCbPrintfW 来替代 C 语言里的 swprintf

NTSTATUS status;
UNICODE_STRING dst;				//目标字符串
WCHAR dst_buf[256];				//定义缓冲区
UNICODE_STRING file_path = RTL_CONSTANT_STRING(L"\\??\\c:\\winddk\\....");
USHORT size = 1024;
//奖目标字符串初始化为拥有缓冲区长度256的空串。
RtlInitEmptyUnicodeString(&dst,dst_buf,256*sizeof(WCHAR));
//进行字符串拼接
status = RtelStringCbPrintfW(dst.buffer,256*sizeof(WCHAR),L"file path = %wZ file size = %d \r\n",&file_path,size);

RtlStringCbPrintfW 在缓冲区内存不足的时候仍然可以打印,但是字符串会进行截断。还有一个注意的点:

  • 对于UNICODE_STRING类型的字符串,需要使用 %wZ 打印可以打印出所有字符串。在不能保证字符串是以空结束的时候,必须避免使用 %ws 或者 %s。其他的打印格式都与C语言相同。

在驱动中可以调用 DbgPrint 函数来打印调试信息。该函数的使用与print基本相同。 或者也可以使用 KdPrint 函数来打印,但是要用括号将所有参数全部括起来。

3.2 内存与链表

3.2.1 内存的分配与释放

在驱动中分配内存,最常用的是调用 ExAllocatePoolWithTag。举例,当一个字符串被拷贝到另一个字符串时,最好根据源字符串的空间长度来分配目标字符串的长度。

NTSTATUS status;
//定义一个内存分配标记
#define MEM_TAG 'MyTt';
//目标字符串,接下来它需要分配空间
UNICODE_STRING dst = {0};
UNICODE_STRING src = RTL_CONSTANT_STRING(L"My source string!");
//根据源字符串的长度,分配空间给目标字符串
dst.Buffer = (PWCHAR)ExAllocatePoolWithTag(NonPagePool,src.Length,MEM_TAG);
if(dst.Bufffer == NULL)
{
	//错误处理
    status = STATUS_INSUFFICIENT_RESOURCES;
}
dst.Length = dst.MaximumLength = src.Length;
RtlCopyUnicodeString(&dst,&src);

​ 其中 ExAllocatePoolWithTag 的三个参数,第一个参数NonPagePool 表示分配的内存是锁定内存;这些内存永远存在于物理内存上,不会被分页交换到硬盘中。第二个参数是长度;第三个参数是一个所谓的“内存分配标识“。

​ 内存分配标识用于检测内存泄漏。一般每一个驱动程序都定义一个自己的内存表示,即使冲突也没有问题。

ExAllocatePoolWithTag 分配的内存可以使用 ExFreePool 来释放,如果不是放则永远泄露在外面,并不会像用户进程那样关闭后自动释放所有分配的空间。唯一的办法是重启计算机。

ExFreePool 只需要提供释放的指针即可。如:

ExFreePool(dst.Buffer);
dst.Buffer = NULL;
dst.Length = dst.MaximumLength = 0;

ExFreePool 不能用于释放一个栈空间指针,否则系统会立即崩溃。如:

UNICODE_STRING src = RTL_CONST_STRING(L"My source string!");
ExFreePool(str.buffer); //错误

ExFreePool 只能释放由 ExAllocatePoolWithTag分配的内存,务必保持一一对应。

3.2.2 使用LIST_ENTRY

LIST_ENTRY 是一个双向链表结构,它在使用的时候总是被插入到已有的数据结构中。举一个例子: 构造一个链表,这个链表的每一个节点都是由一个文件名和一个文件长度两个数据成员的结构。此外还有一个 FILE_OBJECT 指针对象,在驱动中它代表一个文件对象。该链表的作用是保存文件的文件名二号长度。只要传入FILE_OBJECT 指针,使用者就可以遍历链表找到文件名和文件长度。

typedef struct{
    PFILE_OBJECT file_object;
    UNICODE_STRING file_name;
    LARGE_INTEGER file_length;
}MY_FILE_INFOR, *PMY_FILE_INFOR;

LIST_ENTRY 的结构定义如下:

typedef struct _LIST_ENTRY{
    struct _LIST_ENTRY *Flink;
    struct _LIST_ENTRY *Blink;
}LIST_ENTRY,*PLIST_ENTRY;

接下来为了使我们的结构成为链表节点,在里面插入一个LIST_ENTRY 结构。一般都插入到最开头的位置。

typedef struct{
	LIST_ENTRY list_entry; //插入LIST_ENTRY结构
    PFILE_OBJECT file_object;
    UNICODE_STRING file_name;
    LARGE_INTEGER file_length;
}MY_FILE_INFOR, *PMY_FILE_INFOR;

​ 当 LIST_ENTRY 作为链表的头的时候,在使用之前必须调用 InitializeListHead 来进行初始化。示例代码:

//链表头
LIST_ENTRY my_list_head;

//对链表头进行初始化,一般应该在程序入口处调用一下。
void MyFileInforInit()
{
    //让双向链表的两个指针都指向自己。
    InitializeListHead(&my_list_head);
}

//链表节点,里面保存一个文件名和一个文件长度信息。(因为加入了LIST_ENTRY而成为链表节点)
typedef struct{
	LIST_ENTRY list_entry; //插入LIST_ENTRY结构
    PFILE_OBJECT file_object;
    UNICODE_STRING file_name;
    LARGE_INTEGER file_length;
}MY_FILE_INFOR, *PMY_FILE_INFOR;

//增加一个链表节点。其中file_name是外面分配的
//内存分配仍然由使用者管理,该链表并不管理内存
NTSTATUS MyFileInforAppendNode(
	PFILE_OBJECT file_object,
    PUNICODE_STRING file_name,
    PLARGE_INTEGER file_length)
{
    //分配指针类型为MY_FILE_INFOR的内存空间
    PMY_FILE_INFOR my_file_infor = (PMY_FILE_INFOR)ExAllocatePoolWithTag(
    PagePool,sizeof(MY_FILE_INFOR),MEM_TAG);
   	if(my_file_infor == NULL)
        return STATUS_INSUFFICIENT_RESOURES;
    
    //填写数据成员
    my_file_infor->file_object = file_object;
    my_file_infor->file_name = file_name;
    my_file_infor->file_length = file_length;
    
    //插入到链表尾部。这里没有用锁,所以这个函数并不是多线程安全的。
    InsertHeadList(&my_list_head,(PLIST_ENTRY)& my_file_infor);
    return STATUS_SUCCESS;
}

LIST_ENTRY 放在 MY_FILE_INFO 结构中的好处就是,使用MY_FILE_INFO) 看起来就像是一个 LIST_ENTRY。但是并不是所有的结构都将 LIST_ENTRY 放在头部。

遍历链表的示例:

for(p = my_list_head.Flink; p != &my_list_head.Flink; p = p->Flink)
{
    PMY_FILE_INFOR elem = CONTAINING_RECORD(p,MY_FILE_INFOR,list_entry);
    //To do something...
}

其中 CONTAINING_RECORDWDK 中一定定义的宏,作用是通过一个 LIST_ENTRY 结构的指针,找到这个结构所在节点的指针。定义如下:

#define CONTAINING_RECORD(address,type,field)…//暂时没看懂

​ 从上面的代码可以总结如下:

LIST_ENTRY 中的数据成员 Flink 指向下一个 LIST_ENTRY 。整个链表最后一个 LIST_ENTRYFlink 不为空,而是指向投节点。

3.2.3 使用长长整形数据

​ 64位的数据并非要在64位操作系统下才能使用。32位无符号整数只能标识到4GB,而文件的大小经常超过4GB。所以32位不够用。在驱动开发中有一个共用体: LARGE_INTEGER 。这个共用体定义如下:

typedef __int64 LONGLONG;
typedef union _LARGE_INTEGER{
	struct {
        ULONG LowPart;
        LONG HighPart;
    };
    struct {
        ULONG LowPart;
        LONG HighPart;
    }u;
    LONGLONG QuadPart;
}LARGE_INTEGER;

该定义用到匿名结构体。这里给出解释:匿名结构体的解释。计算时候则直接使用QuadPart进行计算。

LARGE_INTEGER a,b;
a.QuadPart = 100;
a.QuadPart *= 100;
b.QuadPart = a.QuadPart;
if(b.QuadPart > 1000)
{
    KdPrint(("LowPart = %x HighPart = %x",b.LowPart,b.HighPart));
}

读的时候有疑惑,为什么只计算 QuadPart 部分就可以影响HighPart 和 LowPart 呢?就去查了一下,这里提供MSDN的解释:如果编译器内置了对64位整数的支持,那么可以使用quadpart成员来存储64位整数。否则,使用LowPart和HighPart成员存储64位整数。 所以具体的用法,还是到测试中使用一下才能知道。

3.3自旋锁

3.3.1 使用自旋锁

​ 链表之类的结构总是涉及多线程编程问题,这时候就必须使用锁。其中自旋锁是最简单的锁。如下的代码初始化获得一个自旋锁:

KSPIN_LOCK my_spin_lock;
KeInitializeSpinLock(&my_spin_lock);

KeInitializeSpinLock 函数没有返回值。以下代码介绍如何使用自旋锁。在 KeAcquireSpinLockKeReleaseSpinLock 之间的代码是只有单线程执行的,其他的线程会停在KeAcquireSpinLock 等候,直到 KeReleaseSpinLock 被调用。换句话说,只有一个线程能够获得自旋锁。此外, KIRQL 是中断级别。 KeAcquireSpinLock 会提高当前中断级别,旧的中断级别被保存到这个参数中。

KIRQL irql;
KeAcquireSpinLock(&my_spin_lock,&irql);
//To do something...涉及多线程操作的事
KeReleaseSpinLock(&my_spin_lock,&irql);

​ 值得注意的是,以下的加锁代码是没有意义的:

void MySafeFunction()
{
    KSPIN_LOCK my_spin_lock;
	KeInitializeSpinLock(&my_spin_lock);
	KIRQL irql;
	KeAcquireSpinLock(&my_spin_lock,&irql);
	//To do something...涉及多线程操作的事
	KeReleaseSpinLock(&my_spin_lock,&irql);
}

原因是 my_spin_lock 这个变量为局部变量,存在于堆栈中。这样每一个线程都会有一个自己的锁,锁就没有意义了。所以锁一般不会定义为局部变量,可以定义为静态变量、全局变量,或者分配在堆中(使用内存的分配与释放)。

3.3.2 在双向链表中使用自旋锁

​ 在 3.2.2 中使用的双向链表,链表本身并不保证多线程安全性,所以常常需要使用自旋锁。所以理论上来说,我们需要在操作链表之前调用 KeAcquireSpinLock 来获取锁,在操作完成之后使用 KeReleaseSpinLock 来释放锁。但是LIST_ENTRY 有一系列操作,这些操作并不需要使用者自己调用获取与释放锁,只需要为每个链表定义并初始化一个锁即可。

LIST_ENTRY my_list_head;		//链表头
KSPIN_LOCK my_list_lock;		//链表的锁

//链表初始化函数
void MyFileInforInit()
{
    InitializeListHead(&my_list_head);
    KeInitializeSpinLock(&my_list_lock);
}
//该初始化完成后,之前插入一个节点,无锁的插入方式如下:
InsertHeadList(&my_list_head,(PLIST_ENTRY)& my_file_infor);
//有锁的插入方式如下
ExInterlockerInsertHeadList(&my_list_head,(PLIST_ENTRY)& my_file_infor,&my_list_lock);

ExInterlockedInsertHeadList 中,会自动使用这个自旋锁进行加锁。类似的还有一个加锁的 Remove 函数,用来移除一个节点。

my_file_infor = ExInterlockedRemoveHeadList(&my_list_head,&my_list_lock);

这个函数从链表中移除第一个节点,并返回到my_file_infor中。

3.3.3 使用队列自旋锁提高性能

​ 队列自旋锁遵循,谁先等待,谁先获取自旋锁的原则。队列自旋锁的使用方式与普通自旋锁相同,初始化队列自旋锁也是使用 KeInitializeSpinLock 函数,唯一的不同是在获取和释放时候使用的函数不同。

VOID KeAcquireInStackQueuedSpinLock(
IN PKSPIN_LOCK SpinLock,
IN PKLOCK_QUEUE_HANDLE LockHandle);

VOID KeReleaseInStackQueuedSpinLock(
IN PKLOCK_QUEUE_HANDLE LockHandle);

​ 下面介绍队列自旋锁的具体用法。

//队列自旋锁的初始化
KSPIN_LOCK my_Queue_SpinLock = {0};
KeInitializeSpinLock(&my_Queue_SpinLock);
//队列自旋锁的获取和释放
KLOCK_QUEUE_HANDLE my_lock_queue_handle;
KeAcquireInStackQueueSpinLock(&my_Queue_SpinLock,&my_lock_queue_handle);
//do something...
KeReleaseInStackQueueSpinLock(&my_lock_queue_handle);

切记一个锁初始化之后,要么按普通自旋锁的方法使用,要么按队列自旋锁的方法使用,绝对不能混用。

明日计划

继续学习驱动编程第四章。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值