抹桥的博客
语言
主题色
250

XCOM: Enemy Within 启动两分钟后卡死的排查经历

目录

本来只是想打开《XCOM: Enemy Within》玩一会儿,结果游戏进了主菜单,过了大概两分钟就卡死了。重新启动,有时还能在菜单里操作几下,然后又没反应。

Steam 启动这个游戏时还会弹出一个选择器,让我选择 Enemy Unknown 或 Enemy Within。Unknown 本身也有启动问题,于是先集中排查 Within,至少把想玩的这个版本跑起来。

最后确实解决了这次启动卡死,不过过程比想象中绕了一些。原本怀疑的是老游戏和新系统、4K 分辨率之间的兼容性,最后线索却落在了一个旧的 Firaxis 在线服务请求上。这里把过程和手动操作的方法记下来,免得以后换台电脑又从头折腾。

先试了几个常规办法

为了方便测试,我让 Codex 帮忙备份配置、存档和日志,再逐项修改条件,观察游戏能坚持多久。Windows 上调老游戏,首先想到的自然是分辨率、窗口模式和覆盖层之类的问题。

于是做了一轮尝试:

尝试结果
将 bShouldUseMcp 改为 false没有可靠地阻止相关请求,还是会卡
从 4K 无边框改为 1280×720 窗口模式初始资源占用变了,卡死仍会出现
调整 Steam、NVIDIA 覆盖层没有解决持续增长的内存,部分开关也没有真正阻止 DLL 注入
使用 DXVK 3.1.1无效
修改在线子系统、改英文、限制 CPU 核心数、关闭语音都没有解决这次问题
加上 -ONETHREAD反而更早失败
完全退出 Steam游戏大概 40 秒后退出,没法正常使用

期间还有一次看起来已经好了:绕过选择器,设置正确的 Steam 环境变量,直接启动 Within,主菜单可以操作了。于是以为只是启动方式的问题。

然而再多等一会儿,还是卡死了。

这也是这次排查最容易误判的地方。打开游戏和能一直玩是两件事,尤其故障本来就不是启动时立刻出现。后来就不再只看“进程还在不在”,而是持续记录窗口是否响应、内存有没有上涨,以及 CPU 时间的变化。

奇怪的网络请求

继续检查日志和网络连接,发现游戏会访问这个地址:

prod.xcom-ew.firaxis.com

在当时的环境里,它解析到 192.81.242.201,游戏连接的是 80 端口。卡死时还留下了两条 CLOSE_WAIT 连接。

CLOSE_WAIT 的意思是对端已经关闭连接,本地程序还没有完成关闭。单看这个状态也不能说明什么,正常程序也可能短暂出现它。但结合游戏一直没反应、内存还在涨,就有点不对劲了。

于是抓了一份卡死时的线程转储,并用 Windows 调试器查看。线程 19520 在大约 75 秒里消耗了约 51 秒 CPU,调用栈的关键部分如下:

ntdll!NtDeviceIoControlFile
mswsock!WSPRecv
ws2_32!WSARecv
wsock32!recv
XComEW+0x2604b5

它正在走网络接收路径,而且 recv 的读取长度参数是 1,也就是每次只读一个字节。

一次调用栈快照当然不能直接还原游戏源码。不过热线程持续消耗 CPU、窗口停止响应、内存不断增长,几条线索放在一起,已经很像是客户端处理这个旧服务请求时陷入了异常的重复读取。

后来做的域名替换对照,也支持了这个判断。

为什么最后会报内存不足

原版程序失败时出现了这样的信息:

Ran out of virtual memory

随后转储里记录到 0xc0000005 访问违规,故障偏移是 XComEW.exe+0x00e77ce1。

这里说的虚拟内存,要结合游戏是 32 位程序来看。进程能使用的虚拟地址空间有上限,持续分配下去,机器就算还有物理内存和磁盘空间,它也可能申请不到新的地址空间了。

所以这次观察到的过程是:先卡住,内存持续上涨,然后分配失败,再发生后续崩溃。把分辨率调低可以减少一部分初始占用,但没有阻止异常读取,自然也就没有解决问题。

