PAYATHON 2026

Windows 7 64-bit 上 VB6 随机崩溃及 Event ID 1000

排障赵师傅

结论

Event ID 1000 只说明 Windows 记录到一次应用程序崩溃,单凭这条事件无法判断根因。faulting module 中出现 ntdll.dll 或 lpk.dll,也不能说明这些系统 DLL 本身存在问题。更常见的原因是程序、ActiveX 控件或错误的 API 声明破坏了内存,系统 DLL 后来访问这块内存时才触发崩溃。

排查时,优先完成以下工作:

  1. 为 program.exe 配置完整崩溃转储。
  2. 保存 Event ID 1000 中的 exception code、fault offset 和模块版本。
  3. 使用 WinDbg 分析 dump。
  4. 在测试环境中启用 Application Verifier 或 PageHeap。
  5. 分组排查第三方控件、API 调用、资源泄漏和兼容模式。

Dependency Walker 报告的 IESHIMS.dll、GRPSVC.dll 缺失以及缺少 symbols,通常无法解释程序运行数小时或数天后发生的随机崩溃。

faulting module 为什么每次可能不同

VB6 程序即使没有显式声明 ntdll.dll,也会间接使用它。VB6 runtime、kernel32.dll、user32.dll、COM 和其他系统组件最终都会调用 ntdll.dll 中的底层实现。

faulting module 表示检测到异常的位置,制造错误的代码可能位于别处。内存越界、错误的回调地址或堆破坏,可能早在数秒甚至数小时前就已发生。lpk.dll 参与部分文字布局和语言处理,它出现在事件日志中时,也可能只是接收到了已经损坏的数据。

VB6 的 On Error 只能处理 VB 能够识别的运行时错误,无法可靠捕获 access violation、stack corruption、无效回调或进程堆损坏。

需要保存 Event ID 1000 中的这些字段:

  • Faulting application version
  • Faulting module name 和 version
  • Exception code
  • Fault offset
  • Process ID
  • Application path
  • Report ID

如果实际提示是“未响应”,还要检查 Event ID 1002。“未响应”可能源于 UI 线程死锁或长时间阻塞;“已停止工作”通常表示进程异常终止,两者需要采用不同的排查方法。

先生成完整崩溃转储

Windows Error Reporting 可以在崩溃时保存 dump。请在管理员命令提示符中执行以下命令:

mkdir C:\CrashDumps

reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\program.exe" /v DumpFolder /t REG_EXPAND_SZ /d "C:\CrashDumps" /f

reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\program.exe" /v DumpType /t REG_DWORD /d 2 /f

reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\program.exe" /v DumpCount /t REG_DWORD /d 10 /f

DumpType=2 表示生成完整用户态转储。确认运行程序的 kiosk 账户能够写入 C:\CrashDumps,并为转储文件预留足够的磁盘空间。

如果现场环境不适合修改 WER,可以改用兼容 Windows 7 的 ProcDump 版本:

procdump.exe -accepteula -ma -e -w program.exe C:\CrashDumps

-w 用于等待进程启动,-e 用于监视未处理异常,-ma 用于生成完整 dump。堆损坏问题通常需要检查完整内存,因此不要只收集 minidump。

使用 WinDbg 分析 dump

VB6 程序是 32-bit 进程。在 Windows 7 64-bit 系统上,它仍运行于 WOW64 环境。分析时需要切换到正确的 32-bit 进程上下文,也可以直接使用合适的 32-bit 调试工具。

基本命令如下:

.symfix
.sympath+ srv*C:\Symbols*https://msdl.microsoft.com/download/symbols
.reload /f
!analyze -v
.ecxr
kv
lm

查看故障模块时使用:

lmvm module_name

分析时主要检查:

  • Exception code 是 0xC0000005、0xC0000374 还是其他异常。
  • 崩溃线程的调用栈中,系统 DLL 之前出现了哪个 OCX、DLL 或程序函数。
  • 读写地址是否明显无效,例如 00000000、已释放对象或带固定填充值的区域。
  • 多个 dump 是否出现相同的上层调用路径。
  • 加载的 OCX/DLL 是否来自预期目录,版本和时间戳是否一致。

0xC0000005 通常表示非法内存访问,0xC0000374 通常表示 Windows 检测到了堆损坏。即使每次记录的 faulting module 不同,只要多个 dump 的调用栈都指向同一控件或同一操作附近,就能据此缩小排查范围。

条件允许时,应保存崩溃现场使用的确切 EXE、OCX、DLL,以及编译时生成的符号文件。即使源代码相同,重新编译后的程序地址也可能发生变化,无法代替现场版本进行符号匹配。

在测试环境中提前捕获内存破坏

随机崩溃常常在错误发生很久后才暴露。PageHeap 可以让进程在更接近首次非法访问的位置停止:

gflags.exe /p /enable program.exe /full

测试完成后关闭:

gflags.exe /p /disable program.exe

还可以在 Application Verifier 中为 program.exe 启用 Basics、Heaps、Handles 和 Locks 等检查。

这些检查会增加内存占用并降低运行速度,不适合在未经验证的情况下长期部署到现场设备。应在配置相同的测试机上进行长时间压力测试,并使用与目标进程位数匹配的调试工具。

