Windows内核面试题(持续更新,目前完成度30%约1.8万字)

WINDOWS内核编程问题与答案
1.WDK和SDK的区别是什么
2.WDK全称叫做
3.如何创建WDK程序
4.WinDbg如何连接虚拟机
5.Windows内核符号表的作用
6.如何设置内核符号表与源文件
7.如何设置断点与源码调试
8.什么时候共享内核空间
9.内核模块与驱动程序的区别是什么
10.内核模块运行在什么空间
11.PsGetCurrentProcessId函数的作用是什么
12.System进程的作用是什么
13.x64和x86操作系统的用户空间和内核空间的范围分别是多少
14.WDK基本数据类型有几种分别是什么
15.内核返回状态类型是什么NT_SUCCESS函数的作用是什么
16.内核字符串使用了什么结构如何创建一个双字符字符串
17.驱动对象的用途是什么它使用了什么结构
18.内核回调函数分为几种他们的作用是什么
19.简单讲讲如何使用内核实现病毒扫描、文件加密
20.内核设备对象的作用是什么它与内核驱动对象是什么关系
21.请写出设备对象是如何进行请求处理的
22.请写出IRP内核请求的结构名和作用
23.什么是IRP栈空间他的作用是什么
24.请写出常见的内核API函数以及Ex、Zw、Nt、Rtl、Io系列函数是做什么的
25.内核编程中主要调用源的作用是什么
26.如何判断内核函数的多线程安全性是否需要,如何做内核多线程的安全性
27.内核中断级共有几级分别叫做什么
28.请写出指定函数位置的预编译指令以及作用
29._UNICODE_STIRNG与_STRING结构的作用是什么
30.如何合法的初始化一个内核字符串
31.如何拷贝一个UNICODE_STRING字符串
32.RTL_CONSTANT_STRING的作用是什么
33.RTLInitEmptyUnicodeString函数的作用是什么
34.RTLCopyUnicodeString()函数的作用是什么
35.NTSTATUS成功与失败分别返回什么
36.内核入口函数和内核卸载函数分别叫做什么
37.RtlAppendUnicodeToString函数的作用是什么
38.ExAllocatePoolWithTag函数和ExFreePool函数的作用是什么他们是什么关系
39.什么是内存分配标识符
40.ExFreePool函数是否可以随意释放栈空间
41.KdPrint和Dbgprintf的区别是什么
42.LARGE_INTEGER结构的作用是什么
43.什么是自旋锁,如何使用自旋锁,使用自旋锁需要注意什么
44.什么是队列自旋锁,它与自旋锁的区别是什么
45.OBJECT_ATTRIBUTES结构的作用是什么
46.VXD、KDM、WDM、WDF分别是什么意思
47.初始化OBJECT_ATTRIBUTES结构的函数是什么
48.内核打开一个和关闭一个文件的函数分别是什么
49.内核文件读/写的函数分别是什么
50.SDK编程与WDK编程中注册表键的区别是什么
51.内核中如何打开或新建注册表键的函数是
52.内核中注册表键的读取使用的函数是
53.内核中注册表键的读使用的3种结构和范围
54.内核如何获取系统自启后滴答数
55.内核如何获得当前时间
56.内核定时器函数是
57.KeInitializeTimer()函数的作用是什么
58.请简单讲讲什么是系统线程
59.内核系统线程使用的函数是
60.内核系统线程中睡眠的函数是
61.内核系统线程如何实现同步事件
62.KeWaiteForSingleObject函数的作用是什么
63.简单讲讲应用程序和内核通信的原理
64.生成设备使用的函数是
65.内核基本框架是由什么组成的
66.设备对象可以没有名字吗?如果没有名字如何与应用层通信
67.符号链接是什么,使用什么函数生成
68.控制设备如何删除函数是
69.设备对象、控制设备、符号链接它们三者的关系试什么
70.什么是GUID生成的函数是什么
71.分发函数的作用是什么,请写出标准分发函数原型。
72.应用层程序如何打开对应内核驱动
73.DeviceIoControl函数的作用是什么
74.内核如何捕获客户端数据
75.客户端如何获取内核的数据
76.如何获得R3级发送给内核的缓冲区、输入缓冲区长度、输出缓冲区长度
77.CTL_CODE宏的作用是什么
78.什么是WOW64子系统
79.PatchGuard技术是什么
80.什么是驱动签名它的作用是什么
81.Win64位和Win32位内核嵌入汇编有什么变化
82.如何使用宏判断当前系统是Win64位还是Win32位
83.NTLDR从注册表什么位置以此加载驱动
答案在下面=.=
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
1.SDK是应用程序开发WDK是内核开发
2.WDK全称:Windows Driver Kit
3.下载Windows WDK包,创建C语言编写的文件使用x86 Checked Build Environment程序cd进入对应的文件夹(只能在WDK安装目录)之后使用bulid生成sys驱动程序文件,之后使用SRVINSTW.EXE文件安装驱动程序,使用net start 驱动名或net stop 驱动名启动或关闭服务
4.Windbg使用串行端口连接并且需要boot.ini文件之后重启前打开Windbg设置Kernel dbg为COM 端口号与\. \pipe\com_1 的pipe模式连接
5.Windows把内核作为一个整体,我们要告诉他代码的路径和符号表路径就可以了,Windwos内核符号表是提供了对应函数名和栈调用的查看,如果调试的时候重视看不见可懂的函数名,这就是符号表的设置问题。
6.符号表设置winDbg在连接内核成功在File下选择Symbol File Path功能目录为build生产的i386文件夹,源码设置选择File下的Soucres File Path功能将包含源码的目录写上即可
7.首先在驱动中入口函数DriverEntry中写入
#if DBG
__asm int 3
#endif
编写成sys驱动文件到虚拟机中安装服务之后Windbg下File->Open soucres File选择源码文件打开,之后net start 驱动名启动服务这个时候就会有提示点击确定源文件会更新现在现在的断点位置,需要注意的是在之前一定要先设置好Soucres File Path功能,F9取消断点、F10单步执行,F11单步进入
8.每个进程的内存空间都是互相独立的,虽然地址的数值看起来都是0~N的一个线性空间,但是大多数进程都要调用操作系统提供的功能,才能成为一个完整的应用程序,因此进程空间被划分为两部分,一部分供进程独立使用,被称为用户空间,另一部分容纳操作系统的内核,称为内核空间或者称为系统空间,比如Windows32位操作系统低2G是操作系统空间站,高2G是内核空间用户空间是各个进程隔离的,但是内核空间是共享的吗,也就是说每个进程看见的高2G空间范围内的数据,绝大部分是一样的,内核空间是受到硬件保护的,比如X86架构下的Ring 0的代码才可以访问内核空间,普通应用程序编译出来之后都是运行在Ring 3层,Ring 3调用Ring 0一般都是通过操作系统提供的一个入口,(该入口调用sysenter指令)来实现,这样应用程序就是可以调用操作系统内核提供的功能,又不至于随意修改内核的权利,内核模块并非和普通程序一样作为一个进程执行,而是运行在内核空间,成为操作系统的一个模块,最终被需要该模块提供功能的应用程序调用(也有可能被操作系统本身调用)。
9.内核模块(kernel Module)实际上可以被称为驱动程序但是并不是太严谨因为内核模块并没有,驱动任何硬件有人称之为软驱动或者称之为内核模块。
10.内核模块位于内核空间,而内核空间又被所有进程共享,因此实际上内核模块位于任意一个进程空间中。
11.PsGetCurrentProcessId函数是获得当前进程的进程号,因为任意一段代码的任意一次执行都是一定位于某个进程空间中的,所以如果知道是哪一个进程的调用就需要使用此函数。
12.System进程是Windwows的一个特殊进程,这个进程的PID始终为4,在调用PsGetCurrentProcessId函数时,就会发现内核模块分发函数调用时,当前进程一般都不是System进程,但是DriverEntry(驱动入口)函数被调用时,一般都是在系统进程中的,这是因为Windwos一般都是用操作系统来加载内核模块的,但并不是说所有内核代码始终运行在System进程里。
13.X86用户空间0x00000000~0x7FFFFFFF
X86内核空间0x80000000~0xFFFFFFFF
X64用户空间0x0000000000000000~0x7FFFFFFFFFFFFFFF
X64内核空间0x8000000000000000~0xFFFFFFFFFFFFFFFF
14.WDK基本数据结构类型,在内核编程时应该遵守WDK编码习惯,虽然并不是必须的但是不同C语言编译器或者不同平台的编译有可能会表示的字节长度不一样,如果长度不一样的话使用重新定义过的类型是有好处的,遇到了是没问题重新定义一下就行了,不至于产生不可控的问题.
(1)unsigned long ULONG
(2)unsigned char UCHAR
(3)unsigned int UINT
(4)void VOID
(5)unsgined long* PULONG
(6)unsigned int* PUINT
(7)void * PVOID

