现在的位置: 首页 > 综合 > 正文

实战DeviceIoControl

2014年08月29日 ⁄ 综合 ⁄ 共 54684字 ⁄ 字号 评论关闭

实战DeviceIoControl

 

Aida整理。希望与大家多交流驱动开发经验。

QQ18918737

E-mail: aida@tom.com

2006.5.12

 

实战DeviceIoControl 之一

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中,预定义的标准设备控制码,都以IOCTLFSCTL开头。例如,IOCTL_DISK_GET_DRIVE_GEOMETRY是对物理驱动器取结构参数(介质类型、柱面数、每柱面磁道数、每磁道扇区数等)的控制码,FSCTL_LOCK_VOLUME是对逻辑驱动器的卷加锁的控制码。

输入输出数据缓冲区是否需要,是何种结构,以及占多少字节空间,完全由不同设备的不同操作类型决定。在头文件winioctl.h中,已经为标准设备预定义了一些输入输出数据结构。重叠操作结构指针设置为NULLDeviceIoControl将进行阻塞调用;否则,应在编程时按异步操作设计。

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获得设备句柄时,访问方式参数设置为0GENERIC_READ|GENERIC_WRITE,共享方式参数设置为FILE_SHARE_READ|FILE_SHARE_WRITE,创建方式参数设置为OPEN_EXISTING,其它参数设置为0NULL

Q 可是,我怎么知道设备名称是什么呢?

A 一些存储设备的名称是微软定义好的,不可能有什么变化。大体列出如下

软盘驱动器

A:, B:

硬盘逻辑分区

C:, D:, E:, ...

物理驱动器

PHYSICALDRIVEx

CD-ROM, DVD/ROM

CDROMx

磁带机

TAPEx

其中,物理驱动器不包括软驱和光驱。逻辑驱动器可以是IDE/SCSI/PCMCIA/USB接口的硬盘分区(卷)、光驱、MOCF卡等,甚至是虚拟盘。x=012 ……

其它的设备名称需通过驱动接口的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);

}

Q 如果将设备名换成“A:”就可以取A盘参数,换成“CDROM0”就可以取CDROM参数,是这样吗?

A 这个问题暂不做回答。请动手试一下。

现在我们总结一下通过DeviceIoControl访问设备驱动程序的三步曲:首先用CreateFile取得设备句柄,然后用DeviceIoControl与设备进行I/O,最后别忘记用CloseHandle关闭设备句柄。

实战DeviceIoControl 之二:获取软盘/硬盘/光盘的参数

Q MSDN的那个demo中,将设备名换成“A:”A盘参数,先用资源管理器读一下盘,再运行这个程序可以成功,但换一张盘后就失败;换成“CDROM0”CDROM参数,无论如何都不行。这个问题如何解决呢?

A 取软盘参数是从软盘上读取格式化后的信息,也就是必须执行读操作,这一点与硬盘不同。将CreateFile中的访问方式改为GENERIC_READ就行了。

IOCTL_DISK_GET_DRIVE_GEOMETRY这个I/O控制码,对软盘和硬盘有效,但对一些可移动媒介如CD/DVD-ROMTAPE等就不管用了。要取CDROM参数,还得另辟蹊径。IOCTL_STORAGE_GET_MEDIA_TYPES_EX能够帮我们解决问题。

Q 使用这些I/O控制码,需要什么样的输入输出数据格式呢?

A DeviceIoControl使用这两个控制码时,都不需要输入数据。

IOCTL_DISK_GET_DRIVE_GEOMETRY直接输出一个DISK_GEOMETRY结构:

typedef struct _DISK_GEOMETRY {
    LARGE_INTEGER Cylinders;   // 柱面数
    MEDIA_TYPE MediaType;      // 介质类型
    DWORD TracksPerCylinder;   // 每柱面的磁道数
    DWORD SectorsPerTrack;     // 每磁道的扇区数
    DWORD BytesPerSector;      // 每扇区的字节数
} DISK_GEOMETRY;