最后只改了一个域名

有效的处理是在原版程序旁边生成一个副本,名字叫 XComEW-Offline.exe,只把里面的一个字符串替换掉:

prod.xcom-ew.firaxis.com
             ↓
off.xcom-ew.invalid.test

两个地址都是 23 个字符,UTF-16LE 编码后都是 46 字节。等长替换,不需要移动程序里面后面的数据。

新地址使用的是保留的 .test 域名。在这次网络环境里,它没有解析出可连接的服务,游戏也就没有再走原先那条请求路径。这里并没有证明 Firaxis 的服务器永久停服,能确定的是:当时的旧服务请求会触发客户端异常,绕过之后,启动卡死没有再复现。

原版 XComEW.exe 保留,Hosts、存档和分辨率也没有做永久修改。Offline 只是副本的名字,游戏仍然需要 Steam;这个处理会阻止通过被替换域名提供的在线功能,多人模式没有测试。

后面又补了一个桌面快捷方式,先确保 Steam 已经启动,再设置环境变量,直接运行这个副本。这样就不用每次经过 Unknown / Within 选择器了。

这次到底测到了哪一步

修复后做了两轮观察:

测试观察结果
修复版启动后运行约 12 分钟72 次采样均为响应状态;加载后私有内存约 966 MB,最后约 918 MB,没有继续线性上涨
从桌面快捷方式重新启动,观察 152 秒15 次采样均为响应状态;私有内存从约 802 MB 降到约 738 MB,没有发现到旧服务 IP 的连接

还有一个细节需要记下来:12 分钟测试摘要里显示有一份转储文件。查日志后发现,它在启动约 5 秒时由 0x406D1388 异常触发,这个代码通常用于传统的调试器线程命名。游戏随后一直运行到测试结束,所以不能把“有转储”直接等同于“又崩了”。

ProcDump 达到一份转储的上限后也退出了,后续证据来自进程和资源采样,不能说它一直监视了所有异常。

到这里,原先反复出现的启动卡死已经在观察窗口里消失了。不过这次验证主要是启动和主菜单,还没有完整跑过战役、各个关卡和在线功能。Enemy Unknown 的问题也没有在这次排查里解决。

手动怎么操作

前面是排查过程,下面把真正需要做的事情整理一下。这一节是留给以后自己查阅,也给遇到同样问题的人参考。

找到 Enemy Within 的程序

先退出游戏,在 Steam 库里右键游戏,选择“管理 → 浏览本地文件”,进入:

XEW\Binaries\Win32

里面应该有 XComEW.exe。注意找的是 Within 的程序,下面的操作不适用于 Unknown。

备份存档,让 Steam 保持运行。接着打开 PowerShell,将下面的 $gameDirectory 改成自己的实际安装路径。

生成一个修复副本

这次验证过的原版 SHA-256 是:

F8D90411AFA46EBB56E65BBFD71098FEE335549768DB7A3FFDAE5E1DE9CDCB7F

文件大小为 27,793,464 字节,替换位置是从 0 开始计数的字节偏移 23899942。下面的代码只适用于这个版本,哈希不一致就会停止。遇到其他版本,需要重新定位字符串,不要把校验删掉以后硬跑。

把整段代码粘贴到 PowerShell 里执行。安装目录没有写权限时,再使用有权限的 PowerShell。

$ErrorActionPreference = 'Stop'
$gameDirectory = 'D:\SteamLibrary\steamapps\common\XCom-Enemy-Unknown\XEW\Binaries\Win32'
$sourcePath = Join-Path $gameDirectory 'XComEW.exe'
$targetPath = Join-Path $gameDirectory 'XComEW-Offline.exe'
$expectedSourceHash = 'F8D90411AFA46EBB56E65BBFD71098FEE335549768DB7A3FFDAE5E1DE9CDCB7F'
$expectedTargetHash = '74648BF164D249D3E8211F2411681C01B8EDA8148AF978AA86E6B37F5C2B64C9'
$patchOffset = 23899942
$originalBytes = [Text.Encoding]::Unicode.GetBytes('prod.xcom-ew.firaxis.com')
$replacementBytes = [Text.Encoding]::Unicode.GetBytes('off.xcom-ew.invalid.test')

