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{ 
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
}UNICODE_STRING, *PUNICODE_STRING;

UNICODE_STRING 初始化

手动初始化

指针赋值

#define MYSTRING L"www.tlhg.top"
// 先定义后,再定义空间
UNICODE_STRING str;
str.Buffer = MYSTRING;
str.Length = str.MaximumLength = wcslen(MYSTRING) * sizeof(WCHAR);

指针Buffer进行了初始化赋值,这个指针指向的全局常量地址空间,所以这一段地址空间只能读,不可写。
如果我们强行对其地址修改,会触发系统的异常,导致蓝屏。

计算赋值

UNICODE_STRING str = { 
sizeof(L"www.tlhg.top") – sizeof((L"www.tlhg.top")[0]),
sizeof(L"www.tlhg.top"),
L"www.tlhg.top"
};

分别对结构体的三个变量进行初始化:

对于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,我们可以直接使用这个宏来初始化常量字符串。

#define RTL_CONSTANT_STRING(s) \
{ \
sizeof( s ) - sizeof( (s)[0] ), \
sizeof( s ) / sizeof(_RTL_CONSTANT_STRING_type_check(s)), \
_RTL_CONSTANT_STRING_remove_const_macro(s) \
}

这样一来,初始化字符串就简单的多了

#include <ntdef.h>
UNICODE_STRING str = RTL_CONSTANT_STRING(L"www.tlhg.top");

要使用宏RTL_CONSTANT_STRING,必须包括头文件ntdef.h,因为这个宏是在ntdef.h头文件中定义的。

函数初始化(RtlInitUnicodeString)

常用的初始化函数为RtlInitUnicodeString,这个函数的作用是把一个以’\0’结尾的WCHAR类型的Unicode字符串初始化成UNICODE_STRING类型的字符串。

VOID RtlInitUnicodeString(
PUNICODE_STRING DestinationString,
PCWSTR SourceString
);

RtlInitUnicodeString函数有两个参数,第一个参数为返回类型,表示需要初始化的UNICODE_STRING结构体,第二个参数为传入参数,表示被用来初始化DestinationString的常量WCHAR类型字符串,这个字符串以’\0’为结束符。

注意:RtlInitUnicodeString函数在使用的过程中,并不会为字符串的缓冲区申请内存,只是把定义的结构体的的缓冲区指向字符串的首地址,所以我们在使用的过程中,应该注意在使用DestinationString期间,必须保证SourceString有效。

UNICODE_STRING str;
RtlInitUnicodeString(&str,L"www.tlhg.top");
...
//可以再次初始化
RtlInitUnicodeString(&str,L"www.tlhg.top");

注意: 初始化的字符串,不用担心内存释放方面的问题。因为我们并没有分配任何内存,因为这些内存都是使用的是常量地址空间。

UNICODE_STRING 拷贝操作

由于 UNICODE_STRING 和ANSI_STRING字符串是一个结构体,所以UNICODE_STRING 和ANSI_STRING字符串的拷贝就不能使用wcscpy和strcpy来进行拷贝了。

常用的拷贝函数为RtlUnicodeStringCopyString,这个函数的功能是把以’\0’结尾的字符串pszSrc拷贝到DestinationString中。

NTSTATUS RelUnicodeStringCopystring(
PUNICODE_STRING DestinationString,
NTSTATUS_PCWSTR pszSrc
);

RtlUnicodeStringCopyString会把函数第二个参数pszSrc字符串拷贝到DestinationString所指向的内存中。

注意

  1. RtlInitUnicodeString函数内部只是简单地使DestinationString.Buffer指向函数的第二个参数SourceString,没有任何的拷贝操作,而RtlUnicodeStringCopyString会把函数第二个参数pszSrc字符串拷贝到DestinationString所指向的内存中。
  2. 使用的时候,要添加头文件 “Ntstrsafe.h”,引入相关的库文件 Ntstrsafe.lib ,简单的做法是在Sources文件中添加一行:TARGETLIBS = $(DDK_LIB_PATH)\ntstrsafe.lib。

UNICODE_STRNG示例

UNICODE_STRING dst;            // 目标字符串
WCHAR dst_buf[256]; // 不分配内存,故先定义缓冲区
UNICODE_STRING src = RTL_CONST_STRING(L"www.tlhg.top");
// 把目标字符串初始化为拥有缓冲区长度为256的UNICODE_STRING空串。
RtlInitEmptyString(dst,dst_buf,256*sizeof(WCHAR));
RtlCopyUnicodeString(&dst,&src); // 字符串拷贝!

以上这个拷贝之所以可以成功,是因为256比 L”www.tlhg.top” 的长度要大。如果小,则拷贝也不会出现任何明示的错误。但是拷贝结束之后,与使用者的目标不符,字符串实际上被截短了。

STRING/ANSI_STRING示例

STRING dst;            // 目标字符串
char dst_buf[256]; // 不分配内存,故先定义缓冲区
STRING src = RTL_CONSTANT_STRING("www.tlhg.top");
// 把目标字符串初始化为拥有缓冲区长度为256的STRING空串。
RtlInitEmptyAnsiString(&dst, dst_buf, 256 * sizeof(char));
RtlCopyString(&dst, &src); // 字符串拷贝!

注意:

如果没有调用RtlInitEmptyString。dst字符串被初始化认为缓冲区长度为0,结果就是dst字符串为空串。