IOCTL_STORAGE_GET_MEDIA_TYPES_EX输出一个GET_MEDIA_TYPES结构:

typedef struct _GET_MEDIA_TYPES {
    DWORD DeviceType;               // 设备类型
    DWORD MediaInfoCount;           // 介质信息条数
    DEVICE_MEDIA_INFO MediaInfo[1]; // 介质信息
} GET_MEDIA_TYPES;

让我们来看一下DEVICE_MEDIA_INFO结构的定义:

typedef struct _DEVICE_MEDIA_INFO {
    union {
        struct {
            LARGE_INTEGER Cylinders;       // 柱面数
            STORAGE_MEDIA_TYPE MediaType;  // 介质类型
            DWORD TracksPerCylinder;       // 每柱面的磁道数
            DWORD SectorsPerTrack;         // 每磁道的扇区数
            DWORD BytesPerSector;          // 每扇区的字节数
            DWORD NumberMediaSides;        // 介质面数
            DWORD MediaCharacteristics;    // 介质特性
        } DiskInfo;            // 硬盘信息
        struct {
            LARGE_INTEGER Cylinders;       // 柱面数
            STORAGE_MEDIA_TYPE MediaType;  // 介质类型
            DWORD TracksPerCylinder;       // 每柱面的磁道数
            DWORD SectorsPerTrack;         // 每磁道的扇区数
            DWORD BytesPerSector;          // 每扇区的字节数
            DWORD NumberMediaSides;        // 介质面数
            DWORD MediaCharacteristics;    // 介质特性
        } RemovableDiskInfo;   // “可移动盘信息
        struct {
            STORAGE_MEDIA_TYPE MediaType;  // 介质类型
            DWORD   MediaCharacteristics;  // 介质特性
            DWORD   CurrentBlockSize;      // 块的大小
        } TapeInfo;           // 磁带信息
    } DeviceSpecific;
} DEVICE_MEDIA_INFO;

其中CD-ROM属于可移动盘的范围。请注意,GET_MEDIA_TYPES结构本身只定义了一条DEVICE_MEDIA_INFO,额外的DEVICE_MEDIA_INFO需要紧接此结构的另外的空间。

Q 调用方法我了解了,请用VC举个例子来实现我所期待已久的功能吧?

A 好,现在就演示一下如何取软盘/硬盘/光盘的参数。测试时,记得要有软盘/光盘插在驱动器里喔!

首先,用MFC AppWizard生成一个单文档的应用程序,取名为DiskGeometry,让它的View基于CEditView

然后,添加以下的.h.cpp文件。

//////////////////////////////////////////////////////////////////////////////
// GetDiskGeometry.h
//////////////////////////////////////////////////////////////////////////////
  
#if !defined(GET_DISK_GEOMETRY_H__)
#define GET_DISK_GEOMETRY_H__
  
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
  
#include <winioctl.h>
  
BOOL GetDriveGeometry(const char* filename, DISK_GEOMETRY *pdg);
  
#endif // !defined(GET_DISK_GEOMETRY_H__)
  
//////////////////////////////////////////////////////////////////////////////
// GetDiskGeometry.cpp
//////////////////////////////////////////////////////////////////////////////
  
#include "stdafx.h"
#include "GetDiskGeometry.h"
  
// IOCTL_STORAGE_GET_MEDIA_TYPES_EX可能返回不止一条DEVICE_MEDIA_INFO,故定义足够的空间
#define MEDIA_INFO_SIZE    sizeof(GET_MEDIA_TYPES)+15*sizeof(DEVICE_MEDIA_INFO)
  