VB6 项目中需要优先检查的地方

API 声明和缓冲区

逐项复核所有 Declare,尤其注意:

  • ByVal 和 ByRef 是否正确。
  • 参数类型、返回类型和结构体字段是否符合 32-bit API 定义。
  • Type 结构体是否缺少字段或使用了错误的长度。
  • 字符串缓冲区是否提前分配。
  • API 是否保存了 VB 临时字符串、数组或对象的地址。
  • 回调函数是否位于标准模块中,签名是否完全匹配。
  • 窗口 subclassing 是否在窗体卸载前恢复了原来的 window procedure。
  • Timer、multimedia 和串口回调是否可能在对象销毁后继续执行。

VB6 在 Windows 7 64-bit 上仍是 32-bit 进程,进程内指针也是 32 bit。不要机械地把所有 Long 改为 64-bit 类型,应逐个核对 API 在 x86 调用约定下的声明。

ActiveX 和第三方控件

优先检查以下组件:

  • Resizer XT
  • Microsoft Comm Control 6.0
  • Microsoft Windows Common Controls 6.0
  • Data Formatting 相关组件
  • DAO 3.51 及其数据库访问路径

“Microsoft 标准控件”同样可能遇到兼容性问题。控件版本不一致、错误注册、重复 DLL、回调生命周期问题和资源泄漏,都可能在长时间运行后才暴露。

可以建立几个测试版本:

  1. 禁用 Resizer XT,暂时恢复固定分辨率布局。
  2. 禁用串口相关功能。
  3. 禁用声音或 winmm 相关调用。
  4. 将数据库写入替换为受控的测试实现。
  5. 逐项恢复功能,并分别进行长时间运行测试。

每次只改变一个变量。否则,即使崩溃消失,也无法确认究竟是哪项修改产生了影响。

在 64-bit Windows 上注册 32-bit OCX 时,应使用:

C:\Windows\SysWOW64\regsvr32.exe component.ocx

不要因为怀疑注册问题,就在现场反复注册控件。先记录文件路径、版本、时间戳和哈希,再与运行正常的设备比较。

资源泄漏

程序运行数小时或数天后才出错,很可能与资源逐渐耗尽有关。需要持续记录:

  • Private Bytes
  • Virtual Bytes
  • Handle Count
  • GDI Objects
  • USER Objects
  • Thread Count

如果某项计数持续上升,应检查是否遗漏了以下清理操作:

  • CloseHandle
  • ReleaseDC
  • DeleteObject
  • timeKillEvent
  • 文件关闭
  • DAO Recordset、Database 和 Workspace 的关闭及对象释放

Windows 任务管理器能够显示部分计数。长时间采样更适合使用 Process Explorer 或 Performance Monitor。

增加操作轨迹日志

日志不能捕获 access violation,但能帮助确认崩溃前程序正在执行什么操作。记录应分别放在操作开始和结束的位置,不能只写在统一错误处理器中。

Private Sub TraceEvent(ByVal Message As String)
    Dim FileNumber As Integer

    On Error Resume Next

    FileNumber = FreeFile
    Open App.Path & "\runtime.log" For Append As #FileNumber
    Print #FileNumber, Format$(Now, "yyyy-mm-dd hh:nn:ss"); _
                       " | "; Message
    Close #FileNumber
End Sub

调用示例:

TraceEvent "Sound playback begin"
Call PlayCurrentSound
TraceEvent "Sound playback end"

TraceEvent "Accounting write begin"
Call SaveAccountingRecord
TraceEvent "Accounting write end"

日志还可以记录当前窗体、游戏阶段、串口状态、数据库操作名和资源计数。避免记录敏感账务数据,并设置日志轮转或大小限制,防止占满磁盘。

如何处理兼容模式和管理员权限

Windows 2000 compatibility mode 会改变部分版本检测、文件和注册表行为。它可用于解决已知的兼容问题,但不应默认启用。管理员权限也无法修复内存破坏。

可以在测试机上分别测试以下组合:

  • 不启用 compatibility mode。
  • 仅禁用 visual themes。
  • 启用 Windows 2000 compatibility mode。
  • 分别使用普通权限和管理员权限运行。

如果只有某个组合会发生崩溃,再结合 dump 和调用栈进行分析。一次测试中不要同时修改兼容模式、控件版本和程序代码。

Dependency Walker 的结果是否有用

目前这些结果没有提供直接的崩溃线索:

  • 旧版 Dependency Walker 经常因为 delay-load、动态加载或工具本身的解析限制,将 IESHIMS.dll 和 GRPSVC.dll 报告为缺失。
  • COMCTL32.dll 或 IEFRAME.dll 缺少 symbols,只表示 Dependency Walker 找不到调试符号,并不表示 DLL 文件缺失或无法执行。
  • 如果静态导入所需的 DLL 确实缺失,程序通常会在启动阶段失败,很少能正常运行数天后再随机崩溃。

Dependency Walker 可以用来确认实际加载了哪个 OCX/DLL,但不适合单独诊断运行数小时后出现的堆损坏。更有用的材料包括完整 dump、exception code、调用栈、模块版本和崩溃前的操作轨迹。

备注:内容仅供参考。