6、Windows驱动开发技术详解笔记(2) 基本语法回顾

本文涉及的产品
实时计算 Flink 版,5000CU*H 3个月
简介:     1、字符串   Unicode 字符串有一个结构体定义如下: typedef struct _UNICODE_STRING { USHORT Length; // 字符串的长度(字节数) USHORT MaximumLength; // 字符串缓冲区的长度(字节数) PWSTR Buffer; // 字符串缓冲区 } UNICODE_STRING, *PUNICODE_STRING; 需要注意的是,当我们定义了一个UNICODE_STRING 变量之后,它的Buffer 域还没有分配空间,因此我们不能直接赋值,好的做法是使用微软提供的Rtl 系列函数。

 

 

1、字符串

 

Unicode 字符串有一个结构体定义如下:

typedef struct _UNICODE_STRING {

USHORT Length; // 字符串的长度(字节数)

USHORT MaximumLength; // 字符串缓冲区的长度(字节数)

PWSTR Buffer; // 字符串缓冲区

} UNICODE_STRING, *PUNICODE_STRING;

需要注意的是,当我们定义了一个UNICODE_STRING 变量之后,它的Buffer 域还没有分配空间,因此我们不能直接赋值,好的做法是使用微软提供的Rtl 系列函数。

UNICODE_STRING str;

RtlInitUnicodeString(&str, L"my first string!");

或者如下所示:

#include <ntdef.h>

UNICODE_STRING str = RTL_CONSTANT_STRING(L"my first string!");

ring3 不同,我们的UNICODE 字符串并不是以“\0”来表示字符串结束的,而是依靠UNICODE_STRING Length 域来确定。

1)字符串的很多操作都有相应的函数,例如字符串的复制可以使用RtlCopyUnicodeString函数,字符串的比较可以使用RtlCompareUnicodeString 函数,字符串转换成大写可以使用RtlUpcaseUnicodeString 函数(没有转换成小写的),字符串与整数数字互相转换分别可以使用RtlUnicodeStringToInteger RtlIntegerToUnicodeString 函数。

比如在输出日志记录的时候,我们往往同时涉及数字、字符等信息,在C语言中我们可以使用sprintf swprintf 函数来完成任务,这两个函数在驱动中仍然可以使用,但很不安全,因为有许多C 语言的运行时函数都是基于Win32 API 的,在驱动中绝对不能使用,如果我们不清楚哪些可以使用哪些不能使用,就都不要使用,而使用微软推荐的Rtl 系列函数。对应sprintf 的功能函数是RtlStringCbPrintfW,它需要包含头文件“ntstrsafe.h”和静态连接库“ntsafestr.lib”

http://msdn.microsoft.com/en-us/library/ff561817%28VS.85%29.aspx

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函数完全相同。可以尽情使用。

2)内核模式下各种开头函数的区别

函数开头含义

Cc

Cache manager

Cm

Configuration manager

Ex

Executive support routines

FsRtl

File system driver run-time library

Hal

Hardware abstraction layer

Io

I/O manager

Ke

Kernel

Lpc

Local Procedure Call

Lsa

Local security authentication

Mm

Memory manager

Nt

Windows 2000 system services (most of which are exported as Win32 functions),例如NtCreateFile 往往导出为CreateFile

Ob

Object manager

Po

Power manager

Pp

PnP manager

Ps

Process support

Rtl

Run-time library

Se

Security

Wmi

Windows Management Instrumentation

Zw

Mirror entry point for system services (beginning with Nt) that sets previous access mode to kernel, which eliminates parametervalidation, since Nt system services validate parameters only if previous access mode is user see Inside Microsoft Windows 2000

3)大部分的Win32 API 都是通过Native API 实现的,Native API 函数一般都是Win32 API函数前面加上Nt 两个字符,例如CreateFile 函数对应着NtCreateFile 函数,这些Nt 函数都是在“ntdll.dll”实现的,而多数Win32 API 都是在“kernel.dll”导出的,也有少部分GDI或窗口相关的函数是在“gdi32.dll”“user32.dll”导出的。

Native API 从用户模式穿越进入到内核模式调用系统服务,这个穿越过程是通过软中断的方式进入的。这个软中断的实现方法在不同版本的Windows 实现方式略有不同,在Win 2K下是通过“int 2eh”实现的,在Win XP 是通过“sysenter”指令完成的。

软中断会将Native API 的参数和系统服务号的参数一起传进内核模式,不同的NativeAPI 会对应不同的系统服务号,这个过程是由SSDT 辅助完成的。

系统服务函数一般和Native API 具有相同的名字,例如都是NtCreateFile,但它们的实现不同,系统服务调用是在“ntoskrnl.exe”导出的。

4)创建文件