// filename -- 用于设备的文件名
// pdg -- 参数缓冲区指针
BOOL GetDriveGeometry(const char* filename, DISK_GEOMETRY *pdg)
{
    HANDLE hDevice;         // 设备句柄
    BOOL bResult;           // DeviceIoControl的返回结果
    GET_MEDIA_TYPES *pmt;   // 内部用的输出缓冲区
    DWORD dwOutBytes;       // 输出数据长度
  
    // 打开设备
    hDevice = ::CreateFile(filename,           // 文件名
        GENERIC_READ,                          // 软驱需要读盘
        FILE_SHARE_READ | FILE_SHARE_WRITE,    // 共享方式
        NULL,                                  // 默认的安全描述符
        OPEN_EXISTING,                         // 创建方式
        0,                                     // 不需设置文件属性
        NULL);                                 // 不需参照模板文件
  
    if (hDevice == INVALID_HANDLE_VALUE)
    {
        // 设备无法打开...
        return FALSE;
    }
  
    // IOCTL_DISK_GET_DRIVE_GEOMETRY取磁盘参数
    bResult = ::DeviceIoControl(hDevice,       // 设备句柄
        IOCTL_DISK_GET_DRIVE_GEOMETRY,         // 取磁盘参数
        NULL, 0,                               // 不需要输入数据
        pdg, sizeof(DISK_GEOMETRY),            // 输出数据缓冲区
        &dwOutBytes,                           // 输出数据长度
        (LPOVERLAPPED)NULL);                   // 用同步I/O
  
    // 如果失败,再用IOCTL_STORAGE_GET_MEDIA_TYPES_EX取介质类型参数
    if (!bResult)
    {
        pmt = (GET_MEDIA_TYPES *)new BYTE[MEDIA_INFO_SIZE];
  
        bResult = ::DeviceIoControl(hDevice,    // 设备句柄
            IOCTL_STORAGE_GET_MEDIA_TYPES_EX,   // 取介质类型参数
            NULL, 0,                            // 不需要输入数据
            pmt, MEDIA_INFO_SIZE,               // 输出数据缓冲区
            &dwOutBytes,                        // 输出数据长度
            (LPOVERLAPPED)NULL);                // 用同步I/O
  
        if (bResult)
        {
            // 注意到结构DEVICE_MEDIA_INFO是在结构DISK_GEOMETRY的基础上扩充的
            // 为简化程序,用memcpy代替如下多条赋值语句:
            // pdg->MediaType = (MEDIA_TYPE)pmt->MediaInfo[0].DeviceSpecific.DiskInfo.MediaType;
            // pdg->Cylinders = pmt->MediaInfo[0].DeviceSpecific.DiskInfo.Cylinders;
            // pdg->TracksPerCylinder = pmt->MediaInfo[0].DeviceSpecific.DiskInfo.TracksPerCylinder;
            // ... ...
            ::memcpy(pdg, pmt->MediaInfo, sizeof(DISK_GEOMETRY));
        }
  
        delete pmt;
    }
  
    // 关闭设备句柄
    ::CloseHandle(hDevice);
  
    return (bResult);
}

然后,在ToolbarIDR_MAINFRAME上添加一个按钮,IDID_GET_DISK_GEOMETRY。打开ClassWizard,在DiskGeometryView

添加ID_GET_DISK_GEOMETRY的映射函数OnGetDiskGeometry。打开DiskGeometryView.cpp,包含头文件GetDiskGeometry.h

