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