2023年1月

有很多因素决定了调试在编程乃至整个计算机世界中的无比重要性。从根本上来说,是计算机系统的设计理念决定了调试的重要地位。现代计算机系统的一个重要设计原则是让硬件在软件的指挥下工作,把灵活和智能留在软件中实现,这同时也把计算机系统的控制权交给了软件。让软件控制强大的计算机硬件是聪明的,运行不同的软件就可以让同一台机器做完全不同的事情也是冒险的,一条错误的指令就可能让系统崩溃甚至导致灾难。 降低风 险的方法是提高软件的质量和我们对软件的控 制力。 但对人 类而言 , 无形 的软件比有形 的硬件更加难以驾驭 ! 软件瑕疵 总是存在但却难以寻找 ; 病毒和恶意软件不请 自来 , 而且挥之不去 ; C P U 不堪重负 , 用 户下达的命令却还得不 到执行。 相对于淳朴的硬件 , 软件更 加复杂 、 多变、 柒鹜、 事故和狡黯 ! 何以应对? 为了控制软件 , 计算机先 驱在一开始设计计算机系统时就设计了各种调试设施 , 包括单步执行和 中 断执行等。 今天我们将这些 功能纳入 到调试器工具中。 一旦进 了调 试器 , 再狂野的软件都会变得服服帖帖 , 所有的宏观结构和微观 细节任由我们审 阅 , 大到整个地址空 间和每个文件 , 小到每一个内存单元和每一条指令 。 如果静态的分析还不够 , 那么我们可 以让 它根据我们的要求来执行 , 执行 一 条指令 , 执行到下一个分支 , 执行到上一级 函数 , 等等。 总之 , 以调试 器 为核心的调 试技术是征服软件和计算机世界的最强大工具 , 其用途很广。

具 体地讲 , 首先 , 调试是 定位软件瑕疵的最直接和最 有效的方法 。 没 有哪个程序员能一下子 写出没有错误 的代码。 而使用 以调试器 为主的调试 工具进行调试是定位瑕疵的最直接方法 , 可以从 问题 的症状入手 , 正 向跟 踪或者反 向追 溯。 对于大多数瑕疵 , 使用合适的调试方法可 以大大提高定 位到问题根源 的效率 。 今天的软件环境在不断向着大型化 、 并行化、 复杂 化 方向发展 , 定位瑕疵的难度也在随之不 断提高。 完全靠读源代码来寻找 b u g 的方法已经很难适应今天的软件发展形势 。 另外 , 枚举和排除法通常 也会因系统中的软硬件模块数量太 多而难 以实施 , 有时候 , 花了几天时间 来做替换仍然找不 到怀疑对 象。

第二 , 调 试可 以帮助程序员提高编写代码 的能力。 因 为调试可 以让程 序员彻底了解程 序的实际执行过程 , 检查与自己设计时的预想是 否一致 , 如果 不一致 , 那 么很可能预示 着有问题存在 , 应该引起重视 。 另外 , 调试 过程可 以让 程序员更好的认识到 提高代码可调试性和代码质量 的重要性 。 从此 , 自觉的改进编码方式 , 合理 添加用来支持调 试的代码 。 编码 和调 试 是程序员 日常工作中的两个最 主要任务 , 这两个任务是相辅相成的 , 编写 具有可 调试性的高质量代码 , 可 以明显提高调试效率 , 节约调试时间。 另 一方面 , 调试可 以让程 序员真切感受程序的实际执行过程 , 反思编码和 设 计中的问题 , 加深对软件和 系统的理解 , 提高对代码的感知力和控制力。

第三 , 调试工具是学习计算机系统和其它软硬件知识 的好帮手 。 通过 软件调试技术的强大观察能力和 断点、 栈回溯 、 跟踪等功能可 以快速的了 解一个软件和系统的模块 、 架构 、 和工 作流程 , 因此是学习 其它软硬件技 术的一 个快速而有效的方 法 。

今天在调试分析一个dump文件时,当我执行了".loadby sos clrjit"指令后,准备分析托管代码的问题,于是我又输入了"!clrstack"扩展指令想看下托管栈,结果Windbg给我如下输出:

0:000> .loadby sos clrjit
0:000> !clrstack
c0000005 Exception in C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\sos.clrstack debugger extension.
      PC: 0c53d0f3  VA: 00000000  R/W: 0  Parameter: 00000000
这还是第一次遇到,当我在执行"!pe"指令时,又有正确输出了

0:000> !pe
Exception object: 1eda1120
Exception type:   System.ExecutionEngineException
Message:          <none>
InnerException:   <none>
StackTrace (generated):
<none>
StackTraceString: <none>
HResult: 80131506
然后在执行"!clrstack",结果也是能正确输出:

0:000> !clrstack
OS Thread Id: 0x6f90 (0)
Child SP       IP Call Site
00efc708 50e51a49 [PrestubMethodFrame: 00efc708] XXXXXX..cctor()
00efc8ec 50e51a49 [GCFrame: 00efc8ec]
00efce54 50e51a49 [PrestubMethodFrame: 00efce54] XXXXXX..ctor()
00efcec4 1d8908f4 XXXXXX(Int64)
00efd208 50e1f066 [DebuggerU2MCatchHandlerFrame: 00efd208]
00efcfc8 50e1f066 [HelperMethodFrame_PROTECTOBJ: 00efcfc8] System.RuntimeMethodHandle.InvokeMethod(System.Object, System.Object[], System.Signature, Boolean)
00efd2a4 78dfc799 System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(System.Object, System.Object[], System.Object[])
00efd2c8 78dfc2aa System.Reflection.RuntimeMethodInfo.Invoke(System.Object, System.Reflection.BindingFlags, System.Reflection.Binder, System.Object[], System.Globalization.CultureInfo)
00efd2fc 1d7b2cf0 *** WARNING: Unable to verify checksum for Adapter.dll
.?A0xc8a1231f.CallFunctionByReflectionInternal(DiyHomePlugin.Plugin, System.String, System.Object ByRef, System.Object[])
00efd364 1d8907eb .XXXX.AppStart(CAdapter*, Int64)
00efd380 1d890731 DomainBoundILStubClass.IL_STUB_ReversePInvoke(Int32, Int64)
后来又多试了几次,发现当加载了sos.dll扩展后,第一个扩展指令(任何指令)都会报错,然后第二条指令不会。看来要研究研究一下,开启Windbg调试自己:

(9ec.4b0): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
sos!GetCMDOption+0x63:
00007ff9`a35ac7e3 488b01          mov     rax,qword ptr [rcx] ds:00000000`00000000=????????????????
0:004> ub
sos!GetCMDOption+0x3f:
00007ff9`a35ac7bf 4889542420      mov     qword ptr [rsp+20h],rdx
00007ff9`a35ac7c4 488bf1          mov     rsi,rcx
00007ff9`a35ac7c7 4d85c0          test    r8,r8
00007ff9`a35ac7ca 743f            je      sos!GetCMDOption+0x8b (00007ff9`a35ac80b)
00007ff9`a35ac7cc 488d5a18        lea     rbx,[rdx+18h]
00007ff9`a35ac7d0 392dd2e20500    cmp     dword ptr [sos!ControlC (00007ff9`a360aaa8)],ebp
00007ff9`a35ac7d6 0f85fe000000    jne     sos!GetCMDOption+0x15a (00007ff9`a35ac8da)
00007ff9`a35ac7dc 488b0dfd6d0500  mov     rcx,qword ptr [sos!g_ExtControl (00007ff9`a36035e0)]
0:004> k
 # Child-SP          RetAddr           Call Site
00 0000000a`05aad2c0 00007ff9`a35973ac sos!GetCMDOption+0x63
01 0000000a`05aad3b0 00007ff9`a86f353b sos!ClrStack+0x21c
02 0000000a`05aad570 00007ff9`a86f3718 dbgeng!ExtensionInfo::CallA+0x233
03 0000000a`05aad630 00007ff9`a86f37f8 dbgeng!ExtensionInfo::Call+0x16c
04 0000000a`05aad830 00007ff9`a86f2689 dbgeng!ExtensionInfo::CallAny+0x78
05 0000000a`05aad870 00007ff9`a872a89b dbgeng!ParseBangCmd+0x4a9
06 0000000a`05aadd30 00007ff9`a872b6ab dbgeng!ProcessCommands+0xa8f
07 0000000a`05aade00 00007ff9`a8685fe8 dbgeng!ProcessCommandsAndCatch+0x8f
08 0000000a`05aade70 00007ff9`a868628f dbgeng!Execute+0x24c
09 0000000a`05aae340 00007ff7`0d0c5c72 dbgeng!DebugClient::ExecuteWide+0x83
0a 0000000a`05aae3a0 00007ff7`0d0c60d5 windbg!ProcessCommand+0x2b2
0b 0000000a`05aae7c0 00007ff7`0d0c7c17 windbg!ProcessEngineCommands+0x185
0c 0000000a`05aaf800 00007ff9`d6a316ad windbg!EngineLoop+0x3e3
0d 0000000a`05aaf840 00007ff9`d7244629 KERNEL32!BaseThreadInitThunk+0xd
0e 0000000a`05aaf870 00000000`00000000 ntdll!RtlUserThreadStart+0x1d

让我们为sos!g_ExtControl变量上的写访问设置一个断点,以查找其归零位置。

0:001> ba w8 sos!g_ExtControl
0:001> g
0:004> k
 # Child-SP          RetAddr           Call Site
00 0000008d`40d2d3f0 00007ff9`a7b1dc6e dbgeng!DebugClient::QueryInterface+0xb
01 0000008d`40d2d420 00007ff9`a7b471d0 sos!ExtQuery+0x2e
02 0000008d`40d2d450 00007ff9`a86f353b sos!ClrStack+0x40
...
0:004> k
 # Child-SP          RetAddr           Call Site