OnGetDiskGeometry中,添加以下代码

    const char *szDevName[]=
    {
        "////.//A:",
        "////.//B:",
        "////.//PhysicalDrive0",
        "////.//PhysicalDrive1",
        "////.//PhysicalDrive2",
        "////.//PhysicalDrive3",
        "////.//Cdrom0",
        "////.//Cdrom1",
    };
    DISK_GEOMETRY dg;
    ULONGLONG DiskSize;
    BOOL bResult;
    CString strMsg;
    CString strTmp;
  
    for (int i = 0; i < sizeof(szDevName)/sizeof(char*); i++)
    {
        bResult = GetDriveGeometry(szDevName[i], &dg);
  
        strTmp.Format("/r/n%s  result = %s/r/n", szDevName[i], bResult ? "success" : "failure");
        strMsg+=strTmp;
  
        if (!bResult) continue;
  
        strTmp.Format("    Media Type = %d/r/n", dg.MediaType);
        strMsg+=strTmp;
  
        strTmp.Format("    Cylinders = %I64d/r/n", dg.Cylinders);
        strMsg+=strTmp;
  
        strTmp.Format("    Tracks per cylinder = %ld/r/n", (ULONG) dg.TracksPerCylinder);
        strMsg+=strTmp;
  
        strTmp.Format("    Sectors per track = %ld/r/n", (ULONG) dg.SectorsPerTrack);
        strMsg+=strTmp;
  
        strTmp.Format("    Bytes per sector = %ld/r/n", (ULONG) dg.BytesPerSector);
        strMsg+=strTmp;
  
        DiskSize = dg.Cylinders.QuadPart * (ULONG)dg.TracksPerCylinder *
            (ULONG)dg.SectorsPerTrack * (ULONG)dg.BytesPerSector;
        strTmp.Format("    Disk size = %I64d (Bytes) = %I64d (Mb)/r/n", DiskSize, DiskSize / (1024 * 1024));
        strMsg+=strTmp;
    }
  
    CEdit& Edit = GetEditCtrl();
  
    Edit.SetWindowText(strMsg);

最后,最后干什么呢?编译,运行......

实战DeviceIoControl 之三:制作磁盘镜像文件

Q 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,我们已经探讨过的。

实战DeviceIoControl 之四:获取硬盘的详细信息 

Q IOCTL_DISK_GET_DRIVE_GEOMETRYIOCTL_STORAGE_GET_MEDIA_TYPES_EX只能得到很少的磁盘参数,我想获得包括硬盘序列号在内的更加详细的信息,有什么办法呀?

A 确实,用你所说的I/O控制码,只能得到最基本的磁盘参数。获取磁盘出厂信息的I/O控制码,微软在VC/MFC环境中没有开放,在DDK中可以发现一些线索。早先,Lynn McGuire写了一个很出名的获取IDE硬盘详细信息的程序DiskID32,下面的例子是在其基础上经过增删和改进而成的。

本例中,我们要用到ATA/APAPIIDENTIFY 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     // ATAID指令(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;
  
// IDEID命令返回的数据
// 512字节(256WORD),这里仅定义了一些感兴趣的项(基本上依据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;

需要引起注意的是IDINFO57-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用于可移动存储设备如CDROMCFMOZIPTAPE等。因为驱动程序的原因,实际上用本例的方法,不管是IDENTIFY DEVICE也好,IDENTIFY PACKET DEVICE也好,获取可移动存储设备的详细信息,一般是做不到的。而且除了IDE硬盘,对SCSIUSB等接口的硬盘也不起作用。除非厂商提供的驱动支持这样的功能。

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/XPDeviceIoControl的应用,如果需要在98/ME中得到包括硬盘序列号在内的更加详细的信息,请参考DiskID32

实战DeviceIoControl 之五:列举已安装的存储设备

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 & lt; MAX_DEVICE; i++)
    {
        delete []szDevicePath[i];
    }

本例的Project中除了要包含winioctl.h外,还要包含initguid.hsetupapi.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_GEOMETRYIOCTL_STORAGE_GET_MEDIA_TYPES_EXI/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返回的数据中,没有得到CDROMUSB接口的外置硬盘的序列号、产品ID等信息。但从设备路径上看,明明是有这些信息的,为什么它没有填充到STORAGE_DEVICE_DESCRIPTOR中呢?再就是为什么硬盘序列号本是“D22P7KHE            ”,为什么它填充的是“3146563447534558202020202020202020202020”这种形式呢?

A 对这两个问题我也是心存疑惑,但又不敢妄加猜测,正琢磨着向微软请教呢。

实战DeviceIoControl 之六:访问物理端口

Q NT/2000/XP中,如何读取CMOS数据?

Q NT/2000/XP中,如何控制speaker发声?

Q NT/2000/XP中,如何直接访问物理端口?

A 看似小小问题,难倒多少好汉!