$sourceHash = (Get-FileHash -LiteralPath $sourcePath -Algorithm SHA256).Hash
if ($sourceHash -ne $expectedSourceHash) {
    throw 'Original executable version does not match. Stop and inspect this version separately.'
}
if (Test-Path -LiteralPath $targetPath) {
    throw 'XComEW-Offline.exe already exists. Verify it or back it up before creating another copy.'
}

$bytes = [IO.File]::ReadAllBytes($sourcePath)
if ($bytes.Length -ne 27793464 -or $originalBytes.Length -ne $replacementBytes.Length) {
    throw 'Executable size or replacement length does not match.'
}
for ($byteIndex = 0; $byteIndex -lt $originalBytes.Length; $byteIndex++) {
    if ($bytes[$patchOffset + $byteIndex] -ne $originalBytes[$byteIndex]) {
        throw 'Original hostname was not found at the verified offset.'
    }
}

[Array]::Copy($replacementBytes, 0, $bytes, $patchOffset, $replacementBytes.Length)
$hasher = [Security.Cryptography.SHA256]::Create()
try {
    $patchedHash = [BitConverter]::ToString($hasher.ComputeHash($bytes)).Replace('-', '')
} finally {
    $hasher.Dispose()
}
if ($patchedHash -ne $expectedTargetHash) {
    throw 'Patched data did not match the verified result.'
}

$outputStream = [IO.File]::Open($targetPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write)
try {
    $outputStream.Write($bytes, 0, $bytes.Length)
} finally {
    $outputStream.Dispose()
}
if ((Get-FileHash -LiteralPath $targetPath -Algorithm SHA256).Hash -ne $expectedTargetHash) {
    throw 'Written copy verification failed. Do not launch this copy.'
}
if ((Get-FileHash -LiteralPath $sourcePath -Algorithm SHA256).Hash -ne $sourceHash) {
    throw 'Original executable hash changed. Inspect the original before continuing.'
}
Get-FileHash -LiteralPath $sourcePath, $targetPath -Algorithm SHA256

成功后,副本的 SHA-256 应该是:

74648BF164D249D3E8211F2411681C01B8EDA8148AF978AA86E6B37F5C2B64C9

如果副本已经存在,可以用代码最后一行核对哈希,一致就不用再生成了。原版文件始终保留,后面要回滚也方便。

先试着启动一次

在同一个 PowerShell 窗口里继续执行:

$env:SteamAppId = '200510'
$env:SteamGameId = '200510'
Start-Process -FilePath (Join-Path $gameDirectory 'XComEW-Offline.exe') -ArgumentList '-FROMLAUNCHER', '-LANGUAGE=CHT' -WorkingDirectory $gameDirectory

-LANGUAGE=CHT 是这次使用的繁体中文,英文可以改为 -LANGUAGE=INT。Steam 环境变量和工作目录也是启动方式的一部分,直接双击 EXE 不一定得到同样的结果。

进入菜单以后,建议至少等 10~15 分钟,再实际切换菜单、读档、进入一场任务。后面这几项是需要自己补充的游玩验证,前面的测试记录没有覆盖完整战役。

做一个桌面快捷方式

退出刚才的游戏,然后在同一个 PowerShell 窗口执行下面这段。$gameDirectory 沿用前面的变量,$steamPath 要按自己的 Steam 安装位置修改。

$steamPath = 'C:\Program Files (x86)\Steam\steam.exe'
$gamePath = Join-Path $gameDirectory 'XComEW-Offline.exe'
$desktopPath = [Environment]::GetFolderPath('Desktop')
$shortcutPath = Join-Path $desktopPath 'XCOM Enemy Within 修复版.lnk'
if (-not (Test-Path -LiteralPath $steamPath)) { throw 'Steam executable was not found.' }
if (-not (Test-Path -LiteralPath $gamePath)) { throw 'Patched game executable was not found.' }
if (Test-Path -LiteralPath $shortcutPath) { throw 'Shortcut already exists. Use a different name.' }

