找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2728

积分

0

好友

379

主题
发表于 5 天前 | 查看: 20| 回复: 0

环境准备:WDK 包已不再支持 Visual Studio 2019,请务必更新至 Visual Studio 2022 版本。在安装过程中搜索最新并全选相应组件即可。
为便于验证环境是否成功搭建,可预先下载WinIo源码:WinIoNote

环境搭建:安装 Visual Studio 2022

开始Windows驱动程序开发的第一步是配置正确的开发环境。您需要安装Visual Studio 2022,并确保选择了正确的开发工作负载和组件。

Visual Studio 2022 安装界面,显示使用C++的桌面开发工作负荷及含Spectre缓解的库组件

如上图所示,安装 Visual Studio 2022 时,请选择 “使用 C++ 的桌面开发” 工作负荷。此外,在 “单独组件” 选项卡下,需要手动添加以下与驱动开发相关的库(建议使用搜索框查找“最新”或“latest spectre”):

  • MSVC v143 - VS 2022 C++ ARM64/ARM64EC Spectre 缓解库(最新版本)
  • MSVC v143 - VS 2022 C++ x64/x86 Spectre 缓解库(最新版本)
  • 带有 Spectre 缓解库的适用于最新 v143 生成工具的 C++ ATL (ARM64/ARM64EC)
  • 带有 Spectre 缓解库的适用于最新 v143 生成工具的 C++ ATL (x86 & x64)
  • 带有 Spectre 缓解库的适用于最新 v143 生成工具的 C++ MFC (ARM64/ARM64EC)
  • 带有 Spectre 缓解库的适用于最新 v143 生成工具的 C++ MFC (x86 & x64)
  • Windows 驱动程序工具包 (WDK)

步骤二:安装 Windows SDK

安装 Visual Studio 时,默认可能不会下载特定版本的 Windows SDK。为了确保与WDK的兼容性,建议检查系统已安装的SDK版本,并手动安装匹配的版本。

重要提示:检查系统本身已存在的SDK环境,版本过多或与WDK版本不匹配,可能导致后续WDK在Visual Studio中集成时报错“版本不匹配”。
下载链接:Microsoft官方WDK下载页面

Windows系统已安装的应用列表,显示已安装的Windows Driver Kit和Software Development Kit版本

安装 Windows SDK 10.0.26100.1 的步骤说明

安装Visual Studio时不会自动下载 Windows SDK 10.0.26100.1,请使用官方提供的链接单独下载。所提供的SDK和WDK链接具有匹配的版本号,这对于套件的协同工作至关重要。如果你决定为不同的Windows版本安装自己的SDK/WDK对,请务必确保版本号一致。

步骤三:安装 Windows Driver Kit (WDK)

WDK 与 Visual Studio 和 Windows 调试工具 (WinDbg) 集成在一起,提供了开发、生成、测试和调试驱动程序所需的全套工具。

WDK 10.0.26100.1 安装程序界面,提示安装Visual Studio扩展

默认的WDK安装包中已包含WDK Visual Studio扩展。如果在安装后,于Visual Studio中找不到驱动程序项目模板,则表示扩展未正确安装。此时,可以手动运行位于 C:\Program Files (x86)\Windows Kits\10\Vsix\VS2022\10.0.26100.1\WDK.vsix 的扩展文件。

如果上述方式失败,也可以直接在Visual Studio 2022的扩展管理器中搜索并安装“Windows Driver Kit”扩展。

Visual Studio 2022 扩展管理器界面,显示Windows Driver Kit扩展

版本兼容性至关重要:WDK的版本必须与你自身的操作系统版本对应,以避免安装后出现版本不兼容问题。例如,Windows 11 23H2 系统应安装对应版本的WDK。

WDK下载页面,列出了不同Windows版本对应的WDK下载链接

安装完成后,请在系统“设置”->“应用”中确认已安装的Windows Driver Kit和Windows Software Development Kit版本号是否一致。

系统应用列表,确认WDK和SDK版本号一致

