實戰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>
#include "MyPort.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; // 服務句柄
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
}
有了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三方麵的源碼,有興趣的話可以深入研究一下。
最後更新:2017-04-03 15:21:56