C# 过早访问进程MainModule触发Win32Exception的最优解决方案问询
问题根因
你遇到的异常本质是.NET的Process.MainModule属性在访问时会直接尝试读取进程的模块信息,如果进程还未完成PE头加载、主模块未完成映射,该属性的内部实现就会直接抛出Win32Exception,你写的if (game.MainModule != null)判断本身就触发了属性的访问逻辑,所以即使是判空操作也会抛异常,这就是测试人员遇到报错的原因。
最优实现方案(无异常、无需反复挂起恢复进程)
核心思路是启动进程时直接将其创建为挂起状态,避免游戏逻辑提前执行,再通过Win32 API循环查询模块加载状态,全程通过返回值判断是否就绪,不会触发异常。
第一步:Win32 API 声明
using System; using System.Diagnostics; using System.Runtime.InteropServices; public static class Win32Api { [StructLayout(LayoutKind.Sequential)] private struct STARTUPINFO { public int cb; public string lpReserved; public string lpDesktop; public string lpTitle; public int dwX; public int dwY; public int dwXSize; public int dwYSize; public int dwXCountChars; public int dwYCountChars; public int dwFillAttribute; public int dwFlags; public short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [StructLayout(LayoutKind.Sequential)] private struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public int dwProcessId; public int dwThreadId; } [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] private static extern bool CreateProcess( string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [DllImport("psapi.dll", SetLastError = true)] private static extern bool EnumProcessModules( IntPtr hProcess, [Out] IntPtr[] lphModule, int cb, out int lpcbNeeded); [DllImport("kernel32.dll", SetLastError = true)] private static extern uint ResumeThread(IntPtr hThread); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle(IntPtr hObject); private const uint CREATE_SUSPENDED = 0x00000004; }
第二步:核心实现逻辑
public static (int processId, IntPtr baseAddress, IntPtr hProcess, IntPtr hMainThread) LaunchGameAndGetBaseAddress(string gamePath, string launchArgs) { var si = new STARTUPINFO(); si.cb = Marshal.SizeOf(si); var pi = new PROCESS_INFORMATION(); // 直接创建挂起状态的进程,完全避免游戏逻辑提前执行 bool createSuccess = CreateProcess( lpApplicationName: gamePath, lpCommandLine: $"{gamePath} {launchArgs}", lpProcessAttributes: IntPtr.Zero, lpThreadAttributes: IntPtr.Zero, bInheritHandles: false, dwCreationFlags: CREATE_SUSPENDED, lpEnvironment: IntPtr.Zero, lpCurrentDirectory: System.IO.Path.GetDirectoryName(gamePath), lpStartupInfo: ref si, lpProcessInformation: out pi); if (!createSuccess) { throw new InvalidOperationException($"进程启动失败,错误码:{Marshal.GetLastWin32Error()}"); } IntPtr[] moduleHandles = new IntPtr[1]; int bytesNeeded = 0; int retryCount = 0; const int maxRetry = 500; // 最多等待500ms,可根据需求调整 // 循环查询模块加载状态,无异常,通过返回值判断 while (retryCount < maxRetry) { if (EnumProcessModules(pi.hProcess, moduleHandles, Marshal.SizeOf<IntPtr>(), out bytesNeeded) && bytesNeeded >= Marshal.SizeOf<IntPtr>()) { // 第一个模块就是主模块,直接拿到BaseAddress return (pi.dwProcessId, moduleHandles[0], pi.hProcess, pi.hThread); } // 加载未完成,等待1ms再试 System.Threading.Thread.Sleep(1); retryCount++; } // 超时处理逻辑 CloseHandle(pi.hThread); CloseHandle(pi.hProcess); throw new TimeoutException("等待进程主模块加载超时"); }
后续使用说明
你拿到返回的主线程句柄hMainThread后,可以先打完补丁再调用ResumeThread(hMainThread)让游戏继续运行即可,全程不需要反复挂起恢复进程,也不会触发非预期异常。
轻量优化方案(不想引入原生API)
如果你不想引入太多Win32 API,可以优化原有逻辑,避免递归,限制重试次数,把异常控制在有限范围内,比原有递归方案更稳定:
Process game = Process.Start(Path.Combine(installPath, "game.exe"), launchCommands); IntPtr baseAddress = IntPtr.Zero; int retryCount = 0; const int maxRetry = 500; while (retryCount < maxRetry) { try { if (game.MainModule != null) { game.Suspend(); baseAddress = game.MainModule.BaseAddress; break; } } catch (Win32Exception) { // 忽略加载未完成的异常,继续重试 } System.Threading.Thread.Sleep(1); retryCount++; } if (baseAddress == IntPtr.Zero) { throw new TimeoutException("等待主模块加载超时"); } int processId = game.Id;
注意事项
- 请确保你的工具架构和游戏架构匹配(32位工具只能访问32位进程,64位工具可以访问32/64位进程),建议将工具编译为
Any CPU并勾选「优先32位」,或者直接对应游戏架构编译。 - 如果游戏有反作弊机制,该方案可能会被拦截,需要单独适配。
内容的提问来源于stack exchange,提问作者Krisan Thyme
相关产品推荐
相关产品推荐