Visual Studio 2022 激活码参考

  • 专业版(Pro): TD244-P4NB7-YQ6XK-Y8MMM-YWV2J
  • 企业版(Enterprise): VHF9H-NXBBB-638P6-6JHCY-88JWH

解决常见初始化问题

未找到 ntddk.h 头文件

在创建或编译驱动项目时,你可能会遇到“无法打开包括文件: ‘ntddk.h’”的错误。

错误提示:无法找到ntddk.h头文件

解决方案:将WDK的头文件目录添加到项目的附加包含目录中。

  1. 打开项目“属性页”。
  2. 导航到 “配置属性” -> “C/C++” -> “常规” -> “附加包含目录”
  3. 添加WDK的include路径,例如:C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\km
    (请将10.0.26100.0替换为你的WDK安装中的实际目录名)。

Visual Studio项目属性页,在C++附加包含目录中添加WDK路径

实战:创建并运行 “Hello World” 驱动程序

让我们通过创建一个最简单的内核模式驱动来验证环境是否配置成功。

创建 KMDF 驱动项目

  1. 打开 Visual Studio 2022。
  2. 点击 “文件” -> “新建” -> “项目”
  3. 在搜索框中输入“KMDF”,选择 “Kernel Mode Driver, Empty (KMDF)” 模板。

Visual Studio创建新项目,搜索并选择KMDF空项目模板

创建完成后,解决方案资源管理器中将生成一个基础的驱动项目结构。

解决方案资源管理器,显示新创建的HelloDriver项目结构

编写驱动入口代码

Source Files 文件夹中,创建或打开 Driver.c 文件,并输入以下代码:

#include <ntddk.h> // 包含所有驱动程序的核心Windows内核定义
#include <wdf.h>   // 基于Windows驱动程序框架WDF的定义

DRIVER_INITIALIZE DriverEntry;
EVT_WDF_DRIVER_DEVICE_ADD KmdfHelloWorldEvtDeviceAdd;

// 驱动程序的入口函数
NTSTATUS
DriverEntry(
    _In_ PDRIVER_OBJECT     DriverObject,
    _In_ PUNICODE_STRING    RegistryPath
)
{
    // NTSTATUS变量,用于记录操作状态
    NTSTATUS status = STATUS_SUCCESS;

    // 分配驱动程序配置对象
    WDF_DRIVER_CONFIG config;

    // 打印“Hello World”到调试器(DriverEntry)
    KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "KmdfHelloWorld: DriverEntry\n"));

    // 初始化驱动配置对象,注册EvtDeviceAdd回调函数
    WDF_DRIVER_CONFIG_INIT(&config,
        KmdfHelloWorldEvtDeviceAdd
    );

    // 最后,创建驱动程序对象
    status = WdfDriverCreate(DriverObject,
        RegistryPath,
        WDF_NO_OBJECT_ATTRIBUTES,
        &config,
        WDF_NO_HANDLE
    );
    return status;
}

// 当系统检测到设备到达时,会调用此函数来初始化设备结构与资源
NTSTATUS
KmdfHelloWorldEvtDeviceAdd(
    _In_    WDFDRIVER       Driver,
    _Inout_ PWDFDEVICE_INIT DeviceInit
)
{
    // 此示例中未使用Driver对象,标记为未引用以避免警告
    UNREFERENCED_PARAMETER(Driver);

    NTSTATUS status;

    // 分配设备对象
    WDFDEVICE hDevice;

    // 打印“Hello World”(EvtDeviceAdd)
    KdPrintEx((DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "KmdfHelloWorld: KmdfHelloWorldEvtDeviceAdd\n"));

    // 创建设备对象
    status = WdfDeviceCreate(&DeviceInit,
        WDF_NO_OBJECT_ATTRIBUTES,
        &hDevice
    );
    return status;
}

编译与生成

代码编写完成后,点击 “生成” -> “生成解决方案”。如果环境配置正确,编译将成功,并在输出目录生成关键的驱动文件。

Visual Studio输出窗口,显示驱动编译成功并生成sys文件