NT/2000/XP从安全性、可靠性、稳定性上考虑,应用程序和操作系统是分开的,操作系统代码运行在核心态,有权访问系统数据和硬件,能执行特权指令;应用程序运行在用户态,能够使用的接口和访问系统数据的权限都受到严格限制。当用户程序调用系统服务时,处理器捕获该调用,然后把调用的线程切换到核心态。当系统服务完成后,操作系统将线程描述表切换回用户态,调用者继续运行。

想在用户态应用程序中实现I/O读写,直接存取硬件,可以通过编写驱动程序,实现CreateFileCloseHandle DeviceIOControlReadFileWriteFile等功能。从Windows 2000开始,引入WDM核心态驱动程序的概念。

下面是本人写的一个非常简单的驱动程序,可实现字节型端口I/O

#include <ntddk.h>
#include "MyPort.h"
  
// 设备类型定义
// 0-32767Microsoft占用,用户自定义可用32768-65535
#define FILE_DEVICE_MYPORT    0x0000f000
  
// I/O控制码定义
// 0-2047Microsoft占用,用户自定义可用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)
  
// IOPM65536个端口的位屏蔽矩阵,包含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;
    }
  
    // 指定驱动程序有关操作的模块入口(函数指针)
    // 涉及以下两个模块:MyPortDispatchMyPortUnload
    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;          // 服务句柄
    DWORD dwLastError;           // 错误码
    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 == NULL)
        {
            if (::GetLastError() == ERROR_SERVICE_EXISTS)
            {
                hService = ::OpenService(hSCManager, lpszServiceName, SERVICE_ALL_ACCESS);
            }
        }
  
        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;                // 返回值
    SERVICE_STATUS ServiceStatus;
  
    bResult = FALSE;
  
    // 打开服务控制管理器
    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
}

有了ReadPortByteWritePortByte这两个函数,我们就能很容易地操纵CMOSspeaker(关于CMOS值的含义以及定时器寄存器定义,请参考相应的硬件资料)

// 0x70CMOS索引端口(只写)
// 0x71CMOS数据端口
BYTE ReadCmos(BYTE index)
{
    BYTE data;
  
    ::WritePortByte(0x70, index);
  
    data = ::ReadPortByte(0x71);
  
    return data;
}
  
// 0x61speaker控制端口
// 0x438253/8254定时器控制端口
// 0x428253/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甚至更高层的ReadCmosWriteCmosSoundNoSound给你调用,是不是会感觉清爽许多?

实际上,我们平常做的基于一定硬件的二次开发,一般会先安装驱动程序(DRV)和用户接口的运行库(DLL),然后在此基础上开发出我们的应用程序(APP)DRVDLLAPP三者分别运行在核心态、核心态/用户态联络带、用户态。比如买了一块图象采集卡,要先安装核心驱动,它的“Development Tool Kit”,提供类似于PCV_Initialize, PCV_Capture等的API,就是扮演核心态和用户态联络员的角色。我们根本不需要CreateFileCloseHandle DeviceIOControlReadFileWriteFile等较低层次的直接调用。

Yariv Kaplan写过一个WinIO的例子,能实现对物理端口和内存的访问,提供了DRVDLLAPP三方面的源码,有兴趣的话可以深入研究一下。

实战DeviceIoControl 之七:在Windows 9X中读写磁盘扇

Windows NT/2K/XP中,直接用CreateFile打开名称类似于"//./A:"文件,就可以与设备驱动打交道,通过ReadFile/WriteFile以绝对地址方式访问磁盘了。但Windows 9X不支持这样的简单方法。本文介绍一种在Windows 9X中实现磁盘直接访问的方法:利用系统的vwin32.vxd,通过DeviceIoControl调用DOS INT21 7305H440DH功能来完成。该调用支持FAT12FAT16FAT32,适用于Windows 95 SR2以及更高版本。

先来了解一下DOS INT21 7305H功能的入口参数:

AX -- 功能号7305H

DS:BX -- 读写扇区的信息结构

CX -- 必须为-1

DL -- 驱动器号: 1=A:, 2=B:, 3=C:, ...

