C# .NET 4.7.2键盘钩子引发AccessViolationException问题排查求助
问题描述
项目基于C# .NET 4.7.2开发,程序每1-2天会被强制关闭,Windows事件日志显示存在AccessViolationException。已知该异常通常与原生代码相关,程序中包含一段原生实现的低级键盘钩子,怀疑是这部分导致问题,代码如下:
{ private const int WH_KEYBOARD_LL = 13; private const int WM_KEYDOWN = 0x0100; private IntPtr hookId = IntPtr.Zero; private LowLevelKeyboardProc proc; public event Action<Key> KeyPressed; public void Start() { proc = HookCallback; hookId = SetHook(proc); } public void Stop() { UnhookWindowsHookEx(hookId); } private IntPtr SetHook(LowLevelKeyboardProc proc) { using (Process curProcess = Process.GetCurrentProcess()) using (ProcessModule curModule = curProcess.MainModule) { return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0); } } private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN) { int vkCode = Marshal.ReadInt32(lParam); Key pressedKey = KeyInterop.KeyFromVirtualKey(vkCode); KeyPressed?.Invoke(pressedKey); } return CallNextHookEx(hookId, nCode, wParam, lParam); } #region Native Methods private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool UnhookWindowsHookEx(IntPtr hhk); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr GetModuleHandle(string lpModuleName); #endregion }
问题排查与解决思路
1. 委托被GC回收的核心风险
低级键盘钩子的回调委托proc是类的私有字段,但.NET GC只会跟踪托管引用——Windows原生钩子仅持有委托的非托管指针,不会阻止GC回收该委托。一旦委托被回收,钩子触发时会访问无效内存,直接引发AccessViolationException。
2. GetModuleHandle的误用
在SetHook方法中,使用GetModuleHandle(curModule.ModuleName)获取模块句柄,但对于.NET程序,主模块是CLR加载的托管程序集,该调用可能返回无效句柄(64位进程下概率更高)。而WH_KEYBOARD_LL是全局钩子的特殊类型,回调在当前进程的消息循环中执行,不需要注入其他进程,因此直接传IntPtr.Zero即可。
3. 钩子生命周期管理不严谨
Stop方法未检查hookId是否有效,可能对已卸载的钩子重复调用UnhookWindowsHookEx- 回调中调用
CallNextHookEx时,若hookId已失效,会导致非法内存访问
修复步骤
解决委托回收问题
确保委托始终被托管代码持有强引用:
- 将
proc声明为静态字段,或者保证类实例在钩子活跃期间不会被GC回收(比如将类实例作为单例持有)。
修正模块句柄获取
修改SetHook方法,移除无效的模块句柄调用:
private IntPtr SetHook(LowLevelKeyboardProc proc) { return SetWindowsHookEx(WH_KEYBOARD_LL, proc, IntPtr.Zero, 0); }
完善钩子生命周期管理
public void Stop() { if (hookId != IntPtr.Zero) { UnhookWindowsHookEx(hookId); hookId = IntPtr.Zero; } }
- 若类实现
IDisposable接口,在Dispose方法中调用Stop,确保程序退出时钩子被正确卸载。
捕获
AccessViolationException的方案 .NET 4.0及以上版本中,AccessViolationException默认无法被普通try-catch捕获,需通过以下方式处理:
方式1:特性标记目标方法
在包含钩子逻辑的方法上添加特性,允许捕获这类异常:
[HandleProcessCorruptedStateExceptions] [SecurityCritical] public void StartHook() { try { Start(); } catch (AccessViolationException) { // 异常恢复逻辑:卸载钩子后重启 Stop(); Start(); } }
方式2:全局配置app.config
添加配置开启旧版异常处理策略,全局允许捕获该类异常:
<configuration> <runtime> <legacyCorruptedStateExceptionsPolicy enabled="true" /> </runtime> </configuration>
注意:该配置会全局生效,建议仅在必要时使用,捕获后需做好异常恢复逻辑,避免程序处于不稳定状态。
内容的提问来源于stack exchange,提问作者junsuchoi
相关产品推荐
相关产品推荐

