宝峰科技

 找回密码
 注册

QQ登录

只需一步,快速开始

智能终端设备维修查询系统注册会员邮箱认证须知!
查看: 2386|回复: 0

[转载] 驱动入门

[复制链接]
  • TA的每日心情
    开心
    2024-12-9 18:45
  • 签到天数: 124 天

    [LV.7]常住居民III

    admin 发表于 2009-12-12 23:53:45 | 显示全部楼层 |阅读模式

    欢迎您注册加入!这里有您将更精采!

    您需要 登录 才可以下载或查看,没有账号?注册

    x
    介绍
    这篇文章尝试去描述如何为windows nt 写一个简单的驱动。关于写驱动的文章和资源在网上有很多,但是,都有各种不足,使得入门有些困难。你可能会想:如果已经有了一篇文章,为什么还要别的?答案是如果有更多的信息则会使你开始入门更加容易。
    这篇文章将会描述如何创建一个简单的设备驱动,动态装载和卸载,和用户模式的交互。
    创建一个简单的驱动
    什么是一个子系统
    在解释如何些一个设备驱动之前,我需要先解释一个概念。编译器和链接器产生一个操作系统能理解的二进制文件,在windows中,这种格式是PE(对于可 执行程序)。这就有了一个概念:子系统。一个子系统,根据PE文件头信息,描述如何加载包含入口点的可执行程序信息到二进制文件中。
    在vc++中,你可以创建控制台程序和GUI应用程序,这是不同的子系统,他们会根据自己对应的子系统信息生成不同的PE二进制信息。这就是为什么在控制 台程序中使用main 而在windows程序中使用WinMain的原因。当你创建工程时,有这样的选项:/SUBSYSTEM: CONSOLE or /SUBSYSTEM:WINDOWS.
    驱动使用另外不同的子系统,叫做"NATIVE"。

    驱动程序的"main"
    “NATIVE”也能运行定义了入口点为“NtProcessStartup”的用户模式的应用程序。这种应用程序默认也是可执行的。你可以覆盖入口点函 数名字,比如:使用编译选项-entry:<functionname>。如果我们想写一个驱动,我们需要写一个入口点,其参数列表和返回类 型符合驱动程序的模式。如果我们装载这个驱动,系统将会安装它并且告诉系统这是一个驱动。
    我们可以使用任意名字,但是一般驱动开发者使用"DriverEntry"作为入口点。这意味者我们需要添加“-entry:DriverEntry” 到编译器选项。如果我们使用DDK,并选择驱动为这一类型,DDK会自动加入这一项。DDK包含了预设选项的开发环境,能够比较容易创建程序。实际上,开 发者可以更改makefile文件来改变这些选项。这就是为什么“DriverEntry”成为了官方驱动入口的原因。
    Dlls 实际上也指定了windows子系统去编译的,但是它还有一个开关:/DLL。驱动程序也有这样的开关,/DRIVER:WDM 和/DRIVER:UP(指定了这个驱动不能被装载在多处理器系统中)。
    编译器创建二进制文件还要根据PE头选项和装载器如何装载。装载器会在执行时做一些验证,并根据文件类型以假定的方式被装载。比如:在进入程序入口点之 前,一些启动代码会先被执行(比如 WinMainCRTStartup 调用 WinMain,初始化 CRT)。你的工作只是简单的写应用程序。
    设置的选项如下:
    /SUBSYSTEM:NATIVE /DRIVER:WDM -entry:DriverEntry
    创建“DriverEntry”之前
    在我们坐下来写“DriverEntry”之前.   这有一些要注意的事情。许多人想直接写一个驱动,然后看它如何工作,这是一般编程的习惯。但是你的程序可能会崩溃,或者消失。如果是一个驱动程序,可能会造成严重的后果,系统崩溃。
    第一个规则是在你还没有理解一个驱动如何工作,如何正确运行之前,最好不要随便改动,然后编译运行。举个例子:关于虚拟内存的例子(省略)。
    任何事情背后都隐藏很多概念,我试图给出一个基本的摘要并告诉你从哪里查找这些信息。在写驱动之前,理解以下的概念很重要。
    什么是IRQL?
    IRQL是Interrupt ReQuest Level,中断请求级别。处理器在一个IRQL上执行线程代码。IRQL是帮助决定线程如何被中断的。在同一处理器上,线程只能被更高级别IRQL的线程能中断。每个处理器都有自己的中断IRQL。
    我们经常遇见的有四种IRQL级别。“Passive”, “APC”, “Dispatch” and “DIRQL”. “DriverEntry”将会在PASSIVE_LEVEL被调用。
    PASSIVE_LEVEL
    IRQL最低级别,没有被屏蔽的中断,在这个级别上,线程执行用户模式,可以访问分页内存。
    APC_LEVEL
    在这个级别上,只有APC级别的中断被屏蔽,可以访问分页内存。当有APC发生时,处理器提升到APC级别,这样,就屏蔽掉其它APC,为了和APC执行 一些同步,驱动程序可以手动提升到这个级别。比如,如果提升到这个级别,APC就不能调用。在这个级别,APC被禁止了,导致禁止一些I/O完成APC, 所以有一些API不能调用。
    DISPATCH_LEVEL
    这个级别,DPC 和更低的中断被屏蔽,不能访问分页内存,所有的被访问的内存不能分页。因为只能处理分页内存,所以在这个级别,能够访问的Api大大减少。
    DIRQL (Device IRQL)
    一般的,更高级的驱动在这个级别上不处理IRQL,但是几乎所有的中断被屏蔽,这实际上是IRQL的一个范围,这是一个决定某个驱动有更高的优先级的方法。

    在这个驱动中,我们只会工作在PASSIVE_LEVEL级别,所以不用担心API不能调用的问题。我们需要知道IRQL的概念。
    什么是IRP
    IRP被称作I/O Request Packet(IO请求包),在驱动堆栈中,它从驱动传给驱动。它是一个数据结构,使得驱动之间能够交互。I/O管理器和其他驱动可能会创建一个IRP传递给你的驱动。IRP包含了被请求的操作信息。
    IRP包含一个sub-requests(子请求,或者叫IRP 堆栈位置)的列表,每个在驱动栈中的驱动关于如何中断IRP都有自己的sub-request。
    我们下面创建的驱动没有这么复杂,在堆栈中只有它一个。
    创建驱动程序
    下面是DriverEntry的原型
    NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath);
    DRIVER_OBJECT 是代表驱动的数据结构。 The DriverEntry routine will use it to populate it with other entry points to the driver for handling specific I/O requests.   这个对象还包含了一个代表一个特定驱动的数据结构的指针。一个驱动实际上可以处理多个设备。DRIVER_OBJECT 包含了这个驱动能够处理的所有设 备的链表。在这里,我们只是创建一个设备。“Registry Path”是一个指向注册表中该驱动存在位置的字符串指针。驱动能够使用注册表这个位置存 储一些信息。
    函数的下一部分:首先创建一个设备。设备有不同类型的设备,工作在不同级别,并不是所有的设备都同硬件打交道。每一个特定的工作有一个设备堆栈去做,堆栈 中高级别的驱动可能同用户模式下的程序通信,低级别的驱动可能同更低级别的驱动或者硬件通信。有网络驱动,显示驱动,文件驱动,每一个都有自己的驱动堆 栈。 堆栈中的驱动为更低级的驱动服务。最高级别的驱动同用户模式通信。

    让我们看DriverEntry的第一部分:
    1. NTSTATUS DriverEntry(PDRIVER_OBJECT   pDriverObject, PUNICODE_STRING   pRegistryPath)
    2. {
    3.      NTSTATUS NtStatus = STATUS_SUCCESS;
    4.      UINT uiIndex = 0;
    5.      PDEVICE_OBJECT pDeviceObject = NULL;
    6.      UNICODE_STRING usDriverName, usDosDeviceName;

    7.      DbgPrint("DriverEntry Called \r\n");

    8.      RtlInitUnicodeString(&usDriverName, L"\\Device\\Example");
    9.      RtlInitUnicodeString(&usDosDeviceName, L"\\DosDevices\\Example");

    10.      NtStatus = IoCreateDevice(pDriverObject, 0,
    11.                                &usDriverName,
    12.                                FILE_DEVICE_UNKNOWN,
    13.                                FILE_DEVICE_SECURE_OPEN,
    14.                                FALSE, &pDeviceObject);
    复制代码

    首先注意DbgPrint 这个函数,类似printf,输入信息到调试窗口,可以使用DBGVIEW工具来查看这些信息。
    注 意函数RtlInitUnicodeString,它初始化一个UNICODE_STRING 数据结构,这个数据结构包含三个值。第一:字符串长度,第 二:字符串最大长度,第三:字符串指针。在驱动开发中,一般使用这个结构。需要注意的是,在这个字符串结尾并不是以0结尾,因为结构中已经有字符串的长 度。这个对于初学者要特别注意的。
    驱动有自己的名字,一般命名为:\Device\<somename> ,这个字符串作为参数传递给IoCreateDevice,第二个字 符串”\DosDevices\Example”在这个程序中不使用。对于IoCreateDevice,pDriverObject(驱动对象指针), 0(指定设备扩展名的字节数),FILE_DEVICE_UNKNOWN,(设备类型),pDeviceObjec(新创建设备对象的指针)。
    现在我们已经成功创建了\Device\Example 设备的驱动。我们需要安装这个设备驱动。下面是IRP主要的调用请求:

          
    1. for(uiIndex = 0; uiIndex < IRP_MJ_MAXIMUM_FUNCTION; uiIndex++)
    2.               pDriverObject->MajorFunction[uiIndex] = Example_UnSupportedFunction;
    3.    
    4.          pDriverObject->MajorFunction[IRP_MJ_CLOSE]              = Example_Close;
    5.          pDriverObject->MajorFunction[IRP_MJ_CREATE]             = Example_Create;
    6.          pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]     = Example_IoControl;
    7.          pDriverObject->MajorFunction[IRP_MJ_READ]               = Example_Read;
    8.          pDriverObject->MajorFunction[IRP_MJ_WRITE]              = USE_WRITE_FUNCTION;
    复制代码

    我们列举了Create, Close, IoControl, Read 和 Write函数。当我们和用户模式应用程序通讯的时候,某些API会直接调用驱动并传递参数。
    CreateFile -> IRP_MJ_CREATE
    CloseHandle -> IRP_MJ_CLEANUP & IRP_MJ_CLOSE
    WriteFile -> IRP_MJ_WRITE
    ReadFile-> IRP_MJ_READ
    DeviceIoControl -> IRP_MJ_DEVICE_CONTROL
    需要注意的是,IRP_MJ_CLOSE在进程创建句柄的时候没有被调用,如果你需要进行进程相关的清除操作,你最好处理IRP_MJ_CLEANUP 。
    用户模式的程序能够通过调用驱动使用这些功能,你可能奇怪,为什么叫做文件,但实际上又不是文件。这些API不仅仅能够访问文件,还能够和暴露给用户模式 的任何驱动通讯。在最好,我们将会写一个和我们的驱动通信的简单的用户模式下的应用程序。关于USE_WRITE_FUNCTION,我将会稍后解释。
    下一段代码很简单,是驱动卸载的功能。
    pDriverObject->DriverUnload =   Example_Unload;
    如果使用这个功能,驱动能够动态卸载;否则不能卸载。
    这之后的代码实际上使用了设备对象DEVICE_OBJECT而不是驱动对象DRIVER_OBJECT,这两种数据结构容易搞混。
    pDeviceObject->Flags |= IO_TYPE;
    pDeviceObject->Flags &= (~DO_DEVICE_INITIALIZING);
    IO_TYPE实际上是我们想要处理的IO类型,在example.h文件中定义。
    DO_DEVICE_INITIALIZING告诉IO管理器,该设备初始化的时候,不要给驱动发送任何IO请求。对于在DriverEntry中创建的 设备,不需要这些代码,因为IO管理器已经清除了这些标志,但是,如果你在其他函数中使用IoCreateDevice创建的设备,必须手动清除标志。这 些标志是被IoCreateDevice函数设置的。
    IoCreateSymbolicLink(&usDosDeviceName, &usDriverName);
    IoCreateSymbolicLink在对象管理器中创建符号链接。如果要浏览对象,可以下载QuickView工具,或者去 www.sysinternals.com下载winboj。符号链接只是简单的映射Dos 设备名字到NT设备名字。在这个例子中,Example是 Dos设备名字,\Device\Example是NT驱动名字。
    不同的驱动有自己的名字。不能设置两个驱动为相同的NT设备名字。例如,你有一个记忆棒,在系统中可能显示为E:,如果你移除该设备,可以将E:映射为网 络驱动器,应用程序以相同的方式与之通信,他们并不关心E:是CDRom,还是软盘,还是网络驱动器。驱动需要去解释和处理这些请求,例如进行网络重定 向,或者传递给相应的硬件驱动。这是通过符号链接完成的。E:是一个符号链接。网络可能映射E:给\Device\NetworkRedirector, 记忆棒可能映射E:给\Device\FujiMemoryStick。
    创建可以卸载的程序
    下面的这个函数是卸载的函数,为了能够支持动态卸载驱动。
    1. VOID Example_Unload(PDRIVER_OBJECT   DriverObject)
    2. {   
    3.    
    4.      UNICODE_STRING usDosDeviceName;
    5.    
    6.      DbgPrint("Example_Unload Called \r\n");
    7.    
    8.      RtlInitUnicodeString(&usDosDeviceName, L"\\DosDevices\\Example");
    9.      IoDeleteSymbolicLink(&usDosDeviceName);

    10.      IoDeleteDevice(DriverObject->DeviceObject);
    11. }
    复制代码

    卸载函数只是删除符号链接和创建的 \Device\Example设备。
    创建IRP_MJ_WRITE
    如果你使用WriteFile 或者 ReadFile,你应该知道,写文件的时候只是简单的传递一个缓冲区数据给设备,读文件的时候从设备读出数据,发 送给设备的参数是在IRP中。IO管理器组织数据发送IRP包给驱动有三种方法:“Direct I/O”, “Buffered I/O”和   “Neither。
    1. #ifdef __USE_DIRECT__
    2. #define IO_TYPE DO_DIRECT_IO
    3. #define USE_WRITE_FUNCTION   Example_WriteDirectIO
    4. #endif

    5. #ifdef __USE_BUFFERED__
    6. #define IO_TYPE DO_BUFFERED_IO
    7. #define USE_WRITE_FUNCTION   Example_WriteBufferedIO
    8. #endif

    9. #ifndef IO_TYPE
    10. #define IO_TYPE 0
    11. #define USE_WRITE_FUNCTION   Example_WriteNeither
    12. #endif

    13. Direct I/O
    14. NTSTATUS Example_WriteDirectIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)
    15. {
    16.      NTSTATUS NtStatus = STATUS_SUCCESS;
    17.      PIO_STACK_LOCATION pIoStackIrp = NULL;
    18.      PCHAR pWriteDataBuffer;

    19.      DbgPrint("Example_WriteDirectIO Called \r\n");
    20.    
    21.      /*
    22.       * Each time the IRP is passed down
    23.       * the driver stack a new stack location is added
    24.       * specifying certain parameters for the IRP to the driver.
    25.       */
    26.      pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
    27.    
    28.      if(pIoStackIrp)
    29.      {
    30.          pWriteDataBuffer =
    31.            MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
    32.    
    33.          if(pWriteDataBuffer)
    34.          {                             
    35.              /*
    36.               * We need to verify that the string
    37.               * is NULL terminated. Bad things can happen
    38.               * if we access memory not valid while in the Kernel.
    39.               */
    40.             if(Example_IsStringTerminated(pWriteDataBuffer,
    41.                pIoStackIrp->Parameters.Write.Length))
    42.             {
    43.                  DbgPrint(pWriteDataBuffer);
    44.             }
    45.          }
    46.      }

    47.      return NtStatus;
    48. }
    复制代码
    函数入口只是简单的提供设备对象和IRP对象。
    IoGetCurrentIrpStackLocation 只是简单的给我们提供IO_STACK_LOCATION。在这个例子中,我们需要的唯一的参数是buffer的长度。
    bufferedIO 工作的方法是提供给你一个MdlAddress(内存描述列表)。这个描述了用户模式的地址和如何映射到物理地址。然后我们使用Irp-> MdlAddress调用MmGetSystemAddressForMdlSafe去完成这个功能。这个操作会给我们一个可读的内存的虚拟地址。
    这么做的原因是因为驱动并不总是在线程中处理用户模式的请求。如果你处理了不同进程的请求,你不能跨进程边界读取用户模式下的内存。如果没有操作系统的支持,两个应用程序不能互相读写。
    所以简单的将用户模式进程的物理页映射到系统内存。我们能够使用从用户模式返回的地址。
    这种方法一般用在大缓冲中使用,因为它不需要内存拷贝。内存中用户模式的内存被锁定,直到使用direct IO的底层的IRP完成。这种方法一般对于大缓冲更加有用。
    Buffered I/O
    1. NTSTATUS Example_WriteBufferedIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)
    2. {
    3.      NTSTATUS NtStatus = STATUS_SUCCESS;
    4.      PIO_STACK_LOCATION pIoStackIrp = NULL;
    5.      PCHAR pWriteDataBuffer;

    6.      DbgPrint("Example_WriteBufferedIO Called \r\n");
    7.    
    8.      /*
    9.       * Each time the IRP is passed down
    10.       * the driver stack a new stack location is added
    11.       * specifying certain parameters for the IRP to the driver.
    12.       */
    13.      pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
    14.    
    15.      if(pIoStackIrp)
    16.      {
    17.          pWriteDataBuffer = (PCHAR)Irp->AssociatedIrp.SystemBuffer;
    18.    
    19.          if(pWriteDataBuffer)
    20.          {                             
    21.              /*
    22.               * We need to verify that the string
    23.               * is NULL terminated. Bad things can happen
    24.               * if we access memory not valid while in the Kernel.
    25.               */
    26.             if(Example_IsStringTerminated(pWriteDataBuffer,
    27.                     pIoStackIrp->Parameters.Write.Length))
    28.             {
    29.                  DbgPrint(pWriteDataBuffer);
    30.             }
    31.          }
    32.      }

    33.      return NtStatus;
    34. }
    复制代码
    正如上边提到的,传递给驱动的数据能够被其它的线程或进程访问。映射内存到非分页内存使得在IRQL 级别的驱动页能够读它。
    需要在当前的进程外部访问内存的原因是某些驱动在系统进程内创建了线程。
    实际上,使用Buffered I/O分配了非分页内存并执行了copy,这是在执行读写操作前就已经进行了的。当使用小缓冲时,这个方法比较好。在使用Direct IO时,用户模式的分页内存不需要被锁定,如果使用大缓冲,需要分配大块的连续的非分页内存。
    Neither Buffered nor Direct
    1. NTSTATUS Example_WriteNeither(PDEVICE_OBJECT DeviceObject, PIRP Irp)
    2. {
    3.      NTSTATUS NtStatus = STATUS_SUCCESS;
    4.      PIO_STACK_LOCATION pIoStackIrp = NULL;
    5.      PCHAR pWriteDataBuffer;

    6.      DbgPrint("Example_WriteNeither Called \r\n");
    7.    
    8.      /*
    9.       * Each time the IRP is passed down
    10.       * the driver stack a new stack location is added
    11.       * specifying certain parameters for the IRP to the driver.
    12.       */
    13.      pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
    14.    
    15.      if(pIoStackIrp)
    16.      {
    17.          /*
    18.           * We need this in an exception handler or else we could trap.
    19.           */
    20.          __try {
    21.         
    22.                  ProbeForRead(Irp->UserBuffer,
    23.                    pIoStackIrp->Parameters.Write.Length,
    24.                    TYPE_ALIGNMENT(char));
    25.                  pWriteDataBuffer = Irp->UserBuffer;
    26.             
    27.                  if(pWriteDataBuffer)
    28.                  {                             
    29.                      /*
    30.                       * We need to verify that the string
    31.                       * is NULL terminated. Bad things can happen
    32.                       * if we access memory not valid while in the Kernel.
    33.                       */
    34.                     if(Example_IsStringTerminated(pWriteDataBuffer,
    35.                            pIoStackIrp->Parameters.Write.Length))
    36.                     {
    37.                          DbgPrint(pWriteDataBuffer);
    38.                     }
    39.                  }

    40.          } __except( EXCEPTION_EXECUTE_HANDLER ) {

    41.                NtStatus = GetExceptionCode();     
    42.          }

    43.      }

    44.      return NtStatus;
    45. }
    复制代码

    在 这种方法中,驱动直接访问用户模式的地址。IO管理器并不拷贝数据,页不锁定内存,只是简单的给出用户模式的内存地址。这种方法的优点是不需要拷贝数据, 不需要分配内存,不需要锁定内存。缺点是你必须处理在被调用线程中处理这个请求,并且能够用户模式的相应进程的地址。在这个进程其他的线程中可能尝试改变 或者释放内存。所以需要使用ProbeForRead和ProbeForWrite并且添加异常处理。在任何时间,访问的内存都可能无效。在读写之前,只 能简单的尝试。缓冲存储在Irp->UserBuffer中。
    #pragma
    你看到的这些指示只是告诉链接器如将代码放在那个段和进行一些设置。
    家庭作业
    你的家庭作业是写一个读的程序实现对每种类型的IO进行处理。你可以参考写的程序。
    动态装载和卸载这个驱动
    1. int _cdecl main(void)
    2. {
    3.      HANDLE hSCManager;
    4.      HANDLE hService;
    5.      SERVICE_STATUS ss;

    6.      hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
    7.    
    8.      printf("Load Driver\n");

    9.      if(hSCManager)
    10.      {
    11.          printf("Create Service\n");

    12.          hService = CreateService(hSCManager, "Example",
    13.                                   "Example Driver",
    14.                                    SERVICE_START | DELETE | SERVICE_STOP,
    15.                                    SERVICE_KERNEL_DRIVER,
    16.                                    SERVICE_DEMAND_START,
    17.                                    SERVICE_ERROR_IGNORE,
    18.                                    "C:\\example.sys",
    19.                                    NULL, NULL, NULL, NULL, NULL);

    20.          if(!hService)
    21.          {
    22.              hService = OpenService(hSCManager, "Example",
    23.                         SERVICE_START | DELETE | SERVICE_STOP);
    24.          }

    25.          if(hService)
    26.          {
    27.              printf("Start Service\n");

    28.              StartService(hService, 0, NULL);
    29.              printf("Press Enter to close service\r\n");
    30.              getchar();
    31.              ControlService(hService, SERVICE_CONTROL_STOP, &ss);

    32.              DeleteService(hService);

    33.              CloseServiceHandle(hService);
    34.             
    35.          }

    36.          CloseServiceHandle(hSCManager);
    37.      }
    38.    
    39.      return 0;
    40. }
    复制代码
    以 上的代码装载驱动并启动它。我们装载驱动的时候使用SERVICE_DEMAND_START表示驱动必须手工启动,当机器重启的时候不会自动启动。这样 当系统蓝屏的时候,我们就不需要启动到保护模式下。你可以在其他的程序中同这个服务通信。以上的代码很容易理解。如果你要使用它可以拷贝这个驱动到C:\ example.sys。如果服务创建失败,可能你已经创建并打开了它。
    同设备驱动的通信
    1. int _cdecl main(void)
    2. {
    3.      HANDLE hFile;
    4.      DWORD dwReturn;

    5.      hFile = CreateFile("\\\\.\\Example",
    6.              GENERIC_READ | GENERIC_WRITE, 0, NULL,
    7.              OPEN_EXISTING, 0, NULL);

    8.      if(hFile)
    9.      {
    10.          WriteFile(hFile, "Hello from user mode!",
    11.                    sizeof("Hello from user mode!"), &dwReturn, NULL);
    12.          CloseHandle(hFile);
    13.      }
    14.    
    15.      return 0;
    16. }
    复制代码
    这 可能比你想象的简单。如果你使用三种不同的方法编译三次驱动,从用户模式发送的信息可以在DBGVIEW中看到。你可以使用DOS驱动名字打开驱动,也可 以使用使用\Device\<Nt Device Name> 打开它。一旦你获得设备句柄,你就可以调用WriteFile,   ReadFile, CloseHandle, DeviceIoControl函数。
    结论
    这篇文章只是写了一个驱动简单例子,并告诉你如何创建,安装,和在用户模式访问驱动。如果你想写驱动,最好熟读驱动的基本概念,特别是这篇文章提到的。
    您需要登录后才可以回帖 登录 | 注册

    本版积分规则

    免责声明

    本站中所有被研究的素材与信息全部来源于互联网,版权争议与本站无关。本站所发布的任何软件编程开发或软件的逆向分析文章、逆向分析视频、补丁、注册机和注册信息,仅限用于学习和研究软件安全的目的。全体用户必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。学习编程开发技术或逆向分析技术是为了更好的完善软件可能存在的不安全因素,提升软件安全意识。所以您如果喜欢某程序,请购买注册正版软件,获得正版优质服务!不得将上述内容私自传播、销售或者用于商业用途!否则,一切后果请用户自负!

    QQ|Archiver|手机版|小黑屋|联系我们|宝峰科技 ( 滇公网安备 53050202000040号 | 滇ICP备09007156号-2 )

    Copyright © 2001-2023 Discuz! Team. GMT+8, 2025-5-8 13:37 , File On Powered by Discuz! X3.49

    快速回复 返回顶部 返回列表