编译成功后,在项目的输出目录(例如 x64\Debug)下,你将看到生成的文件:

  • HelloDriver.sys - 内核模式驱动程序文件。
  • HelloDriver.inf - 安装驱动程序时 Windows 使用的信息文件。
  • HelloDriver.cat - 用于验证驱动程序测试签名的目录文件。

项目输出目录,显示生成的.sys, .inf, .cat等驱动文件

测试与调试驱动程序

使用 DriverMonitor 加载驱动

为了测试我们刚刚编译的驱动,可以使用 DriverMonitor 工具(WDK 自带工具之一)。

  1. 管理员身份运行 DriverMonitor.exe
  2. 初始运行时,可能会提示“Unable to start the support driver.”,这属于正常情况。
  3. 点击菜单 “File” -> “Open”,选择我们生成的 HelloDriver.sys 文件。

以管理员身份运行DriverMonitor

在DriverMonitor中选择要加载的HelloDriver.sys文件

解决驱动签名问题

在加载未签名的驱动程序时,Windows 会因强制驱动程序签名而拦截。你会看到类似“Windows 无法验证此文件的数字签名”的错误。

DriverMonitor显示错误:Windows无法验证文件数字签名

临时调试解决方案(仅用于开发测试)

  1. 以管理员身份打开命令提示符 (CMD)。
  2. 执行命令:bcdedit /set testsigning on
  3. 重启计算机。重启后,桌面右下角会显示“测试模式”的水印,此时即可加载测试签名的驱动。

开启测试模式并重启后,再次在 DriverMonitor 中启动驱动,如果成功,你将看到“Driver started successfully”的提示。

DriverMonitor成功启动驱动

查看驱动输出:驱动代码中的 KdPrintEx 输出的“Hello World”信息,需要使用内核调试器或像 DbgView 这样的工具才能捕获。这涉及到更复杂的内核调试环境配置,后续可以深入研究。

WinIo 工作原理与流程解析

驱动程序是允许操作系统与设备进行通信的软件组件。理解其基本架构是进行深入开发的前提。

应用程序、操作系统、驱动程序和设备之间的交互关系图

上图清晰地展示了从用户层应用程序到底层硬件设备的调用链。而 WinIo 库的具体实现,则涉及到用户态程序、DLL 动态库、系统服务以及内核驱动(SYS)之间的复杂协作。

WinIo用户进程、DLL链接库、服务与SYS驱动之间的调用关系流程图

这张流程图详细描述了 WinIo 的工作流程:

  1. 用户程序调用 InitializeWinIo 初始化。
  2. DLL 链接库负责打开或创建系统服务,并与驱动进行通信。
  3. WinIoNote 服务/SYS 驱动接收来自 DLL 的 IOCTL 控制码,执行实际的端口读写 (WRITE_PORT_UCHAR, READ_PORT_UCHAR) 或内存映射 (MapPhysicalMemoryToLinearAddressSpace) 操作。
  4. 操作完成后,资源被逐层释放。

核心驱动函数源码深度解析

深入理解驱动源码中的关键函数,是掌握 WinIo 乃至 Windows 驱动开发的基础。以下对几个核心函数进行解析,这涉及到驱动程序的内存管理和与系统的交互机制。

DriverEntry

驱动程序的主要初始化入口,由系统进程 System 调用。

NTSTATUS DriverEntry(
  _In_ PDRIVER_OBJECT  DriverObject,
  _In_ PUNICODE_STRING RegistryPath
);
  • 作用:为驱动程序的标准例程提供入口点,进行全局初始化。

IoCreateDevice

用于创建设备对象,这是驱动程序与用户态应用程序通信的桥梁。

NTSTATUS IoCreateDevice(
  [in]           PDRIVER_OBJECT  DriverObject,
  [in]           ULONG           DeviceExtensionSize,
  [in, optional] PUNICODE_STRING DeviceName,
  [in]           DEVICE_TYPE     DeviceType,
  [in]           ULONG           DeviceCharacteristics,
  [in]           BOOLEAN         Exclusive,
  [out]          PDEVICE_OBJECT  *DeviceObject
);
  • WinIo 中的应用:创建名为 \Device\WinIo 的设备对象,类型为 FILE_DEVICE_WINIO

