内核与驱动_03_内存、链表与字符串

内存

内存的分配与释放

  • 内存泄漏是一个长久的顽固问题,不论是用户层程序开发还是内核开发,都需要主要这个问题。
  • 在驱动中进行内存分配最常用的函数是ExAllocatePoolWithTag,下面是一个使用示例
NTSTATUS status=NULL;
//定义一个分配内存标记
//这里要注意标记是有''括起来的,由一到四个由单引号引起来的字符组成,标签中的每个ASCII字符必须是0x20(空格)至0x7E(波浪号)范围内的值
#define MEM_TAG 'MyTt'
//目标字符串,家下来需要给他分配空间
UNICODE_STATUS dst={0};
UNICODE STRING src=RTL_CONSTANT_STRING(L"My source string");
//依据源字符串长度,动态分配空间非目标字符串
dst.Buffer=(PWCHAR)ExAllocatePoolWithTag(NonPagedPool,src.Length,MEM_TAG);
if(dst.Buffer==NULL)
{
    //未分配成功
    status=STATUS_INSUFFICIENT_RESOURCES;
}
dst.Length=dst.MaximumLength=src.Length;
RtlCopyUnicodeString(&dst,&src);
  • 函数的第一个参数NonPagePool表明分配的内存是锁定内存,这些内存永远真实存在于物理内存上,不会被分页交换到硬盘中,第二个参数是长度,第三个参数是一个所谓的“内存分配标识”。
  • 这个内存分配表示用于检测内存泄漏,如果内存泄露了,那么就可以根据占用越来越多的内存分配标识来大概直到泄漏的来源。
  • 使用ExFreePool来释放资源,在内核中如果不释放,就会永远泄漏,并不像用户进程那样关闭后自动释放所有的分配的空间,即使驱动程序动态卸载也不能释放空间,唯一的办法是重启计算机。
ExFreePool(dst.Buffer);
dst.buffer=NULL;
dst.Length=dst.MaximumLength=0;
  • 要注意ExFreePoll不能用来释放一个栈空间的指针,否则系统立刻崩溃蓝屏,比如下述代码:
UNICODE_STRING src=RTL_CONSTANT_STRING(L"Hello Word");
ExFreePool(stc.Buffer);
  • 所以必须保持ExAllocatePoolWithTag和ExFreePool的成对关系。

链表

  • LIST_ENTRY–是一个双向链表结构,总是在使用的时候被插入到已有的数据结构中,配合起来构成一个链表。
//在回顾一遍LIST_ENTRY的结构
typedef struct _LIST_ENTRY
{
    struct _LIST_ENTRY *Flink;	//指向下一个节点的指针
    struct _LIST_ENTRY *Blink;	//指向前一个节点的指针
}LIST_ENTRY,*PLIST_ENTRY;
  • LIST_ENTRY放在自己的结构体开头是一种最简单的做法。

  • 一个完整的链表使用示例:

//构建一个如下的文件节点,节点有一个文件名和一个文件长度以及一个文件对象FILE_OBJECT指针组成
typedef struct _MY_FILE_INFO
{
    PFILE_OBJECT fileObj;
    UNICODE_STRING fileName;
    LARGE_INTEGER fileLength;	//长长整型数据结构
}MY_FILE_INFO,*PMY_FILE_INFO;
//1. 添加LIST_ENTRY变为链表节点结构,选择添加在最开始
typedef struct _MY_FILE_INFO
{
    LIST_ENTRY lsitEntry;
    PFILE_OBJECT fileObj;
    UNICODE_STRING fileName;
    LARGE_INTEGER fileLength;	
}MY_FILE_INFO,*PMY_FILE_INFO;
//当LIST_ENTRY作为链表的头,那么在使用之前
//必须调用InitiaizeListHead来初始化
//代码如下:

//定义一个链表头
LIST_ENTRY myListHead;
//初始化链表头,封装起来,一般在程序入口出调用
void MyFileInfoInit()
{
    InitializeListhead(&myList_head);
}