代码
 
   
1 NTSTATUS
2
3 CreateFileTest( IN PUNICODE_STRING FileName )
4
5 {
6
7 HANDLE hFile = NULL;
8
9 NTSTATUS status;
10
11 IO_STATUS_BLOCK Io_Status_Block;
12
13   // 初始化文件路径
14  
15 OBJECT_ATTRIBUTES obj_attrib;
16
17 InitializeObjectAttributes( & obj_attrib,
18
19 FileName,
20
21 OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,
22
23 NULL,
24
25 NULL );
26
27   // 创建文件
28  
29 status = ZwCreateFile( & hFile,
30
31 GENERIC_ALL,
32
33   & obj_attrib,
34
35 & Io_Status_Block,
36
37 NULL,
38
39 FILE_ATTRIBUTE_NORMAL,
40
41 FILE_SHARE_READ,
42
43 FILE_CREATE,
44
45 FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT,
46
47 NULL,
48
49 0 );
50
51 if (NT_SUCCESS(status))
52
53 {
54
55 status = STATUS_SUCCESS;
56
57 }
58
59 else
60
61 {
62
63 status = Io_Status_Block.Status;
64
65 }
66
67 KdPrint(( " hFile = %08X " , hFile));
68
69 // 关闭句柄
70
71 if (hFile)
72
73 {
74
75 ZwClose(hFile);
76
77 }
78
79 return status;
80
81 }

 


5)长长整形:

typedef __int64 LONGLONG;

typedef union _LARGE_INTEGER {

struct {

ULONG LowPart;

LONG HighPart;

};

struct {

ULONG LowPart;

LONG HighPart;

} u;

LONGLONG   QuadPart;

} LARGE_INTEGER;

这个共用体的方便之处在于,既可以很方便的得到高32位,低32位,也可以方便的得到整个64位。进行运算和比较的时候,使用QuadPart即可。

2、链表

1)内存的分配与释放

传统的C 语言中,分配内存常常使用的函数是malloc,但在驱动开发过程中这个函数不再有效。驱动中分配内存,最常用的是调用ExAllocatePoolWithTag ExAllocatePool

// 定义一个内存分配标记

#define MEM_TAG ‘MyTt’

// 目标字符串,接下来它需要分配空间。

UNICODE_STRING dst = { 0 };

// 分配空间给目标字符串。根据源字符串的长度。

dst.Buffer = (PWCHAR)ExAllocatePoolWithTag(NonpagedPool,src->Length,M EM_TAG);

if(dst.Buffer == NULL)

{

// 错误处理

status = STATUS_INSUFFICIENT_RESOUCRES;

……

}

dst.Length = dst.MaximumLength = src->Length;

ExAllocatePoolWithTag 的第一个参数NonpagedPool 表明分配的内存是非分页内存,这样它们可以永远存在于物理内存,而不会被分页交换到硬盘上去;第二个参数是长度;第三个参数是一个所谓的内存分配标记

内存分配标记用于检测内存泄漏。一般每个驱动程序定义一个自己的内存标记,也可以在每个模块中定义单独的内存标记。内存标记是随意的32位数字,即使冲突也不会有什么问题。此外也可以分配可分页内存,使用PagedPool标识第一个参数即可。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(src.Buffer);

请务必保持ExAllocatePoolWithTag ExAllocatePool ExFreePool 的成对关系。Windows内核提供了一个双向链表结构LIST_ENTRY,此外还有一些其他的结构,比如SINGLE_LIST_ENTRY(单向链表)。

LIST_ENTRY 是一个双向链表结构,通常的做法是我们自定义一个结构体,将LIST_ENTRY作为该结构体的一个子域,将LIST_ENTRY作为结构体的第一个子域是最简单的做法。如下所示:

typedef struct _MYDATASTRUCT{

LIST_ENTRY ListEntry;

ULONG number;

} MYDATASTRUCT, *PMYDATASTRUCT;

如果把它放在后面,这时候,如果我们想获取节点的地址,需要有一个计算偏移的过程,DDK 里面提供了一个宏CONTAINING_RECORD 可以在指定结构中找到节点地址的指针。

http://msdn.microsoft.com/en-us/library/ff554296%28VS.85%29.aspx

如:

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);

// 在这里做需要做的事…

}

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

#define CONTAINING_RECORD(address, type, field) ((type *)( \

(PCHAR)(address) - \

(ULONG_PTR)(&((type *)0)->field)))

从上面的代码中可以总结如下的信息:

LIST_ENTRY中的数据成员Flink指向下一个LIST_ENTRY。整个链表中的最后一个LIST_ENTRYFlink不是空。而是指向头节点。得到LIST_ENTRY之后,要用CONTAINING_RECORD来得到链表节点中的数据。