SI -- 读写标志: 最低位0=, 1=

若调用成功,清除C标志;否则设置C标志。

DS:BX指向一个结构,此结构定义如下:

DISKIO STRUC

    dwStartSector   dd ?    ; 起始扇区

    wSector         dw ?    ; 扇区数

    lpBuffer        dd ?    ; 数据缓冲区地址

DISKIO ENDS

在写操作下,需要锁定驱动器。DOS INT21 440DH4AH/6AH功能可实现逻辑驱动器的加锁/解锁。其入口参数为:

AX -- 功能号440DH

BH -- 锁的级别,0-3级,直接写扇区用1

BL -- 驱动器号: 1=A:, 2=B:, 3=C:, ...

CH -- 0x08

CL -- 0x4A

DX -- 0

AX -- 功能号440DH

BL -- 驱动器号: 1=A:, 2=B:, 3=C:, ...

CH -- 0x08

CL -- 0x6A

以上两个调用,若调用成功,清除C标志;否则设置C标志。

通过IOCTLVWIN32_DIOC_DOS_DRIVEINFO等调用上述中断。实现绝对磁盘读写的关键代码如下:

// INT21IOCTL

#define VWIN32_DIOC_DOS_IOCTL        1

#define VWIN32_DIOC_DOS_DRIVEINFO    6

 

// 寄存器组

typedef struct _DIOC_REGISTERS {

    DWORD reg_EBX;

    DWORD reg_EDX;

    DWORD reg_ECX;

    DWORD reg_EAX;

    DWORD reg_EDI;

    DWORD reg_ESI;

    DWORD reg_Flags;

} DIOC_REGISTERS, *PDIOC_REGISTERS;

 

// IO参数(注意字节对齐方式)

#pragma pack(1)

typedef struct _DISKIO {

    DWORD  dwStartSector;     // 起始扇区

    WORD   wSectors;          // 扇区数

    void*  pBuffer;           // 缓冲区指针

} DISKIO, *PDISKIO;

#pragma pack()

 

BOOL AbsDiskRead(

    BYTE nDiskNumber,         // 盘号, 1=A:, 2=B:, 3= C:, ...

    DWORD dwStartSector,      // 起始扇区

    WORD wSectors,            // 扇区数

    void* pBuffer)            // 数据缓冲区指针

{

    HANDLE hDevice;

    DIOC_REGISTERS regs;

    DISKIO dio;

    DWORD dwOutBytes;

    BOOL bResult;

 

    // 打开设备,获得VxD句柄

    hDevice = CreateFile("////.//vwin32",        // 设备路径

        GENERIC_READ | GENERIC_WRITE,            // 读写方式

        FILE_SHARE_READ | FILE_SHARE_WRITE,      // 共享方式

        NULL,                                    // 默认的安全描述符

        OPEN_EXISTING,                           // 创建方式

        FILE_ATTRIBUTE_NORMAL,                   // 文件属性

        NULL);                                   // 不需参照模板文件

 

    if(hDevice == INVALID_HANDLE_VALUE)

    {

        return FALSE;

    }

 

    // 填充DISKIO参数结构

    dio.dwStartSector = dwStartSector;

    dio.wSectors = wSectors;

    dio.pBuffer = pBuffer;

 

    // 填充寄存器组--中断入口参数

    memset(&regs, 0, sizeof(DIOC_REGISTERS));

    regs.reg_EAX = 0x7305;           // AX=0x7305

    regs.reg_EBX = (DWORD)&dio;      // EBX=DS:BX=参数指针

    regs.reg_ECX = 0xffff;           // CX=-1

    regs.reg_EDX = nDiskNumber;      // DL=盘号

    regs.reg_ESI = 0;                // SI=0 -- 读操作

 

    // VWIN32_DIOC_DOS_DRIVEINFO读磁盘

    dwOutBytes = 0;

    bResult = DeviceIoControl(hDevice,           // 设备句柄

        VWIN32_DIOC_DOS_DRIVEINFO,               // INT21

        &regs, sizeof(regs),                     // 输出数据缓冲区与长度

        &regs, sizeof(regs),                     // 输出数据缓冲区与长度

        &dwOutBytes,                             // 输出数据长度

        NULL);                                   // 用同步I/O

 

    // 确定DeviceIoControlINT21都无错误

    bResult = bResult && !(regs.reg_Flags & 1);

 

    CloseHandle(hDevice);

 

    return bResult;

}

 

