[内核编程] 内核环境及其特殊性,驱动编程基础篇

   在学习汉江独钓一书后,打算总结一下内核编程应该注意的事项,以及有关的一些基础知识。第一次接触内核编程,还真是很生疏,很多东西不能一下马上消化。这里做个回顾总结,好加深自己的印象。

## 1、内核编程环境

这里涉及到两个模式:内核模式用户模式。这个可以和CPU的等级联系到一块:ring0,ring1,ring2,ring3,特权等级依次降低,最底层ring0层拥有最高的特权等级。而windows简化了这样的特权等级层次次关系,分成了内核层和用户层,对应的就是内核模式和用户模式。平时我们所编写的应用程序,运行后,在windows下就会生成一个对应的进程,针对的是单个进程进行程序的编写,受到了隔离保护的,运行环境受到操作系统的保护,很多底层的问题无需考虑,这使得编写应用程序变得相当容易。

【图】CPU特权等级和windows层次对应关系

【图】windows结构

所谓的内核模式,实际上就是不受操作系统管制的最底层结构,为操作系统提供各项服务的核心部件。在这里,理论上可以实现任何可以想到的功能,而不受到操作系统的制约,也正因为如此,问题也就产生了,面对共享的内存空间、共享资源,如何进行同步操作就成了一个关键的问题。再者,内核结构很多都是不公开的,并且又没有操作系统的保护,所以一点系统出错,就是直接蓝屏或死机,这给内核调试带来了很大的麻烦。

windows一般都是用系统进程来加载内核模块的,但这并不是说内核代码始终运行在System进程里,也就是说当DriverEntry被调用时,一般是位于System进程中的,其他时候则不一定。内核模块位于内核空间,而内核空间又被所有的进程共享。因此,内核模块实际上位于任何一个进程空间中。但是任意一段代码的任意一次执行,一定是位于某个进程空间中的。而至于这是哪一个进程,取决于请求的来源、处理的过程等。

## 2、常用数据类型

一般来说,在进行内核编程时,应当遵守WDK的编码习惯,虽然这并不是必须的,但是如果不这么做,有可能还是会导致一些不稳定性的问题。比如unsigned long 在64bit 环境下为8字节、而在32bit 环境下是4字节,这个时候要把数据写到磁盘上时,到底写4个字节还是8个字节呢?问题也就出现了,所以WDK重新定义了这个类型,为ULONG。

以下是一些常用的数据类型的转换关系:

  • unsigned long  --- ULONG
  • unsigned char --- UCHAR
  • unsigned int  --- UINT
  • void --- VOID
  • unsigned long * ---- PULONG
  • unsigned char * --- PUCHAR
  • unsigned int *  --- PUCHAR
  • void* ---PVOID
  • char* --- PCHAR
  • ........

当然也不仅仅是这些,一般来说,指针前面加上P,unsigned 对应 U,基本类型名不便只是换成了大写(例如char --- CHAR),当然这只是我个人见到的,也不排除有特例,我这么对应一般都没有什么问题。

函数一般都会返回操作的状态,以说明操作的情况,内核中这个类型是NTSTATUS。调用函数时一般这样做:

    NTSTATUS status;  status = function(..); if(NT_SUCCESS(status)){... 操作成功后要做的事情  ...}

NT_SUCCESS() 可以判断操作时候成功,当谈status还有其他的状态,可以查看WDK。

驱动力字符串一般用一个结构来保存,是一个宽字符串:

1

2

3

4

5

typedef struct _UNICODE_STRING{

     USHORT  Length;   // Buffer的字节长度,实际存储的长度

     USHORT  MaximumLength; //Buffer的最大长度,即开辟的空间大小

     PWSTR    Buffer;//存储字符的缓冲区

}UNICODE_STRING *PUNICODE_STRING;

## 3、一些重要的数据结构

驱动编程中,比较重要的几个数据结构是:驱动对象(DRIVER_OBJECT)设备对象(DEVICE_OBJECT)请求(IRP)

a. 一个驱动对象代表一个驱动程序,或者说一个内核模块。

