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

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,其基本用处是联络设备驱动和应用程序,象本例中编写设备的用户接口时用到的情形比较多。如果只是限于开发应用程序,可能永远都用不到,也可能被迫使用(如获取硬盘序列号)。

 

你可能感兴趣的:(object,service,Microsoft,null,Access,byte)