BOOL AbsDiskWrite(

    BYTE nDiskNumber,        // 盘号, 1=A:, 2=B:, 3= C:, ...

    DWORD dwStartSector,     // 起始扇区

    WORD wSectors,           // 扇区数

    void* pBuffer)           // 数据缓冲区指针

{

    HANDLE hDevice;

    DIOC_REGISTERS regs;

    DISKIO dio;

    DWORD dwOutBytes;

    BOOL bResult;

 

    // 打开设备,获得VxD句柄

    hDevice = CreateFile("////.//vwin32",        // 设备路径

        GENERIC_READ | GENERIC_WRITE,            // 读写方式

        FILE_SHARE_READ | FILE_SHARE_WRITE,      // 共享方式

        NULL,                                    // 默认的安全描述符

        OPEN_EXISTING,                           // 创建方式

        FILE_ATTRIBUTE_NORMAL,                   // 文件属性

        NULL);                                   // 不需参照模板文件

 

    if(hDevice == INVALID_HANDLE_VALUE)

    {

        return FALSE;

    }

 

    // 填充DISKIO参数结构

    dio.dwStartSector = dwStartSector;

    dio.wSectors = wSectors;

    dio.pBuffer = pBuffer;

 

    // 填充寄存器组--中断入口参数

    memset(&regs, 0, sizeof(DIOC_REGISTERS));

    regs.reg_EAX = 0x7305;             // AX=0x7305

    regs.reg_EBX = (DWORD)&dio;        // EBX=DS:BX=参数指针

    regs.reg_ECX = 0xffff;             // CX=-1

    regs.reg_EDX = nDiskNumber;        // DL=盘号

    regs.reg_ESI = 0x6001;             // SI=0x6001 -- 普通写操作

 

    // VWIN32_DIOC_DOS_DRIVEINFO写磁盘

    dwOutBytes = 0;

    bResult = DeviceIoControl(hDevice,           // 设备句柄

        VWIN32_DIOC_DOS_DRIVEINFO,               // INT21

        &regs, sizeof(regs),                     // 输出数据缓冲区与长度

        &regs, sizeof(regs),                     // 输出数据缓冲区与长度

        &dwOutBytes,                             // 输出数据长度

        NULL);                                   // 用同步I/O

 

    // 确定DeviceIoControlINT21都无错误

    bResult = bResult && !(regs.reg_Flags & 1);

 

    CloseHandle(hDevice);

 

    return bResult;

}

 

BOOL LockVolume(

    BYTE nDiskNumber)         // 盘号, 1=A:, 2=B:, 3=C:, ...

{

    HANDLE hDevice;

    DIOC_REGISTERS regs;

    DWORD dwOutBytes;

    BOOL bResult;

 

    // 打开设备,获得VxD句柄

    hDevice = CreateFile("////.//vwin32",        // 设备路径

        GENERIC_READ | GENERIC_WRITE,            // 读写方式

        FILE_SHARE_READ | FILE_SHARE_WRITE,      // 共享方式

        NULL,                                    // 默认的安全描述符

        OPEN_EXISTING,                           // 创建方式

        FILE_ATTRIBUTE_NORMAL,                   // 文件属性

        NULL);                                   // 不需参照模板文件

 

    if(hDevice == INVALID_HANDLE_VALUE)

    {

        return FALSE;

    }

 

    // 填充寄存器组--中断入口参数

    memset(&regs, 0, sizeof(DIOC_REGISTERS));

    regs.reg_EAX = 0x440D;                       // AX=0x440D

    regs.reg_EBX = 0x0100 | nDiskNumber;         // BH=锁的级别,BL=盘号

    regs.reg_ECX = 0x084A;

    regs.reg_EDX = 0;

 

    // VWIN32_DIOC_DOS_DRIVEINFO读磁盘

    dwOutBytes = 0;

    bResult = DeviceIoControl(hDevice,           // 设备句柄

        VWIN32_DIOC_DOS_IOCTL,                   // INT21

        &regs, sizeof(regs),                     // 输入数据缓冲区与长度

        &regs, sizeof(regs),                     // 输出数据缓冲区与长度

        &dwOutBytes,                             // 输出数据长度

        NULL);                                   // 用同步I/O

 

    // 确定DeviceIoControlINT21都无错误

    bResult = bResult && !(regs.reg_Flags & 1);

 

    CloseHandle(hDevice);

 

    return bResult;

}

 