b. 设备对象是由驱动创建的,一个驱动可以对应多个设备对象,设备对象是唯一能接收请求的实体。

c.windows内核中各部件的通信,是通过请求来完成的,来自用户层的相关操作,会被IO管理器翻译成请求(IRP或者与其等效的其它形式),处理这一个个的请求也就完成相应的操作。

关于这三个对象的结构可以在wdm.h中找到。这里给出重要数据结构的关系图:

【图】设备栈结构,驱动垂直结构

【图】驱动的水平结构,一个驱动可以闯将对个设备对象,这些设备构成一个链表结构

## 4、常用函数,以及内核API的学习

编写内核程序的时候,尽量使用内核API,虽然类似于wcscpy、memcpy等函数也可以用,但这些函数没有长度的检查,很容易发生溢出错误,应该避免使用。

主要的函数前缀有:Rtl-, Io-, Ke-, Zw-, Nt-, Ps-等。

有以下几类:

Ex系列--分配内存,获取互斥体等:
  ExAllocatePool,分配内存
  ExFreePool,释放内存
  ExAcquireFastMutex获取一个快速互斥体
  ExReleaseFastMutex释放一个快速互斥体
  ExRaiseStatus抛出异常

Zw--Nt系列文件操作函数:
  ZwCreateFile--创建文件
  ZwWriteFile--写文件
  ZwReadFile--读取文件
  ZwQueryDirectoryFile--查询目录
  ZwDeviceIoControlFile--发出设备控制请求
  ZwCreateKey--打开一个注册表键
  ZwQueryValueKey--读取一个注册表键

Rtl系列字符串操作函数:
  RtlInitUnicodeString--初始化一个Unicode字符串
  RtlCopyUnicode--拷贝字符串
  RtlAppendUnicodeToString--追加字符串到另一个字符串
  RtlStringCbPrintf--将字符打印到字符串中,相当于格式化字符串
  RtlCopyMemory--拷贝内存
  RtlMoveMemory--移动内存数据块
  RtlZeroMemory--内存数据块清零
  RtlCompareMemory--比较内存
  RtlGetVersion--得到当前windows版本

Io开头的IO管理函数:
  IoCreateFile--打开文件,比ZwCreateFile函数更加底层
  IoCreateDevice--生成一个设备对象
  IoCallDriver发送请求
  IoCompleteRequest--完成IRP请求
  IoCopyCurrentIrpStackLocationToNext--讲当前IRP栈空间拷贝到下一个栈空间
  IoSkipCurrentIrpStackLocationToNext--跳过当前IRP栈空间
  IoGetCurrentIrpStackLocation--得到当前IRP栈空间。

对于详细的说明可以查看WDK的帮助文档,API很多,我们不肯能全都记住,常使用,常动手,常查帮助,是很好的学习方法。见一个学一个,不记得就立即查帮助。相关的结构也可以直接查看WDK的头文件,这里可以查到帮助文档中没有的一些信息哦!!

## 5、Womdows的驱动开发模型

NT(KDM)、WDM、WDF(WDM的升级版)

## 6、WDK编程中的特殊性

调用源:沿着该函数往上走,再也不能找到其调用者,则这个函数就是调用源,一般的个单线程函数的调用源只有一个,也就是主函数,比如我们常见的main函数。而在内核编程中,一个函数往往有多个调用源,主要可以追溯到的调用源有以下几个:入口函数DriverEntry、卸载函数DriverUnload;各种分发函数;处理请求时的完成函数;其它回调函数。

在内核中,一个函数有可能同时被多个线程调用,这个是有就好保证多线程的安全性,也就是在输入相同的情况下要保证输出是相同的。多线程安全性可以依据以下几个规则进行判断:

  • 可能运行于多线程环境下的函数,必须是多线程安全的。
  • 如果函数A的所有调用源都是运行于单线程的,那么函数A也是运行于单线程的。
  • 如果函数A的调用源中,其中有一个可能运行于多线程环境下,并且在调用路径上没有将多线程序列化成单线程,那么函数A也是可能运行于多线程环境下的。
  • 如果函数A的所有可能出现多线程的调用路径上都被单线程化了,那么函数A就是单线程环境下的。
  • 只是用函数内部资源,则函数是多线程安全的
  • 如果函数在访问全局变量或者静态变量的操作采用强制的同步手段进行限制,可以等同于使用内部变量(上一条规则成立)。

