不要再假装自己写的程序没bug了,不可能的,debug工具你早晚得用上。最常见的debug工具非printf(windows上用OutputDebugString函数)莫属,简单方便易学易用,但局限性也是显而易见的,首先它对debugee的影响很大,某些race condition的bug你要多加几个log它就重现不出来了,然后你把log去了发布给客户,结果又成了必现的bug,这种烂事咱们都碰到过,你懂的。其次log能打印的东西有限,有时候你加log追某个变量的值,追到最后发现是其他变量有问题,这时候你又得加log重新跑。最后分析log的过程及其枯燥无聊,而在debug上敲命令分析则充满了乐趣。我知道有些人对debugger持有鄙视的态度,“单步调试是程序员的耻辱”云云。其实我想说的是,有好工具在手上干嘛不用,又不会怀孕,怕什么。
我们今天要来聊聊windbg,windows上debug的神器,(个人感觉)gdb也不如它。不是说windbg本身写的多么多么好,它牛逼的地方在于它是可扩展的,windows上的内核开发人员驱动开发人员用了windbg这么多年,该碰到的问题都碰到过了,该提的需求都提了,该写的扩展也都写了,导致现在的windbg强大无比,基本啥问题都能解决。就算你的需求很奇葩别人没想过,你也可以用windbg提供的开发框架自己写一个扩展,不费什么时间。所以我们看,提供扩展功能的工具都会变成神器,浏览器如此,debug工具也是一样。废话不多说,让我们来看几个非常有用的扩展:
这个命令接收一个driver的名字作为参数,解析名字并找到对应的driver object,然后把由这个驱动程序产生的device object打印出来,如下:
lkd> !drvobj \Driver\usbhub
Driver object (8999a4f8) is for:
\Driver\usbhub
Driver Extension List: (id , addr)
Device Object list:
89701de8 89a3f330 8974ede8 8977cc98
8985ac98 89862c98 898b4c98 89add030
89876c98 89849aa8
我们看到,usbhub驱动程序对应的driver object是8999a4f8,由它生成了10个device object。
这个命令接收device object指针作为参数,并打印出该device object的内容,包括当前处理的irp,refrence count,device extension,以及与这个device object相关的上层驱动和下层驱动等,如下
lkd> !devobj 89701de8
Device object (89701de8) is for:
USBPDO-9 \Driver\usbhub DriverObject 8999a4f8
Current Irp 00000000 RefCount 0 Type 00000022 Flags 00003040
Dacl e16dcf84 DevExt 89701ea0 DevObjExt 89701fd0 DevNode 89998df0
ExtensionFlags (0000000000)
AttachedDevice (Upper) 89712b20 \Driver\HidUsb
Device queue is not busy.
lkd> !devobj 89a3f330
Device object (89a3f330) is for:
USBPDO-8 \Driver\usbhub DriverObject 8999a4f8
Current Irp 00000000 RefCount 0 Type 00000022 Flags 00003040
Dacl e16dcf84 DevExt 89a3f3e8 DevObjExt 89a3f518 DevNode 899b23c8
ExtensionFlags (0000000000)
AttachedDevice (Upper) 8971cb90 \Driver\TcUsb
Device queue is not busy.
我们查看了usbhub产生的10个device中的前两个,可以看出其中有一个是hidusb,另一个是tcusb。顺着AttachedDevice打印出的内容我们可以手动遍历整个驱动栈,不过这看起来有些麻烦,万幸有人以经写好一个扩展可以帮我们遍历了,那就是
该命令也接收device object作为参数,并遍历着把该object以下的驱动栈全部打印出来,直到bus driver为止,如下
lkd> !devstack 89a3f330
!DevObj !DrvObj !DevExt ObjectName
8971cb90 \Driver\TcUsb 8971cc48 000000ae
> 89a3f330 \Driver\usbhub 89a3f3e8 USBPDO-8
!DevNode 899b23c8 :
DeviceInst is "USB\Vid_0483&Pid_2016\5&39a18bdd&0&2"
ServiceName is "TcUsb"
可以看到tc usb在usbhub之上,而usbhub则是硬件"USB\Vid_0483&Pid_2016\5&39a18bdd&0&2“的bus driver。前面有篇博文我们说到过windows里把几乎所有的资源都抽象成了一个"object”的概念,所有的object都有一个结构一致的object head,以方便提供统一的操作接口,以下命令就是打印出obect信息的命令:
kd> !object \
Object: e1001300 Type: (8a65e2c0) Directory
ObjectHeader: e10012e8 (old version)
HandleCount: 0 PointerCount: 39
Directory Object: 00000000 Name: \
292 symbolic links snapped through this directory
Hash Address Type Name
---- ------- ---- ----
00 e100b6e0 Directory ArcName
8a515030 Device Ntfs
01 e2726b88 Port SeLsaCommandPort
03 e1011488 Key \REGISTRY
05 e2728888 Port ThemeApiPort
06 e16c7f68 Port XactSrvLpcPort
09 e1d4d428 Directory NLS
10 e1001078 SymbolicLink DosDevices
13 e1c9e160 Port SeRmCommandPort
14 8a577030 Device Dfs…
我们查看了根目录,并列出了它的所有子项(太多了,没全贴上来),它的功能跟winobj很像,不过没有winobj直观。再来看几个跟power有关的命令。我们知道用wdm写驱动最麻烦的事情之一就是所有的power命令都要自己handle,而wdf则帮我们全包圆了(又回到上会的讨论了不是),没包圆也有它的好处,就是你得强迫自己去理解这部分内容。power irp处理不好,机器很容易就不能进s3/s4或者不能从s3/s4唤醒,这时候我们就得借助windbg来追查问题到底出在哪儿,查看当前power状态的命令是
该命令接收device object为参数,打印它当前的power状态
lkd> !podev 89784b10
Device object is for:
DriverObject 899d4410
Current Irp 00000000 RefCount 0 Type 00000002 DevFlags 00000050
Device queue is not busy.
Device Object Extension: 89784bc8:
PowerFlags: 00000000 =>SystemState=0 DeviceState=0
Dope: 00000000:
我们看到目前device处于d0(working)状态,系统处于s0(idle)状态。但是这个命令只能给我们一个总结,到底哪些power irp正在处理我们没法看出来。以下命令正是列出系统中所有power irp的
lkd> !poreqlist
All active Power Irps from PoRequestPowerIrp
PopReqestedPowerIrpList
FieldOffset = 00000004
Irp 8a60ba20 DevObj 8a5c1d70 \Driver\ACPI Ctx 00000004 Wait Wake S3
Irp 882f1e00 DevObj 89a05440 \Driver\usbuhci Ctx 00000001 Wait Wake S0
Irp 883dee00 DevObj 89a2b218 \Driver\usbuhci Ctx 00000001 Wait Wake S0
Irp 8843c008 DevObj 89a20528 \Driver\usbuhci Ctx 00000001 Wait Wake S0
Irp 884ca220 DevObj 89a10030 \Driver\usbehci Ctx 00000001 Wait Wake S0
Irp 883662d0 DevObj 89b36030 \Driver\usbehci Ctx 00000001 Wait Wake S0
Irp 87ec2008 DevObj 8974ede8 \Driver\usbhub Ctx 00000001 Wait Wake S0
Irp 89b4ec00 DevObj 899b7618 \Driver\usbuhci Ctx 00000001 Wait Wake S0
Irp 882ca7d0 DevObj 89a3f330 \Driver\usbhub Ctx 00000001 Wait Wake S0
我们看到很多的hid设备处于等待唤醒的状态。
最后我们来聊聊内核调试时如何调试用户态的东西。我们知道在进程的用户态部分相互隔离,而内核部分都是share同一个地址空间,由这特性带来的好处坏处我们先不谈,今天先关注具体问题。断在kernel里的debugger要调试别的进程的kernel部分很容易,因为地址是同一块,但是要调试user mode部分就不那么容易了。我们知道user mode是不可能直接访问内核地址的,cpu在将虚地址翻译成物理地址的时候会检查特权级,user mode是第3级而内核是第0级,倘若第三级的指令带的地址是第0级,cpu会抛拒绝访问的异常。反过来,内核指令访问user mode地址虽然可行,不过得考虑进程上下文,如果你不管进程上下文直接访问user mode地址,有两种错误情况会发生:你访问的根本不是你想要的进程,或者你访问的地址根本没有东西。地址空间分为用户态和内核态这种说法是站在用户态角度讲的,它假设所有的线程都有"用户态"部分,实际上PsCreateSystemThread产生的内核线程是没有用户态部分的,它附在一个叫"System”的进程上,而"System”进程只是为管理方便而存在的虚拟的东西,没有实体。所以我们说从内核debugger调试用户态内容远没有kernel调kernel,user调user那么简单,你必须关心进程上下文这个东西。下面这个命令可以列出系统中所有的进程:
lkd> !process -1 0
PROCESS 8854b020 SessionId: 0 Cid: 1060 Peb: 7ffd4000 ParentCid: 04b4
DirBase: 0abc0ae0 ObjectTable: e12e5650 HandleCount: 396.
Image: windbg.exe
lkd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS 881de6f8 SessionId: 0 Cid: 1498 Peb: 7ffde000 ParentCid: 04b4
DirBase: 0abc0c20 ObjectTable: e465aa70 HandleCount: 46.
Image: notepad.exe
如上所示,当前进程是windbg,process object为8854b020,而我们关心notepad进程的process object是881de6f8 ,并不是同一个,我们首先要做的事情就是切换到notepad这个进程上去,使用内置命令.process
lkd> .process 881de6f8
Implicit process is now 881de6f8
然后f5让系统跑一会儿(windbg断住的时候,整个系统是挂起的,进程切换也不会发生),再次断下来的时候你就已经在notepad进程里了(上面列的打印都是在local kernel debug里的,功能很受限,比如系统没挂起,f5也不能用。接下来我要切换到双机调试模式)
kd> !process -1 0
PROCESS 863c22f0 SessionId: 0 Cid: 00bc Peb: 7ffdb000 ParentCid: 05fc
DirBase: 06c602c0 ObjectTable: e16718e8 HandleCount: 29.
Image: notepad.exe
虽然已经在notepad进程里了,但user mode的东西依然不可见,因为windbg会缓存用户态的信息,进程切换后,你得手工刷新缓存,用内置命令.reload /user。做完这一步后,user mode的信息就变得可见了,你可以在用户态函数里下断点:kd> bp /p @$proc ntdll!ntcreatefile,或者列出加载模块:lm 等等,就跟普通的用户态程序调试一摸一样。上面那一套步骤很烦,却是步步都不能省,有没有方法简化它呢?如开头所说,windbg是可扩展的,你想到的需求,别人早就想到,并且已经写好工具等你用了,以下命令做完一整套动作:
!bpid
该命令接收process cid作为参数,找到对应的进程并切换进程上下文,跑一会儿,断下来,刷新用户态内容,全部搞定。怎么样,是不是很方便?