UNICODE_STRING 拼接

RtlAppendUnicodeToString

RtlAppendUnicodeToString : 将一个宽字节接接到 UNICODE_STRING 中

NTSTATUS status;
UNICODE_STRING dst; // 目标字符串
WCHAR dst_buf[256]; // 不分配内存,故先定义缓冲区
// 把目标字符串初始化为拥有缓冲区长度为256的UNICODE_STRING空串
RtlInitEmptyUnicodeString(&dst,dst_buf,256*sizeof(WCHAR));
status = RtlAppendUnicodeToString(&dst,L"www.tlhg.top");

NTSTATUS是常见的返回值类型。如果函数成功,返回STATUS_SUCCESS。否则的话,是一个错误码。RtlAppendUnicodeToString在目标字符串空间不足的时候依然可以连接字符串,但是会返回一个警告性的错误STATUS_BUFFER_TOO_SMALL。

RtlAppendUnicodeStringToString

RtlAppendUnicodeStringToString: 希望连接两个UNICODE_STRING,这个函数的第二个参数也是一个UNICODE_STRING的指针。

NTSTATUS status;
UNICODE_STRING dst; // 目标字符串
WCHAR dst_buf[256]; // 不分配内存,故先定义缓冲区
UNICODE_STRING src = RTL_CONSTANT_STRING(L"www.tlhg.top");
RtlInitEmptyUnicodeString(&dst, dst_buf, 256 * sizeof(WCHAR));
status = RtlAppendUnicodeStringToString(&dst, &src); // 字符串拷贝!

RtlAppendUnicodeStringToString在目标字符串空间不足的时候依然可以连接字符串,但是会返回一个警告性的错误STATUS_BUFFER_TOO_SMALL。

ANSI_STRING的拼接

对于ANSI_STRING类型,使用RtlAppendStringToString函数进行字符串拼接。

NTSTATUS status;
UNICODE_STRING dst;
CHAR dst_buf[256];
RtlInitEmptyString(dst, dst_buf, 256 * sizeof(CHAR));
status = RtlAppendStringToString(&dst,"www.tlhg.top");

UNICODE_STRING 打印

普通打印

字符串的连接另一种常见的情况是字符串和数字的组合。有时数字需要被转换为字符串。有时需要把若干个数字和字符串混合组合起来。这往往用于打印日志的时候。日志中可能含有文件名、时间、和行号,以及其他的信息。

C语言sprintf函数的宽字符版本为swprintf。该函数在驱动开发中依然可以使用,但是不安全。微软建议使用RtlStringCbPrintfW来代替它。RtlStringCbPrintfW需要包含头文件ntstrsafe.h。在连接的时候,还需要连接库ntsafestr.lib。

下面的代码生成一个字符串,字符串中包含文件的路径,和这个文件的大小。

#include <ntstrsafe.h>
/* 任何时候,假设文件路径的长度为有限的都是不对的。应该动态的分配内存。但是动态分配内存的方法还没有讲述,所以这里再次把内存空*间定义在局部变量中,也就是所谓的“在栈中”
*/
WCHAR buf[512] = { 0 };
UNICODE_STRING dst;
NTSTATUS status;
……
// 字符串初始化为空串。缓冲区长度为512*sizeof(WCHAR)
RtlInitEmptyString(dst,dst_buf,512*sizeof(WCHAR));
// 调用RtlStringCbPrintfW来进行打印
status = RtlStringCbPrintfW(
dst->Buffer,L”file path = %wZ file size = %d \r\n”,
&file_path,file_size);
// 这里调用wcslen没问题,这是因为RtlStringCbPrintfW打印的
// 字符串是以空结束的。
dst->Length = wcslen(dst->Buffer) * sizeof(WCHAR);

RtlStringCbPrintfW在目标缓冲区内存不足的时候依然可以打印,但是多余的部分被截去了。返回的status值为STATUS_BUFFER_OVERFLOW。调用这个函数之前很难知道究竟需要多长的缓冲区。一般都采取倍增尝试。每次都传入一个为前次尝试长度为2倍长度的新缓冲区,直到这个函数返回STATUS_SUCCESS为止。

值得注意的是UNICODE_STRING类型的指针,用%wZ打印可以打印出字符串。在不能保证字符串为空结束的时候,必须避免使用%ws或者%s。其他的打印格式字符串与传统C语言中的printf函数完全相同。可以尽情使用。

输出打印

驱动中可以调用DbgPrint()函数来打印调试信息。这个函数的使用和printf基本相同。但是格式字符串要使用宽字符。DbgPrint()的一个缺点在于,发行版本的驱动程序往往不希望附带任何输出信息,只有调试版本才需要调试信息。但是DbgPrint()无论是发行版本还是调试版本编译都会有效。为此可以自己定义一个宏:

#if DBG
KdPrint(a) DbgPrint##a
#else
KdPrint (a)
#endif

不过这样的后果是,由于KdPrint (a)只支持1个参数,因此必须把DbgPrint的所有参数都括起来当作一个参数传入。导致KdPrint看起来很奇特的用了双重括弧:

// 调用KdPrint来进行输出调试信息
status = KdPrint((L"file path = %wZ file size = %d \r\n",&file_path,file_size));

这个宏没有必要自己定义,WDK包中已有。所以可以直接使用KdPrint来代替DbgPrint取得更方便的效果。

⬆︎TOP