$quotedSteam = $steamPath.Replace("'", "''")
$quotedGame = $gamePath.Replace("'", "''")
$quotedDirectory = $gameDirectory.Replace("'", "''")
$launchCommand = "if (-not (Get-Process -Name steam -ErrorAction SilentlyContinue)) { Start-Process -FilePath '$quotedSteam'; Start-Sleep -Seconds 12 }; "
$launchCommand += "`$env:SteamAppId='200510'; `$env:SteamGameId='200510'; "
$launchCommand += "Start-Process -FilePath '$quotedGame' -ArgumentList '-FROMLAUNCHER','-LANGUAGE=CHT' -WorkingDirectory '$quotedDirectory'"
$encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($launchCommand))
$shell = New-Object -ComObject WScript.Shell
$shortcut = $shell.CreateShortcut($shortcutPath)
$shortcut.TargetPath = Join-Path $env:WINDIR 'System32\WindowsPowerShell\v1.0\powershell.exe'
$shortcut.Arguments = "-NoProfile -NonInteractive -WindowStyle Hidden -EncodedCommand $encodedCommand"
$shortcut.WorkingDirectory = $gameDirectory
$shortcut.IconLocation = "$gamePath,0"
$shortcut.WindowStyle = 7
$shortcut.Description = 'XCOM Enemy Within: bypass the legacy Firaxis service request'
$shortcut.Save()
Get-Item -LiteralPath $shortcutPath

以后就用这个快捷方式启动。它会先确保 Steam 已经打开,然后运行修复副本,跳过版本选择器。从 Steam 库直接启动还是原版程序,仍然可能触发相同的问题。

Steam 首次登录、更新,或者启动较慢时,先等它完全就绪,再使用快捷方式。这里的 12 秒只是启动等待时间,不保证 Steam 在所有机器上都能这么快准备好。

检查是否生效,以及怎么回滚

游戏运行时,可以再开一个 PowerShell 窗口检查:

Get-Process -Name XComEW-Offline | Select-Object Id, Responding, @{Name='PrivateMB'; Expression={[math]::Round($_.PrivateMemorySize64 / 1MB, 1)}}
$gameProcess = Get-Process -Name XComEW-Offline | Select-Object -First 1
Get-NetTCPConnection -OwningProcess $gameProcess.Id -ErrorAction SilentlyContinue | Where-Object RemoteAddress -eq '192.81.242.201'

启动加载以后,内存不应再持续单向上涨。第二项在本次测试里没有返回旧服务连接。不过进程响应和没有这条连接都只是辅助检查,实际操作游戏才是最终要看的结果。

日志通常在 Windows“文档”下的 My Games\XCOM - Enemy Within\XComGame\Logs\Launch.log。开启 OneDrive 文档重定向的话,实际路径可能在 OneDrive 里面。里面应该能看到替换后域名的 DNS 查询。

要恢复原版,退出游戏,删除自己生成的副本和快捷方式,再从 Steam 启动就好了。Steam 更新后也要重新核对原版哈希,旧副本未必能和更新后的其他游戏文件配合使用。

最后

想玩一会儿游戏,最后又调试了半天,多少有点无奈。不过至少这次没有停留在“重启一下好像好了”,而是抓到了网络接收路径和内存增长,再用一次很小的改动做了对照验证。

对这种延迟出现的问题,最该记住的还是多等一会儿。能进主菜单,只说明能进主菜单。以后再遇到类似情况,先把故障发生前后的响应、CPU 和内存记录下来,应该能少走一些弯路。

XCOM: Enemy Within 启动两分钟后卡死的排查经历
https://blog.kisnows.com/zh-CN/2026/09/27/xcom-enemy-within-startup-freeze/
作者
Kisnows
发布于
2026-09-27
许可协议
CC BY-NC-ND 4.0