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三方面的源碼,有興趣的話可以深入研究一下。