前言

最近在找工作,很久没有碰驱动的缘故,略生疏,遂复习一番。

驱动开发汇总

1.什么是驱动程序?

由应用层驱动,位于内核层与硬件设备打交道或对内核产生一定影响的程序称为驱动程序。

2.什么是驱动对象?

一个驱动程序的实例称为驱动对象,其在驱动程序加载时由对象管理器创建。
IoCallDriver 调用驱动

3.什么是设备对象?

设备对象保存设备特征和状态,在设备驱动程序中存在一个或多个设备对象。
另外在设备对象结构 _DEVICE_OBJECT 中存在 DeviceExtension , 其作用类似于重叠结构。
IoCreateDevice // 创建一个设备
IoCreateDeviceSecure // 同上,并支持任何用户直接打开设备字符,降低权限要求
IoGetDeviceObjectPointer // 获取设备对象指针,会增加一个引用计数
IoAttachDeviceToDeviceStack // 挂载设备对象到设备栈
IoDetachDevice // 卸载设备对象

4.驱动对象与设备对象的关系?

一个驱动对象可以管理多个设备对象,
在派遣函数中,可以根据不同的设备对象做不同的处理,
另外与该设备相关的信息可以放在其扩展域中 DeviceExtension

5.几种IO方式的区别?

BUFFERED_IO DIRECT_IO OTHER_IO
BUFFERED_IO 直接从用户空间拷贝到内核空间
DIRECT_IO 使用mdl+锁的方式完成用户空间向内核空间的映射
其他方式读写 当用户空间上下文一定时,可以直接读写用户空间,也就是非异步IO的典型

6.WDM驱动?

WDM中通常分为,物理设备对象和功能设备对象,二者时附加被附加关系。
总线驱动会自动创建物理设备对象,但此设备对象并不能被直接操作,需要在其上附加功能设备对象。

7.内核内存?

由于整个内核均位于同一片内存空间,故不宜开辟过大的空间,另外需要尽可能的避免递归调用带来的栈开销。
#pragma alloc_text(INIT, DriverEntry)
#pragma alloc_text(PAGE, SfFastIoRead)
等代码用于解决内存吃紧的情况
依据实际代码分为 INIT PAGE

指定函数加载到非分页内存中
#pragma LOCKEDCODE

分配内存常用方法 -- 加入 "tag" 可以便于调试不加tag的版本会使用默认tag
ExAllocPoolWithTag
ExFreePoolWithTag

池化 - Lookaside 防止内存碎片,适合频繁分配且粒度固定的场景

  • 初始化一个 list, 指定分配器、释放器, 初始化相关锁
    ExInitializeNPagedLookasideList

ExInitializePagedLookasideList

  • 从list首部弹出元素,若为空,则使用分配器分配一块内存
    ExAllocateFromPagedLookasideList

ExAllocateFromNPagedLookasideList

  • 将一块内存挂载到指定 list,若达到 list最大深度则不挂载直接释放
    ExFreeToPagedLookasideList

ExFreeToNPagedLookasideList

  • 使用 ExAllocateFromPagedLookasideList/ExAllocateFromNPagedLookasideList 迭代list,并调用指定释放器
    ExDeleteNPagedLookasideList
  • 内存可用性检测 -- 不推荐使用
    ProbeForRead

ProbeForWrite

8.链表

驱动中通常使用非侵入式的 双向链表,通常需要将 LIST_ENTRY 放入 Node 的首部,便于遍历
IsListEmpty(&head)
InsertHeadList(&head,&node->ListEntry)
InsertTailList(&head,&node->ListEntry)
newhead = RemoveHeadList(&head)

CONTAINING_RECORD 用于得到结构体地址

#define CONTAINING_RECORD(address, type, field) ((type FAR *)( \
                                          (PCHAR)(address) - \
                                          (PCHAR)(&((type *)0)->field)))

FIELD_OFFSET 计算成员偏移
#define FIELD_OFFSET(type, field)    ((LONG)&(((type *)0)->field))

9.运行时

大多数运行时函数可以直接使用,例如
memcpy memcmp memset
也可以使用相应的 Rtl版本