IoCreateSymbolicLink

在设备对象名称和用户可见的名称(如 \\.\WinIo)之间创建符号链接,允许 Win32 应用程序通过路径访问设备。

NTSTATUS IoCreateSymbolicLink(
  [in] PUNICODE_STRING SymbolicLinkName,
  [in] PUNICODE_STRING DeviceName
);

WRITE_PORT_UCHAR / READ_PORT_UCHAR

这两个函数是硬件访问的核心,直接对指定的 I/O 端口地址进行读写操作。

void WRITE_PORT_UCHAR(
  [in] PVOID Port,
  [in] ULONG Value
);

UCHAR READ_PORT_UCHAR(
  [in] PVOID Port
);
  • WinIo 中的应用:在驱动程序的派遣函数中,根据应用程序传来的控制码和参数,调用这些函数执行实际的硬件端口操作。例如,SetPortValGetPortVal 的最终实现就依赖于它们。

ZwMapViewOfSection / ZwUnmapViewOfSection

用于将物理内存映射到进程的虚拟地址空间,或取消映射。这是 WinIo 实现物理内存访问的关键。

NTSYSAPI NTSTATUS ZwMapViewOfSection(
  [in] HANDLE          SectionHandle,
  [in] HANDLE          ProcessHandle,
  [in, out]  PVOID     *BaseAddress,
  [in]   ULONG_PTR     ZeroBits,
  [in]   SIZE_T        CommitSize,
  [in, out, optional] PLARGE_INTEGER SectionOffset,
  [in, out]   PSIZE_T  ViewSize,
  [in]   SECTION_INHERIT InheritDisposition,
  [in]   ULONG         AllocationType,
  [in]    ULONG        Win32Protect
);

NTSYSAPI NTSTATUS ZwUnmapViewOfSection(
  [in] HANDLE ProcessHandle,
  [in, optional] PVOID BaseAddress
);
  • 工作原理:WinIo 驱动首先通过 HalTranslateBusAddress 等函数获取目标物理地址的系统地址,然后创建一个节对象并将其映射到调用进程(即使用 WinIo 的应用程序)的地址空间,最终将映射后的线性地址返回给用户态。

构建与集成:创建动态链接库 (DLL)

为了让上层应用程序方便地调用 WinIo 的功能,我们需要将其封装成动态链接库。这里以创建一个用于获取 SMBIOS 信息的 DLL 为例。

创建 DLL 项目

在 Visual Studio 中选择 “动态链接库 (DLL)” 项目模板。

创建新项目,选择动态链接库(DLL)模板

配置DLL项目名称和存储位置

规划文件结构

清晰的文件结构有助于项目管理。通常包含头文件(声明接口)和源文件(实现功能)。

解决方案资源管理器,展示SMBIOSDLL项目的头文件和源文件结构

提示:本例中的 smbios.hsmbios.cpp 使用了开源库 DumpSMBIOS 来获取SMBIOS数据。

编写 DLL 头文件

头文件用于声明导出函数和数据结构,是 DLL 对外的接口契约。

#ifndef SMBIOSAPI_H_
#define SMBIOSAPI_H_

#include <Windows.h>
#define _DllExport _declspec(dllexport) // 导出宏

// BIOS信息结构体
typedef struct {
    PWCHAR m_wszBIOSVendor;
    PWCHAR m_wszBIOSVersion;
    ...
} BIOSInfo;

// 系统信息结构体
typedef struct {
    PWCHAR m_wszSysManufactor;
    ...
} SystemInfo;

