You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 04:07:10