实战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三方面的源码,有兴趣的话可以深入研究一下。
实战DeviceIoControl 之六:访问物理端口的更多相关文章
- [原创].NET 分布式架构开发实战之三 数据访问深入一点的思考
原文:[原创].NET 分布式架构开发实战之三 数据访问深入一点的思考 .NET 分布式架构开发实战之三 数据访问深入一点的思考 前言:首先,感谢园子里的朋友对文章的支持,感谢大家,希望本系列的文章能 ...
- 只允许指定IP访问指定端口 ufw
那天工作需要在服务器上指定ip才可以访问指定端口,配置命令如下: (服务器是ubuntu 14.04系统) apt-get install ufw ufw enable ufw default den ...
- tomcat第一次使用正常启动后访问8080端口报404错误
问题:tomcat第一次使用正常启动后访问8080端口报404错误 解决办法:双击tomcat调出tomcat的xml文件页面,Server Locations 默认是选第一行即Use Workspa ...
- iptables/mysql设置指定主机访问指定端口
本周,运维告知部署的服务被扫描发现漏洞,涉及的软件分别为mysql,ZooKeeper与Elasticsearch. 因为最近任务繁重,人力资源紧张,因此无法抽出更多时间调整代码,添加权限认证. 与软 ...
- 解决Windows10下无法对docker容器进行端口访问(端口映射的问题)
解决Windows10下无法对docker容器进行端口访问(端口映射的问题) 问题详情 在Windows10系统服务器中安装了docker和docker-compose 并尝试在其中运行Nginx服务 ...
- iptables只允许指定ip地址访问指定端口
首先,清除所有预设置 iptables -F#清除预设表filter中的所有规则链的规则 iptables -X#清除预设表filter中使用者自定链中的规则 其次,设置只允许指定ip地址访问指定端口 ...
- linux下通过iptables只允许指定ip地址访问指定端口的设置方法
这篇文章主要介绍了linux下通过iptables只允许指定ip地址访问指定端口的设置方法,需要的朋友可以参考下. 首先,清除所有预设置 iptables -F#清除预设表filter中的所有规则链的 ...
- docker 容器不能访问宿主端口原因
因为数据包到了eth0的 上的iptables 表,首先匹配PREROUTING 链,这个拒绝了来自docker0的流量,从而跳到input链,input没有放开服务端口,所以容器访问宿主端口失败;但 ...
- BCB直接访问硬件端口和物理内存 - WinIO的应用
BCB直接访问硬件端口和物理内存 - WinIO的应用 (读硬盘参数和主板BIOS信息, 支持 Win9x/NT/2k/XP/2003) 关于直接访问端口, 有很多网站很多文章都讨论过, 但总找不到非 ...
随机推荐
- mongodb- Java API 查询操作
package com.x.mongodb; import java.net.UnknownHostException;import java.util.Collection;import java. ...
- HTML5中的Web Notification桌面通知
大家在做一些浏览器端的聊天功能的时候,或者在一些网站跟在线客服咨询的时候,会看到一些消息通知的提示,常见的有浏览器标签页的闪烁和屏幕右侧的消息通知.本篇博客就在这里简单的介绍一下如何实现这样的功能. ...
- ABP官方文档翻译 2.7 对象到对象的映射
对象到对象的映射 介绍 IObjectMapper接口 AutoMapper集成 安装 创建映射 自动映射属性 自定义映射 MapTo扩展方法 单元测试 预定义映射 LocalizeableStrin ...
- bzoj 2627: JZPKIL [伯努利数 Pollard-rho]
2627: JZPKIL 题意:求 \[ \sum_{i=1}^n (n,i)^x [i,n]^y,\ [i,n] = lcm(i,n) \] \(n \le 10^{18},\ x,y\le 300 ...
- Codeforces Round #395 (Div. 2)(未完)
2.2.2017 9:35~11:35 A - Taymyr is calling you 直接模拟 #include <iostream> #include <cstdio> ...
- BZOJ 1502: [NOI2005]月下柠檬树 [辛普森积分 解析几何 圆]
1502: [NOI2005]月下柠檬树 Time Limit: 5 Sec Memory Limit: 64 MBSubmit: 1070 Solved: 596[Submit][Status] ...
- C++ 内存分配(new,operator new)面面观 (转)
本文主要讲述C++ new运算符和operator new, placement new之间的种种关联,new的底层实现,以及operator new的重载和一些在内存池,STL中的应用. 一 new ...
- ACE-6.1.0 linux 下的编译与安装步骤
ACE-6.1.0 linux 下的编译与安装步骤 引用至http://www.cnblogs.com/liangxiaxu/archive/2013/03/07/2948417.html 1.从 ...
- block,inline,inline-block的区别
最近正在复习,紧张地准备几天后的笔试,然后刚好看到这个地方. block:块级元素,会换行,如div,p,h1~h6,table这些,可以设置宽高: inline:行内元素,不换行,挤在一行显示 ...
- MacbookPro管理员问题
更改用户名重启之后,发现用户名还是用户名,管理员权限降成了普通用户. 参考这个帖子改的用户名: https://zhidao.baidu.com/question/259845860.html 找回管 ...