拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

WinIo驱动级鼠标键盘模拟:端口操作与C#实现

WinIo驱动级鼠标键盘模拟:端口操作与C#实现

简介:驱动级鼠标键盘模拟技术用于在操作系统底层直接控制输入设备,区别于普通 API 模拟,它能绕过常规消息队列直接操作硬件,在自动化测试、游戏外挂、远程控制及底层硬件开发中很有价值。资源围绕 WinIo 驱动工具展开,包含最新版 WinIo 库(WinIo32.sys/WinIo64.sys 与 DLL)、C# 调用示例源码,以及作者提供的“键盘鼠标映射(驱动级)”小工程,适合具备 C# 基础、想深入驱动编程与输入模拟的开发者学习。

压缩包约 260KB,共 85 个文件。除了 22 个 C# 源文件(cs/resx)和 C++ 驱动侧源码(h/cpp/c/sys/dll)外,还包含 Visual Studio 解决方案与工程文件(sln/vcproj/csproj)、编译输出的可执行文件及辅助文件(exe/pdb/config/manifest/settings)、WinIo 帮助文档(chm)和“打开前必读”说明。目录结构清晰,可加载工程直接编译调试,方便对照源码理解 P/Invoke 调用、端口 I/O 与内核驱动的协作机制。

已有 4817 人浏览学习,说明该主题关注度较高。通过学习包内示例,还能掌握底层输入命令构造、驱动安装注意事项以及常见踩坑点,是一份驱动级模拟实战好资料。

1. 驱动级鼠标键盘模拟,先说清 WinIo 到底解决了什么问题

如果你在 Windows 下做过自动化,一定碰过这种怪事:SendInput、keybd_event 写了半天,代码没报错,目标程序的输入框就是没反应。换到老式工控软件、带输入过滤的业务系统、某些对按键时序敏感的场景里,普通 API 模拟出来的键盘鼠标事件经常被当成“合成输入”直接丢掉。这种时候,驱动级模拟就是一条绕开输入栈的路。WinIo 的本质是加载一个内核驱动,让应用层能直接读写 I/O 端口,键盘鼠标控制器那两根端口线 0x60、0x64 就暴露在你面前。这份资源里除了新版 WinIo.dll 和源码,还带一个 C# 小例子,把驱动初始化、鼠标移动、键盘按键串成了一条能跑的流程,适合做自动化测试、设备控制、远程协助这类需要底层输入的活儿。下面按我实际拆包验证的顺序来写。

2. 初始化 WinIo:驱动加载机制、P/Invoke 封装与 64 位系统的坑

2.1 WinIo 不是“模拟器”,是给了你一把物理级的钥匙

想明白 WinIo 驱动级模拟,先得知道普通模拟和它的差别。SendInput、keybd_event、mouse_event 走的是 Windows 输入子系统,消息经过系统输入队列、会话隔离、UIPI 权限检查之后才进目标窗口。这套链路在绝大多数场景下没问题,但只要目标软件自己挂了一层输入钩子,或者在低层轮询硬件状态,合成事件就可能被识别出来然后丢掉。

WinIo 走的是另一条路:它把 WinIo.sys 加载进内核,通过 WinIo.dll 暴露一组 API,让用户态程序直接调用InitializeWinIo、GetPortVal、SetPortVal。这三个函数配合起来,你就能读 8042 键盘鼠标控制器的状态寄存器 0x64 和数据寄存器 0x60。这个级别已经是硬件端口了,Windows 的输入栈根本不会过滤它,因为数据不是“模拟事件”,而是从端口层面直接喂进去的。

这里要提醒一句:WinIo 提供的是端口访问能力,不是“鼠标键盘模拟库”。真正模拟鼠标键盘的动作,是用 SetPortVal 往特定端口写特定数据。所以这套东西能不能用、能不能生效,取决于目标机器的鼠标键盘是不是传统 PS/2 控制器接管。后面我会专门讲这个边界。

2.2 C# 初始化代码:管理员权限、驱动加载与退出清理

资料包里那个 C# 例子,核心其实就三步:初始化、发数据、退出。我先贴一份我常用的封装,把 WinIo 的几个关键 API 通过 P/Invoke 引进来。