BOOL UnlockVolume(

    BYTE nDiskNumber)         // 盘号, 1=A:, 2=B:, 3=C:, ...

{

    HANDLE hDevice;

    DIOC_REGISTERS regs;

    DWORD dwOutBytes;

    BOOL bResult;

 

    // 打开设备,获得VxD句柄

    hDevice = CreateFile("////.//vwin32",        // 设备路径

        GENERIC_READ | GENERIC_WRITE,            // 读写方式

        FILE_SHARE_READ | FILE_SHARE_WRITE,      // 共享方式

        NULL,                                    // 默认的安全描述符

        OPEN_EXISTING,                           // 创建方式

        FILE_ATTRIBUTE_NORMAL,                   // 文件属性

        NULL);                                   // 不需参照模板文件

 

    if(hDevice == INVALID_HANDLE_VALUE)

    {

        return FALSE;

    }

 

    // 填充寄存器组--中断入口参数

    memset(&regs, 0, sizeof(DIOC_REGISTERS));

    regs.reg_EAX = 0x440D;                       // AX=0x440D

    regs.reg_EBX = nDiskNumber;                  // BL=盘号

    regs.reg_ECX = 0x086A;

 

    // VWIN32_DIOC_DOS_DRIVEINFO读磁盘

    dwOutBytes = 0;

    bResult = DeviceIoControl(hDevice,           // 设备句柄

        VWIN32_DIOC_DOS_IOCTL,                   // INT21

        &regs, sizeof(regs),                     // 输入数据缓冲区与长度

        &regs, sizeof(regs),                     // 输出数据缓冲区与长度

        &dwOutBytes,                             // 输出数据长度

        NULL);                                   // 用同步I/O

 

    // 确定DeviceIoControlINT21都无错误

    bResult = bResult && !(regs.reg_Flags & 1);

 

    CloseHandle(hDevice);

 

    return bResult;

}

下面的例子,从A盘的0扇区开始,读取10个扇区的数据,并保存在文件中:

    unsigned char buf[512 * 10];

 

    if (AbsDiskRead(1, 0, 10, buf))

    {

        FILE* fp = fopen("a.dat", "w+b");

        fwrite(buf, 512, 10, fp);

        fclose(fp);

    }

下面的例子,读取D驱动器的第8888扇区,然后写回去:

    unsigned char buf[512];

 

    LockVolume(4);

    if (AbsDiskRead(4, 8888, 1, buf))

    {

        ... ...

        if (AbsDiskWrite(4, 8888, 1, buf))

        {

           ... ...

        }

    }

    UnlockVolume(4);

在写方式下,SI寄存器的位0设置为1,位15-13在磁盘的不同区域需要有不同的值:

Bit 15

Bit 14

Bit 13

Description

0

0

0

Other/Unknown.

0

0

1

FAT data.

0

1

0

Directory data.

0

1

1

Normal file data.

1

0

0

Reserved.

如果不按照上述值操作,尽管能够写成功,但系统无法自动完成相关功能,可能会导致FAT数据备份、驱动器数据压缩等方面的问题。

抱歉!评论已关闭.