Q 在NT/2000/XP中,我想用VC编写应用程序访问硬件设备,如获取磁盘参数、读写绝对扇区数据、测试光驱实际速度等,该从哪里入手呢? A 在NT/2000/XP中,应用程序可以通过API函数DeviceIoControl来实现对设备的访问—获取信息,发送命令,交换数据等。利用该接口函数向指定的设备驱动发送正确的控制码及数据,然后分析它的响应,就可以达到我们的目的。 DeviceIoControl的函数原型为 BOOL DeviceIoControl( HANDLE hDevice, // 设备句柄 DWORD dwIoControlCode, // 控制码 LPVOID lpInBuffer, // 输入数据缓冲区指针 DWORD nInBufferSize, // 输入数据缓冲区长度 LPVOID lpOutBuffer, // 输出数据缓冲区指针 DWORD nOutBufferSize, // 输出数据缓冲区长度 LPDWORD lpBytesReturned, // 输出数据实际长度单元长度 LPOVERLAPPED lpOverlapped // 重叠操作结构指针 ); 设备句柄用来标识你所访问的设备。 发送不同的控制码,可以调用设备驱动程序的不同类型的功能。在头文件winioctl.h中,预定义的标准设备控制码,都以IOCTL或FSCTL开头。例如,IOCTL_DISK_GET_DRIVE_GEOMETRY是对物理驱动器取结构参数(介质类型、柱面数、每柱面磁道数、每磁道扇区数等)的控制码,FSCTL_LOCK_VOLUME是对逻辑驱动器的卷加锁的控制码。 输入输出数据缓冲区是否需要,是何种结构,以及占多少字节空间,完全由不同设备的不同操作类型决定。在头文件winioctl.h中,已经为标准设备预定义了一些输入输出数据结构。重叠操作结构指针设置为NULL,DeviceIoControl将进行阻塞调用;否则,应在编程时按异步操作设计。 Q 设备句柄是从哪里获得的? A 设备句柄可以用API函数CreateFile获得。它的原型为 HANDLE CreateFile( LPCTSTR lpFileName, // 文件名 DWORD dwDesiredAccess, // 访问方式 DWORD dwShareMode, // 共享方式 LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全描述符指针 DWORD dwCreationDisposition, // 创建方式 DWORD dwFlagsAndAttributes, // 文件属性及标志 HANDLE hTemplateFile // 模板文件的句柄 ); CreateFile这个函数用处很多,这里我们用它“打开”设备驱动程序,得到设备的句柄。操作完成后用CloseHandle关闭设备句柄。 与普通文件名有所不同,设备驱动的“文件名”形式固定为“//./DeviceName”(注意在C程序中该字符串写法为“.//DeviceName”),DeviceName必须与设备驱动程序内规定的设备名称一致。 一般地,调用CreateFile获得设备句柄时,访问方式参数设置为0或GENERIC_READ|GENERIC_WRITE,共享方式参数设置为FILE_SHARE_READ|FILE_SHARE_WRITE,创建方式参数设置为OPEN_EXISTING,其它参数设置为0或NULL。 Q 可是,我怎么知道设备名称是什么呢? A 一些存储设备的名称是微软规定好的,不可能有什么变化。大体列出如下 软盘驱动器 A:, B: 逻辑驱动器 C:, D:, E:, …… 物理驱动器 PHYSICALDRIVEx CD-ROM, DVD/ROM CDROMx 磁带机 TAPEx 其中,物理驱动器不包括软驱和光驱。逻辑驱动器可以是IDE/SCSI/PCMCIA/USB接口的硬盘分区(卷)、光驱、MO、CF卡等,甚至是虚拟盘。x=0,1,2 …… 其它的设备名称需通过驱动接口的GUID调用设备管理函数族取得,这里暂不讨论。 Q 请举一个简单的例子说明如何通过DeviceIoControl访问设备驱动程序。 A 这里有一个从MSDN上摘抄来的demo程序,演示在NT/2000/XP中如何通过DeviceIoControl获取硬盘的基本参数。 /* The code of interest is in the subroutine GetDriveGeometry. The code in main shows how to interpret the results of the IOCTL call. */ #include <windows.h> #include <winioctl.h> BOOL GetDriveGeometry(DISK_GEOMETRY *pdg) { HANDLE hDevice; // handle to the drive to be examined BOOL bResult; // results flag DWORD junk; // discard results hDevice = CreateFile(".//PhysicalDrive0", // drive to open 0, // no access to the drive FILE_SHARE_READ | // share mode FILE_SHARE_WRITE, NULL, // default security attributes OPEN_EXISTING, // disposition 0, // file attributes NULL); // do not copy file attributes if (hDevice == INVALID_HANDLE_VALUE) // cannot open the drive { return (FALSE); } bResult = DeviceIoControl(hDevice, // device to be queried IOCTL_DISK_GET_DRIVE_GEOMETRY, // operation to perform NULL, 0, // no input buffer pdg, sizeof(*pdg), // output buffer &junk, // # bytes returned (LPOVERLAPPED) NULL); // synchronous I/O CloseHandle(hDevice); return (bResult); } int main(int argc, char *argv[]) { DISK_GEOMETRY pdg; // disk drive geometry structure BOOL bResult; // generic results flag ULONGLONG DiskSize; // size of the drive, in bytes bResult = GetDriveGeometry (&pdg); if (bResult) { printf("Cylinders = %I64d/n", pdg.Cylinders); printf("Tracks per cylinder = %ld/n", (ULONG) pdg.TracksPerCylinder); printf("Sectors per track = %ld/n", (ULONG) pdg.SectorsPerTrack); printf("Bytes per sector = %ld/n", (ULONG) pdg.BytesPerSector); DiskSize = pdg.Cylinders.QuadPart * (ULONG)pdg.TracksPerCylinder * (ULONG)pdg.SectorsPerTrack * (ULONG)pdg.BytesPerSector; printf("Disk size = %I64d (Bytes) = %I64d (Mb)/n", DiskSize, DiskSize / (1024 * 1024)); } else { printf ("GetDriveGeometry failed. Error %ld./n", GetLastError ()); } return ((int)bResult); } DOS命令DISKCOPY给我很深的印象,现在也有许多“克隆”软件,可以对磁盘进行全盘复制。我想,要制作磁盘镜像文件,DeviceIoControl应该很有用武之地吧? A 是的。这里举一个制作软盘镜像文件,功能类似于“DISKCOPY”的例子。 本例实现其功能的核心代码如下: // 打开磁盘 HANDLE OpenDisk(LPCTSTR filename) { HANDLE hDisk; // 打开设备 hDisk = ::CreateFile(filename, // 文件名 GENERIC_READ | GENERIC_WRITE, // 读写方式 FILE_SHARE_READ | FILE_SHARE_WRITE, // 共享方式 NULL, // 默认的安全描述符 OPEN_EXISTING, // 创建方式 0, // 不需设置文件属性 NULL); // 不需参照模板文件 return hDisk; } // 获取磁盘参数 BOOL GetDiskGeometry(HANDLE hDisk, PDISK_GEOMETRY lpGeometry) { DWORD dwOutBytes; BOOL bResult; // 用IOCTL_DISK_GET_DRIVE_GEOMETRY取磁盘参数 bResult = ::DeviceIoControl(hDisk, // 设备句柄 IOCTL_DISK_GET_DRIVE_GEOMETRY, // 取磁盘参数 NULL, 0, // 不需要输入数据 lpGeometry, sizeof(DISK_GEOMETRY), // 输出数据缓冲区 &dwOutBytes, // 输出数据长度 (LPOVERLAPPED)NULL); // 用同步I/O return bResult; } // 从指定磁道开始读磁盘 BOOL ReadTracks(HANDLE hDisk, PDISK_GEOMETRY lpGeometry, LPVOID pBuf, DWORD dwStartCylinder, DWORD dwCylinderNumber) { DWORD VirtBufSize; DWORD BytesRead; // 大小 VirtBufSize = lpGeometry->TracksPerCylinder * lpGeometry->SectorsPerTrack * lpGeometry->BytesPerSector; // 偏移 ::SetFilePointer(hDisk, VirtBufSize*dwStartCylinder, NULL, FILE_BEGIN); return ::ReadFile(hDisk, pBuf, VirtBufSize*dwCylinderNumber, &BytesRead, NULL); } // 从指定磁道开始写磁盘 BOOL WriteTracks(HANDLE hDisk, PDISK_GEOMETRY lpGeometry, LPVOID pBuf, DWORD dwStartCylinder, DWORD dwCylinderNumber) { DWORD VirtBufSize; DWORD BytesWritten; // 大小 VirtBufSize = lpGeometry->TracksPerCylinder * lpGeometry->SectorsPerTrack * lpGeometry->BytesPerSector; // 偏移 ::SetFilePointer(hDisk, VirtBufSize*dwStartCylinder, NULL, FILE_BEGIN); return ::WriteFile(hDisk, pBuf, VirtBufSize*dwCylinderNumber, &BytesWritten, NULL); } // 从指定磁道开始格式化磁盘 BOOL LowLevelFormatTracks(HANDLE hDisk, PDISK_GEOMETRY lpGeometry, DWORD dwStartCylinder, DWORD dwCylinderNumber) { FORMAT_PARAMETERS FormatParameters; PBAD_TRACK_NUMBER lpBadTrack; DWORD dwOutBytes; DWORD dwBufSize; BOOL bResult; FormatParameters.MediaType = lpGeometry->MediaType; FormatParameters.StartCylinderNumber = dwStartCylinder; FormatParameters.EndCylinderNumber = dwStartCylinder + dwCylinderNumber - 1; FormatParameters.StartHeadNumber = 0; FormatParameters.EndHeadNumber = lpGeometry->TracksPerCylinder - 1; dwBufSize = lpGeometry->TracksPerCylinder * sizeof(BAD_TRACK_NUMBER); lpBadTrack = (PBAD_TRACK_NUMBER) new BYTE[dwBufSize]; // 用IOCTL_DISK_FORMAT_TRACKS对连续磁道进行低级格式化 bResult = ::DeviceIoControl(hDisk, // 设备句柄 IOCTL_DISK_FORMAT_TRACKS, // 低级格式化 &FormatParameters, sizeof(FormatParameters), // 输入数据缓冲区 lpBadTrack, dwBufSize, // 输出数据缓冲区 &dwOutBytes, // 输出数据长度 (LPOVERLAPPED)NULL); // 用同步I/O delete lpBadTrack; return bResult; } // 将卷锁定 BOOL LockVolume(HANDLE hDisk) { DWORD dwOutBytes; BOOL bResult; // 用FSCTL_LOCK_VOLUME锁卷 bResult = ::DeviceIoControl(hDisk, // 设备句柄 FSCTL_LOCK_VOLUME, // 锁卷 NULL, 0, // 不需要输入数据 NULL, 0, // 不需要输出数据 &dwOutBytes, // 输出数据长度 (LPOVERLAPPED)NULL); // 用同步I/O return bResult; } // 将卷解锁 BOOL UnlockVolume(HANDLE hDisk) { DWORD dwOutBytes; BOOL bResult; // 用FSCTL_UNLOCK_VOLUME开卷锁 bResult = ::DeviceIoControl(hDisk, // 设备句柄 FSCTL_UNLOCK_VOLUME, // 开卷锁 NULL, 0, // 不需要输入数据 NULL, 0, // 不需要输出数据 &dwOutBytes, // 输出数据长度 (LPOVERLAPPED)NULL); // 用同步I/O return bResult; } // 将卷卸下 // 该操作使系统重新辨识磁盘,等效于重新插盘 BOOL DismountVolume(HANDLE hDisk) { DWORD dwOutBytes; BOOL bResult; // 用FSCTL_DISMOUNT_VOLUME卸卷 bResult = ::DeviceIoControl(hDisk, // 设备句柄 FSCTL_DISMOUNT_VOLUME, // 卸卷 NULL, 0, // 不需要输入数据 NULL, 0, // 不需要输出数据 &dwOutBytes, // 输出数据长度 (LPOVERLAPPED)NULL); // 用同步I/O return bResult; } 将软盘保存成镜像文件的步骤简单描述为: 1、创建空的镜像文件。 2、调用OpenDisk打开软盘。成功转3,失败转8。 3、调用LockVolume将卷锁定。成功转4,失败转7。 4、调用GetDiskGeometry获取参数。成功转5,失败转6。 5、将磁盘参数写入镜像文件作为文件头。调用ReadTracks按柱面读出数据,保存在镜像文件中。循环次数等于柱面数。 6、调用UnlockVolume将卷解锁。 7、调用CloseDisk关闭软盘。 8、关闭镜像文件。 将镜像文件载入软盘的步骤简单描述为: 1、打开镜像文件。 2、调用OpenDisk打开软盘。成功转3,失败转11。 3、调用LockVolume将卷锁定。成功转4,失败转10。 4、调用GetDiskGeometry获取参数。成功转5,失败转9。 5、从镜像文件中读出文件头,判断两个磁盘参数是否一致。不一致转6,否则转7。 6、调用LowLevelFormatTracks按柱面格式化软盘。循环次数等于柱面数。成功转7,失败转8。 7、从镜像文件中读出数据,并调用WriteTracks按柱面写入磁盘。循环次数等于柱面数。 8、调用DismountVolume将卷卸下。 9、调用UnlockVolume将卷解锁。 10、调用CloseDisk关闭软盘。 11、关闭镜像文件。 Q 我注意到,磁盘读写和格式化是按柱面进行的,有什么道理吗? A 没有特别的原因,只是因为在这个例子中可以方便地显示处理进度。 有一点需要特别提及,按绝对地址读写磁盘数据时,“最小单位”是扇区,地址一定要与扇区对齐,长度也要等于扇区长度的整数倍。比如,每扇区512字节,那么起始地址和数据长度都应能被512整除才行。 Q 我忽然产生了一个伟大的想法,用绝对地址读写的方式使用磁盘,包括U盘啦,MO啦,而不是用现成的文件系统,那不是可以将数据保密了吗? A 当然,只要你喜欢。可千万别在你的系统盘上做试验,否则......可别怪bhw98没有提醒过你喔! Q 我知道怎么测试光驱的传输速度了,就用上面的方法,读出一定长度数据,除以所需时间,应该可以吧? A 可以。但取光盘参数时要用IOCTL_STORAGE_GET_MEDIA_TYPES_EX,我们已经探讨过的。 Q 用IOCTL_DISK_GET_DRIVE_GEOMETRY或IOCTL_STORAGE_GET_MEDIA_TYPES_EX只能得到很少的磁盘参数,我想获得包括硬盘序列号在内的更加详细的信息,有什么办法呀? A 确实,用你所说的I/O控制码,只能得到最基本的磁盘参数。获取磁盘出厂信息的I/O控制码,微软在VC/MFC环境中没有开放,在DDK中可以发现一些线索。早先,Lynn McGuire写了一个很出名的获取IDE硬盘详细信息的程序DiskID32,下面的例子是在其基础上经过增删和改进而成的。 本例中,我们要用到ATA/APAPI的IDENTIFY DEVICE指令。ATA/APAPI是国际组织T13起草和发布的IDE/EIDE/UDMA硬盘及其它可移动存储设备与主机接口的标准,至今已经到了ATA/APAPI-7版本。该接口标准规定了ATA/ATAPI设备的输入输出寄存器和指令集。欲了解更详细的ATA/ATAPI技术资料,可访问T13的站点。 用到的常量及数据结构有以下一些: // IOCTL控制码 // #define DFP_SEND_DRIVE_COMMAND 0x0007c084 #define DFP_SEND_DRIVE_COMMAND CTL_CODE(IOCTL_DISK_BASE, 0x0021, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS) // #define DFP_RECEIVE_DRIVE_DATA 0x0007c088 #define DFP_RECEIVE_DRIVE_DATA CTL_CODE(IOCTL_DISK_BASE, 0x0022, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS) #define FILE_DEVICE_SCSI 0x0000001b #define IOCTL_SCSI_MINIPORT_IDENTIFY ((FILE_DEVICE_SCSI << 16) + 0x0501) #define IOCTL_SCSI_MINIPORT 0x0004D008 // see NTDDSCSI.H for definition // ATA/ATAPI指令 #define IDE_ATA_IDENTIFY 0xEC // ATA的ID指令(IDENTIFY DEVICE) // IDE命令寄存器 typedef struct _IDEREGS { BYTE bFeaturesReg; // 特征寄存器(用于SMART命令) BYTE bSectorCountReg; // 扇区数目寄存器 BYTE bSectorNumberReg; // 开始扇区寄存器 BYTE bCylLowReg; // 开始柱面低字节寄存器 BYTE bCylHighReg; // 开始柱面高字节寄存器 BYTE bDriveHeadReg; // 驱动器/磁头寄存器 BYTE bCommandReg; // 指令寄存器 BYTE bReserved; // 保留 } IDEREGS, *PIDEREGS, *LPIDEREGS; // 从驱动程序返回的状态 typedef struct _DRIVERSTATUS { BYTE bDriverError; // 错误码 BYTE bIDEStatus; // IDE状态寄存器 BYTE bReserved[2]; // 保留 DWORD dwReserved[2]; // 保留 } DRIVERSTATUS, *PDRIVERSTATUS, *LPDRIVERSTATUS; // IDE设备IOCTL输入数据结构 typedef struct _SENDCMDINPARAMS { DWORD cBufferSize; // 缓冲区字节数 IDEREGS irDriveRegs; // IDE寄存器组 BYTE bDriveNumber; // 驱动器号 BYTE bReserved[3]; // 保留 DWORD dwReserved[4]; // 保留 BYTE bBuffer[1]; // 输入缓冲区(此处象征性地包含1字节) } SENDCMDINPARAMS, *PSENDCMDINPARAMS, *LPSENDCMDINPARAMS; // IDE设备IOCTL输出数据结构 typedef struct _SENDCMDOUTPARAMS { DWORD cBufferSize; // 缓冲区字节数 DRIVERSTATUS DriverStatus; // 驱动程序返回状态 BYTE bBuffer[1]; // 输入缓冲区(此处象征性地包含1字节) } SENDCMDOUTPARAMS, *PSENDCMDOUTPARAMS, *LPSENDCMDOUTPARAMS; // IDE的ID命令返回的数据 // 共512字节(256个WORD),这里仅定义了一些感兴趣的项(基本上依据ATA/ATAPI-4) typedef struct _IDINFO { USHORT wGenConfig; // WORD 0: 基本信息字 USHORT wNumCyls; // WORD 1: 柱面数 USHORT wReserved2; // WORD 2: 保留 USHORT wNumHeads; // WORD 3: 磁头数 USHORT wReserved4; // WORD 4: 保留 USHORT wReserved5; // WORD 5: 保留 USHORT wNumSectorsPerTrack; // WORD 6: 每磁道扇区数 USHORT wVendorUnique[3]; // WORD 7-9: 厂家设定值 CHAR sSerialNumber[20]; // WORD 10-19:序列号 USHORT wBufferType; // WORD 20: 缓冲类型 USHORT wBufferSize; // WORD 21: 缓冲大小 USHORT wECCSize; // WORD 22: ECC校验大小 CHAR sFirmwareRev[8]; // WORD 23-26: 固件版本 CHAR sModelNumber[40]; // WORD 27-46: 内部型号 USHORT wMoreVendorUnique; // WORD 47: 厂家设定值 USHORT wReserved48; // WORD 48: 保留 struct { USHORT reserved1:8; USHORT DMA:1; // 1=支持DMA USHORT LBA:1; // 1=支持LBA USHORT DisIORDY:1; // 1=可不使用IORDY USHORT IORDY:1; // 1=支持IORDY USHORT SoftReset:1; // 1=需要ATA软启动 USHORT Overlap:1; // 1=支持重叠操作 USHORT Queue:1; // 1=支持命令队列 USHORT InlDMA:1; // 1=支持交叉存取DMA } wCapabilities; // WORD 49: 一般能力 USHORT wReserved1; // WORD 50: 保留 USHORT wPIOTiming; // WORD 51: PIO时序 USHORT wDMATiming; // WORD 52: DMA时序 struct { USHORT CHSNumber:1; // 1=WORD 54-58有效 USHORT CycleNumber:1; // 1=WORD 64-70有效 USHORT UnltraDMA:1; // 1=WORD 88有效 USHORT reserved:13; } wFieldValidity; // WORD 53: 后续字段有效性标志 USHORT wNumCurCyls; // WORD 54: CHS可寻址的柱面数 USHORT wNumCurHeads; // WORD 55: CHS可寻址的磁头数 USHORT wNumCurSectorsPerTrack; // WORD 56: CHS可寻址每磁道扇区数 USHORT wCurSectorsLow; // WORD 57: CHS可寻址的扇区数低位字 USHORT wCurSectorsHigh; // WORD 58: CHS可寻址的扇区数高位字 struct { USHORT CurNumber:8; // 当前一次性可读写扇区数 USHORT Multi:1; // 1=已选择多扇区读写 USHORT reserved1:7; } wMultSectorStuff; // WORD 59: 多扇区读写设定 ULONG dwTotalSectors; // WORD 60-61: LBA可寻址的扇区数 USHORT wSingleWordDMA; // WORD 62: 单字节DMA支持能力 struct { USHORT Mode0:1; // 1=支持模式0 (4.17Mb/s) USHORT Mode1:1; // 1=支持模式1 (13.3Mb/s) USHORT Mode2:1; // 1=支持模式2 (16.7Mb/s) USHORT Reserved1:5; USHORT Mode0Sel:1; // 1=已选择模式0 USHORT Mode1Sel:1; // 1=已选择模式1 USHORT Mode2Sel:1; // 1=已选择模式2 USHORT Reserved2:5; } wMultiWordDMA; // WORD 63: 多字节DMA支持能力 struct { USHORT AdvPOIModes:8; // 支持高级POI模式数 USHORT reserved:8; } wPIOCapacity; // WORD 64: 高级PIO支持能力 USHORT wMinMultiWordDMACycle; // WORD 65: 多字节DMA传输周期的最小值 USHORT wRecMultiWordDMACycle; // WORD 66: 多字节DMA传输周期的建议值 USHORT wMinPIONoFlowCycle; // WORD 67: 无流控制时PIO传输周期的最小值 USHORT wMinPOIFlowCycle; // WORD 68: 有流控制时PIO传输周期的最小值 USHORT wReserved69[11]; // WORD 69-79: 保留 struct { USHORT Reserved1:1; USHORT ATA1:1; // 1=支持ATA-1 USHORT ATA2:1; // 1=支持ATA-2 USHORT ATA3:1; // 1=支持ATA-3 USHORT ATA4:1; // 1=支持ATA/ATAPI-4 USHORT ATA5:1; // 1=支持ATA/ATAPI-5 USHORT ATA6:1; // 1=支持ATA/ATAPI-6 USHORT ATA7:1; // 1=支持ATA/ATAPI-7 USHORT ATA8:1; // 1=支持ATA/ATAPI-8 USHORT ATA9:1; // 1=支持ATA/ATAPI-9 USHORT ATA10:1; // 1=支持ATA/ATAPI-10 USHORT ATA11:1; // 1=支持ATA/ATAPI-11 USHORT ATA12:1; // 1=支持ATA/ATAPI-12 USHORT ATA13:1; // 1=支持ATA/ATAPI-13 USHORT ATA14:1; // 1=支持ATA/ATAPI-14 USHORT Reserved2:1; } wMajorVersion; // WORD 80: 主版本 USHORT wMinorVersion; // WORD 81: 副版本 USHORT wReserved82[6]; // WORD 82-87: 保留 struct { USHORT Mode0:1; // 1=支持模式0 (16.7Mb/s) USHORT Mode1:1; // 1=支持模式1 (25Mb/s) USHORT Mode2:1; // 1=支持模式2 (33Mb/s) USHORT Mode3:1; // 1=支持模式3 (44Mb/s) USHORT Mode4:1; // 1=支持模式4 (66Mb/s) USHORT Mode5:1; // 1=支持模式5 (100Mb/s) USHORT Mode6:1; // 1=支持模式6 (133Mb/s) USHORT Mode7:1; // 1=支持模式7 (166Mb/s) ??? USHORT Mode0Sel:1; // 1=已选择模式0 USHORT Mode1Sel:1; // 1=已选择模式1 USHORT Mode2Sel:1; // 1=已选择模式2 USHORT Mode3Sel:1; // 1=已选择模式3 USHORT Mode4Sel:1; // 1=已选择模式4 USHORT Mode5Sel:1; // 1=已选择模式5 USHORT Mode6Sel:1; // 1=已选择模式6 USHORT Mode7Sel:1; // 1=已选择模式7 } wUltraDMA; // WORD 88: Ultra DMA支持能力 USHORT wReserved89[167]; // WORD 89-255 } IDINFO, *PIDINFO; // SCSI驱动所需的输入输出共用的结构 typedef struct _SRB_IO_CONTROL { ULONG HeaderLength; // 头长度 UCHAR Signature[8]; // 特征名称 ULONG Timeout; // 超时时间 ULONG ControlCode; // 控制码 ULONG ReturnCode; // 返回码 ULONG Length; // 缓冲区长度 } SRB_IO_CONTROL, *PSRB_IO_CONTROL; 需要引起注意的是IDINFO第57-58 WORD (CHS可寻址的扇区数),因为不满足32位对齐的要求,不可定义为一个ULONG字段。Lynn McGuire的程序里正是由于定义为一个ULONG字段,导致该结构不可用。 以下是核心代码: // 打开设备 // filename: 设备的“文件名” HANDLE OpenDevice(LPCTSTR filename) { HANDLE hDevice; // 打开设备 hDevice= ::CreateFile(filename, // 文件名 GENERIC_READ | GENERIC_WRITE, // 读写方式 FILE_SHARE_READ | FILE_SHARE_WRITE, // 共享方式 NULL, // 默认的安全描述符 OPEN_EXISTING, // 创建方式 0, // 不需设置文件属性 NULL); // 不需参照模板文件 return hDevice; } // 向驱动发“IDENTIFY DEVICE”命令,获得设备信息 // hDevice: 设备句柄 // pIdInfo: 设备信息结构指针 BOOL IdentifyDevice(HANDLE hDevice, PIDINFO pIdInfo) { PSENDCMDINPARAMS pSCIP; // 输入数据结构指针 PSENDCMDOUTPARAMS pSCOP; // 输出数据结构指针 DWORD dwOutBytes; // IOCTL输出数据长度 BOOL bResult; // IOCTL返回值 // 申请输入/输出数据结构空间 pSCIP = (PSENDCMDINPARAMS)::GlobalAlloc(LMEM_ZEROINIT, sizeof(SENDCMDINPARAMS)-1); pSCOP = (PSENDCMDOUTPARAMS)::GlobalAlloc(LMEM_ZEROINIT, sizeof(SENDCMDOUTPARAMS)+sizeof(IDINFO)-1); // 指定ATA/ATAPI命令的寄存器值 // pSCIP->irDriveRegs.bFeaturesReg = 0; // pSCIP->irDriveRegs.bSectorCountReg = 0; // pSCIP->irDriveRegs.bSectorNumberReg = 0; // pSCIP->irDriveRegs.bCylLowReg = 0; // pSCIP->irDriveRegs.bCylHighReg = 0; // pSCIP->irDriveRegs.bDriveHeadReg = 0; pSCIP->irDriveRegs.bCommandReg = IDE_ATA_IDENTIFY; // 指定输入/输出数据缓冲区大小 pSCIP->cBufferSize = 0; pSCOP->cBufferSize = sizeof(IDINFO); // IDENTIFY DEVICE bResult = ::DeviceIoControl(hDevice, // 设备句柄 DFP_RECEIVE_DRIVE_DATA, // 指定IOCTL pSCIP, sizeof(SENDCMDINPARAMS) - 1, // 输入数据缓冲区 pSCOP, sizeof(SENDCMDOUTPARAMS) + sizeof(IDINFO) - 1, // 输出数据缓冲区 &dwOutBytes, // 输出数据长度 (LPOVERLAPPED)NULL); // 用同步I/O // 复制设备参数结构 ::memcpy(pIdInfo, pSCOP->bBuffer, sizeof(IDINFO)); // 释放输入/输出数据空间 ::GlobalFree(pSCOP); ::GlobalFree(pSCIP); return bResult; } // 向SCSI MINI-PORT驱动发“IDENTIFY DEVICE”命令,获得设备信息 // hDevice: 设备句柄 // pIdInfo: 设备信息结构指针 BOOL IdentifyDeviceAsScsi(HANDLE hDevice, int nDrive, PIDINFO pIdInfo) { PSENDCMDINPARAMS pSCIP; // 输入数据结构指针 PSENDCMDOUTPARAMS pSCOP; // 输出数据结构指针 PSRB_IO_CONTROL pSRBIO; // SCSI输入输出数据结构指针 DWORD dwOutBytes; // IOCTL输出数据长度 BOOL bResult; // IOCTL返回值 // 申请输入/输出数据结构空间 pSRBIO = (PSRB_IO_CONTROL)::GlobalAlloc(LMEM_ZEROINIT, sizeof(SRB_IO_CONTROL)+sizeof(SENDCMDOUTPARAMS)+sizeof(IDINFO)-1); pSCIP = (PSENDCMDINPARAMS)((char *)pSRBIO+sizeof(SRB_IO_CONTROL)); pSCOP = (PSENDCMDOUTPARAMS)((char *)pSRBIO+sizeof(SRB_IO_CONTROL)); // 填充输入/输出数据 pSRBIO->HeaderLength = sizeof(SRB_IO_CONTROL); pSRBIO->Timeout = 10000; pSRBIO->Length = sizeof(SENDCMDOUTPARAMS)+sizeof(IDINFO)-1; pSRBIO->ControlCode = IOCTL_SCSI_MINIPORT_IDENTIFY; ::strncpy ((char *)pSRBIO->Signature, "SCSIDISK", 8); // 指定ATA/ATAPI命令的寄存器值 // pSCIP->irDriveRegs.bFeaturesReg = 0; // pSCIP->irDriveRegs.bSectorCountReg = 0; // pSCIP->irDriveRegs.bSectorNumberReg = 0; // pSCIP->irDriveRegs.bCylLowReg = 0; // pSCIP->irDriveRegs.bCylHighReg = 0; // pSCIP->irDriveRegs.bDriveHeadReg = 0; pSCIP->irDriveRegs.bCommandReg = IDE_ATA_IDENTIFY; pSCIP->bDriveNumber = nDrive; // IDENTIFY DEVICE bResult = ::DeviceIoControl(hDevice, // 设备句柄 IOCTL_SCSI_MINIPORT, // 指定IOCTL pSRBIO, sizeof(SRB_IO_CONTROL) +sizeof(SENDCMDINPARAMS) - 1, // 输入数据缓冲区 pSRBIO, sizeof(SRB_IO_CONTROL) +sizeof(SENDCMDOUTPARAMS) + sizeof(IDINFO) - 1, // 输出数据缓冲区 &dwOutBytes, // 输出数据长度 (LPOVERLAPPED)NULL); // 用同步I/O // 复制设备参数结构 ::memcpy(pIdInfo, pSCOP->bBuffer, sizeof(IDINFO)); // 释放输入/输出数据空间 ::GlobalFree(pSRBIO); return bResult; } // 将串中的字符两两颠倒 // 原因是ATA/ATAPI中的WORD,与Windows采用的字节顺序相反 // 驱动程序中已经将收到的数据全部反过来,我们来个负负得正 void AdjustString(char* str, int len) { char ch; int i; // 两两颠倒 for(i=0;i<len;i+=2) { ch = str[i]; str[i] = str[i+1]; str[i+1] = ch; } // 若是右对齐的,调整为左对齐 (去掉左边的空格) i=0; while(i<len && str[i]==' ') i++; ::memmove(str, &str[i], len-i); // 去掉右边的空格 i = len - 1; while(i>=0 && str[i]==' ') { str[i] = '/0'; i--; } } // 读取IDE硬盘的设备信息,必须有足够权限 // nDrive: 驱动器号(0=第一个硬盘,1=0=第二个硬盘,......) // pIdInfo: 设备信息结构指针 BOOL GetPhysicalDriveInfoInNT(int nDrive, PIDINFO pIdInfo) { HANDLE hDevice; // 设备句柄 BOOL bResult; // 返回结果 char szFileName[20]; // 文件名 ::sprintf(szFileName,".//PhysicalDrive%d", nDrive); hDevice = ::OpenDevice(szFileName); if(hDevice == INVALID_HANDLE_VALUE) { return FALSE; } // IDENTIFY DEVICE bResult = ::IdentifyDevice(hDevice, pIdInfo); if(bResult) { // 调整字符串 ::AdjustString(pIdInfo->sSerialNumber, 20); ::AdjustString(pIdInfo->sModelNumber, 40); ::AdjustString(pIdInfo->sFirmwareRev, 8); } ::CloseHandle (hDevice); return bResult; } // 用SCSI驱动读取IDE硬盘的设备信息,不受权限制约 // nDrive: 驱动器号(0=Primary Master, 1=Promary Slave, 2=Secondary master, 3=Secondary slave) // pIdInfo: 设备信息结构指针 BOOL GetIdeDriveAsScsiInfoInNT(int nDrive, PIDINFO pIdInfo) { HANDLE hDevice; // 设备句柄 BOOL bResult; // 返回结果 char szFileName[20]; // 文件名 ::sprintf(szFileName,".//Scsi%d:", nDrive/2); hDevice = ::OpenDevice(szFileName); if(hDevice == INVALID_HANDLE_VALUE) { return FALSE; } // IDENTIFY DEVICE bResult = ::IdentifyDeviceAsScsi(hDevice, nDrive%2, pIdInfo); // 检查是不是空串 if(pIdInfo->sModelNumber[0]=='/0') { bResult = FALSE; } if(bResult) { // 调整字符串 ::AdjustString(pIdInfo->sSerialNumber, 20); ::AdjustString(pIdInfo->sModelNumber, 40); ::AdjustString(pIdInfo->sFirmwareRev, 8); } return bResult; } Q 我注意到ATA/ATAPI里,以及DiskID32里,有一个“IDENTIFY PACKET DEVICE”指令,与“IDENTIFY DEVICE”有什么区别? A IDENTIFY DEVICE专门用于固定硬盘,而IDENTIFY PACKET DEVICE用于可移动存储设备如CDROM、CF、MO、ZIP、TAPE等。因为驱动程序的原因,实际上用本例的方法,不管是IDENTIFY DEVICE也好,IDENTIFY PACKET DEVICE也好,获取可移动存储设备的详细信息,一般是做不到的。而且除了IDE硬盘,对SCSI、USB等接口的硬盘也不起作用。除非厂商提供的驱动支持这样的功能。 Q ATA/ATAPI有很多指令,如READ SECTORS, WRITE SECTORS, SECURITY, SLEEP, STANDBY等,利用上述方法,是否可进行相应操作? A 应该没问题。但切记,要慎重慎重再慎重! Q 关于权限问题,请解释一下好吗? A 在NT/2000/XP下,administrator可以管理设备,上述两种访问驱动的方法都行。但在user身份下,或者登录到域后,用户无法访问PhysicalDrive驱动的核心层,但SCSI MINI-PORT驱动却可以。目前是可以,不知道Windows以后的版本是否支持。因为这肯定是一个安全隐患。 另外,我们着重讨论NT/2000/XP中DeviceIoControl的应用,如果需要在98/ME中得到包括硬盘序列号在内的更加详细的信息,请参考DiskID32。 Q 前几次我们讨论的都是设备名比较清楚的情况,有了设备名(路径),就可以直接调用CreateFile打开设备,进行它所支持的I/O操作了。如果事先并不能确切知道设备名,如何去访问设备呢? A 访问设备必须用设备句柄,而得到设备句柄必须知道设备路径,这个套路以你我之力是改变不了的。每个设备都有它所属类型的GUID,我们顺着这个GUID就能获得设备路径。 GUID是同类或同种设备的全球唯一识别码,它是一个128 bit(16字节)的整形数,真实面目为 typedef struct _GUID { unsigned long Data1; unsigned short Data2; unsigned short Data3; unsigned char Data4[8]; } GUID, *PGUID; 例如,Disk类的GUID为“53f56307-b6bf-11d0-94f2-00a0c91efb8b”,在我们的程序里可以定义为 const GUID DiskClassGuid = {0x53f56307L, 0xb6bf, 0x11d0, {0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b)}; 或者用一个宏来定义 DEFINE_GUID(DiskClassGuid, 0x53f56307L, 0xb6bf, 0x11d0, 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b); 通过GUID找出设备路径,需要用到一组设备管理的API函数 SetupDiGetClassDevs, SetupDiEnumDeviceInterfaces, SetupDiGetInterfaceDeviceDetail, SetupDiDestroyDeviceInfoList, 以及结构SP_DEVICE_INTERFACE_DATA, SP_DEVICE_INTERFACE_DETAIL_DATA。 有关信息请查阅MSDN,这里就不详细介绍了。 实现GUID到设备路径的代码如下: // SetupDiGetInterfaceDeviceDetail所需要的输出长度,定义足够大 #define INTERFACE_DETAIL_SIZE (1024) // 根据GUID获得设备路径 // lpGuid: GUID指针 // pszDevicePath: 设备路径指针的指针 // 返回: 成功得到的设备路径个数,可能不止1个 int GetDevicePath(LPGUID lpGuid, LPTSTR* pszDevicePath) { HDEVINFO hDevInfoSet; SP_DEVICE_INTERFACE_DATA ifdata; PSP_DEVICE_INTERFACE_DETAIL_DATA pDetail; int nCount; BOOL bResult; // 取得一个该GUID相关的设备信息集句柄 hDevInfoSet = ::SetupDiGetClassDevs(lpGuid, // class GUID NULL, // 无关键字 NULL, // 不指定父窗口句柄 DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); // 目前存在的设备 // 失败... if(hDevInfoSet == INVALID_HANDLE_VALUE) { return 0; } // 申请设备接口数据空间 pDetail = (PSP_DEVICE_INTERFACE_DETAIL_DATA)::GlobalAlloc(LMEM_ZEROINIT, INTERFACE_DETAIL_SIZE); pDetail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA); nCount = 0; bResult = TRUE; // 设备序号=0,1,2... 逐一测试设备接口,到失败为止 while (bResult) { ifdata.cbSize=sizeof(ifdata); // 枚举符合该GUID的设备接口 bResult = ::SetupDiEnumDeviceInterfaces( hDevInfoSet, // 设备信息集句柄 NULL, // 不需额外的设备描述 lpGuid, // GUID (ULONG)nCount, // 设备信息集里的设备序号 &ifdata); // 设备接口信息 if(bResult) { // 取得该设备接口的细节(设备路径) bResult = SetupDiGetInterfaceDeviceDetail( hDevInfoSet, // 设备信息集句柄 &ifdata, // 设备接口信息 pDetail, // 设备接口细节(设备路径) INTERFACE_DETAIL_SIZE, // 输出缓冲区大小 NULL, // 不需计算输出缓冲区大小(直接用设定值) NULL); // 不需额外的设备描述 if(bResult) { // 复制设备路径到输出缓冲区 ::strcpy(pszDevicePath[nCount], pDetail->DevicePath); // 调整计数值 nCount++; } } } // 释放设备接口数据空间 ::GlobalFree(pDetail); // 关闭设备信息集句柄 ::SetupDiDestroyDeviceInfoList(hDevInfoSet); return nCount; } 调用GetDevicePath函数时要注意,pszDevicePath是个指向字符串指针的指针,例如可以这样 int i; char* szDevicePath[MAX_DEVICE]; // 设备路径 // 分配需要的空间 for(i=0; i<MAX_DEVICE; i++) szDevicePath[i] = new char[256]; // 取设备路径 nDevice = ::GetDevicePath((LPGUID)&DiskClassGuid, szDevicePath); // 逐一获取设备信息 for(i=0; i<nDevice; i++) { // 打开设备 hDevice = ::OpenDevice(szDevicePath[i]); if(hDevice != INVALID_HANDLE_VALUE) { ... ... // I/O操作 ::CloseHandle(hDevice); } } // 释放空间 for(i=0;i<MAX_DEVICE;i++) delete []szDevicePath[i]; 本例的Project中除了要包含winioctl.h外,还要包含initguid.h,setupapi.h,以及连接setupapi.lib。 Q 得到设备路径后,就可以到下一步,用CreateFile打开设备,然后用DeviceIoControl进行读写了吧? A 是的。尽管该设备路径与以前我们接触的那些不太一样。本是“//./PhysicalDrive0”,现在鸟枪换炮,变成了类似这样的一副尊容: “//?/ide#diskmaxtor_2f040j0__________________________vam51jj0#3146563447534558202020202020202020202020#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}”。 其实这个设备名在注册表的某处可以找到,例如在Win2000中这个名字位于 HKEY_LOCAL_MACHINE/System/CurrentControlSet/Services/Disk/Enum/0, 只不过“#”换成了“/”。分析一下这样的设备路径,你会发现很有趣的东西,它们是由接口类型、产品型号、固件版本、序列号、计算机名、GUID等信息组合而成的。当然,它是没有规范的,不能指望从这里面得到你希望知道的东西。 用CreateFile打开设备后,对于存储设备,IOCTL_DISK_GET_DRIVE_GEOMETRY,IOCTL_STORAGE_GET_MEDIA_TYPES_EX等I/O控制码照常使用。 今天我们讨论一个新的控制码:IOCTL_STORAGE_QUERY_PROPERTY,获取设备属性信息,希望得到系统中所安装的各种固定的和可移动的硬盘、优盘和CD/DVD-ROM/R/W的接口类型、序列号、产品ID等信息。 // IOCTL控制码 #define IOCTL_STORAGE_QUERY_PROPERTY CTL_CODE(IOCTL_STORAGE_BASE, 0x0500, METHOD_BUFFERED, FILE_ANY_ACCESS) // 存储设备的总线类型 typedef enum _STORAGE_BUS_TYPE { BusTypeUnknown = 0x00, BusTypeScsi, BusTypeAtapi, BusTypeAta, BusType1394, BusTypeSsa, BusTypeFibre, BusTypeUsb, BusTypeRAID, BusTypeMaxReserved = 0x7F } STORAGE_BUS_TYPE, *PSTORAGE_BUS_TYPE; // 查询存储设备属性的类型 typedef enum _STORAGE_QUERY_TYPE { PropertyStandardQuery = 0, // 读取描述 PropertyExistsQuery, // 测试是否支持 PropertyMaskQuery, // 读取指定的描述 PropertyQueryMaxDefined // 验证数据 } STORAGE_QUERY_TYPE, *PSTORAGE_QUERY_TYPE; // 查询存储设备还是适配器属性 typedef enum _STORAGE_PROPERTY_ID { StorageDeviceProperty = 0, // 查询设备属性 StorageAdapterProperty // 查询适配器属性 } STORAGE_PROPERTY_ID, *PSTORAGE_PROPERTY_ID; // 查询属性输入的数据结构 typedef struct _STORAGE_PROPERTY_QUERY { STORAGE_PROPERTY_ID PropertyId; // 设备/适配器 STORAGE_QUERY_TYPE QueryType; // 查询类型 UCHAR AdditionalParameters[1]; // 额外的数据(仅定义了象征性的1个字节) } STORAGE_PROPERTY_QUERY, *PSTORAGE_PROPERTY_QUERY; // 查询属性输出的数据结构 typedef struct _STORAGE_DEVICE_DESCRIPTOR { ULONG Version; // 版本 ULONG Size; // 结构大小 UCHAR DeviceType; // 设备类型 UCHAR DeviceTypeModifier; // SCSI-2额外的设备类型 BOOLEAN RemovableMedia; // 是否可移动 BOOLEAN CommandQueueing; // 是否支持命令队列 ULONG VendorIdOffset; // 厂家设定值的偏移 ULONG ProductIdOffset; // 产品ID的偏移 ULONG ProductRevisionOffset; // 产品版本的偏移 ULONG SerialNumberOffset; // 序列号的偏移 STORAGE_BUS_TYPE BusType; // 总线类型 ULONG RawPropertiesLength; // 额外的属性数据长度 UCHAR RawDeviceProperties[1]; // 额外的属性数据(仅定义了象征性的1个字节) } STORAGE_DEVICE_DESCRIPTOR, *PSTORAGE_DEVICE_DESCRIPTOR; // 取设备属性信息 // hDevice -- 设备句柄 // pDevDesc -- 输出的设备描述和属性信息缓冲区指针(包含连接在一起的两部分) BOOL GetDriveProperty(HANDLE hDevice, PSTORAGE_DEVICE_DESCRIPTOR pDevDesc) { STORAGE_PROPERTY_QUERY Query; // 查询输入参数 DWORD dwOutBytes; // IOCTL输出数据长度 BOOL bResult; // IOCTL返回值 // 指定查询方式 Query.PropertyId = StorageDeviceProperty; Query.QueryType = PropertyStandardQuery; // 用IOCTL_STORAGE_QUERY_PROPERTY取设备属性信息 bResult = ::DeviceIoControl(hDevice, // 设备句柄 IOCTL_STORAGE_QUERY_PROPERTY, // 取设备属性信息 &Query, sizeof(STORAGE_PROPERTY_QUERY), // 输入数据缓冲区 pDevDesc, pDevDesc->Size, // 输出数据缓冲区 &dwOutBytes, // 输出数据长度 (LPOVERLAPPED)NULL); // 用同步I/O return bResult; } Q 我用这个方法从IOCTL_STORAGE_QUERY_PROPERTY返回的数据中,没有得到CDROM和USB接口的外置硬盘的序列号、产品ID等信息。但从设备路径上看,明明是有这些信息的,为什么它没有填充到STORAGE_DEVICE_DESCRIPTOR中呢?再就是为什么硬盘序列号本是“D22P7KHE ”,为什么它填充的是“3146563447534558202020202020202020202020”这种形式呢? A 对这两个问题我也是心存疑惑,但又不敢妄加猜测,正琢磨着向微软请教呢。 Q 在NT/2000/XP中,如何读取CMOS数据? Q 在NT/2000/XP中,如何控制speaker发声? Q 在NT/2000/XP中,如何直接访问物理端口? A 看似小小问题,难倒多少好汉! NT/2000/XP从安全性、可靠性、稳定性上考虑,应用程序和操作系统是分开的,操作系统代码运行在核心态,有权访问系统数据和硬件,能执行特权指令;应用程序运行在用户态,能够使用的接口和访问系统数据的权限都受到严格限制。当用户程序调用系统服务时,处理器捕获该调用,然后把调用的线程切换到核心态。当系统服务完成后,操作系统将线程描述表切换回用户态,调用者继续运行。 想在用户态应用程序中实现I/O读写,直接存取硬件,可以通过编写驱动程序,实现CreateFile、CloseHandle、 DeviceIOControl、ReadFile、WriteFile等功能。从Windows 2000开始,引入WDM核心态驱动程序的概念。 下面是我写的一个非常简单的驱动程序,可实现字节型端口I/O。 #include <ntddk.h> // 设备类型定义 // 0-32767被Microsoft占用,用户自定义可用32768-65535 #define FILE_DEVICE_MYPORT 0x0000f000 // I/O控制码定义 // 0-2047被Microsoft占用,用户自定义可用2048-4095 #define MYPORT_IOCTL_BASE 0xf00 #define IOCTL_MYPORT_READ_BYTE CTL_CODE(FILE_DEVICE_MYPORT, MYPORT_IOCTL_BASE, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_MYPORT_WRITE_BYTE CTL_CODE(FILE_DEVICE_MYPORT, MYPORT_IOCTL_BASE+1, METHOD_BUFFERED, FILE_ANY_ACCESS) // IOPM是65536个端口的位屏蔽矩阵,包含8192字节(8192 x 8 = 65536) // 0 bit: 允许应用程序访问对应端口 // 1 bit: 禁止应用程序访问对应端口 #define IOPM_SIZE 8192 typedef UCHAR IOPM[IOPM_SIZE]; IOPM *pIOPM = NULL; // 设备名(要求以UNICODE表示) const WCHAR NameBuffer[] = L"//Device//MyPort"; const WCHAR DOSNameBuffer[] = L"//DosDevices//MyPort"; // 这是两个在ntoskrnl.exe中的未见文档的服务例程 // 没有现成的已经说明它们原型的头文件,我们自己声明 void Ke386SetIoAccessMap(int, IOPM *); void Ke386IoSetAccessProcess(PEPROCESS, int); // 函数原型预先说明 NTSTATUS MyPortDispatch(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp); void MyPortUnload(IN PDRIVER_OBJECT DriverObject); // 驱动程序入口,由系统自动调用,就像WIN32应用程序的WinMain NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { PDEVICE_OBJECT deviceObject; NTSTATUS status; UNICODE_STRING uniNameString, uniDOSString; // 为IOPM分配内存 pIOPM = MmAllocateNonCachedMemory(sizeof(IOPM)); if(pIOPM == 0) { return STATUS_INSUFFICIENT_RESOURCES; } // IOPM全部初始化为0(允许访问所有端口) RtlZeroMemory(pIOPM, sizeof(IOPM)); // 将IOPM加载到当前进程 Ke386IoSetAccessProcess(PsGetCurrentProcess(), 1); Ke386SetIoAccessMap(1, pIOPM); // 指定驱动名字 RtlInitUnicodeString(&uniNameString, NameBuffer); RtlInitUnicodeString(&uniDOSString, DOSNameBuffer); // 创建设备 status = IoCreateDevice(DriverObject, 0, &uniNameString, FILE_DEVICE_MYPORT, 0, FALSE, &deviceObject); if(!NT_SUCCESS(status)) { return status; } // 创建WIN32应用程序需要的符号连接 status = IoCreateSymbolicLink (&uniDOSString, &uniNameString); if (!NT_SUCCESS(status)) { return status; } // 指定驱动程序有关操作的模块入口(函数指针) // 涉及以下两个模块:MyPortDispatch和MyPortUnload DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverObject->MajorFunction[IRP_MJ_CLOSE] = DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyPortDispatch; DriverObject->DriverUnload = MyPortUnload; return STATUS_SUCCESS; } // IRP处理模块 NTSTATUS MyPortDispatch(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { PIO_STACK_LOCATION IrpStack; ULONG dwInputBufferLength; ULONG dwOutputBufferLength; ULONG dwIoControlCode; PULONG pvIOBuffer; NTSTATUS ntStatus; // 填充几个默认值 Irp->IoStatus.Status = STATUS_SUCCESS; // 返回状态 Irp->IoStatus.Information = 0; // 输出长度 IrpStack = IoGetCurrentIrpStackLocation(Irp); // Get the pointer to the input/output buffer and it's length // 输入输出共用的缓冲区 // 因为我们在IOCTL中指定了METHOD_BUFFERED, pvIOBuffer = Irp->AssociatedIrp.SystemBuffer; switch (IrpStack->MajorFunction) { case IRP_MJ_CREATE: // 与WIN32应用程序中的CreateFile对应 break; case IRP_MJ_CLOSE: // 与WIN32应用程序中的CloseHandle对应 break; case IRP_MJ_DEVICE_CONTROL: // 与WIN32应用程序中的DeviceIoControl对应 dwIoControlCode = IrpStack->Parameters.DeviceIoControl.IoControlCode; switch (dwIoControlCode) { // 我们约定,缓冲区共两个DWORD,第一个DWORD为端口,第二个DWORD为数据 // 一般做法是专门定义一个结构,此处简单化处理了 case IOCTL_MYPORT_READ_BYTE: // 从端口读字节 pvIOBuffer[1] = _inp(pvIOBuffer[0]); Irp->IoStatus.Information = 8; // 输出长度为8 break; case IOCTL_MYPORT_WRITE_BYTE: // 写字节到端口 _outp(pvIOBuffer[0], pvIOBuffer[1]); break; default: // 不支持的IOCTL Irp->IoStatus.Status = STATUS_INVALID_PARAMETER; } } ntStatus = Irp->IoStatus.Status; IoCompleteRequest (Irp, IO_NO_INCREMENT); return ntStatus; } // 删除驱动 void MyPortUnload(IN PDRIVER_OBJECT DriverObject) { UNICODE_STRING uniDOSString; if(pIOPM) { // 释放IOPM占用的空间 MmFreeNonCachedMemory(pIOPM, sizeof(IOPM)); } RtlInitUnicodeString(&uniDOSString, DOSNameBuffer); // 删除符号连接和设备 IoDeleteSymbolicLink (&uniDOSString); IoDeleteDevice(DriverObject->DeviceObject); } 下面给出实现设备驱动程序的动态加载的源码。动态加载的好处是,你不用做任何添加新硬件的操作,也不用编辑注册表,更不用重新启动计算机。 // 安装驱动并启动服务 // lpszDriverPath: 驱动程序路径 // lpszServiceName: 服务名 BOOL StartDriver(LPCTSTR lpszDriverPath, LPCTSTR lpszServiceName) { SC_HANDLE hSCManager; // 服务控制管理器句柄 SC_HANDLE hService; // 服务句柄 BOOL bResult = FALSE; // 返回值 // 打开服务控制管理器 hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); // 打开失败... if(hSCManager) { // 创建服务 hService = CreateService(hSCManager, lpszServiceName, lpszServiceName, SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, lpszDriverPath, NULL, NULL, NULL, NULL, NULL); if(hService) { // 启动服务 bResult = StartService(hService, 0, NULL); // 关闭服务句柄 CloseServiceHandle(hService); } // 关闭服务控制管理器句柄 CloseServiceHandle(hSCManager); } return bResult; } // 停止服务并卸下驱动 // lpszServiceName: 服务名 BOOL StopDriver(LPCTSTR lpszServiceName) { SC_HANDLE hSCManager; // 服务控制管理器句柄 SC_HANDLE hService; // 服务句柄 BOOL bResult = FALSE; // 返回值 SERVICE_STATUS ServiceStatus; // 打开服务控制管理器 hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); // 打开失败... if(hSCManager) { // 打开服务 hService = OpenService(hSCManager, lpszServiceName, SERVICE_ALL_ACCESS); if(hService) { // 停止服务 bResult = ControlService(hService, SERVICE_CONTROL_STOP, &ServiceStatus); // 删除服务 bResult = bResult && DeleteService(hService); // 关闭服务句柄 CloseServiceHandle(hService); } // 关闭服务控制管理器句柄 CloseServiceHandle(hSCManager); } return bResult; } 应用程序实现端口I/O的接口如下: // 全局的设备句柄 HANDLE hMyPort; // 打开设备 // lpszDevicePath: 设备的路径 HANDLE OpenDevice(LPCTSTR lpszDevicePath) { HANDLE hDevice; // 打开设备 hDevice= ::CreateFile(lpszDevicePath, // 设备路径 GENERIC_READ | GENERIC_WRITE, // 读写方式 FILE_SHARE_READ | FILE_SHARE_WRITE, // 共享方式 NULL, // 默认的安全描述符 OPEN_EXISTING, // 创建方式 0, // 不需设置文件属性 NULL); // 不需参照模板文件 return hDevice; } // 打开端口驱动 BOOL OpenMyPort() { BOOL bResult; // 设备名为"MyPort",驱动程序位于Windows的"system32/drivers"目录中 bResult = StartDriver("system32//drivers//MyPort.sys", "MyPort"); // 设备路径为"//./MyPort" if(bResult) hMyPort=OpenDevice(".//MyPort"); return (bResult && hMyPort!=INVALID_HANDLE_VALUE); } // 关闭端口驱动 BOOL CloseMyPort() { return (CloseHandle(hMyPort) && StopDriver("MyPort")); } // 从指定端口读一个字节 // port: 端口 BYTE ReadPortByte(WORD port) { DWORD buf[2]; // 输入输出缓冲区 DWORD dwOutBytes; // IOCTL输出数据长度 buf[0] = port; // 第一个DWORD是端口 // buf[1] = 0; // 第二个DWORD是数据 // 用IOCTL_MYPORT_READ_BYTE读端口 ::DeviceIoControl(hMyPort, // 设备句柄 IOCTL_MYPORT_READ_BYTE, // 取设备属性信息 buf, sizeof(buf), // 输入数据缓冲区 buf, sizeof(buf), // 输出数据缓冲区 &dwOutBytes, // 输出数据长度 (LPOVERLAPPED)NULL); // 用同步I/O return (BYTE)buf[1]; } // 将一个字节写到指定端口 // port: 端口 // data: 字节数据 void WritePortByte(WORD port, BYTE data) { DWORD buf[2]; // 输入输出缓冲区 DWORD dwOutBytes; // IOCTL输出数据长度 buf[0] = port; // 第一个DWORD是端口 buf[1] = data; // 第二个DWORD是数据 // 用IOCTL_MYPORT_WRITE_BYTE写端口 ::DeviceIoControl(hMyPort, // 设备句柄 IOCTL_MYPORT_WRITE_BYTE, // 取设备属性信息 buf, sizeof(buf), // 输入数据缓冲区 buf, sizeof(buf), // 输出数据缓冲区 &dwOutBytes, // 输出数据长度 (LPOVERLAPPED)NULL); // 用同步I/O } 有了ReadPortByte和WritePortByte这两个函数,我们就能很容易地操纵CMOS和speaker了(关于CMOS值的含义以及定时器寄存器定义,请参考相应的硬件资料): // 0x70是CMOS索引端口(只写) // 0x71是CMOS数据端口 BYTE ReadCmos(BYTE index) { BYTE data; ::WritePortByte(0x70, index); data = ::ReadPortByte(0x71); return data; } // 0x61是speaker控制端口 // 0x43是8253/8254定时器控制端口 // 0x42是8253/8254定时器通道2的端口 void Sound(DWORD freq ) { BYTE data; if(freq>=20 && freq<=20000) { freq = 1193181 / freq; data = ::ReadPortByte(0x61); if((data & 3) == 0) { ::WritePortByte(0x61, data | 3); ::WritePortByte(0x43, 0xb6); } ::WritePortByte(0x42, (BYTE)(freq%256)); ::WritePortByte(0x42, (BYTE)(freq/256)); } } void NoSound( void ) { BYTE data; data = ::ReadPortByte(0x61); ::WritePortByte(0x61, data & 0xfc); } // 读出CMOS 128个字节 for(int i=0;i<128;i++) { BYTE data = ::ReadCmos(i); ... ... } // 用C调演奏“多-来-米” // 1 = 262 Hz ::Sound(262); ::Sleep(200); ::NoSound(); // 2 = 288 Hz ::Sound(288); ::Sleep(200); ::NoSound(); // 3 = 320 Hz ::Sound(320); ::Sleep(200); ::NoSound(); Q 就是个简单的端口I/O,这么麻烦才能实现,搞得俺头脑稀昏,有没有简洁明了的办法啊? A 上面的例子,之所以从编写驱动程序,到安装驱动,到启动服务,到打开设备,到访问设备,一直到读写端口,这样一路下来,是为了揭示在NT/2000/XP中硬件访问技术的本质。假如将所有过程封装起来,只提供OpenMyPort, CloseMyPort, ReadPortByte, WritePortByte甚至更高层的ReadCmos、WriteCmos、Sound、NoSound给你调用,是不是会感觉清爽许多? 实际上,我们平常做的基于一定硬件的二次开发,一般会先安装驱动程序(DRV)和用户接口的运行库(DLL),然后在此基础上开发出我们的应用程序(APP)。DRV、DLL、APP三者分别运行在核心态、核心态/用户态联络带、用户态。比如买了一块图象采集卡,要先安装核心驱动,它的“Development Tool Kit”,提供类似于PCV_Initialize, PCV_Capture等的API,就是扮演核心态和用户态联络员的角色。我们根本不需要CreateFile、CloseHandle、 DeviceIOControl、ReadFile、WriteFile等较低层次的直接调用。 Yariv Kaplan写过一个WinIO的例子,能实现对物理端口和内存的访问,提供了DRV、DLL、APP三方面的源码,有兴趣的话可以深入研究一下。 驱动程序源码:MyPort.zip (3KB, 编译环境: VC6+2000DDK) 演示程序源码:MyPortIo.zip (22KB, 含MyPort.sys, 该文件需复制到windows的system32/drivers目录中) Yariv Kaplan的主页:http://www.internals.com [作者后记] “实战DeviceIoControl系列”,到此告一段落了。 所谓“实战DeviceIoControl”,其实名不副实,并不是一步一步地介绍一个大型应用的开发,限于篇幅,只是列举一些用到DeviceIoControl的场合的例子而已。对涉及硬件的开发人员,DeviceIoControl是一个非常重要的API,其基本用处是联络设备驱动和应用程序,象本例中编写设备的用户接口时用到的情形比较多。如果只是限于开发应用程序,可能永远都用不到,也可能被迫使用(如获取硬盘序列号)。 |