内核代码主要调用源运行环境:

  DriverEntry、DriverUnload  单线程  —— 这两个函数由系统进程的单一线程调用,不会出现多线程同步调用的情况

  各种分发函数         多线程    —— 没有文档能保证分发函数不会被多线程同步调用,分发函数不会和DriverEntry同步,但是可能和DriverUnload同步

  完成函数           多线程  —— 完成函数随时可能被未知的线程调用

  各种NDIS回调函数       多线程   —— 随时可能被位置的线程调用

代码的中断级:

Windows为CPU的运行状态定义了许多的级别,即IRQL,任一时间中,CPU总是运行在其中的某一级别,各个级别规定了CPU能做哪些事情,哪些不可以做。高中断级可以抢占低中断级。
级别定义如下:
#define PASSIVE_LEVEL  0  ;级别最低,CPU在用户层,或刚进内核运行于管理层时就运行在此级别上
#define LOW_LEVEL  0  ;
#define APC_LEVEL  1  ;比PASSIVE_LEVEL略高,运行APC函数(进程与线程)时需要的级别
#define DISPATCH_LEVEL  2  ;相当于cpu运行在内核层,线程切换时级别从此下降
#define PROFILE_LEVEL  27  ;级别3以上用于硬件中断。
#define CLOCK1_LEVEL  28
#define CLOCK2_LEVEL  28
#define IPI_LEVEL  29
#define POWER_LEVEL  30
#define HIGH_LEVEL  31

两个规则:

  • 如果在调用路径上没有特殊情况(导致中断级的提高或者降低),则一个函数执行时的中断级和它的调用源的中断级相同。
  • 如果在调用路径上又获取自旋锁,则中断级随之升高,释放自旋锁,中断级随之下降。

处于高中断级中的函数不能调用处于低中断级中的API,若dispach级想调用passive级上的AIP,可以另创建一个线程专门执行。

其他:

函数定义中,变量的类型前常常会有:IN、OUT这样的字符,这些字符被定义成空,起到说明性的作用,IN表示输入参数,OUT表示输出参数。

PAGED_CODE(),进行分页测试,只要级别不高于APC_LEVEL,其代码都允许换出,若发现更高级的中断发出缺页中断,则发出异常,让编程者知道。

指定函数位置的预编译指令:

#pragma alloc_text(INIT, DriverEntry)
#pragma alloc_text(INIT, DriverUnload)
#pragma alloc_text(PAGE,NdisProtUnload)
.... ....

这个宏仅仅用来指定某个函数的可执行代码在编译出来后在sys文件中的位置。内核模块编译出来后是一个PE文件的sys文件,这个文件的代码段(text段)中有不同的节(Section),不同的节被加载到内存中之后处理情况不同。主要有3个节:

  • INIT节:在初始化完毕之后就被释放,不再占用内存空间。
  • PAGE节:位于可分页交换的内存空间,这些空间在内存紧张的时候可以被交换到磁盘上以节省空间。
  • PAGELK节:不适用预编译指定的时候,默然的状态。加载后位于不可分页交换的内存空间中。

VOID ASSERT( Expression ); —— 这个宏用于测试一个表达式,如果这个表达式的值是false,它就终止,并跳出到内核调试器。  

## 7、课后习题

(1)内核编程环境和用户应用程序编程环境有哪些不同?
编程模式可分为两种:用户模式和内核模式。
其中用户应用程序的编程采用的是用户模式,这里都是在操作系统的隔离环境中完成的,也就是说对于这个模式来说不用考虑通用寄存器,内存是共享的,可通过操作系统实现进程间的资源共享,这属于单进程编程,利用的都是进程内的资源,不用担心会产生什么冲突。
内核编程使用的是内核模式编程,其内核属于操作系统的一个模块供各个进程调用,在内核空间中资源都是共享的并且不受操作系统的限制,很容易发生冲突。