00 0000008d`40d2c080 00007ff9`a7b1dc6e dbgeng!DebugClient::QueryInterface+0xb
01 0000008d`40d2c0b0 00007ff9`a7b1d973 sos!ExtQuery+0x2e
02 0000008d`40d2c0e0 00007ff9`a86f2d25 sos!DebugExtensionInitialize+0x83
03 0000008d`40d2c120 00007ff9`a86f4182 dbgeng!ExtensionInfo::Load+0x48d
04 0000008d`40d2c3f0 00007ff9`a87312f6 dbgeng!ExtensionInfo::CheckAdd+0x6e
05 0000008d`40d2c430 00007ff9`a8731ca8 dbgeng!LoadSOSAndCheckVer+0x36
06 0000008d`40d2c690 00007ff9`a86f00e1 dbgeng!ProcessInfo::LoadClrDebugDllForExt+0x828
07 0000008d`40d2cec0 00007ff9`a7b61004 dbgeng!ExtIoctl+0xc6d
08 0000008d`40d2d410 00007ff9`a7b4722c sos!LoadClrDebugDll+0x24
09 0000008d`40d2d450 00007ff9`a86f353b sos!ClrStack+0x9c
...
0:004> k
 # Child-SP          RetAddr           Call Site
00 0000008d`40d2c0b0 00007ff9`a7b1d98d sos!ExtRelease+0x28
01 0000008d`40d2c0e0 00007ff9`a86f2d25 sos!DebugExtensionInitialize+0x9d
02 0000008d`40d2c120 00007ff9`a86f4182 dbgeng!ExtensionInfo::Load+0x48d
03 0000008d`40d2c3f0 00007ff9`a87312f6 dbgeng!ExtensionInfo::CheckAdd+0x6e
04 0000008d`40d2c430 00007ff9`a8731ca8 dbgeng!LoadSOSAndCheckVer+0x36
05 0000008d`40d2c690 00007ff9`a86f00e1 dbgeng!ProcessInfo::LoadClrDebugDllForExt+0x828
06 0000008d`40d2cec0 00007ff9`a7b61004 dbgeng!ExtIoctl+0xc6d
07 0000008d`40d2d410 00007ff9`a7b4722c sos!LoadClrDebugDll+0x24
08 0000008d`40d2d450 00007ff9`a86f353b sos!ClrStack+0x9c
...
0:004> g
(fe4.f70): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
sos!GetCMDOption+0x63:
00007ff9`a7b5c7e3 488b01          mov     rax,qword ptr [rcx] ds:00000000`00000000=????????????????
0:004> k
 # Child-SP          RetAddr           Call Site
00 0000008d`40d2d360 00007ff9`a7b473ac sos!GetCMDOption+0x63
01 0000008d`40d2d450 00007ff9`a86f353b sos!ClrStack+0x21c
...
sos!ExtQuery:
00007ff9`6a16dc40 48895c2408      mov     qword ptr [rsp+8],rbx
00007ff9`6a16dc45 4889742410      mov     qword ptr [rsp+10h],rsi
00007ff9`6a16dc4a 57              push    rdi
00007ff9`6a16dc4b 4883ec20        sub     rsp,20h
00007ff9`6a16dc4f 33f6            xor     esi,esi
00007ff9`6a16dc51 4c8d0588590900  lea     r8,[sos!g_ExtControl (00007ff9`6a2035e0)]
00007ff9`6a16dc58 488d1571760600  lea     rdx,[sos!GUID_d4366723_44df_4bed_8c7e_4c05424f4588 (00007ff9`6a1d52d0)]
00007ff9`6a16dc5f 4889357a590900  mov     qword ptr [sos!g_ExtControl (00007ff9`6a2035e0)],rsi
00007ff9`6a16dc66 488b01          mov     rax,qword ptr [rcx]
00007ff9`6a16dc69 488bf9          mov     rdi,rcx
00007ff9`6a16dc6c ff10            call    qword ptr [rax] ds:00007ff9`60e018d0={dbgeng!DebugClient::QueryInterface (00007ff9`60fd0240)}
sos!ExtRelease:
00007ff9`6a16ddc0 4053            push    rbx
00007ff9`6a16ddc2 4883ec20        sub     rsp,20h
00007ff9`6a16ddc6 488b0d13580900  mov     rcx,qword ptr [sos!g_ExtControl (00007ff9`6a2035e0)]
00007ff9`6a16ddcd 33db            xor     ebx,ebx
00007ff9`6a16ddcf 48891d02580900  mov     qword ptr [sos!g_ExtClient (00007ff9`6a2035d8)],rbx
00007ff9`6a16ddd6 4885c9          test    rcx,rcx
00007ff9`6a16ddd9 740d            je      sos!ExtRelease+0x28 (00007ff9`6a16dde8)
00007ff9`6a16dddb 488b01          mov     rax,qword ptr [rcx]
00007ff9`6a16ddde ff5010          call    qword ptr [rax+10h] ds:00007ff9`60e01358={dbgeng!DebugClient::Release (00007ff9`60fd0200)}

我们可以看到,IDebugControl2接口获得了两次调用,然后仍在使用被释放的sos!ClrStack函数。另外,我们可以看到sos.dll是第二次加载的。为了找出原因,让我们调试dbgeng!LoadSOSAndCheckVerr函数。在dbgeng!ExtensionInfo::Add方法我们可以看到它正在调用dbgeng!ExtensionInfo::FindByName方法中按名称查找扩展名dbgeng!ExtensionInfo::s_Chain扩展信息链表

0:003> k
 # Child-SP          RetAddr           Call Site
00 0000003a`a963c970 00007ff9`61044168 dbgeng!ExtensionInfo::Add
01 0000003a`a963c9d0 00007ff9`610812f6 dbgeng!ExtensionInfo::CheckAdd+0x54
02 0000003a`a963ca10 00007ff9`61081ca8 dbgeng!LoadSOSAndCheckVer+0x36
03 0000003a`a963cc70 00007ff9`610400e1 dbgeng!ProcessInfo::LoadClrDebugDllForExt+0x828
04 0000003a`a963d4a0 00007ff9`6a601004 dbgeng!ExtIoctl+0xc6d
05 0000003a`a963d9f0 00007ff9`6a5e722c sos!LoadClrDebugDll+0x24
06 0000003a`a963da30 00007ff9`6104353b sos!ClrStack+0x9c
...
0:003> r
rax=0000000000000036 rbx=0000003aa9505490 rcx=0000003aa963ca40
rdx=0000000000000000 rsi=0000000000000037 rdi=0000003aa963ca40
rip=00007ff961043dc5 rsp=0000003aa963c970 rbp=0000000000000000
 r8=0000000000000000  r9=0000003aa963c9f0 r10=0000000000000000
r11=0000003aa963c9e0 r12=0000000000000000 r13=0000000000000000
r14=0000003aa963c9f0 r15=0000003aa963cd01
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
dbgeng!ExtensionInfo::Add+0x95:
00007ff9`61043dc5 e876feffff      call    dbgeng!ExtensionInfo::FindByName (00007ff9`61043c40)
0:003> du @rcx
0000003a`a963ca40  "C:\Windows\Microsoft.NET\Framewo"
0000003a`a963ca80  "rk64\v4.0.30319\SOS.dll"
0:003> u dbgeng!ExtensionInfo::FindByName
dbgeng!ExtensionInfo::FindByName:
00007ff9`61043c40 488bc4          mov     rax,rsp
00007ff9`61043c43 48895808        mov     qword ptr [rax+8],rbx
00007ff9`61043c47 48896810        mov     qword ptr [rax+10h],rbp
00007ff9`61043c4b 48897018        mov     qword ptr [rax+18h],rsi
00007ff9`61043c4f 48897820        mov     qword ptr [rax+20h],rdi
00007ff9`61043c53 4156            push    r14
00007ff9`61043c55 4883ec20        sub     rsp,20h
00007ff9`61043c59 4883cfff        or      rdi,0FFFFFFFFFFFFFFFFh
00007ff9`61043c5d 488bf2          mov     rsi,rdx
00007ff9`61043c60 488be9          mov     rbp,rcx
00007ff9`61043c63 4533f6          xor     r14d,r14d
00007ff9`61043c66 48ffc7          inc     rdi
00007ff9`61043c69 6644393479      cmp     word ptr [rcx+rdi*2],r14w
00007ff9`61043c6e 75f6            jne     dbgeng!ExtensionInfo::FindByName+0x26 (00007ff9`61043c66)
00007ff9`61043c70 488b1d91041c00  mov     rbx,qword ptr [C:\Windows\Microsoft.NET\Framewo"
0000003a`a94fb360  "rk64\v4.0.30319\sos"
0000003a`a94f8830  "dbghelp"
0000003a`a94f8560  "ext"
0000003a`a94f8290  "exts"
0000003a`a94f7fc0  "uext"
0000003a`a94f7cf0  "ntsdexts"

如果没有,它会叫dbgeng!ExtensionInfo::Link方法将新的扩展dll添加到链接列表中。

0:003> r
rax=0000000000000000 rbx=0000003aa5f1e6b0 rcx=0000003aa5f1e6b0
rdx=0000003aa5f1e6d0 rsi=0000000000000037 rdi=0000003aa963ca40
rip=00007ff961043ed0 rsp=0000003aa963c970 rbp=0000000000000000
 r8=0000000000000000  r9=0000000000000000 r10=0000003aa95abd30
r11=0000003aa5f1e6d0 r12=0000000000000000 r13=0000000000000000
r14=0000003aa963c9f0 r15=0000003aa963cd01
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
dbgeng!ExtensionInfo::Add+0x1a0:
00007ff9`61043ed0 e8e7110000      call    dbgeng!ExtensionInfo::Link (00007ff9`610450bc)
0:003> du @rdx
0000003a`a5f1e6d0  "C:\Windows\Microsoft.NET\Framewo"
0000003a`a5f1e710  "rk64\v4.0.30319\SOS.dll"
0:003> !list -x "du poi(@$extret+8)" poi(dbgeng!ExtensionInfo::s_Chain)
0000003a`a5f1e6d0  "C:\Windows\Microsoft.NET\Framewo"
0000003a`a5f1e710  "rk64\v4.0.30319\SOS.dll"
0000003a`a94fb320  "C:\Windows\Microsoft.NET\Framewo"
0000003a`a94fb360  "rk64\v4.0.30319\sos"
0000003a`a94f8830  "dbghelp"
0000003a`a94f8560  "ext"
0000003a`a94f8290  "exts"
0000003a`a94f7fc0  "uext"
0000003a`a94f7cf0  "ntsdexts"

最后是dbgeng!ExtensionInfo::CheckAdd方法调用dbgeng!ExtensionInfo::Load加载sos.dll:

0:003> u
dbgeng!ExtensionInfo::CheckAdd+0x69:
00007ff9`6104417d e816e7ffff      call    dbgeng!ExtensionInfo::Load (00007ff9`61042898)

这是DbgEng.dll中的一个错误。为coreclr添加了一个解决方案。不确定它是否已经进入了完整的框架。可以通过下面的命令之一来解决:

.loadby sos.dll clr

.loadby sos.dll clrjit

.cordll -ve -u -l

首先 , 学习时应该多动手实验 , 拳不离手 , 曲不离口 。 以我个人的经历 为例 , 干编程以来, 我几 乎每天工作时都使用 调试器 。 除了使用它调 试程 序 、 寻 找代码 中 的问题 , 我还使用调试器认识其它软件 、 探 索操作系统 、 观察硬件等等 。

另外 , 学习调试时要 多思 考, 多问为什 么。 这样就可 以慢慢打通未知领域 , 使自己 的理 解不 断深入 , 直到有一 天 , 不 同方向的 探索纷纷会合 , 融汇贯通 , 那么功夫便学成了 。 因为调试技术的广泛 关联性 , 所 以一 旦把调 试技术都搞通 了, 那么对整个计算机系统的 理解也会有一个质的飞跃 。

关于调 试工 具 , 在 Win d o w s 平 台上 , 我 主要使用的是 W i n D B G 。 在Li n u x 平 台中, 使 用 GDB 。 二者都是 以命令方式为主的。 对于 习惯图形界面的很多初学者来说 , 可 能觉得 命令方式不 好学 , 事实上 , 先学会一些 常用 的命令并不难 , 然后 可 以慢慢学习更 多的命 令。 在入 门后 , 应该学一些 调试原理 , 这样 才能深入 了解不 同 调试功能 的长处和 短处 ,更好的应用他们。

调试是一门实践性强涉及面广的综合技能。所以实际动手是重中之重,且我认为,一定要在实际工作中去学习,因为只有在实际工作中你才会遇到各种问题和真实的问题,只有身经百战,才能学到真正的技能。只有自己亲自动手解决问题,才能够记忆深刻,形成自己的知识系统。

下面介绍的知识性信息来自intel IA-32手册(可以在intel的开发手册或者官方网站查到),提示和补充来自学习调试器实现时的总结。

DRx对应任意的一个调试寄存器。LENn对应任意一个长度。Ln对应任意一个局部置位,DR0-DR7可以直接被读写操作(MOV 指令之类的,DRx可以是源操作数也可以是目的操作数)。但是,DRx的访问是需要一定权限的。比如你用MOV操作的话,你需要在实地址模式,系统管理模式(smm)或者在保护模式(CPL设0).如果权限不够,将会在访问DRx的时候尝产生#GP(general-protection)异常。

DRx可以干些什么?

  • 设置发生断点的地址(线性地址)
  • 设置断点的长度(1,2,4个字节,但是执行断点只能是1)
  • 设置在调试异常产生的地址执行的操作
  • 设置断点是否可用
  • 在调试异常产生时,调试条件是否是可用

我们来看看调试寄存器的一些细节信息。下图很重要,后面的介绍都是针对这个图说的。

  • 调试寄存器 DR0-DR3
    这四个寄存器是用来设置 断点地址的。断点的比对在物理地址转换前(异常产生时,还没有将线性地址转换成物理地址)。由于只有0-3四个保存地址的寄存器,所以,硬件断点,在物理上最多只能有4个。
  • 调试寄存器DR4-DR5
    这两个调试寄存器有CR4的DE标记控制。如果DE置位,那么对这两个寄存器的访问会导致#UD异常。如果DE置0,那么他们就被化名为DR6-DR7(你一定会问原来的DR6-DR7怎么办?这个…… 我也不知道。如果你搞明白了,一定记得告诉我)
  • 调试寄存器DR7(控制寄存器)
    DR7是调试控制寄存器。控制方式嘛!继续看:
    1. L0-L3(由第0,2,4,6位控制):对应DR0-DR3,设置断点作用范围,如果被置位,那么将只对当前任务有效。每次异常后,Lx都被清零。
    2. G0-G3(由第1,3,5,7位控制):对应DR0-DR3,如果置位,那么所有的任务都有效。每次异常后不会被清零。以确保对所有任务有效。但是,不知道为什么,我在测试时:
    设置Gn后,不能返回调试异常给调试器(如果你知道为什么,记得告诉我)
    3.
    LE,GE(由第8,9位控制):这个在P6以下系列CPU上不被支持,在升级版的系列里面:如果被置位,那么cpu将会追踪精确的数据断点。LE是局部的,GE是全局的。(到底什么算精确的,我也不清楚,但是,我知道如果设置了这两个,cpu的速度会降低。我在测试中,都没有置位。)
    4. GD(由第13位控制):如果置位,追踪下一条指令是否会访问调试寄存器。如果是,产生异常。在下面的DR6里面,你会知道他还和另外一个标志位有点关系。
    5. R/W0-R/W3:(由第16,17,20,21,24,25,28,29位控制):这个东西的处理有两种情况。
    如果CR4的DE被置位,那么,他们按照下面的规则处理问题:
    00:执行断点
    01:数据写入断点
    10:I/0读写断点
    11:读写断点,读取指令不算
    如果DE置0,那么问题会这样处理:
    00:执行断点
    01:数据写入断点
    10:未定义
    11:数据读写断点,读取指令不算
    6. LEN0-LEN3:(由第18.19.22.23.26.27.30位控制):指定内存操作的大小。
    00:1字节(执行断点只能是1字节长)
    01:2字节
    10:未定义或者是8字节(和cpu的系列有关系)
    11:4字节
  • 调试寄存器DR6(调试状态寄存器)
    这个寄存器主要是在调试异常产生后,报告产生调试异常的相关信息
    1.B0-B3(DR0-DR3):DRx指定的断点在满足DR7指定的条件下,产生异常。那么Bx就置位。但是,有时,即使Ln和Gn置0,也可能产生Bx被置位。这种现象可能这样出现(提示:在p6系列处理器,REP MOVS在不断循环中产生的调试异常需要执行完了才能准确返回给调试进程):DR0的L0,G0都置0(DR0就是一个不能产生异常的断点了),然后在DR0指定的地址是一个REP指令的循环,这样,DR0就可能在这个循环之后的REP指令产生的调试异常中将B0置位
    2.BD:BD需要DR7的GD置位,才有效。BD是在下一条指令要访问到某一个调试寄存器的时候,被置位的。
    3.BS:单步执行模式时,被置位。单步执行是最高权限的调试异常。
    4.BT:在任务切换的时候,被置位。但是必须在被切换去的任务的TSS段里面的T标记被置位的情况下才有效。在控制权被切换过去后,在执行指令前,返回调试异常。但是,需要注意,如果调试程序是一个任务,那么T标记的设置肯定就冲突了。然后,导致了死循环(BT的这些信息都是按照官方资料翻译而来,由于没有实际的操作,肯定会有理解上的出入。如果要深入的话,建议看官方资料).有些调试异常会将B0-B3清零。但是其他的DR6的位是不能被产生异常的进程清零的。每次调试异常返回后,调试进程都会先将DR6清零,再按照情况设置。以免产生不必要的错误。

对齐问题和64位处理器

对齐问题

这个问题是来源于LENn的设置,如果设置4字节,那么必须4字节对齐。例如:我们下4字节的断点,那么DRx需要是A0000/A0004/A0008这样的地址上。I/O地址是零扩展的(这个……也许意味着必须完全对齐)。因为,intel在比对地址时:用LENn的值去覆盖DRx里面保存的地址的低位。你可以想到,不对齐会有什么后果了吧。注意:执行断点只能是1字节。
再用图片解释下(当然,图片来自intel官方资料):

 

在64位处理器下

调试寄存器当然也是64位的。在操作过程中,写入,前面32位被置零。读取:只返回后32位。MOV DRx操作,前32位被忽略。DR6-DR7的高32位被保留。置零。如果置位,会产生#GP异常。8字节的读写断点完全被支持。最后,还是给个图片(64位处理器的布局):

最后需要提醒一个小问题:数据写入断点设置后。是在原数据被修改后,才产生调试异常。所以,返回异常时,原有数据已经被修改。如果想保留原有数据,需要自己提前保存对应地址的数据。

转自:https://blog.csdn.net/whatday/article/details/8621410

微软给我们提供了一些很好的程序,比如资源监视器,可以从这个软件里获取分析windows的自身的一些性能数据,比如CPU、内存、磁盘数据、文件读写、进程线程等,他具体怎么实现呢,今天这天文章就带你去获取其真实的原理。

1.分析

打开windows任务栏管理器,在其性能选项里,可以看到性能监控的一些机器性能图表

 

这个只是一些概要数据,如果要看详细的内容,可以点击左下角的“打开资源监视器”,会自己开启一个进程perfmon.exe的进程,这个进程界面会显示详细的资源信息

 

 

 

 

我们可以看到每个进程打开了什么文件、读写了什么磁盘数据、以及访问了什么网络的IP都有详细的信息,这个进程既没有文件过滤驱动也没有网络驱动他是如何实现去获取这些详细的信息呢,下面我们来具体分析下。

打开这个软件所在的目录,可以看到一些对应的perfmon.exe 、perfnet.dll、perfDisk.dll、perfos.dll、perfpro.dll进程和模块

 

 

 

IDA打开perfmon.exe看看其进程一些函数

 

 

 

 也只是一些UI相关的函数,说明核心功能并不在这里exe里 ,在继续换一个,用IDA打开perdisk.dll这个模块

 

 

 QQ图片20170616162416.png

 

模块里有一些比较明显的函数CollectDiskObjectData、CollectPDiskObjectData、CollectLDiskObjectData等函数,选择一个函数用windbg调试器去下断点

 

 QQ图片20170616162449.png

 

当第一放过运行时,调试器立马就停了下来,看堆栈区域

 

 QQ图片20170616162548.png

 

大概是这样的

 

# RetAddr           : Args to Child                                                           : Call Site
00 00007ffc`2d132060 : ffffffff`feced300 00000000`00000010 00000000`00000014 00000000`00000000 : perfdisk!CollectDiskObjectData
01 00007ffc`2d1316b5 : 00000000`00007f88 00000000`00000000 00000000`00000000 00000000`00000000 : ADVAPI32!QueryExtensibleData+0x540
02 00007ffc`29a92a99 : 0000021e`67749c66 00000000`000602ff 000000fc`368ff728 00007ffc`ffffffff : ADVAPI32!PerfRegQueryValue+0x325
03 00007ffc`29a9204d : ffffffff`80000004 000000fc`368ff7a4 0000021e`6b4904f0 000000fc`368ff820 : KERNELBASE!MapPredefinedHandleInternal+0x8e9
04 00007ffc`25f063ec : ffffffff`80000004 0000021e`6b490490 0000021e`00008000 000000fc`368ff7a4 : KERNELBASE!RegQueryValueExW+0xed
05 00007ffc`25f056ad : 0000021e`6b490490 0000021e`6ad74c10 00000000`00008000 00000000`00000000 : pdh!GetSystemPerfData+0x9c
06 00007ffc`25f054c9 : 01d2e6b6`135f9140 000000fc`368ff8b8 00000000`00000000 01d2e673`053c5140 : pdh!GetQueryPerfData+0xcd
07 00007ffc`01548566 : 00000000`00000000 00000000`00000003 00000000`00000000 00000000`000002b0 : pdh!PdhCollectQueryData+0x59
08 00007ffc`2ad58364 : 0000021e`6adc1b00 00000000`00000000 00000000`00000000 00000000`00000000 : wdc!WdcTraceControl::QueryThread+0x1a6
09 00007ffc`2d2370d1 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x14
0a 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21

 

堆栈看该函数被调用是从wdc!WdcTraceControl::QueryThread这个函数过来的,看样子是一个单独的数据线程,是wdc.dll这个模块里的函数,继续用IDA打开wdc这个模块。

 

 QQ图片20170616162649.png

 

QQ图片20170616162701.png

 

 

 

查看WdcTraceControl::QueryThread这个函数实现,确实调用了一些监控实例的Query方法,这里我们可以确认wc.dll这个模块是核心功能所在的模块,现在可以分析下是怎么开启这个线程的,开启前做了哪些工作。

 

 QQ图片20170616162742.png

 

发现只有一处调用,就是WdcTraceControl::Start函数调用了这个函数开启线程

 

 QQ图片20170616162753.png

 

继续往上翻阅WdcTraceControl::Start函数实现

 

 QQ图片20170616162806.png

 

QQ图片20170616162818.png

 

 

 

有一个WdcTraceControl::TraceStart 函数,进入该函数 

 

 QQ图片20170616162927.png

 

QQ图片20170616162938.png

 

QQ图片20170616162949.png

 

QQ图片20170616163000.png

 

 

 

 发现该函数调用OpenTrace、StartTrace、EnableTraceEx、processTrace这些函数,从EnableTraceEx的参数ThreadPoolGuid、PsProvGuid、DiskProvGuid、FileProvGuid、NetProvGuid可以判断这些函数就是核心功能,查看微软的CSDN终于发现了秘密,原来这些函数是 微软事件诊断函数(ETW),其中OpenTrace的里的有一个参数是事件的回掉接收函数,我们看到的WdcTraceControl::CallbackEvent这个函数就是,下个断点,确实断了下来

 

 QQ图片20170616163123.png

 

QQ图片20170616163135.png

 

 

 

就是WdcTraceControl::TraceThread线程中的ProcessTrace函数处理获取了内核日志数据然后调用了设置的回调函数WdcTraceControl::CallbackEvent去处理,为了进一步验证,翻阅该回调函数的实现

 

 QQ图片20170616163224.png

 

可以知道里面处理了各种过来的数据包括网络、磁盘、cpu、内存、线程、进程日志信息,去写个demo实例验证我们的结果。

 

2.代码demo

 

#include "stdafx.h"
#define INITGUID  //Include this #define to use SystemTraceControlGuid in Evntrace.h.
#include <Windows.h>
#include <wmistr.h>
#include <evntrace.h>
#include <evntcons.h>
#include <strsafe.h>
#pragma comment(lib,"Advapi32.lib")
#define LOGFILE_PATH    L"kernellogfile.etl"
#define ETL_FILE        L"G:\\OpenSource\\GitHub\\WindowsSDK7-Samples\\winbase\\Eventing\\EtwConsumer\\Output\\16.pdf.etl"
#define __REAL_TIME_MODE
/* 不同类型的GUID,从MSDN手册中找,固定的 */
DEFINE_GUID ( /* 3d6fa8d0-fe05-11d0-9dda-00c04fd7ba7c */
ProcessGuid,
0x3d6fa8d0,
0xfe05,
0x11d0,
0x9d, 0xda, 0x00, 0xc0, 0x4f, 0xd7, 0xba, 0x7c
);
ULONG WINAPI BufferCallback( __in PEVENT_TRACE_LOGFILE LogFile )
{
printf( "BufferCallback!\n" );
return TRUE;
}
/* 可以调用SetTraceCallback 设置单独事件GUID的回调函数,即使如此,EventCallback仍然会收到所有的事件 */
VOID WINAPI EventRecordCallback( __in PEVENT_RECORD Event )
{
if ( IsEqualGUID( Event->EventHeader.ProviderId, ProcessGuid ) )
{
/* 需要解析数据格式 */
printf( "EventRecordCallback  ProcessGuid!\n" );
}
printf( "EventRecordCallback!\n" );
}
VOID WINAPI EventCallback( PEVENT_TRACE pEvent )
{
printf( "EventCallback!\n" );
}
VOID EventConsumer()
{
TRACEHANDLE hTrace = NULL;
EVENT_TRACE_LOGFILE traceFile;
#ifdef __REAL_TIME_MODE
traceFile.LogFileName = NULL;
traceFile.LoggerName  = KERNEL_LOGGER_NAME;
traceFile.ProcessTraceMode = PROCESS_TRACE_MODE_REAL_TIME | PROCESS_TRACE_MODE_EVENT_RECORD;
#else
traceFile.LogFileName = ETL_FILE;
traceFile.LoggerName = NULL;
traceFile.ProcessTraceMode = PROCESS_TRACE_MODE_EVENT_RECORD;
#endif
traceFile.BufferCallback        = BufferCallback;
traceFile.EventCallback         = EventCallback;
if ( traceFile.ProcessTraceMode & PROCESS_TRACE_MODE_EVENT_RECORD )
traceFile.EventRecordCallback   = EventRecordCallback;
traceFile.Context = NULL;
hTrace = OpenTrace( &traceFile );
if ( hTrace == (TRACEHANDLE)INVALID_HANDLE_VALUE || hTrace == 0x0 )
return ;
ULONG status = ProcessTrace( &hTrace, 1, NULL, NULL );
}
void EventController(void)
{
ULONG status = ERROR_SUCCESS;
TRACEHANDLE SessionHandle = 0;
EVENT_TRACE_PROPERTIES* pSessionProperties = NULL;
ULONG BufferSize = 0;
BufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(KERNEL_LOGGER_NAME) + sizeof(LOGFILE_PATH) ;
pSessionProperties = (EVENT_TRACE_PROPERTIES*) malloc(BufferSize);    
if (NULL == pSessionProperties)
{
wprintf(L"Unable to allocate %d bytes for properties structure.\n", BufferSize);
goto cleanup;
}
ZeroMemory(pSessionProperties, BufferSize);
pSessionProperties->Wnode.BufferSize    = BufferSize;
pSessionProperties->Wnode.Flags         = WNODE_FLAG_TRACED_GUID;
pSessionProperties->Wnode.ClientContext = 1; //QPC clock resolution
pSessionProperties->Wnode.Guid          = SystemTraceControlGuid; 
pSessionProperties->EnableFlags         = EVENT_TRACE_FLAG_PROCESS;                 // 关注事件
#ifdef __REAL_TIME_MODE
pSessionProperties->LogFileMode         = EVENT_TRACE_REAL_TIME_MODE;               // EVENT_TRACE_USE_PAGED_MEMORY 该标识在win7上会导致失败
#else
pSessionProperties->LogFileMode         = EVENT_TRACE_FILE_MODE_CIRCULAR;
#endif
pSessionProperties->MaximumFileSize     = 5;  // 5 MB
pSessionProperties->LoggerNameOffset    = sizeof(EVENT_TRACE_PROPERTIES);
pSessionProperties->LogFileNameOffset   = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(KERNEL_LOGGER_NAME); 
#ifndef __REAL_TIME_MODE            // 也可在RealTime模式下开启,但是没必要RealTime都记录到文件
StringCbCopy((LPWSTR)((char*)pSessionProperties + pSessionProperties->LogFileNameOffset), sizeof(LOGFILE_PATH), LOGFILE_PATH);
#endif
status = StartTrace((PTRACEHANDLE)&SessionHandle, KERNEL_LOGGER_NAME, pSessionProperties);
if (ERROR_SUCCESS != status)
{
if (ERROR_ALREADY_EXISTS == status)
wprintf(L"The NT Kernel Logger session is already in use.\n");
else
wprintf(L"EnableTrace() failed with %lu\n", status);
goto cleanup;
}
EventConsumer();
wprintf(L"Press any key to end trace session ");
getchar();
cleanup:
if (SessionHandle)
{
status = ControlTrace(SessionHandle, KERNEL_LOGGER_NAME, pSessionProperties, EVENT_TRACE_CONTROL_STOP);
if (ERROR_SUCCESS != status)
wprintf(L"ControlTrace(stop) failed with %lu\n", status);
}
else
{
/* 开启会话后,若不关闭,即使进程退出,依然会保持开启状态,单独关闭可使用如下方式 */
status = ControlTrace(NULL, KERNEL_LOGGER_NAME, pSessionProperties, EVENT_TRACE_CONTROL_STOP);
if (ERROR_SUCCESS != status)
wprintf(L"ControlTrace(stop) failed with %lu\n", status);
}
if (pSessionProperties)
free(pSessionProperties);
}
int _tmain(int argc, _TCHAR* argv[])
{
EventController();
//EventConsumer();
while (true)
{
Sleep(300000);
}
return 0;
}

 

运行后,当有数据来时就会进入我们设置的回调函数

 

 QQ图片20170616163401.png

 

          此处我们大概知道了该软件的实现原理,剩下具体的对数据的内容的解析不再继续赘述,读者可以自行去研究,从这篇文章里我们知道了微软提供了一套内核事件诊断的函数方便我们去分析系统一些的性能,我们不光可以用这些函数去分析诊断系统,我们可以充分利用这些函数去实现一些我们想要的功能,大家可以自行去发挥。

转自:https://bbs.pediy.com/thread-218627.htm