引子
今天运行一款很流行的第三方驱动更新软件(名称省略)时,提示我的显卡驱动需要更新。打开设备管理器一看,发现当前使用的这一版显卡驱动还是2012年的。果然是够老的。果断选择下载并安装。安装结束后,我没有选择立刻重启系统,因为还有一大堆工作需要继续。
一切都很正常地进行着,直到我启动了Visual Studio 2012。Splash画面持续了5秒钟之后,弹出程序崩溃对话框,并开始收集错误,然后弹出新的对话框,问我是否向微软提交本次崩溃信息。选择拒绝后,程序直接关闭。试过几次,都是如此。
寻找问题
在此之前,Visual Studio 2012一直很稳健地在我的系统里面运行着。这忽然怎么了?我百思不得其解。于是果断上调试器。在调试器中打开Visual Studio 2012的执行文件,路径是:
C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE\Devenv.exe。
运行起来,不过五秒,调试器断住了。细看它的调用栈:
0:010:x86> kc
KERNELBASE!DebugBreak
d3d9!NTStatusToHResult
d3d9!RenderCB
WARNING: Stack unwind information not available....may be wrong.
igdumdim32!OpenAdapter
igdumdim32!OpenAdapter
d3d9!CreateDeviceLHDDI
d3d9!D3D9CreateDirectDrawObject
d3d9!FetchDirectDrawData
d3d9!InternalDirectDrawCreate
d3d9!CEnum::CreateDeviceImpl
d3d9!CEnum::CreateDeviceEx
wpfgfx_v0400!CD3DDeviceManager::CreateNewDevice
wpfgfx_v0400!CD3DDeviceManager::GetD3DDeviceAndPresentParams
wpfgfx_v0400!CHwDisplayRenderTarget::Create
wpfgfx_v0400!CDesktopRenderTarget::Init
wpfgfx_v0400!CDesktopRenderTarget::Create
wpfgfx_v0400!CMILFactory::CreateDesktopRenderTarget
wpfgfx_v0400!CSlaveHWndRenderTarget::EnsureRenderTargetInternal
wpfgfx_v0400!CSlaveHWndRenderTarget::Render
wpfgfx_v0400!CPartitionManager::LogEvent
wpfgfx_v0400!CComposition::Render
wpfgfx_v0400!CComposition::ProcessComposition
wpfgfx_v0400!CComposition::Compose
wpfgfx_v0400!CPartitionThread::RenderPartition
wpfgfx_v0400!CPartitionThread::Run
wpfgfx_v0400!CPartitionThread::ThreadMain
kernel32!BaseThreadInitThunk
ntdll_77a90000!__RtlUserThreadStart
ntdll_77a90000!_RtlUserThreadStart
这个调用栈可以分成两个部分,首先它是一个WPF线程,WPF是Vista以后出现的基于.net和DirectX的图形库。可见VS的界面是基于WPF的。然后它使用了D3D9接口,所以可以看到相关的调用接口。中间还调用了Intel用户层3D驱动的接口函数(igdumdim32模块)。
看到它位于栈顶的帧调用了KERNELBASE!DebugBreak函数,这是用来下断点的函数。当执行的时候,系统会寻找活动的调试器,让调试器挂载到出错的程序。查看第二个函数(NTStatusToHResult)的汇编代码,看到这样几句上下文:
537ff5e0 e90b56ffff jmp d3d9!NTStatusToHResult+0xd0 (537f4bf0)
537ff5e5 e831170a00 call d3d9!DetectedDriverMismatch (538a0d1b)
537ff5ea e9a0f40400 jmp d3d9!NTStatusToHResult+0x81 (5384ea8f)
原来它调用的是函数d3d9!DetectedDriverMismatch。这个函数经过了编译优化,所以在栈上没有显示出来。看下面的实现是怎样的:
0:010:x86> u d3d9!DetectedDriverMismatch d3d9!DetectedDriverMismatch: 538a0d1b 68300d8a53 push offset d3d9!`string' (538a0d30) 538a0d20 ff15e0157f53 call dword ptr [d3d9!_imp__OutputDebugStringW (537f15e0)] 538a0d26 ff25d8157f53 jmp dword ptr [d3d9!_imp__DebugBreak (537f15d8)]
确实是它调用了DebugBreak函数,但在调用前,通过OutputDebugStringW输出了一段调试信息。可以看到调试信息的内容:
0:010:x86> du 538a0d30 538a0d30 "User/kernel-mode driver mismatch"
我在调试器的命令窗口中搜索了一下,果然看到有这个调试输出。这个信息的内容还是很明确的,是指“用户和内核驱动不匹配”。到底是什么驱动的用户和内核模块不匹配呢?考虑到当前是一个D3D相关线程,会想到就是显卡驱动。
为了找到原因,有必要去分析d3d9!NTStatusToHResult函数,因为正是它最终调用了d3d9!DetectedDriverMismatch函数。分析它的汇编代码后,发现了下面的逻辑:
537ff5e5 e831170a00 call d3d9!DetectedDriverMismatch (538a0d1b) ……//省略 5384ea6c 3d09001ec0 cmp eax,0C01E0009h 5384ea77 0f84680bfbff je d3d9!NTStatusToHResult+0x7c (537ff5e5)
这段逻辑判断寄存器eax,如果其值等于0C01E0009h,就跳到地址0x537ff5e5 处执行函数DetectedDriverMismatch。寄存器Eax中保存的是当前函数的输入参数。为了查询这个输入参数的由来,我在它的父函数d3d9!RenderCB开始的地方设置了断点,并重新启动devenv.exe程序。跟踪后发现了下面的逻辑:
0:010:x86> uf d3d9!RenderCB 537f65de ff15ac929953 call dword ptr [d3d9!pfnOsThunkDDIRender] 537f65e4 894508 mov dword ptr [ebp+8],eax ……//省略 5385a234 8b7508 mov esi,dword ptr [ebp+8] 5385a24c 56 push esi 5385a24d e873a9f9ff call d3d9!NTStatusToHResult (537f4bc5)
它在调用了函数OsThunkDDIRender后,检查返回值eax,如果eax不等于0,就转入NTStatusToHResult做错误处理。查看此时的eax值:
0:007:x86> r eax eax=c01e0009
这个值正是NTStatusToHResult中所判断的特征值。用错误查看工具查看c01e0009这个值,发现它对应的描述信息如下:
0xc01e0009(NT_STATUS): The kernel driver detected a version mismatch between it and the user mode driver.(内核驱动程序检测到它和用户模式驱动程序之间的版本不匹配。)
解决办法
问题有点眉目了,Visual Studio使用WPF画图的时候,使用D3D API但是发现用户模块和内核模块的版本已经不匹配了,在这种情况下,3D的用户驱动就拒绝进一步响应3D请求,并调用DebugBreak人为地设置一个断点。
十有八九是更新显卡驱动坏的事!重新启动也没用,甚至系统在重启后运行winsat评估程序的时候,就发生了崩溃。而winsat也是使用D3D9接口的。
现在我有两种选择,一种选择是继续研究,找到驱动更新时到底把哪个文件更新坏了(或漏了)。另外一个选择,就是到显卡厂商的官方网站上,下载官方最新驱动,更新以观其效。对于我来讲,第二种选择显然更划算一些。10分钟之后,驱动下载并安装成功,我仍然没有重启系统,再尝试运行VS,发现已经可以正常启动软件了。
再次使用调试器运行VS的执行程序,在D3D9!RenderCB中调用OsThunkDDIRender的地方下断点,检测其返回值,发现为0——即期望的正确返回值。
我从厂商官方网站下载的驱动版本,和第三方驱动更新软件的驱动版本是完全一样的。为什么会有截然不同的效果呢?其实我的系统里面有两块显卡,一块是Intel的集成显卡(HD4000),一块是独立显卡(Nvidia GTX 670M)。由于Intel的集成显卡装机量非常大,所以AMD和Nvidia都有专门的驱动,针对Intel的集成显卡进行节能优化,让用户选择把指定的3D程序运行在独立显卡或集成显卡上。这样一来,显卡厂商会把同一版驱动,分成几种不同的分发版本:有针对独立显卡的(系统里面只有一块独立显卡),有针对多显卡的(既有独立显卡,也有Intel集成显卡)。后者的驱动安装包中,同时也包含了Intel显卡驱动的部分文件,在安装时进行更新。第三方驱动更新软件如果没有考虑到这一点,用独立显卡驱动包,安装在多显卡系统中,就会导致这样的问题。