(2)Windows有哪几种驱动开发模型?它们的发展现状如何?
model是根据操作系统的类型不同而取名的,有KDM(windows NT),WDM(windows 98 —— windows 2000),WDF(WDM的升级版),一脉相承的,不用担心过时。

(3)什么是用户空间?什么事内核空间?
进程的空间实际上被分成两个部分,一部分是进程独立使用的用户空间,一部分是容纳操作系统内核的内核空间。具体到4G内存控件的32位windows系统上,低2G是用户空间,高2G是内核空间。

(4)内核模块运行在什么进程环境下? 
内核模块无处不在,内核模块属于操作系统的一个部分,为各进程提供服务,也就是说每个进程中都有可能运行有内核模块,但是内核模块一般是通过系统system进程进行加载的。

(5)请简述驱动对象、设备对象、请求之间的关系。
驱动对象、设备对象、请求
内核编程采用的是面向对象的编程思想来进行编程的,把每个事物都看成一个对象。而驱动对象可以说是一个驱动程序也可以说是一个内核模块。设备对象是唯一能接受请求的对象,而设备对象是在内核模块中建立的,也就是说设备对象属于驱动对象,设备对象在接收到请求以后交由驱动对象的分发函数进行处理。请求,内核模块之间的交互都是通过一个个的请求实现的,比如说申请资源、读取信息等,一般使用IRP请求,一个请求有可能要历经多个设备,所以需要暂时存储中间变量的栈空间。

(6)请简述如何判断对一个全局变量的访问是否要加自旋锁或者互斥体使之序列化。
当一个程序有可能会运行在多线程环境下的时候就需要保证其是多线程安全的,也就是说不能出现线程冲突。对一个全局变量的访问时候要加自旋锁或互斥体使之序列化,取决于被使用的变量是否影响到保证多线程的安全。

(7)请简述如何估计当前代码的中断级。
1、如果调用途径没有特殊情况,中断级和调用源一样
2、获自旋锁中断级升高,失自旋锁中断级降低