15.绝大部分内核API返回值是一个返回状态也就是一个错误码,这类型是NTSTATUS
NTSTATUS status;
If(!NT_SUCCESS(status)){错误捕获}
NT_SUCCESS函数是用来检查返回值是否成功。
16.驱动字符串一般是使用下面的结构
typedef struct UNICODE_STRING{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
}UNICODE_STRING *PUNICODE_STRING;
使用例子:
UNICODE_STRING str=RTL_CONSTANT_STRING(L”first:Hello word system”);
DbgPrint(“%wz”,&str);
UNICODE_STRING的指针使用%wZ打印,int使用%d打印,char用%c打印,
UNICODE_STRING除了初始化和打印之外,还可以像普通字符串一样做很多操作,比如
拷贝、连接、转换等。
17.驱动对象是Windows内核采用面向对象的编程方式,虽然使用的是C语言但是仍然模拟C++面向对象编程,Windows内核认为许多东西都是对象,比如一个驱动,一个设备、一个文件、甚至其他的一些东西,对象相当于一个基类。
在Windows启动之后,这些内核对象都在内存中,比如我们在内核中写代码,则可以随意访问他们,每个种类的内核对象都用一个结构体来表示。
一个驱动对象代表了一个驱动程序,或者说一个内核模块。驱动对象使用的结构是DRIVER_OBJECT,可以到WDK中的wdm.h查看,写一个驱动程序或者你内核模块要在Windows中加载。则必须填写这样的一个结构,来告诉Windows提供哪些功能…
18
内核回调函数分为两种,一种是普通分发函数,另一种是快速IO分发函数,相比SDK编程开始执行来生成一个进程就不同了,内核模块并不生成一个进程,只是填写一组回调函数让Windows来调用,而且这组回调函数必须符合Windows内核规定,上述的2中回调函数(分发函数)是用来处理发送给这个内核模块的请求,一个内核模块的所有功能都由它们提供给Windows。
19.Windows中很多组件都有自己的DRIVER_OBJECT比如所有的硬件驱动程序、所有的类驱动(Disk Cdron)文件系统(NTFS、FastFat)都有各自的DRIVER_OBJCET以及其他的内核组件,可以通过编写内核程序,能找到这些关键的驱动对象结构(比如NTFS文件操作系统),然后改写下面的分发函数,替换成我们自己的函数,可能就能捕获Windows文件的功能,比如扫描病毒、文件加密、这是就是分发函数Hook技术。
20.设备对象结构是DEVICE_OBJECT被常常简称为DO,它的作用类似于Windows GUI编程中的窗口,窗口是唯一可以接受消息的东西,任何消息都是发送给一个窗口的,而内核世界中,大部分”消息”都是请求(IRP)的方式传递的,而内核设备对象DEVICE_OBJECT是唯一可以接受请求的实体,任何一个请求(IRP)都是发送给某个设备对象的,比如一个DO(DEVICE_OBJECT)可以代表一个硬盘因为硬盘可以读或者写,一个DO是由内核程序生成的,而一个内核程序是一个驱动对象表示的,所以一个设备对象总是属于一个驱动对象,驱动对象可以生成多个设备对象。
21.Windows向设备对象发送请求,这些请求是被驱动对象的分发函数捕获的,当Windows内核向一个设备发送一个请求时,驱动对象的分发函数中的某一个会被调用。
分发函数例子:
参数device是请求的目标设备、irp是请求指针
NTSTATUS MyDispatch(PDEVICE_OBJECT device,PIRP irp);
处理过程由内核模块的开发者在这函数中编写
22.IRP内核请求类似于一个动作比如网卡发送一个数据包、或者想网卡请求把已经缓存在缓存区收到的包读出来,这就是一个请求,又比如在硬盘64MB地方写入一个512字节的数据这也是一个请求,IRP请求的结构是DECLSPEC_ALIGN、IRP对象、*PIRP指针,IRP结构相对来说比较复杂以为他要传递n个设备才能完成。
23.IRP栈空间是因为IRP往往要传递N个设备才能得以完成,在传递过程中,有可能会有一些中间变换,导致请求参数变化,为了保护这种变化,我们每次中转都留一个栈空间,用来保存中间参数,请求并非简单的一个输入,并等待一个输出,而是经过许多中转才得以完成,而且在中转过程的每个不中输入都是可以被改变的,所以可变部分的输入信息保存在一个类似于栈的机构中,每中转一次,都使用其中的一个位置,域CurrentLoaction表示当前使用了那个一个。
24.
常见Windows内核API
Ex系列函数(常用于内核分配,获取互斥体等)
ExAllocatPool:内存分配,相当于C RunTime库里的malloc函数
ExFreePool:内存释放,相当于 C RunTime库里的free函数
ExAcquireFastMutex:获取一个快速互斥体,互斥体用于多线程环境中的同步
ExReleaseFastMutex:释放一个快速互斥体。
ExRaiseStatus:抛出一个异常,带有错误status值,这个函数用于从代码很深的地方直接报错
Zw系列函数与Nt系列函数(读写、打开、请求)
Zw函数和同名的Nt函数有相同的作用实际上就是简单的跳转关系,被称为Native API
ZwCreateFile、NtCreateFile:打开文件(实际上也可以用于打开一个设备)
ZwWrite、NtWirte:写入文件(实际上也可以用于发送请求给设备)
ZwReadFile、NtReadFile:读同上
ZwQueryDirectoryFile、NtQueryDirectoryFile:目录查询
ZwDeviceIoControFile、NtDeviceIoControFile:发出设备控制请求
ZwCreateKey、NtCreayeKey:打开一个注册表键
ZwQueryValueKey、NtQueryValueKey读取一个注册表中的值
Rtl系列函数(字符串操作、内存操作)
RtInitUincodeString:初始化一个字符串
RtlCopyUnicodeString:拷贝字符串
RtlAppendUnicodeToString:将一个字符串追加到另一个字符串后
RtlStringCbPrintf:将字符串打印到一个字符串中,相当于sprintf
RtlCopyMemory:内存数据块的拷贝
RtlMoveMemory:内存数据块移动
RtlZeroMemory:内存数据清空
RtlCompareMemroy:比较内存
RtlGetVersion:获取当前Windows版本
Io系列函数(IO管理器、IRP、消息发送)
IoCreateFile:打开一个文件相比ZwCrateFile更加底层
IoCreateDevice:生成一个设备对象
IoCallDriver:发送请求,实际上这个函数可能是IoCallDriver的一个别名Windows管理器调用这个函数吧不同IRP发送不同的设备
IoComplateRequest:完成请求,这实际是通知IO管理器这个IRP已经完成了
IoCopyCurrentIrpSatckLocationToNext:获取当前IRP栈空间拷贝到下一个栈空间
IoSkipCurrentIrpStackLocationToNext:跳过当前IRP栈空间
IoGetCurrentIrpStackLocation:获得IRP当前栈空间指针
25.Windows内核编程主要调用源
调用源类似于SDK编程中main函数中调用了其他函数,比如调用了函数B而函数B调用了函数C,那么函数C的调用源就是main函数中,调用路径中包含了函数B,在大部分情况下单线程函的用户态应用程序只有一个调用源那就是主函数,内核编程的一个显著特点是,任意一个函数往往可能有多个调用源,主要可以追溯的调用源如下:
(1)入口函数DriverEntry和卸载函数DriverUnload
(2)各种分发函数(包括普通分发函数和快速IO分发函数)
(3)处理请求时设置的完成函数,也就是说,该请求完成后会被系统调用的回调函数
(4)其他回调用函数(如各类NDIS驱动程序的特征函数)
还有可能包含其他调用源,在写任意代码时,都应该了解这段代码可能是调用源应该在哪里,这对处理函数可重入性和考虑运行中断级都有很大的好处。
26.
多线程安全就是指一个函数被调用过程中,还没有返回时,又被其他线程调用的情况下,函数的执行结果的可靠性,则成为这个函数的多线程安全性,结果不可靠就是非安全的。
相较于SDK开发WDK开发的多线程安全冲突比较常见,但是严格的保证每个函数的多线程安全性是浪费和不可能的,需要通过下面几个规则进行判断是否需要多线程安全。
(规则1)可能运行于多线程环境的函数,必须是多线程安全的,只运行于单线程环境的函数,则不需要多线程安全性。
(规则2)如果函数A的所有调用源只运行于同一个单线程的环境下,则函数A也是只运行在单线程环境下。
(规则3)如果函数A的其中一个调用源可能是运行在多线程环境中,或者多个调用源可能运行于不同的可并发的多线程环境下,而且调用路径上没有采取多线程序列化成单线程的强制措施,则函数A也是可能运行在多线程环境下的。
(规则4)如果函数A所有的可能运行于多线程环境的调用路径上,都有多线程序列化成单线程的强制措施,则函数A是运行在单线程环境下的。
多线程序列化成单线程的强制措施是指如互斥体、自旋锁、等同步手段
内核代码中主要调用源的运行环境
DriverEntry、DriverUnload 单线程 这两个函数由系统进程的单一线程调用
各种分发函数 多线程 没有任何文档保证分发函数不会被多线程同时调用,此外分罚函数不会和DriverEntry并发、但是有可能和DriverUnload并发
完成函数 多线程 完成函数随时可能被未知的线程调用
各种NDIS回调函数 多线程 和完成函数相同
保证多线程安全性规则
(规则1)只使用函数内部资源,完全不使用全局变量,静态变量或者其他全局性资源函数是多线程安全的。
(规则2)如果对某个全局变量或者静态变量的所有访问都被强制的同步手段限制为同一时刻只有一个线程访问,则即使使用了这些安全变量和静态变量,对函数的多线程安全性也是没用影响的,可以等同于内部函数。
27.内核中断级有Dispatch级和Passive级
Dispathch级相比passive级要高
简单函数在Dispatch级中执行,复杂的函数要在passive级中运行
内核代码主要调用源中断级
DriverEntry/DriverUnload Passive级
各种分发函数 Passive级
完成函数 Dispatch级
各种NDIS回调函数 Dispatch级
划分规则
规则1:
如果在调用路径上没有特殊情况(导致中断级的提高或降低)则一个函数执行时的中断级和它的调用源的中断级相同
规则2:
如果在调用路径上有获取自旋锁,则中断级随之升高,如果在调用路径上有释放自旋锁则中断级随之下降。
Dispacth级的代码调用Passive级代码不能强制降低当前中断级,所以需要生成一个线程专门执行Passive级代码。
28.
#pragma alloc_text宏用来指定某个函数可执行代码在编译出来之后在sys文件中的位置,
内核模块编译出来之后是一个PE格式的sys文件,这个文件的代码段(text段)中有不同的节(Section),不同的节被加载到内存中之后处理情况不同,分别有三种节。
INT节:初始化完成之后就释放,也就是说不再占用内存空间了。
PAGE节:可以进行分页交换内存空间,这些内存空间在内存紧张时可以被交换到硬盘上以节省内存。
PAGELK节:是默认节,加载后位置不可分页交换的内存地址中
(1)#pragma alloc_text(INIT,DriverEntry);
(2)#pragma alloc_text(PAGE,NdisProtUnload);
(3)#pragme alloc_text(PAGE,NdisProtOpen);
(4)#pragma alloc_text(PAGE,NdisProtClose);
需要注意的是PAGE节函数不可在Dispatch级中调用,因为这种函数调用可能诱发页中断,而缺页中断处理不能再Dispatch级完成,为此一般都用一个宏PAGED_CODE()进行测试,如果发现当前中断级为Dispacth级,则程序直接报异常。
例子:
#pragma alloc_text(PAGE,SfAttachToMountedDevice)
NTSTATUS
SfAttachToMountedDevice(IN PDEVICE_OBJECT DeviceObject,IN PEDVICE_OBJECT SFilterDeviceObject){
PSFILTER_ERVICE_EXTENSION new DevExt=SfileterDeviceObject
>DeviceExtension;
NTSTATUS status;
ULONG i;
PAGE_CODE();
29.
_UNICODE_STIRNG是内核宽字符串的结构体类似于SDK的string
_STRING是内核的窄字符结构体
30.RtlInitUnicodeString函数可以是初始化UNICODE_STRING结构变量
例子:
UNICODE_STRING str={0};
RtlInitUnicodeString(&str,L”my first string”);

31.例子:
UNICODE_STRING dst;//目标字符串
WCHAR dst_buf[256];//我们现在还不会分配内存,所以先预定缓冲区
UNICODE_STRING src=RTL_CONSTANT_STRING(L”My soucre string”);
//把目标字符串初始化为拥有缓冲区长度为0的UNICODE_STRING空串
RtInitEmptyUnicodeString(&dst,dst_buf,256sizeof(WCHAR));
RtlCopyUnicodeString(&dst,&src);
32.
RTL_CONSTANT_STRING是初始化一个常数字符串的常见宏
例子:UNICODE_STRING src=RTL_CONSTANT_STRING(L”My source srting !”);
33.初始化字符串长度
例子:
UNICODE_STRING dst;
WCHAR dst_buf[256];
RtlInitEmptyUnicodeString(&dsr,dsr_buf,256
sizeof(WHCAR));
34.RtlCopyUnicodeString()函数是拷贝UNICODE_STRING结构字符串
例子:
UNICODE_STRING dst;
UNICODE_STRING dst2=RTL_CONSTANT_STRING(L”My source string”);
WCHAR dst_buf[256];
RtlInitEmptyUnicodeString(&dst,dst_buf,256*sizeof(WCHAR));
RtlCopyUnicodeString(&dst,&dst2);//拷贝

35.NTSTATUS 成功返回STATUS_SUCCESS错误则返回错误码
36.NTSTATUS DriverEntry(PDRIVER_OBEJCT dirve,PUNICODE_STRING reg_path)//内核驱动入口
VOID DriveUnload(PDRIVER_OBJECT drive)//内核驱动卸载函数
37.如果希望两个UNICODE_STRING连接成一个这种情况使用RtlAppendUnicodeToString函数
例子:
UNICODE_STRING str=RTL_CONSTANT_STRING(L”My str”);
UNICODE_STRING dsr=RTL_CONSTANT_STRING(L”My dsr”);
RtlAppendUnicodeToString(&str,&dsr);
38.
ExAllocatePoolWithTag函数类似于SDK的malloc分配内存,该函数分配的内存如果不释放则永远泄漏。
ExFreePool函数是释放ExAllocatePoolWithTag函数分配的内存类似于SDK的freeMalloc
39.内存分配标识符是每个驱动程序都定义的一个主机的内存标识,也可以在每个模块中定义单独的内存标识,内存标识是随意的32位数字,即使冲突也不会导致问题。
40.
ExFreePool不能够随意释放栈空间,它只能释放ExAllocatePoolWithTag分配的内存空间,它们需要成对出现,随意释放极容易导致系统崩溃蓝屏。
41.KdPrint相较于DbgPrint来说它是一个宏定义,而且对于Free版本将被优化点,Check版本不会优化掉,在完成KernelModule的开发后手动去动DbgPrintf比较麻烦所以直接使用KdbPrint然后使用Free版本优化点即可.
42.LARGE_INTEGER这个结构体方便之处在于,既可以方便得到高32位和低32位,也可以方便地得到整个64位,使用union联合体结构,在Windows内核中一个函数参数要使用64位数据,一般是PLARGE_INTEGER类型,VC中64位数据类型为_int64定义为_int64 file_offset.
43.(1)什么是自旋锁
自旋锁是用来解决多线程问题的,被声明了自旋锁的函数在同一时间内只有一个线程可以执行,其他线程则在KeAcquireSpinLock等候,用于防止多线程造成的数据异常。
(2)如何使用自旋锁
KSPIN_LOCK my_spin_lock;
KeInitializeSpinLock(&my_spin_lock);
KIRQL irql;//旧的中断级别被保存到这个参数中
KeAcquireSpinLock(&my_spin_lock,&irql);//单线程代码段起始
//单线程运行的代码段
KeReleaseSpinLock(&my_spin_lock,&irql);//单线程代码段结束
(3)使用自旋锁时需要注意什么
如果声明了一个KSPIN_LOCK局部锁变量在堆栈中,每个线程来执行的时候都会重新初始化一个锁,只有所有的线程共用一个锁,锁才有意义,锁一般不会定义成局部变量这个需要额外注意,可以使用静态变量锁、全局变量锁。
44.队列自旋锁是在Windows Xp系统之后被引入的,和普通自旋锁相比,队列自旋锁在多CPU平台上具有更高的性能表现,并遵循first-come frist-served原则,即:队列自旋锁遵守谁先等待,谁先获取自旋锁的原则,队列自旋锁与普通自锁的使用方法基本一样,初始化自旋锁也是使用KelInitializeSpinLock函数,唯一不同的地方是在获取和释放自旋锁时需要使用新的函数。
例子:

队列自旋锁初始化
KSPIN_LOCK my_Queue_SpinLock={0};
KeInitializeSpinLock(&my_Queue_SpinLock);
队列自旋锁的获取和释放
KLOCK_QUEUE_HANDLE my_lock_queue_handle;
KeAcquireInStackQueueSpinLock(&my_Queue_SpinLock,&my_lock_queue_handle);
//锁代码段
KeReleaseInStackQueuedSpinLock(&my_lock_queue_handle);
从上面的代码可以看出,队列自旋锁的使用增加了一个KLOCK_QUEUE_HANDLE数据结构,这个数据结构唯一的表示一个队列自旋锁。
45.OBJECT_ATTRIBUTES结构无论是打开文件、打开注册表键、还是打开设备都需要使用到OBJECT_ATTRIBUTES结构
46.
Windows在不同版本上开发的驱动程序有过不同的名称
VXD 是在Windows9X上的驱动程序
KDM 是在WindowsNT上的驱动程序
WDM 是在Windows98~Windows2000这个时期出现的驱动
WDF Windows2000~Windows8.1驱动
47.初始化OBJECT_ATTRIBUTES(无论是打开文件、打开注册表、还是打开设备都需要该结构Windows不会开公布)
InitilaizeObjectAttributes()函数是初始化一个OBJECT_ATTRIBUTES结构
48.
ZwCreateFile()函数用于打开一个文件
ZwClose()函数用于关闭一个文件
49.
ZwReadFile()读取文件
ZwWriteFile()写文件
50.
应用程序编程与驱动编程注册表键最大不同的一点是这个路径的写法不一样,一般应用编程中需要提供一个根子健句柄,而驱动编程中则全部用路径表示,实际上应用程序和驱动程序很大不同在于应用程序总是由某个当前用户启动的,因此可以直接读取当前用户的HKEY_CLASSE_ROOT和HKEY_CURRENT_USER。
对照
应用程序子健 驱动程序中路径写法
HEKY_LOCAL_MACHINE \Registry\Machine
HKEY_USERS \Registry\User
HKEY_CLASSES_ROOT 没有对应路径
HKEY_CURRENT_USER 没有简单的对应路径,但是可以求得
51.
ZwOpenKey打开一个注册表键
ZwCreateKey新建或打开一个注册表键
在驱动程序中使用ZwOpenKey打开注册表键的情况比较常见,和ZwCreateFile类似也需要一个OBJECT_ATTRIBUTES指针
52.注册表键的读取使用ZwQueryValueKey函数,需要注意的是注册表中键的值可能有多种数据类型,而且长度也没有定数,为此在读取过程中,就要可能面对很多种可能的情况。
53.KeyValueBasicInformation:获得基础信息,包含值名和类型
KeyValueFullInformation:获得完整信息,包含值名、类型和值的数据
KeyValuePartialInformation:获得局部信息,包含类型和值数据。
54.
熟悉Win32应用开发的读者会知道一个函数GetTickCount,这个函数返回系统自启动后好的毫秒数,在驱动开发中有一个对应的函数KeQueryTickCount不过遗憾的是这是一个滴答数
55.获取当前系统时间
KeQuerySystemTime得到当前时间,但是得到的不是当地时间,而是一个格林威尔时间,可以使用ExSystemTimeLocalTime转换成当地时间
56.
KeSetTime函数是定时器函数
57.KeInitializeTimer()函数是初始化KTIMER结构,KTIMER结构是KetSetTime函数需要的
58.
系统线程类似于应用程序的线程不一样的是,在驱动中做一些任务可能耗时过长容易使整个系统陷入停顿,一个单独的线程长期等待或者执行一些耗时的任务还不至于对系统造成致命的影响,还为此启动一个特殊线程来执行它们是最好的方法,在驱动中生成的线程一般是系统线程,系统线程所在的进程名是“System”。
59.系统线程函数是PsCreateSystemThread()
60.系统线程休眠函数KeDelayExecutionThread()
61.需要使用事件驱动技术,内核中的事件是一个数据结构,线程之间的同步,如一个线程需要等待另一个线程完成某些事后才能做某些事,则可以使用事件等待(类似于SDK互锁函数、事件内核对象),需要使用KEVENT结构,KeInitizeEvent初始化KEVENT结构,设置事件使用KeSetEvent
62.KeWaitForSingleObject(与KEVENT结构一起使用的同步函数)函数是检测事件是否被设置,如果没有被设置,那么就会阻塞在这里继续等待。
63.如果一个驱动需要和应用程序通信,那么首先要生成一个设备对象(Device Object),在Windows驱动开发体系中,设备对象是非常重要的元素。设备对象和分发函数构成了整个内核体系的基本框架。设备对象可以在内核中暴露出来给应用层,应用层可以像操作文件一样操作它,用于和应用程序通信的设备往往用来控制这个内核驱动所以往往被称为,
控制设备对象(Control Device Object CDO)
64.
IoCreateDevice生成设备函数该函数生成的设备默认具有安全属性,必须具有管理员权限的进程才能打开它
IoCreateDeviceSecure对比IoCreateDvice来说可以强迫生成一个任何用户都可以打开的设备,如果是商业软件那么这显然是不安全的。
65.内核基本框架是由设备对象和分发函数构成的,设备对象可以暴露给应用层,应用层可以像操作文件一样操作设备对象
66.设备对象是可以没有名字的,但是控制设备需要一个名字,这样它才会暴露出来,供其他程序打开与通讯,设备的名字可以在调用IoCreateDevice或者IoCreateDeviceSecure时指定,此外应用层是无法直接通过设备的名字来打开对象的,为此必须要要建立给一个暴露给应用程序层的符号链接。
67.符号链接就是记录一个字符串对应到另一个字符串的一种简单结构,生成的函数是IoCreateSymbolicLink函数生成符号链接,需要注意的是符号链接名字如果在系统中已经存在(符号链接会在Windows全局存在)那么这个函数将返回是失败.
68.如果在驱动中生成了控制设备与符号链接,那么在驱动卸载时就应该删除它们,否则符号链接会一直存在,删除符号表的函数是IoDeleteSymbolicLink;
69.设备对象可以没有名字但是控制设备需要一个名字,这样它才会被暴露出来,供应用程序通信,控制设备可以在设备对象创建的时候指定,应用层无法通过设备对象名与内核通信因此需要符号表绑定控制设备名来与内核通信。
70.GUID是全局唯一标识符,是一种由算法生成的二进制长度为128位的数字标识符,也可以手动生成格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx例如:6F9619FF-8B86-D011-B42D-00C04FC964FF 即为有效的 GUID 值,内核生成GUID的函数是CoCreateGuid.
71. 分发函数是一组用来处理发送给设备对象(当然也包括控制设备)的请求的函数,这些函数由内核驱动开发者编写,以便处理这些请求并返回给Windows,分发函数是设置在驱动对象(Driver Object)上的,也就是说每个驱动都有自己一组的分发函数,Windows的Io管理器在收到请求时,会根据请求发送的目标,也就是一个设备对象,来调用这个设备对象所从属的驱动对象上对应的分发函数。
标准分发函数原型:
NTSTATUS cwDispatch(IN PDEVICE_OBJECT dev,IN PIRP irp)
其中dev就是请求要发送给的目标对象,irp则代表了请求内容的数据结构指针,无论如何,分发函数必须首先设置给驱动对象,这个工作一般在DriverEntry中完成
例子:
NTSTATUS DriverEntry(PDRIVER_OBJECT driver,PUNICODE_STRING reg_path){
ULONG i;
for(i=0;i<=IRP_MJ_MAXIMUM_FUNCTION;i++){
driver->MajorFunction[i]=cwkDispatch;
}
注意:上面的片段中将所有的分发函数(实际上MajorFunction是一个函数指针数组,保存所有分发函数的指针)都设置成同一个函数,这是一种简单的处理方案,读者可以为每种请求设置不同的分发函数,如果有很多种类的请求要处理,而且每种请求的处理还都很复杂,那么放在同一个函数里做就会导致那个函数非常庞大、复杂。
72.应用层使用CreateFile函数打开内核驱动的符号链接并与内核的分发函数沟通
例子:
#define CW_DEV_SYM L"\\.\slbkcdo_3948d33e" //内核符号链接名
CreateFile(CW_DEV_SYM,GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_SYSTEM,0);
73.DeviceIoControl这个函数发送一个设备对象的设备控制请求到内核
74.内核分发函数检查是否为IRP_MJ_DEVICD_CONTROL如果是则switch捕获当前栈空间栈空间->Parameters.DeviceIoControl.IoControlCod,如果是对应的请求码则进入
75.Cratefilemapping函数由R3创建一个共享内存,R3发送内核接受,内核往R3创建的共享内存返回数据。
76.irp->AssociatedIrp.SystemBuffer;//获得缓冲区
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);//请求当前栈空间,栈空间结构是适用于内核的设备对象栈结构
ULONG inlen = irpsp->Parameters.DeviceIoControl.InputBufferLength;//获得输入缓冲区的长度
ULONG outlen = irpsp->Parameters.DeviceIoControl.OutputBufferLength;//获得输出缓冲区的长度
77.CTL_CODE宏是SDK头文件提供的,用来生成一个主机的设备控制请求功能号,CTL_CODE有四个参数,其中第一个参数是设备类型,第二参数是生成功能号的核心数字,这个数字用来和其他参数合成功能号,0x0~0x7ff被微软预留所以必须使用大于数字,同时还这个数字不能大于0xfff,如果要定义超过一个的功能号,那么不同的功能号就靠着区分,第三个参数是缓冲方式,最后一个参数是操作需要的权限
例子:
#define CWK_DVC_SEND_STR
(ULONG)CTL_CODE(
FILE_DEVICE_UNKNOWN,
0x912,METHOD_BUFFERED,
FILE_READ_DATA)
FILE_DEVICE_UNKNOWN=没有定义的类型
METHOD_BUFFERED=输入\输出缓冲区会在用户和内核质之间拷贝
FILE_READ_DATA=写入权限
78.
WOW64子系统是64位Windows系统为了兼容32位应用程序而新增的子系统,简单来说,可以先把它理解成一个轻量级的兼容层,这个兼容层主要工作在应用层,主要由三个DLL实现,分别是Wow64.dll、Wow64Win.dll、Wow64Cpu.dll,当一个32位应用程序发起系统调试时,WOW64子系统拦截到这个系统调用,WoW64子系统会把这些指针长度转换为合适的长度,然后把系统请求提交到内核,通常把这个拦截-转换的过程称为thunking.
WOW64子系统有两个重要的模块,分别为文件系统重定向器(File System Redirector)
和注册表重定向器(Registry Redirector)模块。
WOW64系统分别存在两个System32目录,分别为%windir%\System32和%windir%\SysWOW64,前者目录下主要包含64位系统二进制文件,后者目录下主要包含了32位系统二进制文件,一般来说,32位应用程序只能加载32位的DLL,64位应用程序只能加载64位的DLL,但是一个32位应用程序在32位系统下运行时,这个32位应用程序同样会到System32目录下加载所需的系统DLL,而64位系统的System32目录下包含的是64位系统的DLL,这样会导致32位应用程序无法启动,为了解决这个问题32位系统文件访问%windir%\System32目录会被WOW64子系统中定向到%windir\SysWOW64目录。
79.
简单来说对于32位系统,驱动模块可以在内核的数据结构或者关键函数进行挂钩和修改,但这种技术不再适用于Windows64位系统,原因是微软在64位系统中引入了PatchGuard机制,PatchGurad机制就是系统会定时检查系统关键位置,比如SSDT(系统服务描述表),GDT(全局符号表)、IDT(中断描述表)、系统模块(如ntoskrnl.exe、hal.sys)等,一旦发现这些关键位置的数据或代码被篡改,系统就会触发蓝屏(BSOD),正是因为如此,一些在32位系统上能使用的SSDT HOOK、INLINE HOOK、IDT HOOK等技术在64位系统下全部不在适用。
80.
与32位系统相比64位系统需要驱动签名,驱动签名就是要求驱动文件必须要经过微软指定的证书颁发机构颁发的微软代码签名证书签名,只有被签名的驱动文件才可以被加载运行,其目的有两个一个是确保驱动代码没有被非法篡改,二是确保驱动代码来源的可靠性,从而保护驱动代码的完整性。
81.Win64位相比Win32位不也许内核嵌入汇编,如果需要使用汇编,需要把汇编diamante写成函数放入到一个单独的asm文件中,然后通过函数调用,WDK也提供了一些帮助的函数比如__asm int 3 常用的断点调试,有DbgBreakPoint函数,如果使用IDA反汇编那么它和__asm int 3 功能类似。
82.在Windows驱动开发的过程中,有时候代码的逻辑需要分不同的平台编译不同的代码分支,比如需要开发一个注册表监控与驱动,按照以往32位系统的常规做法,是通过挂钩系统服务描述表(SSDT)中注册表相关的API而实现的,但是在64位系统中,导致挂钩SSDT方案不在适用,所以代码必须针对不同系统平台不同方案的代码。
WDK中预置了一些宏来帮助开发者实现对不同系统平台进行编译
比如:
_M_AMD64:当_M_AMD64被定义时,表示当前编译环境是AMD64。
_M_IX64:当_M_IX64被定义时,表示当前编译环境是IA64。
_WIN64被定义说明是Win64系统
#ifdef _WIN64
//64位环境,使用其他方案监控注册表
#else
//32位环境,使用SSDT HOOK 来监控注册表
#endif
83.NTLDR从注册表什么位置以此加载驱动
ntldr依次从HKEY_LOCAL_MACHINE\System\CurrentControlSet键下读取机器安装的驱动程序,然后依次加载驱动程序,初始化底层设备驱动,在注册表HKEY_LACAL_MACHINE\System\CurrentControlSet\Service键下查询Start键的值为0和1的设备驱动
“Start”键的值可以为0,1,2,3,4数值越小,启动越早,SERVICE_BOOT_START(0)表示内核刚刚初始化,此时加载的都是与系统核心有关的重要驱动程序,例如磁盘驱动,SERVICE_SYSTEM_START(1)稍晚一些,SERVICE_AUTO_START(2)是从登陆界面出现的时候开始,如果登陆速度较快,很可能驱动还没有加载就已经登陆了,SERVICE_DEMAN_START(3)表示在需要的时候手动加载,SERVICE_DISABLED(4)表示禁止加载。

你可能感兴趣的:(c++,win内核,内核,面试,c++)