天天看點

實戰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表示)

// 這是兩個在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);

    // 指定驅動程式有關操作的子產品入口(函數指針)

    // 涉及以下兩個子產品: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對應

        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]);

                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));

    // 删除符号連接配接和裝置

    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,

                    service_all_access,

                    service_kernel_driver,

                    service_demand_start,

                    service_error_normal,

                    lpszdriverpath,

                    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;

// 停止服務并卸下驅動

bool stopdriver(lpctstr lpszservicename)

    bool bresult;                // 傳回值

    service_status servicestatus;

    bresult = false;

        // 打開服務

        hservice = openservice(hscmanager, lpszservicename, service_all_access);

            // 停止服務

            bresult = controlservice(hservice, service_control_stop, &servicestatus);

            // 删除服務

            bresult = bresult && deleteservice(hservice);

應用程式實作端口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");

    if (bresult)

    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];

// 将一個位元組寫到指定端口

// data: 位元組資料

void writeportbyte(word port, byte data)

    buf[1] = data;           // 第二個dword是資料

    // 用ioctl_myport_write_byte寫端口

        ioctl_myport_write_byte, // 取裝置屬性資訊

        (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)

    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)

    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);

    // 3 = 320 hz

    ::sound(320);

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

繼續閱讀