首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
86 阅读
2
nvim番外之将配置的插件管理器更新为lazy
73 阅读
3
2018总结与2019规划
55 阅读
4
PDF标准详解(五)——图形状态
37 阅读
5
为 MariaDB 配置远程访问权限
33 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE 运动
菜谱
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
linux
文本编辑器
Java
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
311
篇文章
累计收到
27
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE 运动
菜谱
页面
归档
友情链接
关于
搜索到
190
篇与
的结果
2017-02-16
定时器的实现
使用IO定时器IO定时器每隔1s就会触发一次,从而进入到定时器例程中,如果某个操作是每n秒执行一次(n为正整数)可以考虑在定时器例程中记录一个计数器大小就为n,每次进入定时器例程中时将计数器减一,当计数器为0时,表示到达n秒,这个时候可以执行操作。IO定时器只适合处理整数秒的情况在使用IO定时器之前需要对定时器进行初始化,初始化函数为IoInitializeTimer,定义如下:NTSTATUS IoInitializeTimer( IN PDEVICE_OBJECT DeviceObject, //设备对象指针 IN PIO_TIMER_ROUTINE TimerRoutine,//定时器例程 IN PVOID Context//传给定时器例程的函数 );初始化完成后可以使用IoStartTimer来启动定时器,使用IoStopTimer来停止定时器,下面是一个例子#define PAGEDCODE code_seg("PAGE") #define LOCKEDCODE code_seg() #define INITCODE code_seg("INIT") #define PAGEDDATA data_seg("PAGE") #define LOCKEDDATA data_seg() #define INITDATA data_seg("INIT") typedef struct _tag_DEVICE_EXTENSION { PDEVICE_OBJECT DeviceObject; UNICODE_STRING uDeviceName; UNICODE_STRING uSymbolickName; LONG lTimerCount; //定时器触发时间,以秒为单位 }DEVICE_EXTENSION, *PDEVICE_EXTENSION; NTSTATUS DriverEntry(DRIVER_OBJECT *DriverObject, PUNICODE_STRING RegistryPath) { NTSTATUS status; LONG i; PDEVICE_OBJECT pDeviceObject; UNREFERENCED_PARAMETER(RegistryPath); DriverObject->DriverUnload = DriverUnload; //设置派遣函数,这些代码在这就省略了 status = CreateDevice(DriverEntry, &pDeviceObject); IoStartTimer(pDeviceObject); return status; } #pragma LOCKEDCODE VOID IoTimer(DEVICE_OBJECT *DeviceObject,PVOID Context) { LONG ret; PDEVICE_EXTENSION pDeviceExtension; UNICODE_STRING uProcessName; PEPROCESS pCurrProcess; UNREFERENCED_PARAMETER(Context); pDeviceExtension = (PDEVICE_EXTENSION)(DeviceObject->DeviceExtension); ASSERT(NULL != pDeviceExtension); //采用互锁操作将定时器数减一 InterlockedDecrement(&pDeviceExtension->lTimerCount); //判断当前时间是否到达3秒 ret = InterlockedCompareExchange(&pDeviceExtension->lTimerCount, TIME_OUT, 0); if(0 == ret) { DbgPrint("3s time out\n"); } pCurrProcess = IoGetCurrentProcess(); RtlInitUnicodeString(&uProcessName, (PTSTR)((ULONG)pCurrProcess + 0x174)); DbgPrint("the current process %wZ", uProcessName); } #pragma INITCODE NTSTATUS CreateDevice(PDRIVER_OBJECT pDriverObject,PDEVICE_OBJECT *ppDeviceObject) { NTSTATUS status; UNICODE_STRING uDeviceName; UNICODE_STRING uSymbolickName; PDEVICE_EXTENSION pDeviceExtension; RtlInitUnicodeString(&uDeviceName, DEVICE_NAME); RtlInitUnicodeString(&uSymbolickName, SYMBOLICK_NAME); if(NULL != ppDeviceObject) { //创建设备对象并填充设备扩展中的变量 ... IoInitializeTimer(*ppDeviceObject, IoTimer, NULL); status = IoCreateSymbolicLink(&uSymbolickName, &uDeviceName); if(!NT_SUCCESS(status)) { //出错的话就做一些清理工作 ... return status; } if(NULL != pDeviceExtension) { RtlInitUnicodeString(&pDeviceExtension->uSymbolickName, SYMBOLICK_NAME); } return status; } return STATUS_UNSUCCESSFUL; }需要注意的是IO定时器例程是位于DISPATCH_LEVEL,所以它不能使用分页内存,所以在函数前加上一句#pragma LOCKEDCODE,表示它在非分页内存中DPC定时器DPC定时器相比IO定时器来说更加灵活,它可以指定任何时间间隔。DPC内部使用KTIMER这个内核对象进行定时,每当时间到达设置的时间,那么系统就会将对应的DPC例程加入到DPC队列中,当系统读取DPC队列时,这个DPC例程就会被执行,使用DPC定时器的步骤一般是:分别调用KeInitializeTimer和KeInitializeDpc初始化KTIMER对象和DPC对象用KeSetTimer开启定时器在DPC例程中再次调用KeSetTimer开启定时器调用KeCancelTimer关闭定时器由于每次执行KeSetTimer都只会触发一次DPC例程,所以如果想要周期性的调用DPC例程,需要在DPC例程中再次调用KeSetTimer。这些函数的定义如下:VOID KeInitializeDpc( IN PRKDPC Dpc, //DPC对象 IN PKDEFERRED_ROUTINE DeferredRoutine, //DPC例程 IN PVOID DeferredContext//传给DPC例程的参数 ); BOOLEAN KeSetTimer( IN PKTIMER Timer,//定时器 IN LARGE_INTEGER DueTime, //隔多久触发这个DPC例程,这个值是正数则表示从1601年1月1日到触发这个DPC例程所经历的时间,为负数,则表示从当前时间,间隔多长时间后触发,单位为100ns IN PKDPC Dpc OPTIONAL //传入上面初始化的DPC对象 );下面是一个使用的例子typedef struct _tag_DEVICE_EXTENSION { PDEVICE_OBJECT pDeviceObj; UNICODE_STRING uDeviceName; UNICODE_STRING uSymbolickName; KTIMER timer; KDPC Dpc; }DEVICE_EXTENSION, *PDEVICE_EXTENSION; NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath) { PDEVICE_EXTENSION pDeviceExtension; PDEVICE_OBJECT pDeviceObj; int i; NTSTATUS status; LARGE_INTEGER time_out; UNREFERENCED_PARAMETER(pRegistryPath); pDriverObject->DriverUnload = DriverUnload; //设置派遣函数 ... status = CreateDevice(pDriverObject, &pDeviceObj); //失败处理 ... //设置定时器 time_out.QuadPart = -1 * 10000000; //1s = 1000000000ns status = KeSetTimer(&pDeviceExtension->timer,time_out, &pDeviceExtension->Dpc); return STATUS_SUCCESS; } VOID DriverUnload(PDRIVER_OBJECT pDriverObject) { //该函数主要用来清理相关资源 ... } NTSTATUS DefauleDispatch(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { //默认返回成功 } NTSTATUS CreateDevice(PDRIVER_OBJECT pDriverObj, PDEVICE_OBJECT *ppDeviceObj) { PDEVICE_EXTENSION pDevEx; PDEVICE_OBJECT pDevObj; UNICODE_STRING uDeviceName; UNICODE_STRING uSymbolicName; NTSTATUS status; //创建设备对象,填充扩展设备内容 ... //初始化KTIMER DPC KeInitializeTimer(&pDevEx->timer); KeInitializeDpc(&pDevEx->Dpc, TimerDpc, pDevObj); //设置连接符号 ... return STATUS_SUCCESS; } VOID TimerDpc( __in struct _KDPC *Dpc, __in_opt PVOID DeferredContext, __in_opt PVOID SystemArgument1, __in_opt PVOID SystemArgument2 ) { static int i = 0; PTSTR pProcessName; PEPROCESS pEprocess; LARGE_INTEGER time_out; PDEVICE_OBJECT pDevObj = (PDEVICE_OBJECT)DeferredContext; PDEVICE_EXTENSION pDevEx = (PDEVICE_EXTENSION)(pDevObj->DeviceExtension); ASSERT(NULL != pDevObj); pEprocess = PsGetCurrentProcess(); pProcessName = (PTSTR)((ULONG)pEprocess + 0x174); DbgPrint("%d Call TimerDpc, Process: %s\n", i, pProcessName); time_out.QuadPart = -1 * 10000000; //1s = 1000000000ns KeSetTimer(&pDevEx->timer, time_out, &pDevEx->Dpc); i++; }
2017年02月16日
7 阅读
0 评论
0 点赞
2017-02-15
派遣函数
驱动程序的主要功能是用来处理IO请求,而大部分的IO请求是在派遣函数中完成的,用户模式下所有的IO请求都会被IO管理器封装为一个IRP结构,类似于Windows窗口程序中的消息,不同的IRP被发送到不同的派遣函数中处理IRP与派遣函数IRPIRP(I/O Request Package)输入输出请求包,IRP的两个最基本的结构是MajorFunction和MinorFunction,分别记录IRP的主要类型和子类型,它们是一组函数指针数组,不同的项纪录的是处理当前请求的回调函数,可以在这些派遣函数中继续通过MinorFunction来判断每个驱动都有一个唯一的DRIVER_OBJECT结构,这个结构中有一个MajorFunction数组,通过这个数组可以将IRP与处理它的派遣函数关联起来,当应用层有一个针对于某个设备对象的I/O请求时,会根据这个设备对象所在驱动找到对应的MajorFunction结构,再根据请求类型来找到它对应的处理函数。IRP类型与应用层中有不同的消息类型,系统会根据消息类型调用具体消息处理函数类似,IRP也有不同的类型,在应用层调用不同的函数时会产生不同的IRP类型,例如调用应用层函数CreateFile或者内核函数ZwCreateFile会产生IRP_MJ_CREATE类型的IRP。下面是不同操作所对应产生的IRP请求列表IRP类型来源IRP_MJ_CREATE创建设备,CreateFile会产生此IRPIRP_MJ_CLOSE关闭设备,CloseHandle会产生此IRPIRP_MJ_CLEANUP清除工作,CloseHandle会产生此IRPIRP_MJ_DEVICE_CONTROLDeviceIoControl函数会产生此IRPIRP_MJ_PNP即插即用消息,NT驱动不支持此中IRP,只有WDM驱动才支持此中驱动IRP_MJ_POWER在操作系统处理电源消息时会产生此IRPIRP_MJ_QUERY_INFORMATION获取文件长度,GetFileSize会产生此IRPIRP_MJ_READ读取设备内容,ReadFile会产生此IRPIRP_MJ_SET_INFORMATION设置文件长度,SetFileSize会产生此IRPIRP_MJ_SHUTDOWN关闭系统前会产生此IRPIRP_MJ_SYSTEM_CONTROL系统内部产生控制信息,蕾西与调用DeviceIoControl函数IRP_MJ_WRITE对设备进行WriteFile时会产生此IRP对派遣函数的简单处理大部分的I/O请求都来自于应用层调用相应的API对设备进行I/O操作类似于CreateFile、ReadFile等函数产生,最简单的做法是将IRP设置为成功,然后结束IRP请求,并让派遣函数返回成功,结束这个IRP调用函数IoCompleteRequest。VOID IoCompleteRequest( IN PIRP Irp, //代表要结束的IRP IN CCHAR PriorityBoost//代笔线程恢复时的优先级别 );其实当应用层调用相关函数进行I/O操作时,会陷入睡眠或者阻塞状态,等待派遣函数成功返回,当派遣函数返回时会唤醒之前的等待线程,而第二个参数就是制定这个被唤醒的线程以何种优先级别运行。一般设置为IO_NO_INCREMENT表示不增加优先级,对于键盘,或者鼠标一类的需要更快的相应,这个时候可以设置为IO_MOUSE_INCREMENT 或者IO_KEYBOARD_INCREMENT下面是完成优先级的一个表在应用层打开设备在应用层一般通过设备名称打开驱动中的设备对象,设备名称一般只能在内核层使用,应用层能看到的是设备的符号链接名,符号链接名一般以"??\"开头,在应用层的写法有些不同,应用层设备的符号链接名称以“\.\开头”,因此在内核层符号链接为:"??\HelloDevice"到了应用层则应该写为"\.\HelloDevice"。设备栈驱动对象会创建多个设备对象,并将这些设备对象叠成一个垂直的结构,这种垂直结构被称作设备栈,IRP请求首先被发往设备栈上的顶层设备上,如果这个设备不处理可以将它下发到下层的设备对象中,直到某个设备结束这个IRP请求。为了记录IRP在每层设备中做的操作,IRP中有一个IO_STACK_LOCATION数组,这个数组对应于设备栈中各个设备对IRP所做的操作。在本层的设备中可以使用函数IoGetCurrentIrpStackLocation得到本层设备对应的IO_STACK_LOCATION结构,下面是它对应的结构图缓冲区方式读写操作在调用IoCreateDeivce函数完成设备对象的创建之后,需要设置该设备对象的缓冲区读写方式,这个值是由DEVICE_OBJECT中的Flag来设置,主要有三种DO_DIRECT_IO、DO_BUFFERED_IO 、0。应用层在对设备进行读写操作时,会提供一个缓冲区用于保存需要传入到设备对象或者保存由设备对象传入的数据,Flag值规定就规定了设备对象是如何使用这个缓冲区的。DO_DIRECT_IO:内核直接通过地址映射的方式将那块缓冲区映射为内核地址,然后在驱动中使用。当使用这种方式时内核可以在IO_STACK_LOCATION结构中的MdlAddress拿到这块内存,通过函数MmGetSystemAddressFromMdlSafe传入MdlAddress值可以得到应用层传下来的缓冲区地址DO_BUFFERED_IO:内核会在内核的地址空间空另外开辟一段内存,将缓冲区的数据简单拷贝到这个新开辟的空间中。通过这种方式的读写可以在IRP结构的AssociatedIrp.SystemBuffer中获取。0:内核直接使用应用层的地址,对那块内存进行操作,这种方式是十分危险的,如果进行线程切换,这个地址就不一定指向之前的内存,这样就可能造成系统崩溃蓝屏。这种方式可以通过IRP中的UserBuffer拿到缓冲区地址另外缓冲区的长度可以通过IO_STACK_LOCATION中的Parameters.Read.Length和Parameters.WriteLength分别获取读写缓冲区的长度IO设备控制操作DeviceIoControl与驱动设备交互BOOL DeviceIoControl( HANDLE hDevice, //驱动对象句柄 DWORD dwIoControlCode, //控制码 LPVOID lpInBuffer, //传入到驱动中的数据缓冲 DWORD nInBufferSize, //缓冲大小 LPVOID lpOutBuffer, //驱动传出数据的缓冲 DWORD nOutBufferSize, //输出数据缓冲区的大小 LPDWORD lpBytesReturned, //实际返回数据的大小 LPOVERLAPPED lpOverlapped//异步函数 );这是一个应用层的API函数,用于向驱动发送控制码,在驱动中,根据控制吗的不同而采用不同的处理方式进行处理,应用层可以通过后面几个参数实现与驱动的数据共享。控制码采用宏CTL_CODE来定义#define CTL_CODE(DeviceType, Function, Method, Access)这个宏有四个参数,第一个是设备对象的类型,就是在调用IoCreateDevice创建设备对象时传入的DeviceType的值,第二个参数是设备对象的控制码,为了与系统定义的区分开,一般用户自定义的取值在0x800之上。第三个参数是操作模式,主要有这样几个值:METHOD_BUFFERED、METHOD_IN_DIRECT、METHOD_OUT_DIRECT、METHOD_NEITHER,这些值主要针对的是设备对象的三种缓冲区的读写方式。第四个参数是访问权限,一般给FILL_ANY_ACCESS;这个函数在使用的时候需要注意下面几点:这个函数是在应用层调用,所以必须在调用这个函数前使用CreateFile打开这个设备对象。在调用CreateFile时会向I/O管理器发送一个Create请求,这个请求被I/O管理器包装成IRP,这个IRP的类型为IRP_MJ_CREATE,I/O管理器需要根据驱动的返回值来判断怎么处理这个请求,只有当驱动向I/O管理器返回一个成功的时候才会为其分配句柄,所以驱动中需要自己实现Create的分发派遣函数。驱动中需要自定义一个分发函数用于处理这个IOControl发下来的信息,函数中可以从IO_STACK_LOCATION结构中的Parameters.DeviceIoControl.IoControlCode获得用户层传下来的控制码默认情况下我们会在结束IOControl这个IRP的时候会给定一个返回长度为0,这个时候I/O管理器会将这个值回填到DeviceIoControl函数中的倒数第二个参数中,因此DeviceIoControl的这个参数不能为NULL,否则会造成程序崩溃
2017年02月15日
3 阅读
0 评论
0 点赞
2017-02-07
IRP的同步
应用层对设备的同步与异步操作以WriteFile为例,一般的同步操作是调用WriteFile完成后,并不会返回,应用程序会在此处暂停,一直等到函数将数据写入文件中并正常返回,而异步操作则是调用WriteFile后会马上返回,但是操作系统有另一线程在继续执行写的操作,这段时间并不影响应用程序的代码往下执行,一般异步操作都有一个事件用来通知应用程序,异步操作的完成,以下图分别来表示同步和异步操作:在调用这些函数时可以看做操作系统提供一个专门的线程来处理,然后如果选择同步,那么应用层线程会等待底层的线程处理完成后接着执行才执行后面的操作,而异步则是应用层线程接着执行后面的代码,而由操作系统来通知,因此一般来说异步相比较与同步少去了等待操作返回的过程,效率更高一些,但是选择同步还是异步,应该具体问题具体分析同步操作设备如果需要对设备进行同步操作,那么在使用CreateFile时就需要以同步的方式打开,这个函数的第六个参数dwFlagsAndAttributes是同步和异步操作的关键,如果给定了参数FILE_FLAG_OVERLAPPED则是异步的,否则是同步的。一旦用这个函数指定了操作方式,那么以后在使用这个函数返回的句柄进行操作时就是该中操作方式,但是这个函数本身不存在异步操作方式,一来这个函数没有什么耗时的操作,二来,如果它不正常返回,那么针对这个设备的操作也不能进行。一般像WriteFile、ReadFile、DeviceIoControl函数最后一个参数lpOverlapped,是一个OVERLAPPED类型的指针,如果是同步操作,需要给这个参数赋值为NULL异步操作方式设置Overlapped参数实现同步一般能够异步操作的函数都设置一个OVERLAPPED类型的参数,它的定义如下typedef struct _OVERLAPPED { ULONG_PTR Internal; ULONG_PTR InternalHigh; DWORD Offset; DWORD OffsetHigh; HANDLE hEvent; } OVERLAPPED; 对于这个参数在使用时,其余的我们都不需要关心,一般只使用最后一个hEvent成员,这个成员是一个事件对象的句柄,在使用时,先创建一个事件对象,并设置事件对象无信号,并将句柄赋值给这个成员,一旦异步操作完成,那么系统会将这个事件设置为有信号,在需要同步的地方使用Wait系列的函数进行等待即可。int main() { HANDLE hDevice = CreateFile("test.dat", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,//此处设置FILE_FLAG_OVERLAPPED NULL ); if (hDevice == INVALID_HANDLE_VALUE) { printf("Read Error\n"); return 1; } UCHAR buffer[BUFFER_SIZE]; DWORD dwRead; //初始化overlap使其内部全部为零 OVERLAPPED overlap={0}; //创建overlap事件 //设置事件采用自动赋值的方式,且初始化为无信号,这样操作系统才能在异步操作完成时自动给其赋值为有信号 overlap.hEvent = CreateEvent(NULL,FALSE,FALSE,NULL); //这里没有设置OVERLAP参数,因此是异步操作 ReadFile(hDevice,buffer,BUFFER_SIZE,&dwRead,&overlap); //做一些其他操作,这些操作会与读设备并行执行 //等待读设备结束 WaitForSingleObject(overlap.hEvent,INFINITE); CloseHandle(hDevice); return 0; }使用完成函数来实现异步操作异步函数是在异步操作完成时由操作系统调用的函数,所以我们可以在需要同步的地方等待一个同步对象,然后在异步函数中将这个对象设置为有信号。使用异步函数必须使用带有Ex的设备操作函数,像ReadFileEx,WriteFileEx等等,Ex系列的函数相比于不带Ex的函数来说,多了最后一个参数,LPOVERLAPPED_COMPLETION_ROUTINE 类型的回调函数,这个函数的原型如下:VOID CALLBACK FileIOCompletionRoutine( __in DWORD dwErrorCode, __in DWORD dwNumberOfBytesTransfered, __in LPOVERLAPPED lpOverlapped );第一个参数是一个错误码,如果异步操作出错,那么他的错误码可以由这个参数得到,第二个参数是实际操作的字节数对于Write类型的函数来说这个就是实际读取的字节数,第三个是一个异步对象。在使用这个方式进行异步时Ex函数中的OVERLAPPED参数一般不需要为其设置事件句柄,只需传入一个已经清空的OVERLAPPED类型的内存地址即可。当我们设置了该函数后,操作系统会将这个函数插入到相应的队列中,一旦完成这个异步操作,系统就会调用这个函数,Windows中将这种机制叫做异步过程调用(APC Asynchronous Produre Call);这种机制也不是一定会执行,一般只有程序进入警戒状态时才会执行,想要程序进入警戒状态需要调用带有Ex的等待函数,包括SleepEx,在其中的bAlertable设置为TRUE那么当其进入等待状态时就会调用APC队列中的函数,需要注意的是所谓的APC就是系统借当前线程的线程环境来执行我们提供的回调函数,是用当前线程环境模拟了一个轻量级的线程,这个线程没有自己的线程上下文,所以在回调函数中不要进行耗时的操作,否则一旦原始线程等到的它的执行条件而被唤醒,而APC例程还没有被执行完成的话,就会造成一定的错误。下面是使用这种方式进行异步操作的例子:VOID CALLBACK MyFileIOCompletionRoutine( DWORD dwErrorCode, // 对于此次操作返回的状态 DWORD dwNumberOfBytesTransfered, // 告诉已经操作了多少字节,也就是在IRP里的Infomation LPOVERLAPPED lpOverlapped // 这个数据结构 ) { SetEvent(lpOverlapped->hEvent); printf("IO operation end!\n"); } int main() { HANDLE hDevice = CreateFile("test.dat", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,//此处设置FILE_FLAG_OVERLAPPED NULL ); if (hDevice == INVALID_HANDLE_VALUE) { printf("Read Error\n"); return 1; } UCHAR buffer[BUFFER_SIZE]; //初始化overlap使其内部全部为零 //不用初始化事件!! OVERLAPPED overlap={0}; //这里没有设置OVERLAP参数,因此是异步操作 overlap.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); ReadFileEx(hDevice, buffer, BUFFER_SIZE,&overlap,MyFileIOCompletionRoutine); //做一些其他操作,这些操作会与读设备并行执行 printf("在此处可以执行其他操作\n"); //进入alterable,只是为了有机会执行APC函数 SleepEx(1000, TRUE); //在此处进行同步,只有当读操作完成才关闭句柄 WaitForSingleObject(overlap.hEvent, INFINITE); CloseHandle(hDevice); return 0; }在最后SleepEx让线程休眠而使其有机会执行APC例程,然后使用WaitForSingleObject来等待事件,我们在APC例程中将事件置为有信号,这样只有当异步操作完成,才会返回,利用这个可以在关键的位置实现同步,在这里按理来说可以直接用WaitForSingleObjectEx来替换这两个函数的调用,但是不知道为什么使用WaitForSingleObjectEx时,即使我没有设置为有信号的状态它也能正常返回,所以为了体现这点,我是使用了SleepEx和WaitForSingleObject两个函数。IRP中的同步和异步操作上述的同步和异步操作必须得到内核的支持,其实所有对设备的操作最终都会转化为IRP请求,并传递到相应的派遣函数中,在派遣函数中可以直接结束IRP,或者让派遣函数返回,在以后的某个时候处理,由于应用层会等待派遣函数返回,所以直接结束IRP的方式可以看做是同步,而先返回以后处理的方式可以看做是异步处理。在CreateFile中没有异步的方式,所以它会一直等待派遣函数调用IoCompleteRequest结束,所以当调用CreateFile打开一个自己写的设备时需要编写一个用来处理IRP_MJ_CREATE的派遣函数,并且需要在函数中结束IRP,否则CreateFile会报错,之前本人曾经犯过这样的错误,没有为设备对象准备IRP__MJ_CREATE的派遣函数,结果CreateFile直接返回-1.对于ReadFile和WriteFile来说,它们支持异步操作,在调用这两个函数进行同步操作时,内部会生成一个事件并等待这个事件,这个事件会和IRP一起发送的派遣函数中,当IRP被结束时,事件会被置为有信号,这样函数中的等待就可以正常返回。而异步操作就不会产生这个事件。而是使用函数中的overlapped参数,这时它内部不会等待这个事件,而由程序员自己在合适的位置等待。而调用带有Ex的I/O函数则略有不同,他不会设置overlapped参数中的事件,而是当进入警告模式时调用提供的APC函数。在派遣函数中可以调用IoCompleteRequest函数来结束IRP的处理或者调用IoMarkIrpPending来暂时挂起IRP,将IRP进行异步处理。该函数原型如下:VOID IoMarkIrpPending( IN OUT PIRP Irp );下面的例子演示了如何进行IRP的异步处理typedef struct IRP_QUEUE_struct { LIST_ENTRY IRPlist; PIRP pPendingIrp; }IRP_QUEUE, *LPIRP_QUEUE; NTSTATUS DefaultDispatch( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { NTSTATUS status; PIO_STACK_LOCATION pIrpStack; pIrpStack = IoGetCurrentIrpStackLocation(Irp); switch(pIrpStack->MajorFunction) { case IRP_MJ_READ: { PLIST_ENTRY pQueueHead; LPIRP_QUEUE pQueue; Irp->IoStatus.Information = 0; Irp->IoStatus.Status = STATUS_PENDING; pQueue = (LPIRP_QUEUE)ExAllocatePoolWithTag(PagedPool, sizeof(IRP_QUEUE), TAG); if(pQueue != NULL) { pQueue->pPendingIrp = Irp; pQueueHead = (PLIST_ENTRY)(DeviceObject->DeviceExtension); InsertHeadList(pQueueHead, &(pQueue->IRPlist)); } IoMarkIrpPending(Irp); return STATUS_PENDING; } break; case IRP_MJ_CLEANUP: { PLIST_ENTRY pQueueHead; LPIRP_QUEUE pQueue; PLIST_ENTRY pDelete; pQueueHead = (PLIST_ENTRY)(DeviceObject->DeviceExtension); if(NULL != pQueueHead) { while(!IsListEmpty(pQueueHead)) { pDelete = RemoveHeadList(pQueueHead); pQueue = CONTAINING_RECORD(pDelete, IRP_QUEUE, IRPlist); IoCompleteRequest(pQueue->pPendingIrp, IO_NO_INCREMENT); ExFreePoolWithTag(pQueue, TAG); pQueue = NULL; } } } default: { Irp->IoStatus.Information = 0; Irp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } break; } } VOID DriverUnload(IN PDRIVER_OBJECT DriverObject) { UNICODE_STRING uDeviceName; UNICODE_STRING uSymbolickName; UNREFERENCED_PARAMETER(DriverObject); RtlInitUnicodeString(&uDeviceName, DEVICE_NAME); RtlInitUnicodeString(&uSymbolickName, SYMBOLIC_NAME); IoDeleteSymbolicLink(&uSymbolickName); IoDeleteDevice(DriverObject->DeviceObject); DbgPrint("GoodBye World\n"); } NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { NTSTATUS status; int i = 0; PDEVICE_OBJECT pDeviceObject; UNREFERENCED_PARAMETER(RegistryPath); DriverObject->DriverUnload = DriverUnload; status = CreateDevice(DriverObject, &pDeviceObject); for(i = 0; i < IRP_MJ_MAXIMUM_FUNCTION + 1; i++) { DriverObject->MajorFunction[i] = DefaultDispatch; } DbgPrint("Hello world\n"); return status; } NTSTATUS CreateDevice(PDRIVER_OBJECT pDriverObject,PDEVICE_OBJECT *ppDeviceObject) { NTSTATUS status; PLIST_ENTRY pIrpQueue = NULL; UNICODE_STRING uDeviceName; UNICODE_STRING uSymbolickName; RtlInitUnicodeString(&uDeviceName, &DEVICE_NAME); RtlInitUnicodeString(&uSymbolickName, SYMBOLIC_NAME); if(NULL != ppDeviceObject) { status = IoCreateDevice(pDriverObject, sizeof(LIST_ENTRY), &uDeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, ppDeviceObject); if(!NT_SUCCESS(status)) { return status; } (*ppDeviceObject)->Flags |= DO_BUFFERED_IO; status = IoCreateSymbolicLink(&uSymbolickName, &uDeviceName); if(!NT_SUCCESS(status)) { IoDeleteDevice(*ppDeviceObject); *ppDeviceObject = NULL; return status; } pIrpQueue = (PLIST_ENTRY)((*ppDeviceObject)->DeviceExtension); InitializeListHead(pIrpQueue); return status; } return STATUS_UNSUCCESSFUL; }在上述代码中,定义一个链表用来保存未处理的IRP,然后在DriverEntry中创建一个设备对象,将链表头指针放入到设备对象的扩展中,在驱动的IRP_MJ_READ请求中,将IRP保存到链表中,然后直接调用IoMarkIrpPending,将IRP挂起。一般的IRP_MJ_CLOSE用来关闭内核创建的内核对象,对应用层来说也就是句柄,而IRP_MJ_CLEANUP用来处理被挂起的IRP,所以在这我们需要对CLEANUP的IRP进行处理,在处理它时,我们从链表中依次取出IRP,调用IoCompleteRequest直接结束并清除这个节点。对于其他类型的IRP则直接结束掉即可。在应用层,利用异步处理的方式多次调用ReadFile,最后再IrpTrace工具中可以看到,有多个显示状态位Pending的IRP。在处理IRP时除了调用IoCompleteRequest结束之外还可以调用IoCancelIrp来取消IRP请求。这个函数原型如下:BOOLEAN IoCancelIrp( IN PIRP Irp );当调用这个函数取消相关的IRP时,对应的取消例程将会被执行,在DDK中可以使用函数IoSetCancelRoutine,该函数可以通过第二个参数为IRP设置一个取消例程,如果第二个参数为NULL,那么就将之前绑定到IRP上的取消例程给清除。函数原型如下:PDRIVER_CANCEL IoSetCancelRoutine( IN PIRP Irp, IN PDRIVER_CANCEL CancelRoutine );在调用IoCancelIrp函数时系统在内部会获取一个名为cancel的自旋锁,然后进行相关操作,但是自旋锁的释放需要自己来进行,一般在取消例程中进行释放操作。这个自旋锁可以通过函数IoAcquireCancelSpinLock来获取,通过IoReleaseCancelSpinLock来释放,下面是一个演示如何使用取消例程的例子。//取消例程 VOID CancelReadIrp( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { Irp->IoStatus.Information = 0; Irp->IoStatus.Status = STATUS_CANCELLED; IoCompleteRequest(Irp, IO_NO_INCREMENT); IoReleaseCancelSpinLock(Irp->CancelIrql); } //IRP_MJ_READ 处理 case IRP_MJ_READ: { Irp->IoStatus.Information = 0; Irp->IoStatus.Status = STATUS_PENDING; IoSetCancelRoutine(Irp, CancelReadIrp); IoMarkIrpPending(Irp); return STATUS_PENDING; }在R3层可以利用CancelIO,来使系统调用取消例程。这个API传入的是设备的句柄,当调用它时所有针对该设备的被挂起的IRP都会调用对应的取消例程,在这就不需要像上面那样保存被挂起的IRP,每当有READ请求过来时都会调用case里面的内容,将该IRP和取消例程绑定,每当有IRP被取消时都会调用对应的取消例程,就不再需要自己维护了。另外在取消时,系统会自己获取这个cancel自旋锁,并提升对应的IRQL,IRP所处的IRQL被保存在IRP这个结构的CancelIrql成员中,而调用IoReleaseCancelSpinLock函数释放自旋锁时需要的参数正是这个IRP对应的IRQL,所以这里直接传入Irp->CancelIrql
2017年02月07日
2 阅读
0 评论
0 点赞
2017-01-16
duilib基本流程
duilib的基本流程如上图,通过解析一个xml文件,将文件中的内容渲染为窗口界面,这个解析过程由WindowImplBase类来完成。基本框架如下:首先在公共头文件中加入如下内容:#include <objbase.h> #include <DuiLib\UIlib.h> using namespace DuiLib; #ifdef _DEBUG #pragma comment(lib, "DuiLib_ud.lib"); #else #pragma comment(lib, "DuiLib_d.lib"); #endif这个是duilib的一些基本配置从WindowImplBase类中派生一个类,然后实现这样3个基本函数:virtual CDuiString GetSkinFolder() { return _T("skin"); }; virtual CDuiString GetSkinFile() { return _T("HelloWnd.xml"); }; virtual LPCTSTR GetWindowClassName(void) const { return _T("HelloWnd"); };这三个函数的说明如下:1、 GetSkinFolder () 需要返回 皮肤XML 所在的文件夹2、GetSkinFile () 需要返回 皮肤 XML 的文件名(也可以包含路径)3、GetWindowClassName () 需要返回这个窗口的类名,这个类名用于 RegisterClass.这三个函数告知duilib库应该从哪个文件夹下解析哪个xml文件,并定义对应窗口的名字,以后这个类就代表这个xml文件所描述的窗口需要注意的是这些函数必须在头文件中这样写,我自己写在CPP文件中它在运行时报错,可能是库本身的bug在WinMain主函数中添加如下代码int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, _In_ int nCmdShow) { CPaintManagerUI::SetInstance(hInstance);// 加载XML的时候,需要使用该句柄去定位EXE的路径,才能加载XML的路径 CHelloWnd* wnd = new CHelloWnd; // 生成对象 wnd->Create(NULL, NULL, UI_WNDSTYLE_DIALOG, 0); // 创建DLG窗口 wnd->CenterWindow(); // 窗口居中 // wnd->ShowWindow();//作为非模态对话框显示 wnd->ShowModal(); // 显示 // CPaintManagerUI::MessageLoop(); // 消息循环,是一个默认的消息循环,什么消息都不响应 delete wnd; // 删除对象 return 0; }在这创建了一个对话框,但是如果加上消息循环就表示它是一个非模态对话框,这个窗口我没有给它菜单栏,也就没有关闭按钮,如果作为非模态对话框,要加上一句CPaintManagerUI::MessageLoop();给它一个消息循环。但是它将不能关闭,只能通过任务管理器强制结束,使用ShowModal表示将它作为模态对话框,在win32中模态对话框使用它自己的消息循环,也就不需要自己给它一个消息循环,它可以在任务栏上被关闭。类的Create函数定义如下:HWND Create(HWND hwndParent, LPCTSTR pstrName, DWORD dwStyle, DWORD dwExStyle, int x = CW_USEDEFAULT, int y = CW_USEDEFAULT, int cx = CW_USEDEFAULT, int cy = CW_USEDEFAULT, HMENU hMenu = NULL);可以看到它就是对WIN32中CreateWindow的封装,在这duilib为窗口自定义了一些类型,其中主要的类型如下:#define UI_WNDSTYLE_CONTAINER (0) #define UI_WNDSTYLE_FRAME (WS_VISIBLE | WS_OVERLAPPEDWINDOW) #define UI_WNDSTYLE_CHILD (WS_VISIBLE | WS_CHILD | WS_CLIPSIBLINGS | WS_CLIPCHILDREN) #define UI_WNDSTYLE_DIALOG (WS_VISIBLE | WS_POPUPWINDOW | WS_CAPTION | WS_DLGFRAME | WS_CLIPSIBLINGS | WS_CLIPCHILDREN) //下面是窗口的扩展类型 #define UI_WNDSTYLE_EX_FRAME (WS_EX_WINDOWEDGE) #define UI_WNDSTYLE_EX_DIALOG (WS_EX_TOOLWINDOW | WS_EX_DLGMODALFRAME) //下面是窗口类类型 #define UI_CLASSSTYLE_CONTAINER (0) #define UI_CLASSSTYLE_FRAME (CS_VREDRAW | CS_HREDRAW) #define UI_CLASSSTYLE_CHILD (CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS | CS_SAVEBITS) #define UI_CLASSSTYLE_DIALOG (CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS | CS_SAVEBITS)大部分都是对win32中窗口类型的一个组合。如果熟悉WIN32编程,那么很容易知道这些都代表什么
2017年01月16日
6 阅读
0 评论
0 点赞
2017-01-09
驱动程序的同步处理
驱动程序运行在系统的内核地址空间,而所有进程共享这2GB的虚拟地址空间,所以绝大多数驱动程序是运行在多线程环境中,有的时候需要对程序进行同步处理,使某些操作是严格串行化的,这就要用到同步的相关内容。异步是指两个线程各自运行互不干扰,而当某个线程运行取决与另一个线程,也就是要在线程之间进行串行化处理时就需要同步机制。中断请求级别在进行I/O操作时会产生中断,以便告知CPU当前I/O操作已完成,此时CPU会停下手头的工作,来处理这个中断请求,在Windows操作系统中,分为硬件中断和软件中断。并且将这些中断映射为不同级别的中断请求级。硬件中断是由硬件产生的中断,软件中断是由int指令产生的。在传统的PC中,一般可以接收16种中断信号,每个信号对应一个中断号。硬件中断分为可屏蔽中断和不可屏蔽中断。可屏蔽中断是由可编程中断控制器(PIC)产生,这是一个硬件设备。在后面的PC机中采用了高级可编程中断控制器(APIC)代替。在APIC中将中断扩展为24个,每个都有对应的优先级,一般正在运行的线程可以被中断打断,进入中断处理程序,当优先级高的中断来临时处在低优先级的中断也会被打断。在Windows中中断请求级别有32个,但是在编程或者在MSDN上只需要关心两类级别,PASSIVE_LEVEL:用户级别,这个中断级别最低。DISPATCH_LEVEL:级别相对较高。在运用内核函数时可以查看MSDN,运行在低优先级的函数不能进行高优先级的一些操作。下面是一些常用函数的优先级函数优先级DriverEntry AddDevice DriverUnload等函数PASSIVE_LEVEL各种分发派遣函数PASSIVE_LEVEL完成函数DISPATCH_LEVELNDIS回调函数DISPATCH_LEVEL在内核模式中可以调用KeGetCurrentIrql得到当前的IRQL需要注意的是,线程优先级只针对于应用程序,只有在IRQL处于PASSIVE_LEVEL级别才有意义。当线程运行在PASSIVE_LEVEL级别的时候可以进行线程切换,而当IRQL提升到DISPATCH_LEVEL,就不再出现线程切换PASSIVE_LEVEL是应用层的中断级别,可以有线程切换,处在这个IRQL下的程序是位于进程上下文,可以进行线程的切换休眠等操作,而处于DISPACTH_LEVEL的程序属于中断上下文,CPU会一直执行这个环境下的代码,没有线程切换,不能进行线程的休眠操作,否则,一旦休眠则没有线程能够唤醒。在内存的使用上,PASSIVE_LEVEL级别的程序可以使用分页内存,一旦发生缺页中断,系统可以进行线程切换,切换到其他进程,将缺页装载在内存,但是在DISPATCH_LEVEL没有线程切换,一旦发生缺页中断就会导致系统崩溃,所以DISPATCH_LEVEL只能使用非分页内存。我们可以在程序中手动提升和降低当前的IRQL。VOID KeRaiseIrql( IN KIRQL NewIrql, //新IRQL OUT PKIRQL OldIrql//当前的IRQL ); VOID KeLowerIrql( IN KIRQL NewIrql //新IRQL );自旋锁自旋锁是一种同步机制,他能保证某个资源只被一个线程所拥有。在初始化自旋锁的时候,处于解锁状态,这个时候线程可以获取自旋锁并访问同步资源,一旦有一个线程获取到自旋锁,必须等到它释放以后,才能被其他线程获取。自旋锁被锁上之后当切换到另外的线程时,线程会不停的询问是否可以获取自旋锁。此时线程处于空转的情况,白白浪费了CPU资源,所以一般要慎用自旋锁使用方法自旋锁用结构体KSPIN_LOCK来表示使用自旋锁的时候需要对其进行初始化,初始化可以使用函数KeInitializeSpinLock,一般在驱动加载函数DriverEntry或者AddDevice函数中初始化自旋锁。VOID KeInitializeSpinLock( IN PKSPIN_LOCK SpinLock );申请自旋锁可以使用函数KeAcquireSpinLock。VOID KeAcquireSpinLock( IN PKSPIN_LOCK SpinLock, OUT PKIRQL OldIrql //自旋锁以前所处的IRQL );释放自旋锁可以使用函数KeReleaseSpinLock内核模式下线程的创建在内核模式中线程使用PsCreateSystemThread;该函数的原型如下:NTSTATUS PsCreateSystemThread( OUT PHANDLE ThreadHandle, //线程的句柄指针,这个参数作为一个输出参数 IN ULONG DesiredAccess,//新线程的权限,在驱动中这个值一般给0 IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,//线程的属性,一般给NULL IN HANDLE ProcessHandle OPTIONAL,//该线程所属的进程句柄,如果给NULL表示创建一个系统进程的线程 OUT PCLIENT_ID ClientId OPTIONAL,//指向客户结构的一个指针,在驱动中这个值一般给NULL IN PKSTART_ROUTINE StartRoutine,//新线程的函数地址 IN PVOID StartContext//线程函数的参数 );第4个参数表示创建线程的类型,如果给NULL则表示创建一个系统线程,否则表示将创建一个用户线程,DDK提供了一个宏NtCurrentThread()来获取当前进程的句柄,这个当前进程表示的是像驱动发送IRP请求的进程的句柄。获取进程名在XP中EPROCESS结构的0X174偏移位置记录着线程名,我们可以使用IoGetCurrentProcess()函数来获取当前进程的EPROCESS结构,这样我们 可以利用这样的代码来获取进程名:PEPROCESS pEprocess = IoGetCurrentProcess(); ASSERT(NULL != pEprocess); DbgPrint("the process name is %S\n", (PTSTR)((ULONG)pEprocess + 0x174));下面是一个使用线程的例子VOID MyProcessThread(PVOID pContext) { //获取当前发送IRP请求的线程名 PEPROCESS pCurrProcess = IoGetCurrentProcess(); PTSTR pProcessName = (PTSTR)((CHAR*)pCurrProcess + 0x174); // UNREFERENCED_PARAMETER(pContext); DbgPrint("MyProcessThread Current Process %s\n", pProcessName); PsTerminateSystemThread(0); } VOID SystemThread(PVOID pContext) { //获取系统进程名 PEPROCESS pCurrProcess = IoGetCurrentProcess(); PTSTR pProcessName = (PTSTR)((CHAR*)pCurrProcess + 0x174); // UNREFERENCED_PARAMETER(pContext); DbgPrint("MyProcessThread Current Process %s\n", pProcessName); PsTerminateSystemThread(0); } VOID CreateThread_Test() { HANDLE hSysThread = NULL; HANDLE hMyProcThread = NULL; NTSTATUS status; //创建系统进程 status = PsCreateSystemThread(&hSysThread, 0, NULL, NULL, NULL, SystemThread, NULL); //创建用户进程 status = PsCreateSystemThread(&hMyProcThread, 0, NULL, NtCurrentProcess(), NULL, MyProcessThread, NULL); } 内核模式下的同步对象内核模式下的同步对象与应用层的大致相同,所以理解了应用的线程同步对象,那么内核层的也很好理解内核模式下的等待函数内核模式下的等待函数是KeWaitForSingleObject 和 KeWaitForMultipleObjects,一个是用来等待单个事件,一个是用来等待多个事件。NTSTATUS KeWaitForSingleObject( IN PVOID Object, /第一个参数是一个指向同步对象的指针 IN KWAIT_REASON WaitReason,//第二个参数是等待原因,在驱动中这个值应该被设置为Executive IN KPROCESSOR_MODE WaitMode,//等待模式,处在低优先级的驱动应该将这个值设置为KernelMode IN BOOLEAN Alertable,//是否是警惕的 IN PLARGE_INTEGER Timeout OPTIONAL//等待时间,如果是正数则表示从1601年1月1日到现在的时间如果是负数则表示从现在算起的时间,单位是100ns );函数如果是等待到了对应的事件则返回STATUS_SUCCESS如果是由于等待时间到了,则返回STATUS_TIMEOUT内核模式下的事件对象在内核中用KEVENT来表示一个事件对象,在使用事件对象时需要对其进行初始化,使用函数KeInitializeEventVOID KeInitializeEvent( IN PRKEVENT Event, //事件对象的指针 IN EVENT_TYPE Type, //事件类型,一般分为两种:NotificationEvent 通知事件和同步事件SynchronizationEvent IN BOOLEAN State//是否是激发状态 );所谓的激发状态就是有信号状态,没有线程拥有这个事件。在这个状态下其他线程中的等待函数可以等到这个事件这两种类型的事件对象的区别在于如果是通知事件需要程序员手动的更改事件的状态,如果是同步事件,在等待函数等到这个事件对象后会自动将这个对象设置为无信号状态可以使用函数KeSetEvent设置事件为有信号,这样其他线程的等待函数就可以等到这个事件LONG KeSetEvent( IN PRKEVENT Event, //事件对象的指针 IN KPRIORITY Increment,//被唤起的线程将以何种优先级执行,这个参数与IoCompleteRequest的第二个参数含义相同 IN BOOLEAN Wait //一般给FALSE );下面是这个它的使用例子VOID Event_Test() { KEVENT keEvent; HANDLE hThread; //初始化事件对象,并设置为无信号 KeInitializeEvent(&keEvent, NotificationEvent, FALSE); //创建新线程,将事件对象传入线程函数中,新线程将会设置事件对象为有状态 PsCreateSystemThread(&hThread, 0, NULL, NULL, NULL, EventThread, &keEvent); if(NULL == hThread) { DbgPrint("Create Event Thread Error!\n"); return; } KeWaitForSingleObject(&keEvent, Executive, KernelMode, FALSE, NULL); } VOID EventThread(PVOID pContext) { PKEVENT pEvent = (PKEVENT)pContext; DbgPrint("This is Event Thread\n"); KeSetEvent(pEvent, IO_NO_INCREMENT, FALSE); PsTerminateSystemThread(0); }驱动程序与应用程序交互事件对象本质上用户层和内核层的事件对象是同一个东西,在用户层用句柄代替,看不到它的具体结构,在内核层是一个KEVENT,能知道它的具体数据成员。我们可以先在应用层创建一个事件对象的句柄,然后通过DeviceIoControl传到应用层,然后利用函数ObReferenceObjectByHandle将这个句柄转化为对应的事件对象,在利用这个函数转化成功后会将事件对象的计数 + 1所以在使用完后应该调用函数ObDereferenceObject使计数减1NTSTATUS ObReferenceObjectByHandle( IN HANDLE Handle, //用户层传下来的内核对象句柄 IN ACCESS_MASK DesiredAccess, //访问权限对于同步事件一般给EVENT_MODIFY_STATE IN POBJECT_TYPE ObjectType OPTIONAL,//转化何种类型的内核结构 IN KPROCESSOR_MODE AccessMode,//模式,一般有KernelMode和UserMode OUT PVOID *Object,//对应结构的指针 OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL//这个参数在内核模式下为NULL );第三个参数根据转化的内核结构的不同可以有下面的结构。参数值对应的结构*IoFileObjectTypePFILE_OBJECT*ExEventObjectTypePKEVENT*PsProcessTypePEPROCESS或者PKPROCESS*PsThreadTypePETHREAD或者PKTHREAD下面是内核层的例子else if(IOCTL_TRANS_EVENT == pIrps->Parameters.DeviceIoControl.IoControlCode) { //接收从应用层下发的下来的事件句柄 hEvent = *(PHANDLE)(Irp->AssociatedIrp.SystemBuffer); if(NULL == hEvent) { DbgPrint("Invalied Handle\n"); goto __RET; } status = ObReferenceObjectByHandle(hEvent, EVENT_MODIFY_STATE, *ExEventObjectType, KernelMode, &pkEvent, NULL); if(!NT_SUCCESS(status)) { //失败 DbgPrint("Translate Event Error\n"); goto __RET; } //将事件设置为有信号 KeSetEvent(pkEvent, IO_NO_INCREMENT, FALSE); //引用计数 -1 ObDereferenceObject(pkEvent); }驱动程序与驱动程序交互事件对象在内核驱动中可以通过给某个内核对象创建一个命名对象,然后在另一个驱动中通过名字来获取这个对象,然后操作它来实现两个驱动之间的内核对象的通讯,针对事件对象来说,要实现两个驱动交互事件对象,通过这样几步:在驱动A中调用IoCreateNotificationEvent或者IoCreateSynchronizationEvent来创建一个通知事件对象或者同步事件对象在驱动B中调用 IoCreateNotificationEvent或者IoCreateSynchronizationEvent获取已经有名字的内核对象的句柄在驱动B中调用ObReferenceObjectByHandle根据上面两个函数返回的句柄来获取A中的事件对象,并操作它操作完成后调用ObDereferenceObject解引用PKEVENT IoCreateNotificationEvent( IN PUNICODE_STRING EventName, OUT PHANDLE EventHandle );如果指定名称的事件存在那么将会通过EventHandle来返回这个事件对象的句柄,如果不存在则会创建一个事件并通过返回值直接返回这个事件对象的结构指针,需要注意的是这个名字必须以L"\BaseNamedObjects\” 开头另外不能在DriverEntry中等待过长时间,否则会造成系统蓝屏内核模式下的信号量在操作系统相关的书籍中但凡说到线程的同步问题就会涉及到信号量,当多个线程共享一个公共资源时在某一时刻只能有一个线程在运行,这个时候一般用事件对象控制,而当多个线程共享多个公共资源时,可以有多个线程同时在运行,这个时候就可以用信号量,可以把信号量想象成一个盒子,里面有多盏灯,当只要有一盏灯是亮的,就有线程可以执行,每当有一个线程在访问共享资源时,亮灯的数量-1,当线程不再访问共享资源时,亮灯的数目 +1而当灯全部熄灭时就不再允许线程访问。当盒子中只有一盏灯的时候,就相当于一个互斥体信号量的初始化函数为KeInitializeSemaphoreVOID KeInitializeSemaphore( IN PRKSEMAPHORE Semaphore,//将要被初始化的信号量的指针 IN LONG Count,//当前信号量中有多少个灯亮 IN LONG Limit//总共有多少灯 );释放信号量会增加信号灯计数。对应的函数是KeReleaseSemaphore。可以利用这个函数指定增量值,获得的信号灯可以使用Wait函数等待如果获得就熄灭一盏灯,否则就陷入等待。。利用函数KeReadStateSemaphore可以得到当前有多少盏灯是亮的下面是使用的例子VOID Semaphore_Test() { KSEMAPHORE keSemaphore; HANDLE hThread; NTSTATUS status = STATUS_SUCCESS; ULONG uCount = 0;//当前有多少盏灯亮着 //初始化,使其里面有两盏灯,两盏灯全亮 KeInitializeSemaphore(&keSemaphore, 2, 2); if(!NT_SUCCESS(status)) { DbgPrint("Initialize Semaphore Error\n"); return; } //当前有多少盏灯亮着 uCount = KeReadStateSemaphore(&keSemaphore); DbgPrint("the count = %ul", uCount); //函数会成功返回并熄灭一盏灯 KeWaitForSingleObject(&keSemaphore, Executive, KernelMode, FALSE, 0); uCount = KeReadStateSemaphore(&keSemaphore); DbgPrint("the count = %ul", uCount); //函数会成功返回并熄灭一盏灯 KeWaitForSingleObject(&keSemaphore, Executive, KernelMode, FALSE, 0); uCount = KeReadStateSemaphore(&keSemaphore); DbgPrint("the count = %ul", uCount); //创建新线程 PsCreateSystemThread(&hThread, 0, NULL, NULL, NULL, SemaphoreThread, &keSemaphore); //这时没有灯亮,函数会陷入等待状态 KeWaitForSingleObject(&keSemaphore, Executive, KernelMode, FALSE, 0); }VOID SemaphoreThread(PVOID pContext) { //线程函数 PKSEMAPHORE pkeSemaphore = (PKSEMAPHORE)pContext; DbgPrint("Entry My Thread\n"); //点亮其中的一盏灯 KeReleaseSemaphore(pkeSemaphore, IO_NO_INCREMENT, 1, FALSE); //结束线程 PsTerminateSystemThread(0); }内核模式下的互斥体互斥体在内核结构中的定义为KMUTEX使用前需要使用函数KeInitializeMutex进行初始化VOID KeInitializeMutex( IN PRKMUTEX Mutex, IN ULONG Level//系统保留参数一般给0 );初始化之后就可以使用Wait系列的函数进行等待,一旦函数返回,那么该线程就拥有了该互斥体,线程可以调用函数KeReleaseMutex来主动释放互斥体LONG KeReleaseMutex( IN PRKMUTEX Mutex, IN BOOLEAN Wait );与同步对象相比,互斥体可以在某个线程中递归获取,这个时候每当获取一次,那么它被引用的次数也将加1,在释放时,被引用多少次就应该释放多少次,只有当计数为0时才能被其他线程获取互锁操作进行同步互锁操作就是定义了一个原子操作,当原子操作没有完成时,线程是不允许切换的,系统会保证原子操作要么都完成了,要么都没有完成。在Windows中为一些常用的操作定义了一组互锁操作函数
2017年01月09日
5 阅读
0 评论
0 点赞
2016-12-26
WFP在包含fwpmu.h头的时候出错
最近在学WFP驱动框架,在使用VS2013写代码调用WFP的函数时会包含fwpmu.h这个头,但是在包含这个头的时候会报错,就像下面这个图这样:我百度了一下,然后在这个网站上面找到了解决方案:https://social.msdn.microsoft.com/Forums/windowsdesktop/en-US/8fd93a3d-a794-4233-9ff7-09b89eed6b1f/compiling-with-wfp?forum=wfp虽然我不太看得懂英文,但是根据上面的说明,我们点击第一个错误,然后定位到这个网上说的位置:在FwpTypes.h的第275行,343行出现这样的错误:从上面的错误来看,很明显是在定义宏的时候明明有换行的标志,但是它居然新换了一行,不知道是安装WDK的时候出现的还是什么原因,总之我们把这些换行去掉,然后再次编译就OK了。
2016年12月26日
7 阅读
0 评论
0 点赞
2016-12-01
自己写的驱动用CreateFile打开时错误码返回1的问题
就像题目上说的,今天在写一个例子代码时遇到了这个问题,下面是当时驱动层和应用层的代码:#include <ntddk.h> #define BASE_CODE 0x800 #define CREATE_THREAD_COMMAND CTL_CODE(FILE_DEVICE_UNKNOWN, BASE_CODE + 1, METHOD_BUFFERED, FILE_ANY_ACCESS) #define DEVICE_NAME L"\\Device\\ThreadDevice" #define LINK_NAME L"\\??\\ControlDevice" VOID DriverUnload(PDRIVER_OBJECT pDriverObject) { UNICODE_STRING uLinkName; PDEVICE_OBJECT pDev = pDriverObject->DeviceObject; RtlInitUnicodeString(&uLinkName, LINK_NAME); IoDeleteSymbolicLink(&uLinkName); IoDeleteDevice(pDev); DbgPrint("Goodbye world!\n"); } VOID MyProcessThread(PVOID pContext) { //获取当前发送IRP请求的线程名 PEPROCESS pCurrProcess = IoGetCurrentProcess(); PTSTR pProcessName = (PTSTR)((CHAR*)pCurrProcess + 0x174); // UNREFERENCED_PARAMETER(pContext); DbgPrint("MyProcessThread Current Process %s\n", pProcessName); PsTerminateSystemThread(0); } VOID SystemThread(PVOID pContext) { //获取系统进程名 PEPROCESS pCurrProcess = IoGetCurrentProcess(); PTSTR pProcessName = (PTSTR)((CHAR*)pCurrProcess + 0x174); // UNREFERENCED_PARAMETER(pContext); DbgPrint("MyProcessThread Current Process %s\n", pProcessName); PsTerminateSystemThread(0); } VOID CreateThread_Test() { HANDLE hSysThread = NULL; HANDLE hMyProcThread = NULL; NTSTATUS status; //创建系统进程 status = PsCreateSystemThread(&hSysThread, 0, NULL, NULL, NULL, SystemThread, NULL); //创建用户进程 status = PsCreateSystemThread(&hMyProcThread, 0, NULL, NtCurrentProcess(), NULL, MyProcessThread, NULL); } NTSTATUS IoControlDispatch( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { NTSTATUS status = STATUS_SUCCESS; PIO_STACK_LOCATION pIrps = IoGetCurrentIrpStackLocation(Irp); if(CREATE_THREAD_COMMAND == pIrps->Parameters.DeviceIoControl.IoControlCode) { //创建线程 CreateThread_Test(); } Irp->IoStatus.Status = status; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } NTSTATUS DriverEntry( IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath) { UNICODE_STRING uDeviceName; UNICODE_STRING uSymbolicName; PDEVICE_OBJECT pDevObj; NTSTATUS status; DbgPrint("Hello, world\n"); pDriverObject->DriverUnload = DriverUnload; //初始化设备名称和链接名称 RtlInitUnicodeString(&uDeviceName, DEVICE_NAME); RtlInitUnicodeString(&uSymbolicName, LINK_NAME); //创建设备对像 status = IoCreateDevice(pDriverObject, 0, &uDeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &pDevObj); if(!NT_SUCCESS(status)) { DbgPrint("Create Device Error!\n"); return status; } status = IoCreateSymbolicLink(&uSymbolicName, &uDeviceName); if(!NT_SUCCESS(status)) { DbgPrint("Create SymbolicLink Error!\n"); IoDeleteDevice(pDevObj); return status; } //设置IRP_MJ_DEVICE_CONTROL分发派遣函数 pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IoControlDispatch; return STATUS_SUCCESS; }#include <stdio.h> #include <Windows.h> #include <tchar.h> #define BASE_CODE 0x800 #define CREATE_THREAD_COMMAND CTL_CODE(FILE_DEVICE_UNKNOWN, BASE_CODE + 1, METHOD_BUFFERED, FILE_ANY_ACCESS) int _tmain(int argc, TCHAR *argv[]) { BOOL bRet = FALSE; HANDLE hDevice = CreateFile(_T("\\\\.\\ControlDevice"), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (INVALID_HANDLE_VALUE == hDevice) { printf("Create File Error %d \n", GetLastError()); _tsystem(_T("PAUSE")); return 0; } printf("Create File Success hDevice = %08x\n", hDevice); bRet = DeviceIoControl(hDevice, CREATE_THREAD_COMMAND, NULL, 0, NULL, 0, NULL, NULL); if (!bRet) { printf("Control Device Error, code : %d\n", GetLastError()); } _tsystem(_T("PAUSE")); return 0; }这些代码非常简单,就是直接在应用层通过CreateFile打开,然后下发一个控制命令,驱动层接收到这个命令,创建两个线程,一个获取当前下发命令的应用程序的进程名,一个获取系统进程的进程名。这段代码当时主要是有两个问题,第一个就是CreateFile打开时错误,并返回错误码1,乍看好像没有什么问题,其实这个问题我估计还是自己对应用层如何调用驱动层不太熟。首先驱动程序跟应用层的窗口程序类似,都是靠事件驱动的,在窗口程序中把这种事件叫做消息,窗口程序的主要功能是处理各种消息,由系统根据消息负责调用消息处理函数,驱动同样是这样。驱动中的设备对象就好像窗口一样,应用层下发的事件都是针对设备对象的。应用层针对不同设备对象下发的请求通过I/O管理器进行封装,变为一个个的IRP,根据不同的设备对象所属的驱动程序的不同,系统会自动调用我们事先准备好的处理程序,在程序中主要做这样几件事:对下发的事件进行适当的处理决定下发这个IRP或者结束这个IRP决定如何向I/O管理器和本层驱动程序返回值I/O管理器会根据返回的值来决定如何给上层返回一个值,就拿CreateFile来说,这个API在调用时会经过I/O管理器生成一个IRP_MJ_CREATE类型的IRP,系统根据函数所针对的设备(这个设备可以通过第一个参数知道)找到对应的驱动,然后调用驱动中对应的处理函数,然后将这个处理函数中返回的值返回给I/O管理器,I/O管理器根据这个值决定如何返回值给应用层的API。说道这,这个问题的答案基本上已经出来了,这个问题的原因就是这段代码没有给定IRP_MJ_CREATE的处理函数,I/O管理器并没有收到一个成功的返回,所以它给应用层返回一个错误,我们加上一个Create的处理函数,并在DriverEntry中指定就好了。NTSTATUS CreateDispatch( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } //然后需要在DriverEntry中加上这句话,相当于在系统中注册了一个Create事件的处理函数 pDriverObject->MajorFunction[IRP_MJ_CREATE] = CreateDispatch;这里有两个NTSTATUS的值,通过return返回的是给驱动程序的,而通过Irp->IoStatus.Status返回的是给I/O管理器的,上面说的I/O管理器没有收到成功,说的也是这个值没有给STATUS_SUCCESS做完这些工作,这个问题就这样解决了,但是接着执行后面的代码,发现程序崩溃了,会弹出一个内存读写错误的提示框,这个时候可以肯定是应用层的问题,因为如果是内核层出现内存读写错误,系统肯定蓝屏了。当时我推测可能是句柄为NULL,或者DeviceIoControl中哪个缓冲区不能为NULL,为了知道是哪的问题,我在调用DeviceIoControl之前加了一条输出语句,我发现这条语句输出的句柄值是正常的,那就肯定是DeviceIoControl的问题,我先试着吧所有的输入输出缓冲区都给定了一个值,通过排查最后发现是倒数第二个参数不能为NULL,这个参数表示的是驱动层实际返回的缓冲区的大小。最后通过查看驱动层的代码,我终于知道这个值为什么不能为NULL。这就要说到DeviceIoControl与驱动通信的方式,DeviceIoControl的定义如下:BOOL DeviceIoControl( HANDLE hDevice, DWORD dwIoControlCode, LPVOID lpInBuffer, DWORD nInBufferSize, LPVOID lpOutBuffer, DWORD nOutBufferSize, LPDWORD lpBytesReturned, LPOVERLAPPED lpOverlapped );DeviceIoControl通过dwIoControlCode向驱动下发控制码,这个控制码在驱动中可以通过IO_STACK_LOCATION 结构中的Parameters.DeviceIoControl.IoControlCode值获得。另外函数根据lpInBuffer和nInBufferSize来给驱动传递数据,通过参数lpOutBuffer和nOutBufferSize来接受来自驱动上传的数据,另外还有一个是驱动真实上传数据的大小,大小都是以字节为单位的。那么这个真实大小是怎么来的呢?答案就是通过Irp->IoStatus.Information这个值,I/O管理器取这个值,将它填充到lpBytesReturned所指向的内存中,既然我们在驱动中指定了这个值为0,自然要给它在应用层分配相应的缓冲区了,前面的由于给的是NULL,I/O管理器不可能将这个值填入NULL缓冲区,所以自然会弹出这个内存读写的错误。最后来总结下:如果我们要打开对应的驱动中的设备对象,在驱动层需要提供IRP_MJ_CREATE的处理函数,将返回给I/O管理器的值填入到IRP的IoStatus这个结构中。类似的,如果我们通过类似WriteFile、ReadFile的函数发送的相应的IRP,即使我们不需要对这个IRP进行特殊的处理,我们也需要为这些操作提供一个默认的处理函数,至少要向I/0管理器返回一个成功,这样应用层的函数才能调用成功。DeviceIoControl函数,如果不需要跟驱动层进行交互,那么他的输入输出缓冲区是可以给NULL的,但是由于I/O管理器会像它返回驱动层实际返回的数据的大小,所以这个真实大小的缓冲区一定不能为NULL
2016年12月01日
4 阅读
0 评论
0 点赞
2016-11-30
Windows内核函数
字符串处理在驱动中一般使用的是ANSI字符串和宽字节字符串,在驱动中我们仍然可以使用C中提供的字符串操作函数,但是在DDK中不提倡这样做,由于C函数容易导致缓冲区溢出漏洞,针对字符串的操作它提供了一组函数分别用来处理ANSI字符串和UNICODE字符串。针对两种字符串,首先定义了它们的结构体typedef struct _STRING { USHORT Length;//字符串的长度 USHORT MaximumLength;//字符缓冲的长度 PCHAR Buffer;//字符缓冲的地址 } ANSI_STRING, *PANSI_STRING; typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, *PUNICODE_STRING;对于这两个字符串的打印,可以使用%wZ打印UNICODE_STRING用%Z打印ANSI_STRING字符串的初始化VOID RtlInitAnsiString( IN OUT PANSI_STRING DestinationString, IN PCSZ SourceString ); VOID RtlInitUnicodeString( IN OUT PUNICODE_STRING DestinationString, IN PCWSTR SourceString );这两个函数只是简单的将SourceString 的首地址赋值给Buffer成员,并初始化相关的长度,所以在使用时需要考虑缓冲的生命周期,权限,同时如果我们改变SourceString 里面存储的字符串,那么对应的UNICODE_STRING 或者ANSI_STRING中的值也会改变,比如下面的代码RtlInitUnicodeString(&uTest, L"Hello World"); RtlCopyMemory(uTest.Buffer, L"Test");由于Buffer指向的是不可修改的常量内存部分,所以后面试图修改它的时候会造成程序崩溃。void InitString(&pUnicodeString) { WCHAR szBuf[255] = L"Hello world"; RtlInitUnicodeString(pUnicodeString, szBuffer); } void test() { UNICODE_STRING uTest; InitString(&uTest); //后面的操作 }我们在另外一个函数中利用局部变量来初始化这个字符串的时候由于当函数调用完成,函数中局部变量被销毁,这个时候指向的那块内存可能已经被其他函数所占用,而我们后面通过操作UNICODE_STRING,又要操作这段内存,这个时候一定会出现问题,所以一般如果要在多个函数中使用这个UNICODE_STRING时一般申请一段堆内存,但是在使用完成后一定要记得自己回收这段内存,否则会造成内存泄露,对此DDK专门提供了一组函数来销毁字符串中的堆内存VOID RtlFreeAnsiString( IN PANSI_STRING AnsiString ); VOID RtlFreeUnicodeString( IN PUNICODE_STRING UnicodeString );字符串拷贝:VOID RtlCopyString( IN OUT PSTRING DestinationString, IN PSTRING SourceString OPTIONAL ); VOID RtlCopyUnicodeString( IN OUT PUNICODE_STRING DestinationString, IN PUNICODE_STRING SourceString );字符串比较LONG RtlCompareString( IN PSTRING String1, IN PSTRING String2, BOOLEAN CaseInSensitive//是否忽略大小写 ); LONG RtlCompareUnicodeString( IN PUNICODE_STRING String1, IN PUNICODE_STRING String2, IN BOOLEAN CaseInSensitive );字符串转化为大写VOID RtlUpperString( IN OUT PSTRING DestinationString, IN PSTRING SourceString ); NTSTATUS RtlUpcaseUnicodeString( IN OUT PUNICODE_STRING DestinationString, IN PCUNICODE_STRING SourceString, IN BOOLEAN AllocateDestinationString//是否要求该函数自行为输出参数分配内存 );这两个函数在调用是目标字符串和源字符串可以是同一个字符串字符串与整形数字之间的转化可以使用函数NTSTATUS RtlUnicodeStringToInteger( IN PUNICODE_STRING String, IN ULONG Base OPTIONAL,//需要的数的进制 OUT PULONG Value ); NTSTATUS RtlIntegerToUnicodeString( IN ULONG Value, IN ULONG Base OPTIONAL, IN OUT PUNICODE_STRING String );ANSI与UNICODE字符串的相互转化可以使用下面的函数NTSTATUS RtlUnicodeStringToAnsiString( IN OUT PANSI_STRING DestinationString, IN PUNICODE_STRING SourceString, IN BOOLEAN AllocateDestinationString ); NTSTATUS RtlAnsiStringToUnicodeString( IN OUT PUNICODE_STRING DestinationString, IN PANSI_STRING SourceString, IN BOOLEAN AllocateDestinationString );文件操作创建或者打开一个文件文件的创建和打开都是使用函数ZwCreateFileNTSTATUS ZwCreateFile( OUT PHANDLE FileHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, OUT PIO_STATUS_BLOCK IoStatusBlock, IN PLARGE_INTEGER AllocationSize OPTIONAL, IN ULONG FileAttributes, IN ULONG ShareAccess, IN ULONG CreateDisposition, IN ULONG CreateOptions, IN PVOID EaBuffer OPTIONAL, IN ULONG EaLength );FileHandle:这个函数通过这个参数返回文件句柄DesiredAccess:以何种权限打开或者创建这个文件,GENERIC_READ可读,GENERIC_WRITE可写,GENERIC_EXECUTE可执行,GENERIC_ALL所有权限ObjectAttributes:这是一个文件属性的结构体,里面包含有要打开的文件的名称IoStatusBlock:接受函数操作文件的结果状态AllocationSize:指定在创建爱女或者写文件时初始大小,如果给0,则文件大小会随着写入数据的增加而动态的增加FileAttributes:指定新创建文件的属性,一般给0或者FILE_ATTRIBUTE_NORMALShareAccess:文件的共享权限,其他线程或者进程通过这个句柄访问文件的权限,给0表示不允许其他进程通过这个句柄访问,FILE_SHARE_READ读, FILE_SHARE_WRITE写,FILE_SHARE_DELETE删除CreateDisposition:指定当文件存在或者不存在时这个函数的动作。它的取值可以有下面几个取值文件存在文件不存在FILE_SUPERSEDE新建一个文件替代新建文件FILE_CREATE返回一个错误创建文件FILE_OPEN打开文件返回一个错误FILE_OPEN_IF打开文件创建文件FILE_OVERWRITE打开,并且将之前的内容覆盖返回错误FILE_OVERWRITE_IF打开,并且将之前的内容覆盖创建文件CreateOptions打开或者创建文件时的附加操作,一般给FILE_SYNCHRONOUS_IO_NONALERTEaBuffer指向扩展空间的指针EaLength扩展空间的大小这个函数与应用层的CreateFile不同的时,在指定打开或者创建文件名时是使用结构OBJECT_ATTRIBUTES来指定,针对这个结构,有一个函数能够初始化它VOID InitializeObjectAttributes( OUT POBJECT_ATTRIBUTES InitializedAttributes, IN PUNICODE_STRING ObjectName,//文件名 IN ULONG Attributes, IN HANDLE RootDirectory, IN PSECURITY_DESCRIPTOR SecurityDescriptor );Attributes:该对象的描述信息,一般给OBJ_CASE_INSENSITIVE 表示对大小写敏感RootDirectory :该文件的根目录,一般给NULLSecurityDescriptor :安全描述符,一般也是给NULL另外这里的名称必须使用符号链接名或者设备名,而不是我们熟悉的“C:\”这种形式对于C盘可以使用名称“\??\C”或者“\Device\HarddiskVolum1”这种形式当程序结束时需要调用ZwClose来清理文件句柄这个函数的参数比较简单,只是简单的传入文件句柄即可获取和设置文件的相关信息可以下面两个函数分别获取和设置文件的相关信息NTSTATUS ZwQueryInformationFile( IN HANDLE FileHandle, OUT PIO_STATUS_BLOCK IoStatusBlock, OUT PVOID FileInformation, IN ULONG Length, IN FILE_INFORMATION_CLASS FileInformationClass ); NTSTATUS ZwSetInformationFile( IN HANDLE FileHandle, OUT PIO_STATUS_BLOCK IoStatusBlock, IN PVOID FileInformation, IN ULONG Length, IN FILE_INFORMATION_CLASS FileInformationClass );其中FileInformationClass是一个枚举值,根据这个值得不同FileInformation可以被解析成不同的内容。当这个参数为FileStandardInformation时,使用结构体FILE_STANDARD_INFORMATIONtypedef struct FILE_STANDARD_INFORMATION { LARGE_INTEGER AllocationSize; //为文件分配簇所占空间的大小 LARGE_INTEGER EndOfFile;//距离文件结尾还有多少字节,当文件指针位于文件头时,这个值就是文件本身大小 ULONG NumberOfLinks;//有多少个链接文件 BOOLEAN DeletePending;//是否准备删除 BOOLEAN Directory;//是否为目录 } FILE_STANDARD_INFORMATION, *PFILE_STANDARD_INFORMATION;当这个参数为FileBasicInformation使用结构体FILE_BASIC_INFORMATIONtypedef struct FILE_BASIC_INFORMATION { LARGE_INTEGER CreationTime; //创建时间 LARGE_INTEGER LastAccessTime;//上次访问时间 LARGE_INTEGER LastWriteTime;//上次写文件时间 LARGE_INTEGER ChangeTime;//上次修改时间 ULONG FileAttributes;//文件属性 } FILE_BASIC_INFORMATION, *PFILE_BASIC_INFORMATION;其中时间参数是一个LARGE_INTEGER类型的整数,代表从1601年到现在经过多少个100ns。文件属性参数如果为FILE_ATTRIBUTE_DIRECTORY表示这是一个目录文件,FILE_ATTRIBUTE_NORMAL表示是一个普通文件,FILE_ATTRIBUTE_HIDDEN表示这是一个隐藏文件,FILE_ATTRIBUTE_SYSTEM表示这是一个系统文件,FILE_ATTRIBUTE_READONLY表示这是一个只读文件当这个参数为FileNameInformation时,使用结构体FILE_NAME_INFORMATIONtypedef struct _FILE_NAME_INFORMATION { ULONG FileNameLength;//文件名长度 WCHAR FileName[1];//文件名 } FILE_NAME_INFORMATION, *PFILE_NAME_INFORMATION;当这个参数是FilePositionInformation时,使用结构体FILE_POSITION_INFORMATIONtypedef struct FILE_POSITION_INFORMATION { LARGE_INTEGER CurrentByteOffset;//当前文件指针的位置 } FILE_POSITION_INFORMATION, *PFILE_POSITION_INFORMATION;读写文件写文件调用函数ZwCreateFileNTSTATUS ZwWriteFile( IN HANDLE FileHandle,//文件句柄 IN HANDLE Event OPTIONAL,//时间对象一般给NULL IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,//一般给NULL IN PVOID ApcContext OPTIONAL,//一般给NULL OUT PIO_STATUS_BLOCK IoStatusBlock,//记录写操作的状态用里面的Information成员记录实际写了多少字节 IN PVOID Buffer,//写入文件中缓冲区的指针 IN ULONG Length,//缓冲区中数据的长度 IN PLARGE_INTEGER ByteOffset OPTIONAL,//从文件的多少地址开始写 IN PULONG Key OPTIONAL//一般给NULL );读文件使用函数ZwReadFileNTSTATUS ZwReadFile( IN HANDLE FileHandle,//文件句柄 IN HANDLE Event OPTIONAL,//一般给NULL IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,//一般给NULL IN PVOID ApcContext OPTIONAL,//一般给NULL OUT PIO_STATUS_BLOCK IoStatusBlock, //读取的字节数保存在结构的成员Information中 OUT PVOID Buffer,//缓冲区的指针 IN ULONG Length,//缓冲区的长度 IN PLARGE_INTEGER ByteOffset OPTIONAL,//从文件的多少位置开始读 IN PULONG Key OPTIONAL//一般给NULL );注册表操作注册表中有下面几个概念:注册表项:注册表项类似于目录的概念,下面可以有子项或者注册表的键-值对注册表子项:类似于子目录的概念键名:通过键名可以寻找到相应的键值键值类别:每个键值在存储的时候有不同的类型,相当于变量的类型,主要有字符串和整型键值:键名下对应存储的数据创建和关闭注册表创建注册表使用函数ZwCreateKeyNTSTATUS ZwCreateKey( OUT PHANDLE KeyHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, IN ULONG TitleIndex, IN PUNICODE_STRING Class OPTIONAL, IN ULONG CreateOptions, OUT PULONG Disposition OPTIONAL );KeyHandle:输出一个注册表对应项的句柄,以后针对这个项操作都是以这个句柄作为标示DesiredAccess:访问权限,一般都设置为KEY_ALL_ACCESSObjectAttributes:用法与文件操作中的用法相同其中应用层中注册表项与内核中注册表项的对应关系如下:应用层中的子健内核中的路径HKEY_CLASSES_ROOT没有对应的路径HKEY_CURRENT_USER没有简单的对应路径,但是可以求得HKEY_USERS\Registry\UserHKEY_LOCAL_MACHINE\Registry\MachineTitleIndex:一般设置为0Class 一般给NULLCreateOptions:创建选项,一般给REG_OPTION_NON_VOLATILEDisposition:返回创建的状态,如果是REG_CREATED_NEW_KEY表示创建了一个新的注册表项如果是REG_OPENED_EXISTING_KEY表示打开一个已有的注册表项添加、修改注册表键注册表中的键是类似与字典中的键值对,通过键名找到对应的值,键值的类型大致可以分为下面几种分类描述REG_BINARY键值采用二进制存储REG_SZ键值用宽字符串,以\0结尾REG_EXPAND_SZ与上面的REG_SZ相同,它是上面那个字符串的扩展字符REG_MULTI_SZ能够存储多个字符串,每个都以\0隔开REG_DWORD键值用4字节整型存储(这个类型的数据在驱动中使用ULONG来替代)REG_QWORD键值用8字节存储(这个用LONGLONG)用函数ZwSetValueKey可以添加和修改注册表的一项内容 NTSTATUS ZwSetValueKey( IN HANDLE KeyHandle, //注册表句柄 IN PUNICODE_STRING ValueName,//要修改或者新建的键名 IN ULONG TitleIndex OPTIONAL,//一般设置为0 IN ULONG Type,//在上面的表中选择一个 IN PVOID Data,//键值 IN ULONG DataSize//键值数据的大小 );当传入的键值不存在则创建一个新键值,否则就修改原来的键值查询注册表查询注册表使用函数ZwQueryValueKeyNTSTATUS ZwQueryValueKey( IN HANDLE KeyHandle, //注册表句柄 IN PUNICODE_STRING ValueName,//注册表键名 IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass, OUT PVOID KeyValueInformation,//接收返回信息的缓冲区 IN ULONG Length,//缓冲区的大小 OUT PULONG ResultLength//真实缓冲区的大小 );使用这个函数时利用参数KeyValueInformationClass来指定接收数据的类型,根据这个值的不同,函数会返回不同的结构体放到一个缓冲区中。一般这个值可取:KeyValueBasicInformation 返回注册表项的基础信息KeyValueFullInformation 返回注册表的全部信息KeyValuePartialInformation 返回注册表的部分信息一般情况下使用KeyValuePartialInformation查询键值数据利用这个函数来查询时一般也是采用两次调用的方式,第一次返回数据所需缓冲,然后分配缓冲并进行第二次调用枚举子项DDK提供了两个函数用于这个功能NTSTATUS ZwQueryKey( IN HANDLE KeyHandle,//注册表句柄 IN KEY_INFORMATION_CLASS KeyInformationClass,//保存注册表信息的结构体的类型 OUT PVOID KeyInformation,//返回查询到信息的缓冲 IN ULONG Length,//缓冲的大小 OUT PULONG ResultLength//真正信息的大小 ); NTSTATUS ZwEnumerateKey( IN HANDLE KeyHandle,//句柄 IN ULONG Index,//这个值是表示第几个子项 IN KEY_INFORMATION_CLASS KeyInformationClass,//查询到的信息的结构体 OUT PVOID KeyInformation,//返回信息的缓冲 IN ULONG Length,//缓冲长度 OUT PULONG ResultLength//返回信息的长度 );其中ZwQueryKey函数用于查询某个注册表项中有多少个子项,在调用这个函数时传入的KeyInformationClass的值一般给KeyFullInformation,在这个结构体中的SubKeys表示有多少个子项,而ZwEnumerateKey则是用于查询各个子项中的具体内容,通过指定Index表示我们要查询该项中的第几个子项,将KeyInformationClass填入KeyBasicInformation,这样在结构体的Name里面可以得到具体的注册表子项的名称枚举子健枚举子键的方法于上面的大致相同,首先利用ZwQueryKey查询注册表,然后取结构体KeyFullInformation的成员Values,根据这个值在循环中依次调用函数ZwEnumerateValueKey,结构体类填入 KeyValueBasicInformation查询基本信息即可删除子项删除子项使用的内核函数是ZwDeleteKeyNTSTATUS ZwDeleteKey( IN HANDLE KeyHandle );这个函数只能删除没有子项的项目,如果有子项,则需要先删除所有子项。其他注册表函数为了简化注册表操作,DDK提供了另外一组以Rtl开头的函数,把之前的Zw函数进行了封装,下面是这些函数与它们功能的对应关系函数名描述RtlCreateRegistryKey创建注册表项RtlCheckRegistryKey查看注册表中的某项是否存在RtlWriteRegistryValue写注册表RtlDeleteRegistryValue删除注册表的子键
2016年11月30日
2 阅读
0 评论
0 点赞
2016-11-26
Windows内核中的内存管理
内存管理的要点内核内存是在虚拟地址空间的高2GB位置,且由所有进程所共享,进程进行切换时改变的只是进程的用户分区的内存驱动程序就像一个特殊的DLL,这个DLL被加载到内核的地址空间中,DriverEntry和AddDevice例程在系统的system进程中运行,派遣函数会运行在应用程序的进程上下文中所能访问的地址空间是这个进程的虚拟地址空间利用_EPROCESS结构可以查看该进程的相关信息当程序的中断级别在DISPATCH_LEVEL之上时,必须使用非分页内存,否则会造成系统蓝屏,在编译WDK相关例程时,可以使用如下的宏指定某个例程或者某个全局变量是位于分页内存还是运行于非分页内存#define PAGEDCODE code_seg("PAGE") //分页内存 #define LOCKEDCODE code_seg() //非分页内存 #define INITCODE code_seg("INIT") //指定在相关函数执行完成后就从内存中卸载下来 #define PAGEDDATA data_seg("PAGE") #define LOCKEDDATA data_seg() #define INITDATA data_seg("INIT")在使用时直接使用#pragma直接加载这些宏即可比如:#pragma PAGEDCODE VOID DoSomething() { PAGED_CODE() //函数体 }其中PAGED_CODE是一个WDK中提供的一个宏,只在debug版本中生效,用于判断当前的中断请求级别,当级别高于DISPATCH_LEVEL(包含这个级别)时会产生一个断言内核中的堆申请函数PVOID ExAllocatePool( IN POOL_TYPE PoolType, IN SIZE_T NumberOfBytes ); PVOID ExAllocatePoolWithTag( IN POOL_TYPE PoolType, IN SIZE_T NumberOfBytes, IN ULONG Tag ); PVOID ExAllocatePoolWithQuota( IN POOL_TYPE PoolType, IN SIZE_T NumberOfBytes ); PVOID ExAllocatePoolWithQuotaTag( IN POOL_TYPE PoolType, IN SIZE_T NumberOfBytes, IN ULONG Tag );PoolType:这是一个枚举变量,用来表示分配内存的种类,如果为PagedPool表示分配的是分页内存,如果是NonPagedPool表示分配的是非分页内存NumberOfBytes:分配内存的大小,为了效率最好分配4的倍数上面这些函数主要分为带有标记和不带标记的两种,其中有Quota的是按配额分配,带有标记的函数可以通过这个标记来判断这块内存最后有没有被分配,标记是一个字符串,但是这个字符串是用单引号引起来的。一般给4个字符,由于IntelCPU采用的是高位优先的存储方式,所以为了阅读方便,一般将这个字符串倒着写这些函数分配的内存一般使用下面的函数来释放VOID ExFreePool( IN PVOID P ); NTKERNELAPI VOID ExFreePoolWithTag( IN PVOID P, IN ULONG Tag ); 在驱动中使用链表WDK给程序员提供了两种基本的链表结构,分别是单向链表和双向链表双向链表的结构体定义如下:typedef struct _LIST_ENTRY { struct _LIST_ENTRY *Flink; //指向下一个节点 struct _LIST_ENTRY *Blink; //指向上一个节点 } LIST_ENTRY, *PLIST_ENTRY;初始化链表使用宏InitializeListHead,它需要传入一个链表的头节点指针它的定义如下VOID InitializeListHead( IN PLIST_ENTRY ListHead );这个宏只是简单的将链表头的Flink和Blink指针指向它本身。利用宏IsListEmpty可以检查一个链表是否为空,它也是只简单的检查这两个指针是否指向其自身在定义自己的数据结构的时候需要将这个结构体放到自定义结构体中,比如typedef struct _MYSTRUCT { LIST_ENTRY listEntry; ULONG i; ULONG j; }MYSTRUCT, *PMYSTRUCT一般插入链表有两种方法,头插法和尾插法,DDK根据这两种方法都给出了具体的函数可供操作://头插法,采用头插法只改变链表数据的顺序,链表头仍然是链表中的第一个元素 VOID InsertHeadList( IN PLIST_ENTRY ListHead, //链表头指针 IN PLIST_ENTRY Entry //对应节点中的LIST_ENTRY指针 ); //尾插法 VOID InsertTailList( IN PLIST_ENTRY ListHead, IN PLIST_ENTRY Entry );删除节点使用的是这样两个函数,同样采用的是从头部开始删除和从尾部开始删除,就是查找链表中节点的方向不同。//从头部开始删除 PLIST_ENTRY RemoveHeadList( IN PLIST_ENTRY ListHead ); //从尾部开始删除 PLIST_ENTRY RemoveTailList( IN PLIST_ENTRY ListHead );这两个函数都是传入头节点的指针,返回被删除那个节点的指针,这里有一个问题,我们如何根据返回PLIST_ENTRY结构找到对应的用户定义的数据,如果我们将LIST_ENTRY,这个节点放在自定义结构体的首部的时候,返回的地址就是结构体的地址,如果是放在其他位置,则需要根据结构体的定义来进行转化,对此WDK提供了这样一个宏来帮我们完成这个工作:PCHAR CONTAINING_RECORD( IN PCHAR Address, IN TYPE Type, IN PCHAR Field );这个宏返回自定义结构体的首地址,传入的是第一个参数是结构体中某个成员的地址,第二个参数是结构体名,第三个参数是我们传入第一个指针的类型在结构体中对应的成员变量值,比如对于上面那个MYSTRUCT结构体可以这样使用typedef struct _MY_LIST_DATA { LIST_ENTRY list; ULONG i; }MY_LIST_DATA, *PMY_LIST_DATA; PLIST_ENTRY pListData = RemoveHeadList(&head);//head是链表的头节点 PMYSTRUCT pData = CONTAINING_RECORD(pListData, MYSTRUCT, list);Lookaside结构频繁的申请和释放内存将造成内存空洞,即出现大量小块的不连续的内存片段,这个时候即使内存仍有剩余,但是我们也申请不了内存,一般在操作系统空闲的时候会进行内存整理,将空洞内存进行合并,如果驱动需要频繁的从内存中申请释放相同大小的内存块,DDK提供了Lookaside内存容器,在初始时它先向系统申请了一块比较大的内存,以后程序每次申请内存的时候不是直接在Windows堆中进行分配,而是在这个容器中,Lookaside结构会智能的避免产生内存空洞,如果申请的内存过多,lookaside结构中的内存不够时,他会自动向操作系统申请更多的内存,如果lookaside内部有大量未使用的内存时,他会自动释放一部分,总之它是一个智能的自动调整内存大小的一个容器。一般应用于以下几个方面:程序每次申请固定大小的内存申请和回收的操作十分频繁使用时首先初始化Lookaside对象,调用函数VOID ExInitializeNPagedLookasideList( IN PNPAGED_LOOKASIDE_LIST Lookaside, IN PALLOCATE_FUNCTION Allocate OPTIONAL, IN PFREE_FUNCTION Free OPTIONAL, IN ULONG Flags, IN SIZE_T Size, IN ULONG Tag, IN USHORT Depth );或者VOID ExInitializePagedLookasideList( IN PPAGED_LOOKASIDE_LIST Lookaside, IN PALLOCATE_FUNCTION Allocate OPTIONAL, IN PFREE_FUNCTION Free OPTIONAL, IN ULONG Flags, IN SIZE_T Size, IN ULONG Tag, IN USHORT Depth );这两个函数一个是操作的是非分页内存,一个是分页内存。Lookaside:这个参数是一个NPAGED_LOOKASIDE_LIST的指针,在初始化前需要创建这样一个结构体的变量,但是不用填写其中的数据。Allocate:这个参数是一个分配内存的回调函数,一般这个值填NULLFree:这是一个释放的函数,一般也填NULL这两个函数有点类似于C++中的构造与析构函数,如果我们对申请的内存没有特殊的初始化的操作,一般这个两个都给NULLFlags:这是一个保留字节,必须为NULLSize:指明明我们每次在lookaside容器中申请的内存块的大小每次申请的内存块的标志,这个标志与上面的WithTag函Tag:数申请内存时填写的标志相同Depth:系统保留,必须填0创建容器之后,可以用下面两个函数来分配内存PVOID ExAllocateFromNPagedLookasideList( IN PNPAGED_LOOKASIDE_LIST Lookaside ); PVOID ExAllocateFromPagedLookasideList( IN PPAGED_LOOKASIDE_LIST Lookaside );用下面两个函数来释放内存VOID ExFreeToNPagedLookasideList( IN PNPAGED_LOOKASIDE_LIST Lookaside, IN PVOID Entry ); VOID ExFreeToPagedLookasideList( IN PPAGED_LOOKASIDE_LIST Lookaside, IN PVOID Entry );最后可以使用下面两个函数来释放Lookaside对象VOID ExDeleteNPagedLookasideList( IN PNPAGED_LOOKASIDE_LIST Lookaside ); VOID ExDeletePagedLookasideList( IN PPAGED_LOOKASIDE_LIST Lookaside );其他内存函数内存拷贝函数VOID RtlCopyMemory( IN VOID UNALIGNED *Destination, IN CONST VOID UNALIGNED *Source, IN SIZE_T Length );需要注意的是这个函数没有考虑到内存重叠的情况,假如内存发生重叠例如这样:这个时候AC内存块和BD内存块有部分重叠,如果将AC拷贝到BD那么会改变AC的值,这样在拷贝到BD中的值也会发生变化,有可能造成错误,为了保证重叠也可以正常拷贝,可以使用函数void MoveMemory( __in PVOID Destination, __in const VOID* Source, __in SIZE_T Length );填充内存一般使用函数void FillMemory( [out] PVOID Destination, [in] SIZE_T Length, [in] BYTE Fill );另外DDK另外提供了一个将内存清零的函数VOID RtlZeroMemory( IN VOID UNALIGNED *Destination, IN SIZE_T Length );内存比较函数ULONG RtlEqualMemory( CONST VOID *Source1, CONST VOID *Source2, SIZE_T Length );这个函数返回的是两块内存中相同的字节数,如果要比较两块内存是否完全相同,可以将返回值与Length相比较,如果相等则说明两块内存相同,否则不相同,另外为了实现这个功能DDK提供了一个与该函数同名的宏来判断,具体在编写代码时可以根据情况判断调用的是函数还是宏。在内核中,对于内存的读写要相当的谨慎,稍不注意就可能产生一个新漏洞或者造成系统的蓝屏崩溃,有时在读写内存前需要判断该内存是否合法可供读写,DDK提供了两个函数来判断内存是否可读可写VOID ProbeForRead( IN CONST VOID *Address, IN SIZE_T Length, IN ULONG Alignment//当前内存是以多少字节对齐的 ); VOID ProbeForWrite( IN CONST VOID *Address, IN SIZE_T Length, IN ULONG Alignment );这两个函数在内存不可读写的时候引发一个异常,需要用结构化异常进行处理,这里使用结构化异常的方式与在应用层的使用方式相同其他数据结构typedef union _LARGE_INTEGER { struct { DWORD LowPart; LONG HighPart; }; struct { DWORD LowPart; LONG HighPart; } u; LONGLONG QuadPart; } LARGE_INTEGER, *PLARGE_INTEGER;这个结构用来表示64位二进制的整形数据,它是一个共用体,占内存大小是64位8个字节,从定义上来看可以看做一个LONGLONG型数据,也可以看做两个4字节的数据。
2016年11月26日
3 阅读
0 评论
0 点赞
2016-11-23
windbg蓝屏调试
一般在写Windows内核程序的时候,经常会出现蓝屏的问题,这个时候一般是采用记录下dump文件然后用windbg查看得方式,具体的过程就不说了,网上一大堆的内容。现在我主要记录自己当初按照网上的方案出现windbg的open crashdump项呈现灰色的情况。就像下面这样这个问题曾今百思不得其解,曾今一度以为是自己的win10不能很好的兼容这个,后来发现自己想多了( ^_^ ),现在公布这个问题的解决方案。主要是确保下面的工作完成1)首先需要在虚拟机上确保我们打开了抓取dump文件的功能,怎么打开百度上有一大堆。2)接着就是真实机上也要打开这个功能3)然后最重要的就是关闭虚拟机,不要让windbg连上了虚拟机,它连上了虚拟机就会呈现选项变灰的情况,查看dump文件是我们在真实机里面进行的,之前一直不知道这点,结果怎么试都不行。如果还是不行,可以考虑关了虚拟机之后重启windbg。然后可以看到已经能使用这个选项了。在调试dump文件时要确保自己已经下载了Windows内核的符号表,然后打开dump文件就可以分析出错的位置了
2016年11月23日
4 阅读
0 评论
0 点赞
1
...
13
14
15
...
19