2)使用自旋锁

代码
 
   
1 KSPIN_LOCK my_spin_lock;
2
3 KIRQL irql;
4
5 // 初始化
6
7 KeInitializeSpinLock( & my_spin_lock);
8
9 KeAcquireSpinLock( & my_spin_lock, & irql);
10
11 // do something …
12
13 KeReleaseSpinLock( & my_spin_lock,irql);
14
15 一个例子程序:
16
17 VOID
18
19 LinkListTest()
20
21 {
22
23 LIST_ENTRY linkListHead; // 链表
24
25 PMYDATASTRUCT pData; // 节点数据
26
27 ULONG i = 0 ; // 计数
28
29 KSPIN_LOCK spin_lock; // 自旋锁
30
31 KIRQL irql; // 中断级别
32
33 // 初始化
34
35 InitializeListHead( & linkListHead);
36
37 KeInitializeSpinLock( & spin_lock);
38
39 // 向链表中插入10 个元素
40
41 KdPrint(( " [Test] Begin insert to link list " ));
42
43 // 锁定,注意这里的irql 是个指针
44
45 KeAcquireSpinLock( & spin_lock, & irql);
46
47 for (i = 0 ; i < 10 ; i ++ )
48
49 {
50
51 pData = (PMYDATASTRUCT)ExAllocatePool(PagedPool, sizeof (MYDATASTRUCT));
52
53 pData -> number = i;
54
55 InsertHeadList( & linkListHead, & pData -> ListEntry);
56
57 }
58
59 // 解锁,注意这里的irql 不是指针
60
61 KeReleaseSpinLock( & spin_lock, irql);
62
63 // 从链表中取出所有数据并显示
64
65 KdPrint(( " [Test] Begin remove from link list\n " ));
66
67 // 锁定
68
69 KeAcquireSpinLock( & spin_lock, & irql);
70
71 while ( ! IsListEmpty( & linkListHead))
72
73 {
74
75 PLIST_ENTRY pEntry = RemoveTailList( & linkListHead);
76
77 // 获取节点地址
78
79 pData = CONTAINING_RECORD(pEntry, MYDATASTRUCT, ListEntry);
80
81 // 读取节点数据
82
83 KdPrint(( " [Test] %d\n " ,pData -> number));
84
85 ExFreePool(pData);
86
87 }
88
89 // 解锁
90
91 KeReleaseSpinLock( & spin_lock, irql);
92
93 }

 


需要注意的是,像上述代码中在函数中定义一个锁的做法是没有实际意义的,因为它是一个局部变量,被定义在栈中,每当有线程调用该函数时,都会重新初始化一个锁,因此它就失去了本来的作用。

在实际的编程中,我们应该把锁定义为一个全局变量、静态(static )变量或者将其定义在堆中。不过在驱动中应该尽量避免使用全局变量,良好的做法是将需要全局访问的变量

定义为设备扩展结构体DEVICE_EXTENSION 的一个子域。另外,我们还可以为每个链表都定义并初始化一个锁,在需要向该链表插入或移除节点时不使用前面介绍的普通函数,而是使用如下方法:

ExInterlockedInsertHeadList(&linkListHead, &pData->ListEntry, &spin_lock);

pData = (PMYDATASTRUCT)ExInterlockedRemoveHeadList(&linkListHead, &spin_lock);

此时在向链表中插入或移除节点时会自动调用关联的锁进行加锁操作,可以有效地保证多线程安全性,加裁驱动除了使用DriverStudio外,还可以使用KmdManager等工具。

NT式驱动的安装是基于服务的,可以通过修改注册表进行,也可以直接通过服务函数如CreateService 进行安装;但WDM 式驱动不同,它安装的时候需要通过编写一个inf 文件进行控制。除此之外,它们所使用的头文件也不大相同,例如NT 式驱动往往需要导入一个名为“ntddk.h”的头文件,而WDM 式驱动需要的却是“wdm.h”头文件。我们在学习的过程中所编写的大多属于NT 式驱动,除非我们需要自己的设备支持即插即用,才需要考虑编写WDM 式驱动程序。