[内核编程] 内核环境及其特殊性,驱动编程基础篇相关推荐

  1. 编程模拟飞船加速变轨过程-物理基础篇(5) 摄动方程

    编程模拟飞船加速变轨过程物理基础篇(5) 摄动方程 根据前面的内容,我们已经能够做到当已知某时刻位矢和速度时,求出轨道形状了.但是,这些都是在只受中心引力的前提下求得的.本篇在原作中是讲其他微小力(稀 ...

  2. 编程模拟飞船加速变轨过程-物理基础篇(3)Kepler轨道及其描述(上)

    编程模拟飞船加速变轨过程物理基础篇(3) Kepler轨道及其描述(上) 开普勒轨道及其描述是用若干要素表示出飞船在地球中心引力场中绕地球飞行具体形状的一种方法,我们希望在已知这些要素的前提下精确的描 ...

  3. Oracle、Mysql数据库编程开发基本操作命令语法脚本_基础篇(入门级)

    Oracle.Mysql数据库开发基本操作命令语法脚本_基础篇(入门级) 文章目录 Oracle.Mysql数据库开发基本操作命令语法脚本_基础篇(入门级) 一.数据库中常用的字段类型 二.操作数据库 ...

  4. 转载:谢谢原作者:块设备驱动实战基础篇一 (170行代码构建一个逻辑块设备驱动)

    1   内核块设备驱动基础学习与实战 1.1 设备驱动IO架构初探 操作系统是如何将数据读到缓冲区的,发生了什么?我们带着这样的问题,粗略走一下read调用系统过程,希望这个初探,可以唤起大家研究操作 ...

  5. 转载:谢谢原作者:块设备驱动实战基础篇四 (逐渐成型,加入ioctl通信机制)

    1.6介绍一种内核与用户空间通信的方法-misc设备ioctl机制 块设备驱动开发中往往需要配合用户态的管理程序工具,管理我们的块设备,此时我们需要涉及用户空间程序与块设备通信的方法,ioctl机制就 ...

  6. 转载:谢谢原作者: 块设备驱动实战基础篇二 (继续完善170行过滤驱动代码至200行)

    1.3块设备驱动关键数据结构及函数API详细剖析 经过上节四个步骤我们已经熟悉并实战了一个最基本的过滤块设备驱动的设计技巧,我们这一节先不继续实战,我们本节把上节170行代码中接触到的块设备核心数据结 ...

  7. 2022谷粒商城SpringCloud项目环境搭建+项目流程(基础篇)

    谷粒商城项目 前言:在真正开始敲代码实现项目功能之前,关于此项目的介绍和环境搭建读者阅读本篇就可以了(笔者自己还记录了一下项目中遇到的小bug以及知识点).后面的功能实现从简考虑没有附上具体的代码(篇 ...

  8. Linux环境下的数据库(基础篇上)

    数据库 一.Linux环境下数据库的安装 二.数据库的基础操作 三.数据库中表的操作 一.Linux环境下数据库的安装 需要在 root 用户权限下进行数据库的一系列安装操作 安装 mariadb 服 ...

  9. 转载:谢谢原作者:块设备驱动实战基础篇三 (BIO请求回调机制)

    1.5 块设备请求返回处理回调机制 本节我们继续完善1.4节中的代码,在上节我们完成了请求的过滤转发,那么请求被磁盘处理完成后返回回来的路径处理是怎样的,本节我们继续带着这样的问题再一次完善我们的驱动 ...

  10. Linux设备驱动开发详解【三】_Linux 内核及内核编程

    本文简介 由于 Linux 驱动编程本质属于 Linux 内核编程,因此有必要掌握 Linux 内核及内核编程的基础知识.         3.1-3.2 节讲解 Linux 内核的演变及 Linux ...

最新文章

  1. 零基础入门学习Python(3) 变量与字符串
  2. Java7/8 中的 HashMap 和 ConcurrentHashMap
  3. java虚拟机内存告警_Java虚拟机总结
  4. 2009年北京突然的一场雪
  5. Linux chown命令:给资源绑定用户权限
  6. linux权限值前面的d,linux中rwx权限前的c和d都表示什么意思
  7. 端侧AI:高隐私、高可靠的智能个性化服务
  8. android电池(五):电池 充电IC(PM2301)驱动分析篇【转】
  9. 详解Python中的算术乘法、数组乘法与矩阵乘法
  10. [转]nodejs深入学(7)理解Buffer
  11. 谷歌修复4个已遭利用的安卓 0day
  12. 牛掰!某厂风控大佬整理的一套黑客学习思维导图及教程!
  13. 为什么相机模型假设成像平面位于焦平面?
  14. ③电子产品拆解分析-充电宝台灯
  15. 计算机无法屏幕亮度,win7电脑屏幕亮度无法调节怎么办?调节屏幕亮度的方法...
  16. C语言程序中紧急情况,《C语言及程序设计》实践参考——紧急救援
  17. MySQL学生综合测评系统_大学生综合测评系统的设计和实现(MySQL)
  18. 激人奋进的七个经典故事
  19. IT专业人士如何高效的学习专业知识
  20. 个人信息安全工程指南

热门文章

  1. 枚举型是什么类型_金属缠绕垫片如何选用?A、B、C、D型都是什么类型?
  2. html标签强制转换位置,王老师html零基础学习笔记第4课——样式初始化+类型转化...
  3. .php t=,关于php:意外的T_VARIABLE,期望T_FUNCTION
  4. apache 域名不变_apache 如何配置只能通过域名访问,不允许通过ip访问?
  5. 中怎么均化走线_PCB板上多长的走线才是传输线?答案是……
  6. python中json方法_在python中使用JSON库(通用方法),json,的,常用
  7. python多进程加快for循环_python多进程 通过for循环 join 的问题
  8. Object c 基础知识
  9. hdu 2196 树形dp
  10. [µC/GUI 学习]µC/GUI移植