// 声明导出函数
extern "C"
{
    _DllExport void SMBIOS_INIT(); // 初始化SMBIOS数据
    _DllExport void SMBIOS_Free(); // 释放资源
    _DllExport DWORD SMBIOS_Get_BIOS_Version();
    _DllExport void SMBIOS_Get_BIOS_Info(BIOSInfo* info);
    _DllExport void SMBIOS_Get_SYSTEM_Info(SystemInfo* info);
    _DllExport void SMBIOS_Get_BOARD_Info(BoardInfo* info);
    _DllExport wchar_t* SMBIOS_Get_UUID_Info(const char* UUID_Type);
}

#endif // SMBIOSAPI_H_

实现 DLL 源文件

源文件包含函数的具体实现。注意全局变量的管理,避免内存泄漏。

#include "pch.h"
#include "smbiosapi.h"
#include "smbios.h"
#include <memory>

// 全局数据结构指针
BIOSInfo* g_bios_info = nullptr;
SystemInfo* g_system_info = nullptr;
BoardInfo* g_board_info = nullptr;

_DllExport void SMBIOS_INIT()
{
    if (g_bios_info == nullptr) { g_bios_info = new BIOSInfo; }
    // ... 类似地初始化其他结构体

    const SMBIOS& SmBios = SMBIOS::getInstance();
    // 从 SmBios 实例填充全局结构体数据
    g_bios_info->m_wszBIOSVendor = SmBios.BIOSVendor();
    // ... 填充其他字段
}

_DllExport void SMBIOS_Free()
{
    // 释放所有动态分配的内存
    if (g_bios_info != nullptr) { delete g_bios_info; g_bios_info = nullptr; }
    // ... 释放其他结构体
}

// ... 其他函数实现(SMBIOS_Get_BIOS_Version, SMBIOS_Get_BIOS_Info 等)

在应用程序中调用 DLL

DLL 编译生成后,应用程序需要通过静态链接或动态加载的方式使用它。

静态链接(以 Visual Studio 控制台程序为例)

  1. 配置库目录:在应用程序项目属性中,添加 DLL 对应的 .lib 文件所在目录。
    项目属性页,配置附加库目录

  2. 添加依赖项:在链接器输入中,添加 .lib 文件名。
    项目属性页,在附加依赖项中添加.lib文件

  3. 包含头文件并调用函数

    
    #include <iostream>
    #include "../SMBIOSDLL/smbiosapi.h"
    #pragma comment(lib, "SMBIOSDLL.lib") // 或在项目设置中链接

int main() {
SMBIOS_INIT();
DWORD ver = SMBIOS_Get_BIOS_Version();
std::cout << "BIOS版本:" << ver << std::endl;
// ... 调用其他函数
SMBIOS_Free();
return 0;
}


### 动态加载(以 Qt 项目为例)

动态加载无需在编译时链接 `.lib` 文件,更灵活。在 Qt 项目中,可以通过 `.pro` 文件配置,或直接使用 `QLibrary`。

**在 `.pro` 文件中配置外部库**:

win32: LIBS += -L

$$PWD/./ -lSMBIOSDLL INCLUDEPATH +=$$
PWD/
DEPENDPATH += $$PWD/