10.IRP

当上层应用与驱动程序通信时,会由系统做语义翻译,转为IRP结构,类似调试系统中的包装。

对于一般的IRP请求,可以选择跳过或者完成。
IoSkipCurrentIrpStackLocation--------// 宏
IoCopyCurrentIrpStackLocationToNext--// 主要涉及完成例程,下层堆栈执行完成例程后,会将IRP的控制权交付给本层堆栈
IoSetCompletionRoutine---------------// 在设置完成例程后,需要通过 KeWaitForSingleObject 自行处理
-----------------------------------------//STATUS_PENDING / 在完成例程中通过 KeSetEvent 设置事件
-----------------------------------------// 同时返回 STATUS_MORE_PROCESSING_REQUIRED,会暂停向上回卷
-----------------------------------------// 或者在完成例程中通过 IoMarkIrpPending 传播挂起状态
-----------------------------------------// 二者区别在于是否完成IRP请求,由于调用 IoCallDriver
-----------------------------------------// (异步操作时,立即返回 STATUS_PENDING) 失去了对IRP的控制
-----------------------------------------// 需要通过事件的方式重获控制然后完成IRP
-----------------------------------------//
-----------------------------------------// 另外可以在完成例程中通过 IoCallDriver
-----------------------------------------// 继续转发,并返回 STATUS_MORE_PROCESSING_REQUIRED
-----------------------------------------// 详细参考 《驱动开发详解》 第21章

IoCompleteRequest// 函数

IO_STACK_LOCATION // io堆栈

IoMarkIrpPending // 挂起IRP
IoCancelIrp // 取消IRP

IoSetCancelRoutine // 设置取消例程 -> 需要注意在回调中调用 IoReleaseCancleSpinLock(Irp->CancelIrql),取消自旋锁是全局自旋锁,所有驱动共用,只需要调用一次。

IoAllocateIrp // 创建IRP

11.IRQL与锁

IRQL 软件使用了 0-2 3个级别,剩下的为硬件中断等级
由于中断例程也运行于 DISPATCH_LEVEL,故IRQL 为DISPATCH_LEVEL时执行的代码不能位于分页内存,且内部不能调用可能触发异常的代码。

spin Lock
KeAcquiresSpinLock -----> KxWaitForSpinLockAndAcquire (其内部就是通过nop自旋)/stdCall _KeRaiseIrql, <DISPATCH_LEVEL, esp> 提高运行级别
KeAcquireSpinLockAtDpcLevel -----> 少了提升运行级别这一步

KfReleaseSpinLock
KeReleaseSpinLockFromDpcLevel

需要注意,DISPATCH_LEVEL并不意味着线程安全,在多核情况下依旧有用锁的必要。
另外需要注意,自旋锁是一种实时性较高但效率偏低的锁。

快速互斥锁不可重入
另外部分同步对象可以在应用层和驱动层中交互

另外可以使用 ExInterLocked 系列函数内部通过自旋锁实现需要提供自旋锁,同时不能操作分页内存
InterLocked 系列不通过自旋锁,可以操作分页内存。

12.异步操作

类似网络编程中的异步IO

13.StartIO

实现IRP串行
StartIO 例程位于 DISPATCH_LEVEL 级别,函数定义需要加上 #pragma LOCKEDCODE 保证其运行于非分页内存

14.DPC 与 APC

延迟/异步过程调用,运行在 DISPATCH_LEVEL/APC_LEVEL 级别,差异在于一个是全局的 一个是针对线程的

KeInitializeDpc
KeInitializeApc

KeInsertQueueApc
KeInsertQueueDpc

15.时间

定时器
IoInitializeTimer IO定时器
KeInitializeTimer DPC定时器
二者的回调例程均运行于 DISPATCH_LEVEL

等待
KeWaitForSingleObject
KeDelayExecutionThread
KeStallExecutionProcessor -- 忙等待,类似自旋锁

获取时间
KeQuerySystemTime

16.对象

ObReferenceObjectByName // 获取命名对象指针,会增加引用计数

总结

针对常见常用的概念,函数进行汇总说明。

参考

《windows驱动开发详解》 张帆