//实现一个链表尾部追加的函数
NTSTATUS MyFileInfoAppendNode(
	PFILE_OBJECT fileObj,
    PUNICODE_STRING fileName,
    PLAEGE_INTEGER fileLength)
{
    PMY_FILE_INFO myFileInfo=(PMY_FILE_INFO)ExAllocatePoolWithTag(PagedPool,sizeof(PMY_FILE_INFO),MEM_TAG);
    if(myFileInfo==NULL)
        return STATUS_INSUFFICIENT_RESOURES;
    //填写数据成员
    myFileInof->fileObj=fileObj;
    myFileInfo->fileName=fileName;
    myFileInfo->fileLength=filelength;
    
    //将其插入链表末尾,这里由于没有任何锁,所以函数并不是多线程安全的,还有需要改进的地方
    InsertHeadList(&myListHead,(PLIST_ENTRY)&myFileInfo);
    return STATUS_SUCC
}
  • LIST-ENTRY插入到结构体头部的好处显而易见,但是并不是所有结构都是这样,比如MS的许多结构喜欢一开头就是结构的长度,因此在通过LIST_ENTRY来获取所在节点的地址时,需要计算地址偏移。
    `

  • LIST-ENTRY插入到结构体头部的好处显而易见,但是并不是所有结构都是这样,比如MS的许多结构喜欢一开头就是结构的长度,因此在通过LIST_ENTRY来获取所在节点的地址时,需要计算地址偏移。

  • 不过在WDK中已经定义了一个宏CONTAINING_RECORD,作用是通过一个LIST_ENTRY结构的指针,找到这个结构所在的节点的指针。

字符串操作

  • Windows驱动开发可以调用一部分C标准的库函数与WDK提供的Windows内核函数,虽然字符串操作完全可以通过标准C函数来完成,但是在Windows驱动程序的开发中操作字符串有自己的惯例。

使用字符串结构

  • 使用C中以‘\0’结尾的字符串容易造成缓冲溢出漏洞,就像咋C++高级特性中使用std::stringCstring一样,驱动开发中定义了一个结构体UNICODE_STRING来实现高级特性。
  • 与Unicode字符串对应的还有一个ANSI字符串,ANSI字符串就是C中常用的使用单字节表示一个字符的多字节字符串。
typedef struct _STRING{
    USHORT Length;			//字符串长度(字节数)
    USHORT MaximumLength;	//字符串缓冲去长度
    PSTR Buffer;			//字符串缓冲区
}ANSI_STRING,*PANSI_STRING;
  • 一般来说内核编程使用Unicode编码,只有在某些特殊情况下才使用ANSI字符串。
  • UNICODE_STRING不保证Buffer是;以空结束,所以一些求长度的的API就会导致内核崩溃:
//如下的使用方式会导致内核崩溃
UNICODE_STRING str;
len = wcslen(str.Buffer);	//求长度
DbgPrint("%ws",str.Buffer);	//试图打印
  • 所以需要使用Rtl系列的API来进行字符串的操作才是最明智的选择。

字符串的初始化

  • 可以使用如下的方式:
UNICODE_STRING str={0};
//注意这里要给str.Buffer指定一个缓冲区,不然会在之后的操作中引发空指针异常
WCHAR strBuff[0x100]={0};
str.Buffer=strBuff;
//之后进行字符串初始化,也就是结构体初始化
wcscpy(str.Buffer,L"This is First String");
str.Length=str.MaximumLength=wcslen(L"This is First String")*sizeof(WCHAR);
//也可以直接赋值str.Buffer
//如
UNICODE_STRING str={0};
str.Buffer=L"This is Second String");
str.Length=str.MaximumLength=wcslen(L"This is Second String")*sizeof(WCHAR);
  • 上面的方式比较麻烦,WDK提供了一个API RtlInitUnicodeString()来简单的初始化一个UNCIDOE_STRINRG字符串。使用方法如下示例:
UNICODE_STRING str={0};
RtlInitUnicodeString(&str,L"This is Second String");

字符串拷贝

  • 使用RtlUnicodeString来进行拷贝,这种拷贝要注意目标字符串的Buffer指向的缓冲区必须有足够的空间,否则拷贝会不完全。
UNICODE_STRING dst;
WCHAR dstBuff[256]={0};
//把目标字符串初始化为拥有缓冲区长度为256的UNICODE_STRING空串
RtlInitEmptyUnicodeString(&dst,dstBuff,256*sizeof(WCHAR));
UNICODE_STRING src=RTL_CONSTANT_STRING(L"source string");
RtlCopyUnicodeString(&dst,&src);	//拷贝
//宏:RTL_CONSTANT_STRING是用于初始化一个常数字符串常用的宏

字符串的连接

  • 使用函数RtlAppendUnicodeToString进行连接
NTSTATUS status;
UNICODE_STRING dst;
WCHAR dstBuff[256]={0};
UNICODE_STRING src=RTL_CONSTANT_STRING(L"source string");
RtlInitEmptyUnicodeString(&dst,dstBuff,256*sizeof(WCHAR));
RtlCopyUnicodeString(&dst,&src);
status=RtlAppendUnicodeToString(&dst,L"second string");
  • 还可以将两个UNICODE_STRING字符串连接起来
NTSTATUS status;
UNICODE_STRINR dst;
WCHAR dstBuff[256]={0};
RtlInitEmptyUnicodeString(&dst,dstBuff,256*sizeof(WCHAR));
RtlCopyUnicodeString(&dst,L"hello");
UNICODE_STRING src=RTL_CONSTANG_STRING(L"world“);
status=RtlAppendUnicodeToString(&dst,&src);
  • 函数成功返回STATUS_SUCCESS,错误则返回错误码,如果目标缓冲区大小不够,那么返回一个STATUS_BUFFER_TOO_SMALL错误码。

字符串打印

  • 有时候也需要将数字转换为字符串,有时候需要把若干个数字和字符混合组合起来,这种情况下在C中有函数sprintfswprintf,这两个函数在驱动开发中依然有用,但是不安全。微软建议使用RtlStringCbPrintfW来代替它。
    • RtlStringCbPrintfW需要包含头文件ntstrsafe.h,在链接是还需要链接库ntsafestr.lib
#include <ntstrsafe.h>
#pragma comment(lib,"ntsafestr.lib")

WCHAR dstBuff[512]={0};
UNICODE_STRING dst;
NTSTATUS status=STATUS_SUCCESS;
UNICODE_STRING path=RTL_CONSTANT_STRING(L"\\??\\c:\\winddk\\1111.h");
USHORT fileSzie=1024;
//字符串初始化为空串
RtlInitEmptyUnicodeString(&dst,dstBuff,512*sizeof(WCHAR));
//调用RtlStringCbPrintfW来进行拼接
status=RtlStringCbPrintfW(dst.Buffer,512*sizeof(WCHAR),L"file path=%wZ , file size = %d \n",&path.Buffer,fileSize);
//RtlStringCbPrintfW打印的字符串是以空结束的,所以可以使用wcslen来计算长度
dst.Length=wcslen(dst.Buffer)*sizeof(WCHAR);
  • 函数在目标缓冲去内存不足的时候依然可以打印,但是多余的部分会被截去,返回的status值为STATUS_BUFFER_OVERFLOW
  • 还有两个用于调试的输出打印函数:
    • DbgPrintf()
    • KdPrintf(())
  • DbgPrint函数来打印调试信息时使用方法和printf基本相同,DbgPrint有一个缺点就是,发行版本的驱动程序往往不希望附带任何输出信息,只有调试版本才需要调试信息,但是DbgPrint无论是发行版本还是调试版本编译都会有效,所以可以自定义一个宏
#if DBG
	kdPrint(a) DbgPrint
#else 
    kdPrint(a)
#endif
  • 不过这样定义的宏,由于Kdprint只支持一个参数,所以必须吧DbgPrint的所有参数都括起来当作一个参数,所以使用起来会像这样KdPrint(())需要两个括号。
  • 这个宏在WDK中已经包含,可以直接使用。
  • 有个需要注意的地方:使用DbgPrint函数输出Unicode字符串时(如使用%ws,%wc),必须确保当前中断级别是PASSIVE_LEVEL,可以通过KeGetCurrentIrql函数来获取当前的中断级别。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值