访问物理内存、端口和 PCI 配置空间
在用户模式下玩转物理内存、端口和 PCI 配置空间
引言
我最近对 ACPI 编程产生了浓厚的兴趣。通过 Google,我找到了 Intel 的 ACPICA 开源库。当然,要使其正常工作(例如读取 ACPI 表、评估 ACPI 方法),我必须实现一些函数来访问物理内存、端口和 PCI 配置空间,甚至安装 ISR。
在内核模式下实现这些函数非常容易。但我不想将整个 ACPICA 库放入一个“.sys”文件中,那样调试起来会非常困难。调试对我来说很重要,因为我总是想弄清楚到底发生了什么。因此,唯一的解决方案是在用户模式下访问这些资源。
起初我使用了 WinIO,并且它工作正常。但在阅读其源代码后,我发现它使用了太多“未公开”和“过时”的函数。我决定创建一个更优雅的解决方案,并添加访问 PCI 配置空间的功能。
背景
1. 架构
我借鉴了 WinIO 的软件架构:一个内核模式驱动程序“phymem.sys”和一个用户模式 DLL“pmdll.dll”。应用程序可以通过 pmdll.dll 导出的函数轻松访问物理内存,该函数将通过标准的“DeviceIoControl
”与 phymem.sys 进行通信。
按照 DDK 推荐的方法访问 PCI 配置空间,我编写了一个 PCI 总线过滤器驱动程序“PCIFlt.sys”。有了这个过滤器驱动程序,我们就可以找到位于我们命名过滤器驱动程序之下的未命名 PCI 总线驱动程序。然后,我们使用“驱动程序接口”直接读写 PCI 配置空间。
2. 访问物理端口
基于 IA 的 PC 使用分离的端口和内存地址空间。在内核模式下,我们可以使用类似 WRITE_PORT_UCHAR
、READ_PORT_UCHAR
这样的函数来读写端口。
3. 访问物理内存
要在用户模式下访问物理内存,我们必须将该内存区域映射到用户进程的地址空间。一种实现方式是通过 \Device\PhysicalMemory 段对象。这在旧的 NT DDK 示例中首次引入。它使用了一些不推荐使用的过时函数;而且代码实在难以理解。
MSDN 中可以找到更好的实现。只需要三个步骤:
- 使用
MmMapIoSpace
将物理地址映射到内核模式虚拟地址,驱动程序可以访问此虚拟地址,但用户模式无法访问。 - 使用
IoAllocateMdl
和MmBuildMdlForNonPagedPool
为映射的物理地址构建一个 MDL。 - 使用
MmMapLockedPages
将 MDL 描述的物理页面映射到用户模式虚拟地址。由于我们的驱动程序将始终是最高层的驱动程序,并且在当前进程的上下文中运行,因此此用户模式虚拟地址对调用者是有效的。
4. 访问 PCI 配置空间
Windows XP 总线驱动程序必须实现“驱动程序接口”,可以通过发送一个主代码为 IRP_MN_QUERY_INTERFACE
的 IRP 来获取。获取“驱动程序接口”后,我们就可以通过调用接口提供的 ReadConfig
和 WriteConfig
例程来访问总线地址空间。
麻烦的是 PCI 总线驱动程序没有名称,也就是说,我们找不到它的设备对象。没有 PCI 总线驱动程序的设备对象,我们就无法查询其“驱动程序接口”。解决方案是提供一个 PCI 总线过滤器驱动程序,它将位于实际功能总线驱动程序的上方。
Using the Code
所有源代码都在 Visual C++ 6.0、XP DDK 2600 和 Windows XP SP3 下构建。要在 Visual C++ IDE 中构建驱动程序(.sys),请按照接下来的两个步骤操作:
- 将环境变量
$DDKROOT
设置为 DDK 安装目录,例如“D:\WINDDK\2600”。 - 在 VC++ IDE 中,选择“工具”->“选项”->“目录”->“显示目录用于”,选择“可执行文件”,添加 DDK 的 bin 目录,并将其移至第一行,例如“D:\WINDDK\2600\BIN\X86”。
驱动程序源代码使用 PHDDebugPrint
进行调试。请参考 Chris Cant 著的《Writing Windows WDM Device Drivers》一书。
手动
- 将 pmdll.h、pmdll.lib 复制到您的源代码目录,然后包含并链接。
- 将 pmdll.dll、phymem.sys 复制到您的应用程序目录以供运行。
- 函数参考
BOOL LoadPhyMemDriver()
动态加载 phymem.sys 到内存;如果成功则返回
TRUE
,否则返回FALSE
。VOID UnloadPhyMemDriver()
卸载 phymem.sys 的内存。
PVOID MapPhyMem(DWORD phyAddr, DWORD memSize)
将物理内存映射到用户虚拟空间
phyAddr
= 物理内存地址memSize
= 内存大小(字节)
VOID UnmapPhyMem(PVOID pVirAddr, DWORD memSize)
取消映射已映射的用户虚拟地址
pVirAddr
= 已映射的用户虚拟地址(MapPhyMem
的返回值)memSize
= 内存大小(字节)busNum
:总线号 (0-255)devNum
:设备号 (0-31)funcNum
:函数号 (0-7)regOff
:寄存器偏移量 (0-255)bytes
:要读取的字节数pValue
:用于接收返回值的缓冲区(必须由函数调用者分配)busNum
:总线号 (0-255)devNum
:设备号 (0-31)funcNum
:函数号 (0-7)regOff
:寄存器偏移量 (0-255)bytes
:要读取的字节数pValue
:要写入的新值
BYTE ReadPortByte(WORD portAddr)
WORD ReadPortWord(WORD portAddr)
DWORD ReadPortLong(WORD portAddr)
从端口地址 portAddr
读取一个字节、两个字节和四个字节。
VOID WritePortByte(WORD portAddr, BYTE portValue)
VOID WritePortWord(WORD portAddr, WORD portValue)
VOID WritePortLong(WORD portAddr, DWORD portValue)
向端口地址 portAddr
写入一个字节、两个字节和四个字节。
BOOL ReadPCI(DWORD busNum, DWORD devNum, DWORD funcNum,
DWORD regOff, DWORD bytes, PVOID pValue)
读取 PCI 配置空间
BOOL WritePCI(DWORD busNum, DWORD devNum, DWORD funcNum,
DWORD regOff, DWORD bytes, PVOID pValue)
写入 PCI 配置空间
如何安装 PCI 过滤器驱动程序
如果要访问 PCI 配置空间,必须安装 PCI 过滤器驱动程序“PCIFlt.sys”。在“设备管理器”中,找到“PCI 总线”并选择“更新驱动程序”,选择 PCIFilter.inf。不要自动搜索 INF 文件,选择手动选择驱动程序。
PCI 过滤器驱动程序可能会导致您的计算机完全崩溃,请自行承担风险使用。