程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> 匯編語言 >> 實戰DeviceIoControl之六:訪問物理端口

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved