ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

显示驱动调试工具链:从WinDbg到GPUView与WPP实战指南

显示驱动调试工具链:从WinDbg到GPUView与WPP实战指南 干显示驱动这行写文档和看代码的时间加起来可能都不如和调试器相亲相爱的时间多。这个系列写到现在第5篇前面零零散散聊了不少初始化流程、模式设置、电源管理这些话题但一直没正经说说我们每天都在用的家伙什儿。这次就把我工作台上常年开着、出了问题第一个打开的那批调试工具拉出来聊一遍从内核调试器到GPU性能追踪从串口日志到WPP软追踪把它们的用途、配置、常用招数和坑全摊开讲希望能给刚入行或者正在被显示驱动疑难杂症折磨的朋友一点参考。先说个实在话显示驱动调试的难度很大一部分不在驱动代码本身而在调试环境。它不像普通内核驱动你可以在VMware里跑个Windows随手打个断点看变量。显示驱动一旦坏在模式切换、Gamma校正、睡眠唤醒这些节骨眼上屏幕可能直接黑掉或者花得你根本看不清调试器输出。更麻烦的是现代GPU越来越复杂显存里跑着微码显示引擎连着DDI,驱动只是最上面那一层薄皮。所以这一行真正吃香的调试手段往往是组合拳——内核调试器抓崩溃状态GPU日志抓调度时序WPP抓驱动内部执行流三管齐下才能定位问题。如果你正在做Windows平台的显示驱动开发或者做图形栈的兼容性测试这篇文章应该能帮你在工具选型和排查思路上省不少时间。1. 为什么显示驱动调试离不开一套组合式工具链不少人问我Windows下调试显示驱动是不是装个WinDbg就够了真要这么简单我们就不用天天加班了。WinDbg是内核调试的基石但它只能告诉你系统现在是什么状态很难告诉你系统之前经历了什么才变成这个状态。而显示驱动的问题偏偏绝大多数都是历史问题——上一次模式切换的时序错了下一个VSYNC周期才表现为闪屏某个DPC里多耗时了几微秒积累几秒后桌面开始掉帧。这类问题你靠断点和单步是调不出来的。看动图时代的显示驱动有两类工具是并行的。第一类是状态类工具比如WinDbg内核调试器和Driver Verifier它们在异常发生的时候定格现场让你检查设备对象、IRP队列、工作线程状态、内存池标记非常适合定位蓝屏、挂起、内存破坏这类硬故障。第二类是事件类工具比如ETWEvent Tracing for Windows加GPUView它们是录像机把GPU上下文切换、渲染命令提交、VSYNC中断、DPC延迟这些事件按时间轴存下来非常适合追性能抖动、闪黑、间歇性卡顿这类软故障。还有个常常被忽视的第三类——驱动自带的追踪日志。Windows驱动圈子一般直接叫WPP软件跟踪也有用ETW Provider自己实现的。这类日志的作用说白了就是飞行记录仪把关键代码路径上的状态值、返回值、延迟打进环形缓冲出问题时先把日志倒出来比全屏打印靠谱得多因为在全屏打印之下很多bug是会被掩盖和延迟的你看到的现象根本不是真实时序下的行为。我自己的习惯是这样的日常开发开WPP日志辅助验证冒烟测试和兼容性测试挂Driver Verifier出蓝屏了WinDbg看dump怀疑性能问题时GPUView抓ETL。这套组合基本覆盖了驱动开发八成以上的问题场景。下面逐一展开说。2. 内核调试器的搭建与常用命令2.1 调试连接方式的选型现在的Windows版本做内核调试最常用的就是网络调试KDNET和USB调试串口COM口已经越来越边缘化了但老平台还有些文档和习惯在沿用。网络调试是微软主推的方案从Windows 10 1803前后开始用口令字key做简单认证配起来相对简单。USB调试主要用在无法稳定走网络的环境但需要专用的USB线缆和USB3.0调试端口支持而且大部分零售主板根本不带这个能力所以实用性受限。串口调试还在但波特率、线缆、中断共享这些麻烦事让它只适合做最后兜底。从我的实际体验来说强烈推荐网络调试。微软提供了现成的工具kdnet.exe你用管理员权限跑一下它会自动从网卡固件里读取MAC地址准确说是一个与设备绑定的密钥配置调试器的目标参数输出像这样的信息Network debugging is enabled for this computer. Key: 1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a Ethernet IPv6: fe80::xxxx:xxxx:xxxx:xxxx你把Key抄下来在WinDbg启动时加参数-k net:port50000,key1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a就能连上目标机了。不过要提醒一句这个Key只在当前安装的Windows系统上有效重装系统或者换网卡之后都会变必须重新跑一遍kdnet.exe。有段时间我们的测试机经常连不上调试器查了半天是因为有人更新系统时刷掉了调试配置bcdedit里已经没有debug条目了。2.2 调试前必须配好的符号路径内核调试最大的痛点就是符号。不加载符号你看到的堆栈全是十六进制地址基本等于瞎猜。配符号路径建议做成系统环境变量而不是每次启动WinDbg手动设置。我是这样配的setx _NT_SYMBOL_PATH srv*D:\Symbols*https://msdl.microsoft.com/download/symbols这样设置之后WinDbg会从微软符号服务器下载公共符号到本地缓存。但显示驱动工程师都知道光有微软公共符号还不够。比如你是某个GPU厂商的驱动开发你手里那层驱动是私有的微软服务器上根本没有你的符号或者只有阉割版。所以通常要把自己的私有符号路径加在最前面让调试器优先加载你自己的PDB否则一堆dxgkrnl!的公共符号看得你脑壳疼。有个细节如果你的符号路径里既有私有又有公共中间用分号分隔而且私有符号路径放在前面。另外推荐直接养成加载完驱动之后马上验证符号是否匹配的习惯方法是随便找一两个你的驱动函数下断点如果断不下来还能直接说明符号有问题别等到现场翻了车再去怀疑符号。2.3 一天至少用一次的命令集WinDbg的命令分两级用户态命令通常直接操作系统内部结构内核态还有很多扩展命令要走DML。显示驱动内核调试最常用的命令我按使用频率大概排个序!analyze -v是蓝屏dump的第一语句没有之一。它会把bugcheck码、异常记录、堆栈、可能的原因自动汇总成一段完整的分析报告。但它只是起手式不能迷信它的结论它经常会把责任错误地归到某个碰巧在栈上的模块上。真正靠谱的是往下追几层看调用链是否合理再结合你的驱动代码走查判断。!devobj DeviceObject和!irp IRP是我的第二常用。显示驱动里最烦的问题就是待处理的IRP卡住不完成或者多个IRP竞争同一块显存资源。!irp命令会列出IRP的类型、当前状态、关联设备对象、已入队的栈位置一眼看出它卡在哪个分发函数里。!devobj则能看设备对象的当前电源状态、PDO/PDO栈关系配合排查电源IRP挂起特别有效。!verifier配合Driver Verifier用。Driver Verifier会在驱动加载时插入大量非法操作检查包括内存池越界、锁未释放、IRQL违规、DDI合规性等。如果目标机开着Driver Verifier并且触发了违规系统会故意蓝屏以便你把现场dump出来这时!verifier能看到详细的违规类型和涉及的驱动名。!poolmon是查内存泄漏的神器。显示驱动里的泄漏绝大多数不是真正的忘了释放而是某个分配动作的频率高于释放频率而且往往发生在特定分辨率、特定刷新率的路径上。!poolmon把内存池按Tag字母序排列显示分配次数和释放次数差那些牌子持续变大的Tag就是泄漏点。我们曾经靠它抓到一个在D3D全屏独占切换时反复分配Gamma Ramp内存却从不释放的bug触发频率大约是三分钟100次分配非常隐蔽。还有两个低频但救命用的!locks检查和显示引擎相关的自旋锁是否有异常持锁!thread ThreadPointer查看某个工作线程的状态和栈如果显示引擎的渲染线程长期处在一个预期外的等待状态多半是同步信号没等到。3. GPUView与ETW从时间轴上抓软故障3.1 为什么光靠断点解决不了闪屏和掉帧闪屏、卡顿、间歇性黑屏这类问题别说断点了连内核dump都很难抓因为你强制中断系统的那一瞬间现场早就恢复了。而且这类问题的诱因常常在时间线上拉得很长比如某个vsync周期里DPC延迟超过了预算几微秒导致显示扫描输出丢了一帧屏幕看起来就是闪了一下。你不可能在每一帧VSYNC都手动断一下那样没有任何意义。所以我们需要的是录像。ETW是Windows后台的全局事件追踪框架它允许各内核组件和驱动注册自己的事件源然后由一个统一的会话把这些事件带时间戳地记录到文件里。GPUView.exe就是微软官方提供的一个ETLEvent Trace Log文件可视化工具手短说它画出来的时间轴能直观看清CPU活动、GPU活动、DPC/ISR分布、上下文切换和VSYNC的关系。3.2 抓ETL的实际操作抓GPU相关的ETL最常用的命令是Windows Performance Recorderwpr它已经内置了GPU、显示和图形栈相关的Profile不用自己手写一堆Provider GUID。我一般这样抓wpr -start GPU -filemode [跑测试场景] wpr -stop output.etl这里的GPU是预置的Profile名它会同时启用Dxgkrnl、DXG、DisplayClass相关的内核Provider也会记录进程和线程的CPU采样。如果问题只出现在特定应用场景比如跑某个demo时掉帧就只抓那个时间段尽可能缩短ETL文件体积因为文件太大会让GPUView打开时卡到怀疑人生。还有一个细节抓ETL的时候测试机的负载要尽量贴近真实使用场景不要开着杀毒软件全盘扫描的时候去抓性能问题否则出来的数据会让GPUView的时间轴上铺满噪音过滤半天都找不到真正的问题窗口。GPUView打开ETL之后最常见的是看底部多个CPU核的绿色活动条以及GPU引擎的蓝色活动条。对显示驱动开发来说真正要盯的是几个事件流VSync信号之间的间隔是否均匀、每个GPU上下文的切换时间、Command Buffer在GPU硬件队列里的驻留时长、以及从Present提交到画面真正扫出来的延迟。如果你的GPU活动条尾部经常出现很长的空洞说明GPU在等CPU驱动侧的同步原语可能被过度等待拖慢了。3.3 从ETL里定位到代码ETL能告诉你问题的时间段和涉及的引擎但它不能直接告诉你代码行。所以我的习惯是先通过GPUView确认GPU欠载或过载的时间窗口然后回到WPP日志里找同一时间戳附近驱动在干什么。这要求驱动里的WPP日志必须打好时间戳和足够的上下文至少要有请求类型、宽高、刷新率、路径分支等信息不然你无法把GPU等待了2ms映射到驱动在处理模式切换时多等了2ms。曾经有个问题双屏扩展模式下副屏切换分辨率时偶尔花一帧。GPUView看在窗口内GPU引擎有个清晰的下沉但CPU侧没有明显的异常。当时怎么也定位不到后来把WPP抓回来一看发现驱动在DxgkDdiSetVidPnSourceAddressWithModify里会对同一DisplayTarget做一次不必要的全量寄存器初始化耗时大约1.8ms但只在特定分辨率组合下才会触发。如果当时没有WPP的上下文日志单凭ETL很难缩小到这个函数。所以我的经验是ETL找时间点WPP找代码路径两者配合定位效率翻倍。4. WPP软件跟踪显示驱动里最实用的printf4.1 为什么不用DbgPrint很多刚起步的驱动开发者在调试时用DbgPrint打印是简单但缺陷很明显第一它必须依赖内核调试器在线目标机蓝屏、断线或者调试链路不稳的时候输出就断了第二它会同步阻塞调用者频繁打印会显著改变时序特别是显示引擎对时序敏感打印一多问题现象就变了你调试的是被调试器改变过的系统而不是真实系统第三没有分级和过滤机制日志一多很难查。WPPWindows Software Trace Preprocessor解决的正是这几个痛点。它把日志写到内核的软追踪缓冲默认是环形缓冲区就算没有调试器连接日志也在后台持续记录出问题之后再抓取。而且WPP原生支持分级FATAL/ERR/WARN/INFO/VERBOSE和按GUID做开关过滤你可以只开某个模块的详细日志其他模块保持静默。4.2 WPP接入的坑在显示驱动里加WPP需要改驱动源代码里的几处地方包括trace.h、WPP_INIT_TRACING、以及每个需要追踪的.c文件头部的#include。这个流程在WDF框架下有向导支持但如果你是自己的万国牌驱动就需要手动加。有一个常见的坑如果某个源文件没有调用WPP_MAIN_INIT或者没有正确包含trace头文件链接器不会报错但运行期该文件的WPP消息全部静默。这就导致你debug版本正常、测试版就是没有日志非常隐蔽。还有一个显示驱动领域独有的大坑WPP的模块追踪时如果追踪代码写得太频繁即使输出到环形缓冲不是同步写设备仍然会带来不可忽略的开销尤其是你在高频路径上做逐像素循环的追踪性能会被拖垮。正确的做法是在非常高频的路径上做成计数器累加定期汇总输出或者用WMIC的时删开关来控制低频采集别让每一个像素操作都产生一条日志。实时查看WPP日志常用traceview.exe或者tracelog.exe。tracelog -enable MyDriverGuid -f logfile.etl之后消息会落到ETL里配合tracefmt格式化成文本。实效上我通常是先靠WPP把驱动逻辑走到哪一步看清楚再用内核调试器去核那几个可疑点的状态值。4.3 WPP也要讲究信息密度WPP日志不是印的越多越好。我看到过不少同事把驱动里每个寄存器读写都打一条结果日志文件10分钟就有几百MB找一条关键错误要翻半天。好的WPP日志应该是有重点的。我的经验是入口和出口必打带关键参数、失败分支必打带错误码和返回路径、超时等待必打带等待时长和当前状态、状态机迁移必打带旧状态、新状态和触发源。至于中间那几十个寄存器赋值除非你专门在硬件寄存器调试模式下开启显式打印否则都留到寄存器dump里去看。显示驱动的寄存器级状态用调试器直接读MMIO更高效没有必要让WPP来干这个活。5. 显示驱动专项从寄存器到信号链5.1 用调试器直读GPU寄存器显示驱动调试到深层不可避免地要触碰硬件寄存器。传统的办法是把寄存器基地址映射到虚拟地址通过调试器的内存读写命令去读但内核驱动开发里更多时候是直接通过PCI配置空间、MMIO空间来访问。WinDbg连上目标机后可以用物理内存读写命令!dd或者直接通过驱动暴露的调试接口读。不过现代GPU普遍高度集成光看寄存器表还不够。厂商调试工具往往提供了更深层的访问比如读某个显示引擎的内部状态Firmware状态、扫描输出当前像素位置、甚至把显存里的帧缓冲直接倒出来分析。这些工具多数是厂商内部SDK的一部分不太方便公开讨论但思路是通用的显示驱动调试永远要从现在的图像被渲染到什么程度这个状态出发反向推断驱动发往硬件的命令序列。5.2 信号链上的几个关键状态在显示面板黑屏、闪屏这类问题上我习惯把状态分成几层排查系统是刚完成模式切换还是已经稳定运行了信号是从GPU显示引擎生成到Encoder再通过DP/HDMI线到显示器哪一层断开这两问很基础但很多人会直接在驱动代码里搜寄存器操作完全忽略了可能是DPCD训练失败、链路进入低功耗状态之后没抬起来这类硬件协议问题。如果定位到链路训练一般需要抓DPCD的链路状态、训练模式、电压摆幅等级这时候普通的寄存器dump已经不太够用需要依靠驱动往协议栈里做的状态记录。这类问题的排查核心思路是先把哪一层断掉确认不要在驱动代码里无目的地翻找。5.3 帧缓冲与扫描输出调试遇到显示内容花屏、撕裂、偏移这类问题就要进入帧缓冲层面。常见的一种情况某个surface的pitch或格式没有和显示引擎的扫描配置匹配上导致画面撕裂或颜色错乱。排查时我会先把当前显示模式下的帧缓冲基址、pitch、像素格式、扫描起始地址抓出来和驱动申请surface时保存的记录做对比。只要有一处对不上大概率就是这里的参数传递链出了问题。这类问题有一个经典特点低分辨率下正常高分辨率下花屏而且花屏位置通常在画面上半部或下半部。原因往往是把多个surface暂时合并到一个线性空间时一个有对齐约束的缓冲没有按显示引擎的台阶重新调整pitch。我把这种问题叫作pitch病它靠读帧缓冲内容往往看不出但把surface元数据打出来基本能一眼定位。6. 常见疑难杂症的排查思路6.1 蓝屏第一件事不是看代码而是看dump显示驱动导致的蓝屏bugcheck码大多是VIDEO_TDR_TIMEOUT_DETECTED0x116、VIDEO_SCHEDULER_INTERNAL_ERROR0x119、或者VIDEO_ENGINE_TIMEOUT_DETECTED0x141。遇到这类错误码!analyze -v会指向Dxgkrnl但它只是受害者。你需要再倒几层看到底是哪条IOCTL或哪个硬件命令执行中超时。拿TDR来说它的本质是GPU在限定时间内没有完成某个操作操作系统果断发起GPU复位。排查重点不是为什么GPU卡住而是没事的时候GPU为什么没有得到应有的喂食。喂食问题常在命令提交路径、同步对象、以及外部应用程序的恶性提交模式里。把这些路径和GPU活动记录对齐才是真正能复现问题的方法。6.2 黑屏无信号模式切换瞬间的黄金窗口黑屏问题让人抓狂的原因在于一旦黑屏你看不到代码走到哪了。我的办法是提前在驱动里布好非视觉的信标比如通过GPIO控制一个板载LED或者在WPP里输出开始切换已关闭扫描已重新使能扫描输出的阶段标记。之后抓WPP日志看黑屏发生时驱动停在哪个阶段。多数黑屏停在了扫描输出已关闭但未重新打开的时间窗口里要么是寄存器配置写了一半要么是某个DPC里被卡死。这里还有一个看家的技巧利用双显卡或者集显的错位观察。如果目标机器有第二块可用显示输出你可以把调试信息和驱动阶段日志同时输出到第二块屏上从而在主屏黑屏时继续观察执行过程。这个办法听着土但实战价值极高。6.3 闪屏和掉帧用补齐的信息去猜复杂时序闪屏类问题十有八九和VSYNC或者Present的时序有关。用过GPUView先定位闪屏的时间点看这个时间点附近有没有DPC超时、有没有Present批处理延迟、有没有过度同步等待。如果抓了几轮都找不到规律就得考虑是否和电源管理有关——比如空闲时降频、D3cold状态切换这些操作同样会改变VSYNC的节奏。我处理过一个闪屏案例最终定位到是驱动在显示器不支持特定刷新率时仍往硬件里写了一个超出范围的参数导致硬件自动把VSYNC周期跳了一拍表现就是每隔几秒闪一次。排查的时候!gdiinfo和ETL只能看到现象最终是靠WPP日志里的模式设置参数和硬件允许值对比才定位。此类案件的共同点在于硬件比驱动更像守规矩的一方驱动写了什么就必须看到硬件反馈什么。7. 工具链之外的几个关键习惯工具再多调试还是人来做。最后聊几个我踩坑踩出来的习惯第一每次大规模改动驱动后在Driver Verifier里添加一个专门的测试机再验证周期。不要只在出问题的那台机器上开而要放到覆盖所有关键特征的测试集合里。很多时候Verifier能提前三天把某个内存泄漏截出来比用户报告问题早得多。第二WPP日志要养成分级和模块化的好习惯不要一个GUID用到底。至少在显示引擎和电源管理两个子系统分别开GUID这样你可以只开电源管理的追踪去排查睡眠唤醒问题又不会丢掉显示引擎的帧提交信息。第三抓ETL要在问题还在发生的时候抓不要等场景跑完了才想起开记录。GPUView里的数据是可遇不可求的一次没抓对宁可重跑也不要硬着头皮分析一份有缺陷的日志。第四不要只看自己驱动的符号。核外适配的显示器固件、链路接收端的能力解析这些内容偶尔也要通过第三方日志或硬件调试工具交叉验证不要把所有问题都归到自己头上。从我个人的实际体会来看显示驱动调试更像是一门状态考古学——工具能让你看到现场但让你破案的是对状态的解读和对时序的理解。WinDbg、GPUView、WPP、Driver Verifier每个工具解决一面的问题配合在一起才是完整的图谱。希望这篇能把大家手上的家伙什儿理清楚真正遇到疑难bug的时候能少走一点弯路。
返回列表