UNICODE_STRING
UNICODE_STRING数据类型
内核层编程一般不直接使用WCHAR类型的Unicode字符串,而是使用UNICODE_STRING类型来表示Unicode。
这里简单介绍一下 UNICODE_STRING 类型。
UNICODE_STRING是内核中表示字符串的结构体;
Buffer为一个指针,指向一个UNICODE类型的字符串缓冲区;
MaximumLength表示Buffer所指向缓冲区的总空间大小,一般等于Buffer被分配时的内存大小,单位为字节;
Length表示Buffer所指向缓冲区中字符串的长度,单位也是字节;
注意:Buffer指向的字符串,并不要求以’\0’作为结束,在大多数情况下,Buffer指向的字符串没有以’\0’结尾
typedef struct _UNICODE_STRING{ |
UNICODE_STRING 初始化
手动初始化
指针赋值
|
指针Buffer进行了初始化赋值,这个指针指向的全局常量地址空间,所以这一段地址空间只能读,不可写。
如果我们强行对其地址修改,会触发系统的异常,导致蓝屏。
计算赋值
UNICODE_STRING str = { |
分别对结构体的三个变量进行初始化:
对于UNICODE_STRING的Length,相当于
str.Length = sizeof(L"www.tlhg.top") – sizeof((L"www.tlhg.top")[0]); |
- sizeof(L”www.tlhg.top”) 计算该字符串所占的内存空间,这里包括L’\0’。
- sizeof((L”www.tlhg.top”)[0])相当于取的是这个字符串的第一个字符长度,这里因为宽字节,所以是2,两者相减,就计算出了字符串的长度。这里看到确实是以字节为单位计算的,而不是以宽字节的长度为单位的。
对于UNIOCDE_STRING的MaximumLength成同,则直接使用sizeof计算其所占内存空间。所以这里也就包括了L’\0’2个字节的长度。
对于UNIOCDE_STRING的Buffer成员,直接使用常量地址空间的地址赋值。
宏初始化(RTL_CONSTANT_STRING)
微软封装了字符串初始化的代码,封装成了一个宏RTL_CONSTANT_STRING,我们可以直接使用这个宏来初始化常量字符串。
这样一来,初始化字符串就简单的多了
|
要使用宏RTL_CONSTANT_STRING,必须包括头文件ntdef.h,因为这个宏是在ntdef.h头文件中定义的。
函数初始化(RtlInitUnicodeString)
常用的初始化函数为RtlInitUnicodeString,这个函数的作用是把一个以’\0’结尾的WCHAR类型的Unicode字符串初始化成UNICODE_STRING类型的字符串。
VOID RtlInitUnicodeString( |
RtlInitUnicodeString函数有两个参数,第一个参数为返回类型,表示需要初始化的UNICODE_STRING结构体,第二个参数为传入参数,表示被用来初始化DestinationString的常量WCHAR类型字符串,这个字符串以’\0’为结束符。
注意:RtlInitUnicodeString函数在使用的过程中,并不会为字符串的缓冲区申请内存,只是把定义的结构体的的缓冲区指向字符串的首地址,所以我们在使用的过程中,应该注意在使用DestinationString期间,必须保证SourceString有效。
UNICODE_STRING str; |
注意: 初始化的字符串,不用担心内存释放方面的问题。因为我们并没有分配任何内存,因为这些内存都是使用的是常量地址空间。
UNICODE_STRING 拷贝操作
由于 UNICODE_STRING 和ANSI_STRING字符串是一个结构体,所以UNICODE_STRING 和ANSI_STRING字符串的拷贝就不能使用wcscpy和strcpy来进行拷贝了。
常用的拷贝函数为RtlUnicodeStringCopyString,这个函数的功能是把以’\0’结尾的字符串pszSrc拷贝到DestinationString中。
NTSTATUS RelUnicodeStringCopystring( |
RtlUnicodeStringCopyString会把函数第二个参数pszSrc字符串拷贝到DestinationString所指向的内存中。
注意:
- RtlInitUnicodeString函数内部只是简单地使DestinationString.Buffer指向函数的第二个参数SourceString,没有任何的拷贝操作,而RtlUnicodeStringCopyString会把函数第二个参数pszSrc字符串拷贝到DestinationString所指向的内存中。
- 使用的时候,要添加头文件 “Ntstrsafe.h”,引入相关的库文件 Ntstrsafe.lib ,简单的做法是在Sources文件中添加一行:TARGETLIBS = $(DDK_LIB_PATH)\ntstrsafe.lib。
UNICODE_STRNG示例
UNICODE_STRING dst; // 目标字符串 |
以上这个拷贝之所以可以成功,是因为256比 L”www.tlhg.top” 的长度要大。如果小,则拷贝也不会出现任何明示的错误。但是拷贝结束之后,与使用者的目标不符,字符串实际上被截短了。
STRING/ANSI_STRING示例
STRING dst; // 目标字符串 |
注意:
如果没有调用RtlInitEmptyString。dst字符串被初始化认为缓冲区长度为0,结果就是dst字符串为空串。
UNICODE_STRING 拼接
RtlAppendUnicodeToString
RtlAppendUnicodeToString : 将一个宽字节接接到 UNICODE_STRING 中
NTSTATUS status; |
NTSTATUS是常见的返回值类型。如果函数成功,返回STATUS_SUCCESS。否则的话,是一个错误码。RtlAppendUnicodeToString在目标字符串空间不足的时候依然可以连接字符串,但是会返回一个警告性的错误STATUS_BUFFER_TOO_SMALL。
RtlAppendUnicodeStringToString
RtlAppendUnicodeStringToString: 希望连接两个UNICODE_STRING,这个函数的第二个参数也是一个UNICODE_STRING的指针。
NTSTATUS status; |
RtlAppendUnicodeStringToString在目标字符串空间不足的时候依然可以连接字符串,但是会返回一个警告性的错误STATUS_BUFFER_TOO_SMALL。
ANSI_STRING的拼接
对于ANSI_STRING类型,使用RtlAppendStringToString函数进行字符串拼接。
NTSTATUS status; |
UNICODE_STRING 打印
普通打印
字符串的连接另一种常见的情况是字符串和数字的组合。有时数字需要被转换为字符串。有时需要把若干个数字和字符串混合组合起来。这往往用于打印日志的时候。日志中可能含有文件名、时间、和行号,以及其他的信息。
C语言sprintf函数的宽字符版本为swprintf。该函数在驱动开发中依然可以使用,但是不安全。微软建议使用RtlStringCbPrintfW来代替它。RtlStringCbPrintfW需要包含头文件ntstrsafe.h。在连接的时候,还需要连接库ntsafestr.lib。
下面的代码生成一个字符串,字符串中包含文件的路径,和这个文件的大小。
|
RtlStringCbPrintfW在目标缓冲区内存不足的时候依然可以打印,但是多余的部分被截去了。返回的status值为STATUS_BUFFER_OVERFLOW。调用这个函数之前很难知道究竟需要多长的缓冲区。一般都采取倍增尝试。每次都传入一个为前次尝试长度为2倍长度的新缓冲区,直到这个函数返回STATUS_SUCCESS为止。
值得注意的是UNICODE_STRING类型的指针,用%wZ打印可以打印出字符串。在不能保证字符串为空结束的时候,必须避免使用%ws或者%s。其他的打印格式字符串与传统C语言中的printf函数完全相同。可以尽情使用。
输出打印
驱动中可以调用DbgPrint()函数来打印调试信息。这个函数的使用和printf基本相同。但是格式字符串要使用宽字符。DbgPrint()的一个缺点在于,发行版本的驱动程序往往不希望附带任何输出信息,只有调试版本才需要调试信息。但是DbgPrint()无论是发行版本还是调试版本编译都会有效。为此可以自己定义一个宏:
|
不过这样的后果是,由于KdPrint (a)只支持1个参数,因此必须把DbgPrint的所有参数都括起来当作一个参数传入。导致KdPrint看起来很奇特的用了双重括弧:
// 调用KdPrint来进行输出调试信息 |
这个宏没有必要自己定义,WDK包中已有。所以可以直接使用KdPrint来代替DbgPrint取得更方便的效果。