using System; using System.Runtime.InteropServices; using System.Security.Principal; namespace PortDemo { public sealed class WinIoPort : IDisposable { [DllImport("winio.dll", EntryPoint = "InitializeWinIo")] private static extern bool InitializeWinIo(); [DllImport("winio.dll", EntryPoint = "ShutdownWinIo")] private static extern void ShutdownWinIo(); [DllImport("winio.dll", EntryPoint = "GetPortVal")] private static extern bool GetPortVal(ushort wPortAddr, out uint dwPortVal, byte bSize); [DllImport("winio.dll", EntryPoint = "SetPortVal")] private static extern bool SetPortVal(ushort wPortAddr, uint dwPortVal, byte bSize); private bool _opened; public bool Open() { if (!IsAdministrator()) { Console.WriteLine("必须以管理员身份运行,驱动加载需要权限"); return false; } _opened = InitializeWinIo(); if (!_opened) { Console.WriteLine("InitializeWinIo 失败,请检查 winio.dll 与 winio.sys 是否就位"); } return _opened; } private static bool IsAdministrator() { using (WindowsIdentity identity = WindowsIdentity.GetCurrent()) { WindowsPrincipal principal = new WindowsPrincipal(identity); return principal.IsInRole(WindowsBuiltInRole.Administrator); } } public void Dispose() { if (_opened) { ShutdownWinIo(); _opened = false; } } } }

这段代码有几个参数细节值得说。GetPortVal的第二个参数是out uint,因为要从端口读回一个值;SetPortVal的第二个参数是要写进去的值;第三个参数bSize表示读写宽度,1 代表字节,2 代表字,4 代表双字。操作 8042 端口时,0x64 和 0x60 都是单字节访问,所以统一传 1。

InitializeWinIo返回false时别急着怀疑环境,先确认三件事:程序是不是管理员权限、winio.dll 和 winio.sys 是否在程序目录或系统目录下、杀毒软件是否拦截了驱动加载。64 位系统上还要确认你用的是 64 位版本的 WinIo,如果拿 32 位 WinIo 硬跑,初始化大概率直接失败。

2.3 64 位系统与驱动签名:初始化之前就要先过的一关

WinIo 本身是个比较“老江湖”的库,最早的版本主要面向 32 位系统。后来社区和原始作者都做过 64 位适配,但 64 位 Windows 对内核驱动有强制签名要求,未签名的 winio.sys 默认加载不出来。

常见做法是先把系统切到测试签名模式,再用新版 WinIo64.sys。命令如下,需要管理员 CMD:

bcdedit /set testsigning on

然后重启机器,桌面右下角会出现“测试模式”水印,这时候再运行程序。我一般会在项目说明里强调:测试签名模式只在你自己的开发机或专用测试机上开,不要在重要工作机上长期挂着,调试完用以下命令关掉:

bcdedit /set testsigning off

这不是玄学,是驱动开发的常规状态管理。忘了关会导致系统安全基线下降,后续装其他软件也容易莫名其妙弹签名警告。资源包里如果已经带了签名过的新版驱动,那就不需要开测试模式,直接跑就好;如果没带,就用上面这套流程。

3. 鼠标模拟:0x60 与 0x64 端口的读写顺序是成败关键

3.1 PS/2 鼠标数据包结构

鼠标端口 0x60 是数据口,0x64 是状态与命令口。要往鼠标发命令,第一步永远是往 0x64 写 0xD4,意思是“后面这条数据发给鼠标”,第二步才往 0x60 写真正的数据。写反了或者漏了 0xD4,数据会被键盘控制器当成键盘扫描码处理,表现出来就是鼠标没动,反而多了一串乱按键。

Windows 能接收的 PS/2 鼠标数据是三字节包:第一字节是标志位,第二字节是 X 方向位移,第三字节是 Y 方向位移。第一字节里 bit3 固定为 1,bit2 是中键,bit1 是右键,bit0 是左键,bit4 是 X 方向符号位,bit5 是 Y 方向符号位。位移是带符号的 8 位整数,正数向右或向下,负数向左或向上。

所以想模拟“按住左键向右移动 3 个像素”,三字节包就是 0x09、0x03、0x00。0x09 是 bit3 和 bit0 同时置位,表示左键按下;位移只有 8 位,超过 127 就得拆成多个包来发。