**在代码中调用**:
```cpp
#include "smbiosapi.h"
SMBIOS_INIT();
qDebug() << "BIOS版本信息:" << SMBIOS_Get_BIOS_Version();
// ... 其他调用
SMBIOS_Free();

深度定制:为 WinIo 添加自定义功能

原始的 WinIo 提供了基础的端口和内存访问功能。在实际硬件开发中,我们经常需要基于它进行封装,添加针对特定硬件(如 Super I/O、SMBus)的操作函数。

扩展 WinIo 头文件

首先,在 winio.h 中声明我们计划添加的自定义函数。

// 添加自定义函数,例如针对 ITE Super I/O 芯片
WINIO_API bool _stdcall IoWrite8(UINT8 Register, UINT8 Value);
WINIO_API UINT8 _stdcall IoRead8(UINT8 Register);

// Super I/O 使能函数
WINIO_API bool _stdcall OpenSioITEDecode();
WINIO_API bool _stdcall CloseSioITEDecode();

// 操作 Super I/O 寄存器单个 Bit 位
WINIO_API int _stdcall IoRead8_Single_Bit(WORD port, int bit);
WINIO_API bool _stdcall IoWrite8_Single_Bit(WORD addr, int bit, int bit_value);

// 访问 SMBus 函数
WINIO_API bool _stdcall SmbusWriteByte(WORD slav_address, WORD offset_address, WORD write_data);
WINIO_API bool _stdcall SmbusReadByte(WORD slav_address, WORD offset_address, WORD* read_data);

// 内存测试函数
WINIO_API bool _stdcall MemoryTest();
WINIO_API DWORD _stdcall GetMemoryTest();

实现自定义函数

在单独的 .cpp 文件中实现这些函数。实现时应直接调用 WinIo 的基础 API(如 SetPortVal, GetPortVal),避免过度封装导致调用链过长和潜在问题。

Super I/O 操作示例

#include <windows.h>
#include "winio.h"

UINT8 IT8728F_CONFIG_INDEX = 0x2e;
UINT8 IT8728F_CONFIG_DATA = 0x2f;

bool _stdcall IoWrite8(UINT8 Register, UINT8 Value) {
    return SetPortVal((WORD)Register, (WORD)Value, 1); // 直接调用基础API
}

bool _stdcall OpenSioITEDecode() {
    // 进入 ITE Super I/O 配置模式的标准序列
    IoWrite8(IT8728F_CONFIG_INDEX, 0x87);
    IoWrite8(IT8728F_CONFIG_INDEX, 0x01);
    IoWrite8(IT8728F_CONFIG_INDEX, 0x55);
    IoWrite8(IT8728F_CONFIG_INDEX, 0x55);
    return true;
}

int _stdcall IoRead8_Single_Bit(WORD port, int bit) {
    DWORD val = 0x0;
    if (GetPortVal(port, &val, 1)) {
        int bitValue = (val >> bit) & 1; // 提取特定位
        return bitValue;
    }
    return -1;
}

SMBus 访问示例

#include <windows.h>
#include "winio.h"
WORD Sumbus_base = 0xf000; // 假设的SMBus控制器基址

bool _stdcall SmbusWriteByte(WORD slav_address, WORD offset_address, WORD write_data) {
    int Timer_Num = 0;
    DWORD val = 0x0;
    unsigned long timeout = 0;

    // 1. 写入数据
    SetPortVal(Sumbus_base + 0x05, write_data, 1);
    // 2. 发送开始命令
    SetPortVal(Sumbus_base, 0x1f, 1);
    // 3. 轮询等待操作完成
    while (1) {
        GetPortVal(Sumbus_base, &val, 1);
        Sleep(10);
        if ((val & 0x1) == 0) break; // 检查忙位
        if (timeout++ > 100) { Timer_Num++; break; }
    }
    // ... 后续设置从机地址、偏移量并发送写命令
    // 返回操作结果
    return (Timer_Num == 0);
}

WinIo项目结构,突出显示自定义的SmBus.cpp和SuperIO.cpp文件

通过以上步骤,我们不仅成功搭建了Windows驱动开发环境,编译运行了第一个驱动,还深入分析了WinIo的工作原理和源码,并实践了如何通过DLL封装和功能扩展,将底层驱动能力安全、便捷地暴露给上层应用程序。这种从环境到原理,再到实践和扩展的学习路径,是掌握系统编程与硬件交互技术的有效方法。希望这份指南能帮助你在云栈社区的探索之路上更进一步。

关于驱动签名:用于正式发布的驱动程序,必须通过微软认证的第三方机构进行数字签名。签名后,还需要相应修改驱动程序的安装信息文件(.inf),替换其中的证书信息。具体操作可参考开源项目中的提交记录。


相关资源与参考资料




上一篇:Type-C接口硬件原理与通信协议详解:引脚功能、PD快充及RK平台设计示例
下一篇:资深开发者精选:C++/系统/算法等12本经典计算机书籍开发者书单
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 03:07 , Processed in 0.512893 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表