相关实践学习
基于Hologres轻松玩转一站式实时仓库
本场景介绍如何利用阿里云MaxCompute、实时计算Flink和交互式分析服务Hologres开发离线、实时数据融合分析的数据大屏应用。
Linux入门到精通
本套课程是从入门开始的Linux学习课程,适合初学者阅读。由浅入深案例丰富,通俗易懂。主要涉及基础的系统操作以及工作中常用的各种服务软件的应用、部署和优化。即使是零基础的学员,只要能够坚持把所有章节都学完,也一定会受益匪浅。
目录
相关文章
|
3月前
|
缓存 网络协议 数据安全/隐私保护
[运维笔记] - (命令).Windows server常用网络相关命令总结
[运维笔记] - (命令).Windows server常用网络相关命令总结
191 0
|
1月前
|
数据可视化 数据库 C++
Qt 5.14.2揭秘高效开发:如何用VS2022快速部署Qt 5.14.2,打造无与伦比的Windows应用
Qt 5.14.2揭秘高效开发:如何用VS2022快速部署Qt 5.14.2,打造无与伦比的Windows应用
|
3月前
|
存储 Ubuntu 开发工具
ffmpeg笔记(二)windows下和ubuntu-16.04下ffmpeg编译
ffmpeg笔记(二)windows下和ubuntu-16.04下ffmpeg编译
|
4月前
|
Linux API C++
音视频windows安装ffmpeg6.0并使用vs调试源码笔记
音视频windows安装ffmpeg6.0并使用vs调试源码笔记
117 0
|
4月前
|
监控 安全 API
5.9 Windows驱动开发:内核InlineHook挂钩技术
在上一章`《内核LDE64引擎计算汇编长度》`中,`LyShark`教大家如何通过`LDE64`引擎实现计算反汇编指令长度,本章将在此基础之上实现内联函数挂钩,内核中的`InlineHook`函数挂钩其实与应用层一致,都是使用`劫持执行流`并跳转到我们自己的函数上来做处理,唯一的不同的是内核`Hook`只针对`内核API`函数,但由于其身处在`最底层`所以一旦被挂钩其整个应用层都将会受到影响,这就直接决定了在内核层挂钩的效果是应用层无法比拟的,对于安全从业者来说学会使用内核挂钩也是很重要。
40 1
5.9 Windows驱动开发:内核InlineHook挂钩技术
|
4月前
|
监控 API C++
8.4 Windows驱动开发:文件微过滤驱动入门
MiniFilter 微过滤驱动是相对于`SFilter`传统过滤驱动而言的,传统文件过滤驱动相对来说较为复杂,且接口不清晰并不符合快速开发的需求,为了解决复杂的开发问题,微过滤驱动就此诞生,微过滤驱动在编写时更简单,多数`IRP`操作都由过滤管理器`(FilterManager或Fltmgr)`所接管,因为有了兼容层,所以在开发中不需要考虑底层`IRP`如何派发,更无需要考虑兼容性问题,用户只需要编写对应的回调函数处理请求即可,这极大的提高了文件过滤驱动的开发效率。
41 0
|
4月前
|
监控 Windows
7.4 Windows驱动开发:内核运用LoadImage屏蔽驱动
在笔者上一篇文章`《内核监视LoadImage映像回调》`中`LyShark`简单介绍了如何通过`PsSetLoadImageNotifyRoutine`函数注册回调来`监视驱动`模块的加载,注意我这里用的是`监视`而不是`监控`之所以是监视而不是监控那是因为`PsSetLoadImageNotifyRoutine`无法实现参数控制,而如果我们想要控制特定驱动的加载则需要自己做一些事情来实现,如下`LyShark`将解密如何实现屏蔽特定驱动的加载。
32 0
7.4 Windows驱动开发:内核运用LoadImage屏蔽驱动
|
18天前
|
监控 安全 API
7.3 Windows驱动开发:内核监视LoadImage映像回调
在笔者上一篇文章`《内核注册并监控对象回调》`介绍了如何运用`ObRegisterCallbacks`注册`进程与线程`回调,并通过该回调实现了`拦截`指定进行运行的效果,本章`LyShark`将带大家继续探索一个新的回调注册函数,`PsSetLoadImageNotifyRoutine`常用于注册`LoadImage`映像监视,当有模块被系统加载时则可以第一时间获取到加载模块信息,需要注意的是该回调函数内无法进行拦截,如需要拦截则需写入返回指令这部分内容将在下一章进行讲解,本章将主要实现对模块的监视功能。
36 0
7.3 Windows驱动开发:内核监视LoadImage映像回调
|
4月前
|
监控 安全 API
7.2 Windows驱动开发:内核注册并监控对象回调
在笔者上一篇文章`《内核枚举进程与线程ObCall回调》`简单介绍了如何枚举系统中已经存在的`进程与线程`回调,本章`LyShark`将通过对象回调实现对进程线程的`句柄`监控,在内核中提供了`ObRegisterCallbacks`回调,使用这个内核`回调`函数,可注册一个`对象`回调,不过目前该函数`只能`监控进程与线程句柄操作,通过监控进程或线程句柄,可实现保护指定进程线程不被终止的目的。
30 0
7.2 Windows驱动开发:内核注册并监控对象回调
|
Windows 数据安全/隐私保护 网络协议