3.2 C# 封装:移动鼠标、按住与松开

我把资料包里的例子重构成下面这组方法,便于按自己的逻辑调:

public class MouseSimulator : WinIoPort { private bool SendMouseCommand(byte cmd) { if (!WaitInputBufferEmpty()) return false; // 0xD4 表示后续数据属于鼠标设备 if (!SetPortVal(0x64, 0xD4, 1)) return false; if (!WaitInputBufferEmpty()) return false; return SetPortVal(0x60, cmd, 1); } private bool WaitInputBufferEmpty() { uint status; DateTime deadline = DateTime.Now.AddMilliseconds(200); while (DateTime.Now < deadline) { if (GetPortVal(0x64, out status, 1)) { // bit1 为 1 表示输入缓冲区满,需要等待 if ((status & 0x02) == 0) return true; } Thread.Sleep(1); } return false; } public bool MoveRelative(sbyte dx, sbyte dy, bool leftButton) { byte flags = 0x08; // bit3 固定 if (leftButton) flags |= 0x01; byte[] packet = new byte[] { flags, (byte)dx, (byte)dy }; foreach (byte b in packet) { if (!SendMouseCommand(b)) return false; Thread.Sleep(2); } return true; } }

这段代码真正干活的是SendMouseCommand。我特意把“等待输入缓冲区空”单独抽出来,是因为很多人直接连着写 0x64 和 0x60,端口数据还没被控制器拿走,下一个数据就写进去了,轻则丢包,重则整个控制器状态错乱。每字节之间加 1 到 2 毫秒延时,是比较稳的做法,尤其是多包数据时不能省。

参数方面,MoveRelative的 dx dy 是 8 位带符号数,想移动更远就循环调用。左键状态放在 flags 里,如果要模拟右键,把 flags 改成 0x0A;中键是 0x0C。资料包里那个例子没把按键和移动分开,我拆开之后发现复用性高很多。

3.3 USB 鼠标与笔记本触摸板的边界

写到这里必须把丑话说在前面:这套端口模拟对传统 PS/2 鼠标有效,对大多数 USB 鼠标和笔记本触摸板无效。原因很简单,USB 鼠标走的是 HID 协议,数据从 USB 控制器进去,根本不经过 8042 那两个端口。你往 0x60 写再多包,USB 鼠标也不会动。

我拆这份资源时在台式机上试过,PS/2 接口的老鼠标完全按预期移动;换成 USB 鼠标之后,SetPortVal 调用仍然返回 true,但屏幕上的光标纹丝不动。这不是 WinIo 坏了,而是物理链路不对。所以用之前先确认设备类型,别拿 USB 鼠标踩坑后反过来说资源有问题。

如果你要模拟的目标机器是笔记本或纯 USB 键鼠环境,WinIo 端口方案基本可以放弃,更合适的是用驱动过滤方案或者直接走 SendInput。WinIo 的价值更多体现在老工控机、PS/2 键鼠环境、以及那些专门盯端口状态的软件上。

4. 键盘模拟:扫描码按下释放、组合键与例子里的循环演示

4.1 键盘扫描码:按下与释放差 0x80

键盘控制器那边,模拟按键比鼠标简单一点,但有个关键概念必须知道:扫描码的“按下”和“释放”是两个不同值。以左 Ctrl 为例,按下是 0x1D,释放是 0x9D;字母 A 按下是 0x1E,释放是 0x9E。规律很直白:释放码等于按下码加 0x80。

很多第一次接触驱动级键盘模拟的人只发按下码,不发释放码,结果就是目标软件里那个键一直处于按住状态。在文本输入框里会变成连续重复,在游戏里则表现为角色一直往前跑。所以我封装时坚持按下和释放成对出现:

public class KeyboardSimulator : WinIoPort { private bool WriteScanCode(byte scanCode) { if (!WaitInputBufferEmpty()) return false; return SetPortVal(0x60, scanCode, 1); } public void KeyDown(byte downCode) { WriteScanCode(downCode); Thread.Sleep(10); } public void KeyUp(byte downCode) { WriteScanCode((byte)(downCode + 0x80)); Thread.Sleep(10); } public void Tap(byte downCode) { KeyDown(downCode); Thread.Sleep(30); KeyUp(downCode); } public void CtrlA() { KeyDown(0x1D); // 左 Ctrl 按下 Tap(0x1E); // A 按下再释放 KeyUp(0x1D); // 左 Ctrl 释放 } }

注意KeyUp不是直接传“释放码”,而是传“按下码”让它自己加 0x80。这样接口更统一,调用方不用记两套值。Tap中间的 30 毫秒延时很关键,Windows 对键盘事件有时序要求,按下后立刻释放可能被键盘控制器合并成无效数据。

扩展键要特别处理,比如右 Ctrl 的按下码是 0xE0 0x1D,右 Alt 是 0xE0 0x38。这类键需要先写 0xE0 前缀,再写实际扫描码,释放时同样要写 0xE0 前缀。上面代码里的WriteScanCode只写单字节,所以碰到扩展键得自己拆两步写。资料包里那个例子没区分普通键和扩展键,实际用下来右 Ctrl 会失灵,这是我觉得需要补强的点。

4.2 用端口写扫描码和 keybd_event 的差别

有人会问:既然 keybd_event 也能模拟 Ctrl+A,为什么要费劲往端口写扫描码?差别在于事件层级。keybd_event 把虚拟键码交给系统输入队列,系统再翻译成扫描码;WinIo 是在端口层直接把扫描码喂给键盘控制器。前者会被钩子、输入法、UIPI 拦截,后者只要端口数据被控制器接受,就直接进入硬件层逻辑。

这也是为什么很多输入法环境下,keybd_event 模拟的快捷键不生效,而 WinIo 模拟的却生效。输入法在处理虚拟键事件时可能吞掉组合键,但控制器层面的扫描码已经绕过了输入法。不过要反向强调一句:如果目标软件不是读硬件而是读 Windows 消息,那 WinIo 模拟的按键也可能因为“没有对应消息”而显得无效。驱动级不等于万能,它只是换了一条更底层的链路。

4.3 楼主的例子:初始化、移动鼠标、按键循环

资料包里那个楼主小例子,逻辑几乎可以浓缩成下面的伪代码结构:

static void Main() { using (var port = new WinIoPort()) { if (!port.Open()) { Console.WriteLine("WinIo 初始化失败"); return; } var mouse = new MouseSimulator(); var keyboard = new KeyboardSimulator(); for (int i = 0; i < 5; i++) { mouse.MoveRelative(10, 5, true); Thread.Sleep(50); keyboard.Tap(0x1E); // A Thread.Sleep(100); } } }

这个循环演示了两件事:一是鼠标移动和按键可以交替执行,二是 driving 级操作不需要焦点窗口。窗口在后台、甚至最小化时,控制器层面不会在乎焦点在哪。不过也正因如此,这套东西只建议在你的测试机或自己可控的机器上跑,别拿去做干扰他人的操作。我一般会把循环次数和延时参数留成外部配置,方便调试不同机器对端口响应速度的差异。

5. 避坑指南:WinIo 初始化失败、鼠标乱飘、按键卡死的五个现场

5.1 现象:InitializeWinIo 返回 false,程序像没加载一样直接退出

原因:最常见是 winio.sys 没被正确加载,或者 winio.dll 找的不是同一个目录版本。64 位系统上更容易犯的错是拿 32 位 WinIo 硬跑,系统拒绝加载 32 位驱动;另外杀毒软件对驱动加载的拦截也会导致返回 false。

解决:先确认进程是管理员权限;然后把 winio.dll、winio.sys 和 exe 放到同一目录,不要只放 dll。64 位系统检查是否用了 WinIo64.sys,必要时先bcdedit /set testsigning on重启。杀毒软件如果弹窗,把工程目录加入信任区域,但前提是这台机器本来就是你干活的开发机。

5.2 现象:SetPortVal 调用全部返回 true,但鼠标完全不动

原因:机器用的是 USB 鼠标或笔记本触摸板,数据链路不经过 8042 控制器。WinIo 端口写操作本身成功,但没有硬件消费这些数据,所以光标不动。

解决:换 PS/2 接口鼠标,或者在 BIOS 里看是否有 legacy USB 开启的选项。部分老主板上 USB 键鼠模拟成 PS/2 设备,WinIo 就能生效;现代机器上就不要抱太大期望。判断标准很简单:设备管理器里看鼠标属性,如果是“PS/2 兼容鼠标”,就可以;如果是“HID 兼容鼠标”,WinIo 端口方案天然不匹配。

5.3 现象:鼠标动是动了,但方向是反的或斜着飘

原因:三字节包里标志位写错,导致系统把负位移当成正位移,或者溢出位被误置。常见错误是把 0x08 写成 0x00,PS/2 协议要求第三位必须为 1,否则包同步会被破坏,控制器会一直等待下一个字节,表现就是乱飘。

解决:检查发给鼠标的每个包第一字节,0x08 是底线。移动方向用带符号数表达,向左 2 像素的 dx 应该是 254 而不是 2。如果发现数据“偶尔对、偶尔错”,重点查发送前是否清空了输入缓冲区,见 3.2 节里的WaitInputBufferEmpty。

5.4 现象:按键按下后像卡住,目标软件里成了长按连发

原因:只发了按下扫描码,没发释放码。很多初版代码会把Tap简写成只调KeyDown,或者释放码写错,导致控制器认为按键从未抬起。

解决:按下和释放必须成对。另外释放码计算要小心,碰到扩展键比如右 Ctrl,释放时要重新写 0xE0 前缀再写释放码,不能只发 0xE0 0x9D 之外的简化值。我习惯在KeyUp方法内打一条调试日志,打印完整扫描码序列,避免肉眼排查。

5.5 现象:同一份代码,在 A 机器正常,在 B 机器就丢数据

原因:不同主板芯片组对 8042 控制器的响应速度不一致,鼠标包三字节如果发得太快,控制器来不及逐字节处理,中间就会丢一个字节,整个包就错位了。

解决:不要用固定延时,改成每字节之间轮询状态端口,等输入缓冲区空再发下一个。我在SendMouseCommand里已经做了这个逻辑,严谨性远高于固定 sleep。如果换了机器依然丢包,就把轮询超时从 200 毫秒加到 500 毫秒,同时每字节后的Thread.Sleep(2)保留,基本能覆盖大多数差异。

6. 进阶验证:用状态轮询与日志判断端口模拟到底生效在哪一层

聊完避坑,最后给一个我每次拿到这类资源都会做的验证方法:写一个“单步执行 + 日志回显”的小工具,把 WinIo 的每步操作打印出来,手动确认数据链路。这比直接跑完整自动化更管用,因为你一眼能看出是端口写入失败,还是硬件根本不吃这套数据。

private static void Step(string label, bool ok) { Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} [{label}] {(ok ? "OK" : "FAIL")}"); } private static void VerifyMouseLoop(MouseSimulator mouse) { Step("发送 0xD4 到 0x64", mouse.SendMouseCommand(0xD4)); Step("发送 0xF4 启用鼠标", mouse.SendMouseCommand(0xF4)); Step("移动 5,5 左键按下", mouse.MoveRelative(5, 5, true)); Thread.Sleep(300); Step("移动 -5,-5 松开左键", mouse.MoveRelative(-5, -5, false)); }

验证时不追求速度和循环次数,只求把每一步都打出来。第一步看 0xD4 是否被控制器接受,第二步看鼠标数据包是否有回执,第三步看实际位移是否落到屏幕上。如果第一步失败,问题在驱动初始化或端口权限;如果第三步失败但前两步成功,问题在硬件链路,比如 USB 鼠标环境。

我通常还会加一个“人工观察窗口期”:调用MoveRelative后 sleep 300 毫秒,让肉眼能确认光标从哪到哪。数据包发送太快,人眼根本来不及判断,日志里全是 OK 也没用。这一点是纯经验,不写日志直接跑全流程,翻车了都不知道从哪开始查。

从那以后,我每次在 C# 里加载 WinIo 驱动,都会强制走一遍这个验证流程:先测端口状态轮询,再测单字节写入,最后测完整数据包,确认硬件链路没问题才写业务逻辑。这套习惯帮我避开了很多“代码看着没问题但在别人机器上就是不动”的尴尬。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表