总目录
1. WinDbg概述
2. WinDbg主要功能
3. WinDbg程序调试示例
4. CPU寄存器及指令系统
5. CPU保护模式概述
6. 汇编语言不等于CPU指令
7. 用WinDbg观察托管程序架构
8. Windows PE/COFF文件格式简述
9. 让WinDbg自动打开DotNet Runtime源程序
10. WinDbg综合实战
前言
本篇是WinDbg系列文章的收官之作。
通过前面的9篇文章,我们完成了WinDbg基本使用的介绍,尤其第1篇《1 WinDbg概述》详细地说明了WinDbg的下载、安装、如何列出WinDbg所有命令、如何通过帮助系统学习WinDbg命令。可以说,只要掌握了第一篇的内容,就掌握了如何自学WinDbg。
作为本系列文章最后一篇,以后会不断补充新的实战项目,请对WinDbg感兴趣的同学注意收藏,以便有更新时能及时得到提醒。
备注:阅读时请注意观察右侧的目录,便于即时找到新增实战项目。
本文当前实战项目包括:
- 《C#设置Main断点》
- 《wt命令详解》
- 《跟踪JIT本机代码生成》
C#设置Main断点
概述: 都知道托管代码映像文件中并没有本机代码,而是MSIL中间语言代码,而WinDbg只能调试本机代码。托管程序运行时,需要.net 运行时(CLR)参与,即时(Just-in-time JIT)将MSIL编译成本机代码,然后再提交给CPU运行。这种机制下,WinDbg的使用受到了诸多限制,比如无法给Main方法入口点下断点。本项目以实战记录方式,演示了如何解决这个问题。
源代码
因为.NET Framework已经过时,所以这次使用的被调试程序依旧使用 .NET 8.0框架。
namespace BasicGrammar;
class Program
{
static void Main()
{
int sum = 0;
for(int i = 1; i <= 100; i++)
{
sum += i;
Console.WriteLine(sum);
}
Person person = new Person();
person.age = 10;
Console.WriteLine(person.GetAge());
Console.ReadKey();
}
}
class Person
{
public int age;
public string name;
public int GetAge() { return age; }
}
托管代码加载
如果使用.NET Framework,那么生成的exe文件就是托管代码程序集(Assembly);但如果使用的是.NET 5.0+,则实际托管代码已经是跨平台代码了,程序集则是.dll文件。比如针对我们这个示例,真正的程序集文件是Core.dll。
微软规定,运行.dll程序需要通过.NET CLI(Common language interface),在Windows环境下,cli命令通过dotnet参数方式提供,比如 dotnet core.dll,如下图所示:
至于同目录下的core.exe文件,其实它是应用程序宿主(AppHost)文件,其主要目的是为.dll文件运行提供一种Windows环境兼容运行方式,也就是如果双击core.exe,那么它会在Windows环境下为core.dll的运行初始化运行环境,然后再将core.dll加载进来,最后将控制权移交给core.dll。
不过,WinDbg是Windows应用程序调试器,它并不能直接调试托管dll,所以我们只能通过core.exe这个二传手的帮助,才能在WinDbg中调试core.dll。
下面我们正式开始调试操作。
首先用WinDbg加载core.exe。主界面显示如下:
ModLoad: 00007ff7`c6d00000 00007ff7`c6d29000 apphost.exe
ModLoad: 00007ff9`bc290000 00007ff9`bc4a7000 ntdll.dll
ModLoad: 00007ff9`ba2c0000 00007ff9`ba384000 C:\Windows\System32\KERNEL32.DLL
ModLoad: 00007ff9`b9ab0000 00007ff9`b9e5c000 C:\Windows\System32\KERNELBASE.dll
ModLoad: 00007ff9`bb0e0000 00007ff9`bb28e000 C:\Windows\System32\USER32.dll
ModLoad: 00007ff9`b9940000 00007ff9`b9966000 C:\Windows\System32\win32u.dll
ModLoad: 00007ff9`bc220000 00007ff9`bc249000 C:\Windows\System32\GDI32.dll
ModLoad: 00007ff9`b9680000 00007ff9`b9799000 C:\Windows\System32\gdi32full.dll
ModLoad: 00007ff9`b9a10000 00007ff9`b9aaa000 C:\Windows\System32\msvcp_win.dll
ModLoad: 00007ff9`b97a0000 00007ff9`b98b1000 C:\Windows\System32\ucrtbase.dll
ModLoad: 00007ff9`bb390000 00007ff9`bbbec000 C:\Windows\System32\SHELL32.dll
ModLoad: 00007ff9`ba510000 00007ff9`ba5c2000 C:\Windows\System32\ADVAPI32.dll
ModLoad: 00007ff9`bad80000 00007ff9`bae27000 C:\Windows\System32\msvcrt.dll
ModLoad: 00007ff9`bbbf0000 00007ff9`bbc9a000 C:\Windows\System32\sechost.dll
ModLoad: 00007ff9`b9970000 00007ff9`b9998000 C:\Windows\System32\bcrypt.dll
ModLoad: 00007ff9`ba390000 00007ff9`ba4a5000 C:\Windows\System32\RPCRT4.dll
(6010.2a28): Break instruction exception - code 80000003 (first chance)
ntdll!LdrpDoDebuggerBreak+0x30:
00007ff9`bc36bed4 cc int 3
以上信息显示,WinDbg调试Core.exe时,加载了很多模块。
最先被加载的是apphost.exe,它其实就是Core.exe。
Visual Studio编译C#项目时,会在Bin文件夹生成dll和exe文件,同时还会在Obj文件夹生成一个apphost.exe文件。如果用文件比较工具比较,会发现apphost.exe和core.exe是完全相同的。
另外也可以使用WinDbg的 lmvm apphost 命令列出apphost.exe的详细信息(如以下列表所示)。
0:000> lmvm apphost
Browse full module list
start end module name
00007ff7`c6d00000 00007ff7`c6d29000 apphost C (private pdb symbols) C:\ProgramData\Dbg\sym\apphost.pdb\5633DAB747FE452D91289F0AE5A53DEB1\apphost.pdb
Loaded symbol image file: E:\test\a\Core\bin\Debug\net8.0\Core.exe
Image path: apphost.exe
Image name: apphost.exe
Browse all global symbols functions data
Timestamp: Thu Feb 15 02:01:36 2024 (65CD0000)
CheckSum: 00000000
ImageSize: 00029000
File version: 1.0.0.0
Product version: 1.0.0.0
File flags: 0 (Mask 3F)
File OS: 4 Unknown Win32
File type: 1.0 App
File date: 00000000.00000000
Translations: 0000.04b0
Information from resource tables:
CompanyName: Core
ProductName: Core
InternalName: Core.dll
OriginalFilename: Core.dll
ProductVersion: 1.0.0
FileVersion: 1.0.0.0
FileDescription: Core
LegalCopyright:
可以发现 Loaded symbol image file 是 E:\test\a\Core\bin\Debug\net8.0\Core.exe,而该模块实际指向的内部名则是core.dll:
InternalName: Core.dll
OriginalFilename: Core.dll
也就是说,无论Core.exe还是apphost.exe,实际指向的都是Core.dll。
备注:以上关于exe的说明与本主题无关,Just for information. 下面我们进入正题。
Windows加载完apphost.exe映像文件以后,就会加载ntdll.dll文件,并将控制权转移给ntdll。
ntdll在完成了所有静态链接模块的装载以后,如果检测到有调试器附加,就会调用ntdll!LdrpDoDebuggerBreak()方法,命中 int 3断点,该断点会被WinDbg捕获,这就是ntdll初始断点。
注意:初始断点命中时,被加载的exe及dll的初始化尚未完成,间接依赖项更尚未被加载,也就是说,此时WinDbg还不知道我们加载的是托管代码,也不知道我们的程序入口是Main,更不知道未来需要CLR支持。所以此时根本无法给Main方法下断点。
不过,WinDbg通过使用微软的sos.dll扩展命令,则可以对托管代码进行调试。比如在加载了sos.dll的前提下,可以使用如下命令为Main方法设置一个延迟断点:
0:000> !bpmd Core.dll Program.Main
这种命令之所以可用,原理就是这个命令告诉WinDbg,在发现CLR即时编译了Core!Program.Main()方法以后,用cch将Main入口的第一个字节替换,这个cch就是int 3软件中断的机器码。
不过,如欲使用bpmd扩展命令,首先必须加载sos.dll。
如果使用WinDbg调试托管代码,初始断点命中以后,如果继续输入g命令执行,WinDbg会自动加载sos.dll,后续还会加载coreclr.dll及crljit.dll等,直到所有该加载的模块都加载完毕以后,clrjit会即时编译Main方法,然后再经过一系列初始化以后,最终控制权才会交给Main入口。所以,初始断点以后,只要输入g命令,sos扩展就会被加载。
当然,在初始断点命中时,我们也可以使用.load或.loadby命令手工加载sos.dll。
不过,此次我们打算走点儿弯路,不使用 .load 或 .loadby,而是通过g命令让WinDbg自动加载。
问题是,如果自动加载,程序控制权交给Main方法以后并不会出现断点,程序会继续执行Main方法,对于我们的示例程序,则会直接执行完成,而无法断到Main入口。
为解决这个问题,我们需要使用sxe命令。
sxe断点
WinDbg提供了sx系列命令,该系列命令用于设置调试器对调试事件的响应。
调试器可以接收多种调试事件,比如创建新进程会产生cpr(create process)事件,模块加载完毕时会产生ld(Load module)事件。
可以使用sx命令(不带任何参数)显示所有WinDbg调试事件,如下面列表所示(我在部分典型常用事件后面增加了注释):
0:000> sx
ct - Create thread - ignore //发生创建线程事件时,WinDbg忽略之(当作什么都未发生)
et - Exit thread - ignore
cpr - Create process - ignore
epr - Exit process - ignore
ld - Load module - output //当模块被加载时,在命令窗口显示信息
ud - Unload module - ignore
ser - System error - ignore
ibp - Initial breakpoint - ignore
iml - Initial module load - ignore
out - Debuggee output - output
av - Access violation - break - not handled //当发生越权访问时,中断到调试器且异常被设置成尚未被处理
asrt - Assertion failure - break - not handled
aph - Application hang - break - not handled
bpe - Break instruction exception - break
bpec - Break instruction exception continue - handled
eh - C++ EH exception - second-chance break - not handled
clr - CLR exception - second-chance break - not handled
clrn - CLR notification exception - second-chance break - handled
cce - Control-Break exception - break
cc - Control-Break exception continue - handled
cce - Control-C exception - break
cc - Control-C exception continue - handled
dm - Data misaligned - break - not handled
dbce - Debugger command exception - ignore - handled
gp - Guard page violation - break - not handled
ii - Illegal instruction - second-chance break - not handled
ip - In-page I/O error - break - not handled
dz - Integer divide-by-zero - break - not handled
iov - Integer overflow - break - not handled
ch - Invalid handle - break
hc - Invalid handle continue - not handled
lsq - Invalid lock sequence - break - not handled
isc - Invalid system call - break - not handled
3c - Port disconnected - second-chance break - not handled
svh - Service hang - break - not handled
sse - Single step exception - break
ssec - Single step exception continue - handled
sbo - Security check failure or stack buffer overrun - break - not handled
sov - Stack overflow - break - not handled
vs - Verifier stop - break - not handled
vcpp - Visual C++ exception - ignore - handled
wkd - Wake debugger - break - not handled
rto - Windows Runtime Originate Error - second-chance break - not handled
rtt - Windows Runtime Transform Error - second-chance break - not handled
wob - WOW64 breakpoint - break - handled
wos - WOW64 single step exception - break - handled
也可以使用如下四个sx命令设置WinDbg对调试事件的相应方式,也可以使用sxr命令重置回默认状态:
sxe, sxd, sxi, sxn
这四个命令的主要用法如下表所示:
本次,我们就将使用sxe命令,让WinDbg加载完Core.dll以后暂停到调试器(之所以选择Core而不是sos,是因为加载Core的时机比sos更晚,如果选择System.Runtime,则断下时机会更晚,但选择sos也没有问题)。
0:000> sxe ld:core.dll
0:000> sx
ct - Create thread - ignore
et - Exit thread - ignore
cpr - Create process - ignore
epr - Exit process - ignore
ld - Load module - break
(only break for core.dll)
...
下完sxe断点以后,我们就可以输入g命令了:
0:000> g
ModLoad: 00007ff9`bad40000 00007ff9`bad71000 C:\Windows\System32\IMM32.DLL
ModLoad: 00007ff9`702c0000 00007ff9`70319000 C:\Program Files\dotnet\host\fxr\8.0.3\hostfxr.dll
ModLoad: 00007ff9`332c0000 00007ff9`33324000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\hostpolicy.dll
ModLoad: 00007ff8`b1240000 00007ff8`b1726000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\coreclr.dll
ModLoad: 00007ff9`ba050000 00007ff9`ba1f5000 C:\Windows\System32\ole32.dll
ModLoad: 00007ff9`ba9b0000 00007ff9`bad38000 C:\Windows\System32\combase.dll
ModLoad: 00007ff9`ba890000 00007ff9`ba967000 C:\Windows\System32\OLEAUT32.dll
ModLoad: 00007ff9`b98c0000 00007ff9`b993b000 C:\Windows\System32\bcryptPrimitives.dll
(6010.2a28): Unknown exception - code 04242420 (first chance)
ModLoad: 00007ff8`b02e0000 00007ff8`b0f6c000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\System.Private.CoreLib.dll
ModLoad: 00007ff9`32570000 00007ff9`32729000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\clrjit.dll
ModLoad: 00007ff9`b8680000 00007ff9`b8698000 C:\Windows\SYSTEM32\kernel.appcore.dll
ModLoad: 00000000`00610000 00000000`00618000 E:\test\a\Core\bin\Debug\net8.0\Core.dll
ntdll!NtMapViewOfSection+0x14:
00007ff9`bc3304a4 c3 ret
上面倒数第3行显示,已经加载core.dll。为了确认此时已经加载了sos.dll,我们可以使用.chain元命令:
0:000> .chain
Extension DLL chain:
sos: image 8.0.510501, API 2.0.0, built Tue Feb 6 06:03:41 2024
[path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2402.24001.0_x64__8wekyb3d8bbwe\amd64\winext\sos\sos.dll]
CLRComposition: image 10.0.27553.1004, API 0.0.0,
[path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2402.24001.0_x64__8wekyb3d8bbwe\amd64\winext\CLRComposition.dll]
dbghelp: image 10.0.27553.1004, API 10.0.6,
[path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2402.24001.0_x64__8wekyb3d8bbwe\amd64\dbghelp.dll]
exts: image 10.0.27553.1004, API 1.0.0,
[path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2402.24001.0_x64__8wekyb3d8bbwe\amd64\WINXP\exts.dll]
uext: image 10.0.27553.1004, API 1.0.0,
[path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2402.24001.0_x64__8wekyb3d8bbwe\amd64\winext\uext.dll]
ntsdexts: image 10.0.27553.1004, API 1.0.0,
[path: C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2402.24001.0_x64__8wekyb3d8bbwe\amd64\WINXP\ntsdexts.dll]
从以上输出可以发现,列表中第一个扩展命令模块就是sos.dll。
接下来就可以使用sos扩展命令bpmd给Main方法下断点了(注意Program.Main必须严格区分大小写):
0:000> !bpmd Core.dll Program.Main
Adding pending breakpoints...
0:000> g
界面显示,已经成功添加了延迟断点(pending breakpoints),完整界面截图如下:
此时再次输入g命令运行,断点再次被击中以后,打开反汇编窗口(Disassembly),可以看出,程序刚好断在了Main入口ProLogue部分,如下图所示。
coreclr!RunMain
此外,也可以将断点下到coreclr!RunMain方法中,该方法的第一个参数就是指向Main方法的MethodDesc的指针。
0:000> x *!runmain
00007ffc`34692ef4 coreclr!RunMain (class MethodDesc *, int *, class PtrArray **, short)
0:000> bp 00007ffc`34692ef4
0:000> g
Breakpoint 0 hit
coreclr!RunMain:
00007ffc`34692ef4 48895c2418 mov qword ptr [rsp+18h],rbx ss:00000000`001cf030=00007ffbd4c600c0
0:000> !dumpmd 00007ffbd4c600c0
Method Name: BasicGrammar.Program.Main()
Class: 00007ffbd4c4fb40
MethodTable: 00007ffbd4c600e8
mdToken: 0000000006000001
Module: 00007ffbd4c3e0a0
IsJitted: no
Current CodeAddr: ffffffffffffffff
Version History:
ILCodeVersion: 0000000000000000
ReJIT ID: 0
IL Addr: 00000000021b2050
CodeAddr: 0000000000000000 (MinOptJitted)
NativeCodeVersion: 0000000000000000
找到Method Table以后,就可以使用诸多手段继续调试了,不过这不是本次实战的目标,所以只是点到为止。
断点命令
最后,我们再补充一下WinDbg的断点命令。
WinDbg主要有bp, bu, bm和ba四个断点,前三个称为代码断点或软件断点,最后一个ba则称为CPU断点或硬件断点。
软件断点的基本原理是:调试器首先将需要设置断点的位置处的指令保存到调试器的临时区域,然后用int 3指令(十六进制cc)替换掉原位置的第一个字节。此后,程序一旦运行到该位置,就会触发int 3陷阱,于是就中断到了调试器。当继续运行时,调试器会首先将之前保存的指令替换回正常指令,然后重新从该位置继续运行。
硬件断点的原理与软件断点不同。硬件断点不需要修改程序代码,而是告诉CPU去监控某些地址和事件,一旦设置的地址发生了设置的事件,就由CPU直接触发中断。CPU共有8个调试寄存器,分别为DR0~DR7,其中DR0 ~ DR3可以用来设置监控地址(内存或端口地址),用DR6和DR7设置读、写或执行事件。一旦CPU访问以设定事件方式访问了设定的地址,CPU就会自动产生异常,从而产生断点。
硬件断点功能十分强大,我们可以监控某段内存,一旦有数据改变立即中断。再比如针对ROM程序是无法设bp/bu/bm断点的,因为存储器是只读的,无法写入int 3,这种情况下就只能使用 ba 断点。我们也将在第三个项目《跟踪JIT本机代码生成》中具体演示 ba 命令的使用。
至于bp, bu, bm之间的区别,这里仅做简单解释:
- bp:针对地址下断点;
- bu:针对符号下断点;比如针对Main方法下断点,此后WinDbg会实时监控Main方法的入口地址。假设clr或操作系统后来将Main移动到了另外的地址,则bu下的断点地址也会跟随修改,依旧可以断到Main中;
- bm:可以使用通配符下符号断点,比如bm Mai*会给所有以Mai开头的方法下断点,很少用到。
本小节介绍的断点知识都比较粗线条,如果需要详细了解,可以看帮助:
以上,我们完成了第一个实战项目,下面开始讲解第二个实战项目。
wt命令详解
WinDbg的wt命令在分析复杂的程序调用结构时非常有用,它可以用直观方式显示程序的所有调用过程。
wt命令用途
有时被调试程序会调用子程序,而子程序可能又会嵌套调用下级子程序,有时候调用关系特别复杂,如果单步跟踪到底会非常困难。比如在《3. WinDbg程序调试》的故障描述部分,我们曾经使用过这个命令,但当时我们并未对此命令的使用方法进行详细说明。这篇文章中演示了C#的一句Console.WriteLine(“Hello”),实际底层调用总计需要13109条CPU指令,总计涉及到157个子程序,总计发生了397次调用。更复杂的函数甚至会发生上万次的子程序调用,如ucrtbased模块的exit()方法。这种情况下,使用单步跟踪几乎是不可能的。
此时,我们可以借助wt命令来帮忙。
示例代码
由于C#程序经过JIT编译后缺少调试符号,所以本次我们以c++程序作为演示,依旧使用Visual Studio 2022,编译成Debug模式x64位格式,可执行文件名为cpp.exe,程序源代码如下:
extern "C"
{
int MyReturn(int x)
{
return x;
}
int addSomeInt(int x, int y)
{
int x1 = x + y;
int x2 = MyReturn(x1);
x2 = MyReturn(x2);
x2 = MyReturn(x2);
return x2;
}
int main()
{
int x = addSomeInt(3, 4);
x = addSomeInt(5, 6);
x = addSomeInt(10, 20);
}
}
这段程序本身没有任何现实意义,仅仅为了演示wt命令。
加载过程
首先用WinDbg加载cpp.exe文件。
加载完成后,程序断在ntdll初始断点处:
ModLoad: 00007ff6`8b510000 00007ff6`8b535000 cpp.exe
ModLoad: 00007fff`e6670000 00007fff`e6887000 ntdll.dll
ModLoad: 00007fff`e4b10000 00007fff`e4bd4000 C:\Windows\System32\KERNEL32.DLL
ModLoad: 00007fff`e3d10000 00007fff`e40bd000 C:\Windows\System32\KERNELBASE.dll
ModLoad: 00007fff`c7d70000 00007fff`c7d9e000 C:\Windows\SYSTEM32\VCRUNTIME140D.dll
ModLoad: 00007fff`6b5b0000 00007fff`6b7d2000 C:\Windows\SYSTEM32\ucrtbased.dll
ModLoad: 00000000`00920000 00000000`00b42000 C:\Windows\SYSTEM32\ucrtbased.dll
(6668.6880): Break instruction exception - code 80000003 (first chance)
ntdll!LdrpDoDebuggerBreak+0x30:
00007fff`e674bed4 cc int 3
由于这次是本机代码,所以加载了cpp.exe以后,其符号信息已经被同时加载到了WinDbg(C:\ProgramData\Dbg\sym\cpp.pdb\70DC4F4167F14F93858AD4A5B75F66BEa\cpp.pdb)。如下列表所示:
0:000> lm
start end module name
00007ff6`8b510000 00007ff6`8b535000 cpp C (private pdb symbols) C:\ProgramData\Dbg\sym\cpp.pdb\70DC4F4167F14F93858AD4A5B75F66BEa\cpp.pdb
00007fff`6b5b0000 00007fff`6b7d2000 ucrtbased (deferred)
00007fff`c7d70000 00007fff`c7d9e000 VCRUNTIME140D (deferred)
00007fff`e3d10000 00007fff`e40bd000 KERNELBASE (deferred)
00007fff`e4b10000 00007fff`e4bd4000 KERNEL32 (deferred)
00007fff`e6670000 00007fff`e6887000 ntdll (pdb symbols) C:\ProgramData\Dbg\sym\ntdll.pdb\3F9B0A9DA2F01CB5571242F6EE73BFD61\ntdll.pdb
0:000> lm cpp
所以,此时我们就可以使用bp或bu命令直接给main入口下一个断点。
0:000> bp /1 cpp!main
bp 后面的/1参数告诉WinDbg我们只下一次性断点,命中以后自动清除。然后,我们可以使用g命令让程序自动运行到main入口,如下图所示:
此时,已完成cpp.exe的加载,并执行到了入口函数第一条指令 push rbp处被断点断下。
不带参数的wt命令
wt命令最简单的使用方式就是不带任何参数,直接输入wt并回车。为了更详细说明,我们此时先用k命令看一下线程栈:
0:000> k
# Child-SP RetAddr Call Site
00 00000000`0014fe08 00007ff6`8b521d69 cpp!main [E:\Add\cpp\cpp\cpp.cpp @ 18]
01 00000000`0014fe10 00007ff6`8b521c0e cpp!invoke_main+0x39 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 79]
02 00000000`0014fe60 00007ff6`8b521ace cpp!__scrt_common_main_seh+0x12e [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]
03 00000000`0014fed0 00007ff6`8b521dfe cpp!__scrt_common_main+0xe [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 331]
04 00000000`0014ff00 00007fff`e4b2257d cpp!mainCRTStartup+0xe [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp @ 17]
05 00000000`0014ff30 00007fff`e66caf28 KERNEL32!BaseThreadInitThunk+0x1d
06 00000000`0014ff60 00000000`00000000 ntdll!RtlUserThreadStart+0x28
接下来我们执行wt命令:
0:000> wt
Tracing cpp!main to return address 00007ff6`8b521d69
6 0 [ 0] cpp!main
1 0 [ 1] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 1] cpp!__CheckForDebuggerJustMyCode
9 13 [ 0] cpp!main
1 0 [ 1] cpp!ILT+950(addSomeInt)
8 0 [ 1] cpp!addSomeInt
1 0 [ 2] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 2] cpp!__CheckForDebuggerJustMyCode
15 13 [ 1] cpp!addSomeInt
1 0 [ 2] cpp!ILT+955(MyReturn)
7 0 [ 2] cpp!MyReturn
1 0 [ 3] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 3] cpp!__CheckForDebuggerJustMyCode
12 13 [ 2] cpp!MyReturn
18 39 [ 1] cpp!addSomeInt
1 0 [ 2] cpp!ILT+955(MyReturn)
7 0 [ 2] cpp!MyReturn
1 0 [ 3] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 3] cpp!__CheckForDebuggerJustMyCode
12 13 [ 2] cpp!MyReturn
21 65 [ 1] cpp!addSomeInt
1 0 [ 2] cpp!ILT+955(MyReturn)
7 0 [ 2] cpp!MyReturn
1 0 [ 3] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 3] cpp!__CheckForDebuggerJustMyCode
12 13 [ 2] cpp!MyReturn
27 91 [ 1] cpp!addSomeInt
13 132 [ 0] cpp!main
1 0 [ 1] cpp!ILT+950(addSomeInt)
8 0 [ 1] cpp!addSomeInt
1 0 [ 2] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 2] cpp!__CheckForDebuggerJustMyCode
15 13 [ 1] cpp!addSomeInt
1 0 [ 2] cpp!ILT+955(MyReturn)
7 0 [ 2] cpp!MyReturn
1 0 [ 3] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 3] cpp!__CheckForDebuggerJustMyCode
12 13 [ 2] cpp!MyReturn
18 39 [ 1] cpp!addSomeInt
1 0 [ 2] cpp!ILT+955(MyReturn)
7 0 [ 2] cpp!MyReturn
1 0 [ 3] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 3] cpp!__CheckForDebuggerJustMyCode
12 13 [ 2] cpp!MyReturn
21 65 [ 1] cpp!addSomeInt
1 0 [ 2] cpp!ILT+955(MyReturn)
7 0 [ 2] cpp!MyReturn
1 0 [ 3] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 3] cpp!__CheckForDebuggerJustMyCode
12 13 [ 2] cpp!MyReturn
27 91 [ 1] cpp!addSomeInt
17 251 [ 0] cpp!main
1 0 [ 1] cpp!ILT+950(addSomeInt)
8 0 [ 1] cpp!addSomeInt
1 0 [ 2] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 2] cpp!__CheckForDebuggerJustMyCode
15 13 [ 1] cpp!addSomeInt
1 0 [ 2] cpp!ILT+955(MyReturn)
7 0 [ 2] cpp!MyReturn
1 0 [ 3] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 3] cpp!__CheckForDebuggerJustMyCode
12 13 [ 2] cpp!MyReturn
18 39 [ 1] cpp!addSomeInt
1 0 [ 2] cpp!ILT+955(MyReturn)
7 0 [ 2] cpp!MyReturn
1 0 [ 3] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 3] cpp!__CheckForDebuggerJustMyCode
12 13 [ 2] cpp!MyReturn
21 65 [ 1] cpp!addSomeInt
1 0 [ 2] cpp!ILT+955(MyReturn)
7 0 [ 2] cpp!MyReturn
1 0 [ 3] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 3] cpp!__CheckForDebuggerJustMyCode
12 13 [ 2] cpp!MyReturn
27 91 [ 1] cpp!addSomeInt
23 370 [ 0] cpp!main
393 instructions were executed in 392 events (0 from other threads)
Function Name Invocations MinInst MaxInst AvgInst
cpp!ILT+855(__CheckForDebuggerJustMyCode) 13 1 1 1
cpp!ILT+950(addSomeInt) 3 1 1 1
cpp!ILT+955(MyReturn) 9 1 1 1
cpp!MyReturn 9 12 12 12
cpp!__CheckForDebuggerJustMyCode 13 12 12 12
cpp!addSomeInt 3 27 27 27
cpp!main 1 23 23 23
0 system calls were executed
cpp!invoke_main+0x39:
00007ff6`8b521d69 4883c448 add rsp,48h
显示的信息很多,大概我们能猜到,这个命令展示了main方法调用的所有子函数。注意看最下面两行:此时的指令指针是00007ff6`8b521d69,刚好是前面我们用k命令显示的第一行的返回地址。而当前指令指针对应的代码位于cpp!invoke_main+0x39,又刚好是k命令显示的第二行:
0:000> k
# Child-SP RetAddr Call Site
00 00000000`0014fe08 00007ff6`8b521d69 cpp!main [E:\Add\cpp\cpp\cpp.cpp @ 18]
01 00000000`0014fe10 00007ff6`8b521c0e cpp!invoke_main+0x39 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 79]
所以我们知道,wt命令导致WinDbg执行完了整个main方法,并返回到了调用栈上一级cpp!invoke_main+0x39,也即是从main的第一个指令开始,一直执行完main的return指令。
不过,我们以上理解是否完整准确呢?为了防止出现盲人摸象效应,我们还是有必要查查帮助怎么讲:
从下面的帮助的描述,我们知道,如果当前指令指针恰好对应某符号,wt命令会导致WinDbg执行该符号内指令,直到遇到符号中ret指令为止。
If the program counter is at a point that corresponds to a symbol (such as the beginning of a function or entry point into a module), the wt command traces until it reaches the current return address.
具体到我们的示例,在我们运行wt命令前,指令指针位置指向的位置00007ff68b521830其实就是cpp!main符号地址,因此完全适用上面这条规则。我们可以通过?命令证实cpp!main == 00007ff6
8b521830:
0:000> ? cpp!main
Evaluate expression: 140696876095536 = 00007ff6`8b521830
接下来,帮助文件中又有如下描述:
If the wt command is issued somewhere other than the beginning of a function, the command behaves like the p (Step) command.
这段话的意思是说,如果运行wt命令时,程序指针不对应任何符号,且不对应call指令,那么wt就相当于p命令(只会单步执行一条指令)。
下面我们就验证一下这个说法。
点击Restart重新启动调试,依旧断在cpp!main,然后点击Step Over运行一条CPU指令,让指令指针既不属于任何符号,也不指向call指令,如下图所示:
图示我们执行了两次wt指令,其效果果然和p单步一致。
接下来,我们让程序执行到00007ff6`8b521855位置,也就是第一次 call cpp!@ILT+950(addSomeInt) (7ff68b5213bb)处。因此此时指令是call,指向了cpp!@ILT+950(addSomeInt) ,此时输入wt,应该会有trace信息输出吧?下面是实际情况列表:
0:000> wt
2 0 [ 0] cpp!ILT+950(addSomeInt)
8 0 [ 0] cpp!addSomeInt
1 0 [ 1] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 1] cpp!__CheckForDebuggerJustMyCode
15 13 [ 0] cpp!addSomeInt
1 0 [ 1] cpp!ILT+955(MyReturn)
7 0 [ 1] cpp!MyReturn
1 0 [ 2] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 2] cpp!__CheckForDebuggerJustMyCode
12 13 [ 1] cpp!MyReturn
18 39 [ 0] cpp!addSomeInt
1 0 [ 1] cpp!ILT+955(MyReturn)
7 0 [ 1] cpp!MyReturn
1 0 [ 2] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 2] cpp!__CheckForDebuggerJustMyCode
12 13 [ 1] cpp!MyReturn
21 65 [ 0] cpp!addSomeInt
1 0 [ 1] cpp!ILT+955(MyReturn)
7 0 [ 1] cpp!MyReturn
1 0 [ 2] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 2] cpp!__CheckForDebuggerJustMyCode
12 13 [ 1] cpp!MyReturn
27 91 [ 0] cpp!addSomeInt
120 instructions were executed in 119 events (0 from other threads)
Function Name Invocations MinInst MaxInst AvgInst
cpp!ILT+855(__CheckForDebuggerJustMyCode) 4 1 1 1
cpp!ILT+950(addSomeInt) 1 2 2 2
cpp!ILT+955(MyReturn) 3 1 1 1
cpp!MyReturn 3 12 12 12
cpp!__CheckForDebuggerJustMyCode 4 12 12 12
cpp!addSomeInt 1 27 27 27
0 system calls were executed
cpp!main+0x2a:
00007ff6`8b52185a 894504 mov dword ptr [rbp+4],eax ss:00000000`0014fd14=00000000
仔细观察我们知道,wt命令其实依旧只执行了一条call指令,然后断点在下一条00007ff6`8b52185a 894504 mov dword ptr [x (rbp+4)], eax处,不过trace了addSomeInt的完整执行过程。
我们再运行到下一个call处,然后单步t进入到cpp!@ILT+950(addSomeInt)内部再运行wt,发现结果与在call指令处运行wt几乎完全相同,唯一差别就是总指令数少了1条指令。
所以,the wt command traces until it reaches the current return address. 也就是说,wt命令会运行到当前符号cpp!main中的ret命令。
接下来帮助文件中又有如下描述:
wt输出讲解
下面我们详细介绍wt命令的输出。先讲调用列表:
0:000> wt
Tracing cpp!addSomeInt to return address 00007ff6`8b52187e
8 0 [ 0] cpp!addSomeInt
1 0 [ 1] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 1] cpp!__CheckForDebuggerJustMyCode
15 13 [ 0] cpp!addSomeInt
1 0 [ 1] cpp!ILT+955(MyReturn)
7 0 [ 1] cpp!MyReturn
1 0 [ 2] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 2] cpp!__CheckForDebuggerJustMyCode
12 13 [ 1] cpp!MyReturn
18 39 [ 0] cpp!addSomeInt
1 0 [ 1] cpp!ILT+955(MyReturn)
7 0 [ 1] cpp!MyReturn
1 0 [ 2] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 2] cpp!__CheckForDebuggerJustMyCode
12 13 [ 1] cpp!MyReturn
21 65 [ 0] cpp!addSomeInt
1 0 [ 1] cpp!ILT+955(MyReturn)
7 0 [ 1] cpp!MyReturn
1 0 [ 2] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 2] cpp!__CheckForDebuggerJustMyCode
12 13 [ 1] cpp!MyReturn
27 91 [ 0] cpp!addSomeInt
最左侧数据代表的是已经在该方法中执行了多少条语句之后才会进入下一级调用。以下面三行为例:
8 0 [ 0] cpp!addSomeInt
1 0 [ 1] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 1] cpp!__CheckForDebuggerJustMyCode
15 13 [ 0] cpp!addSomeInt
第一个中的8代表的是程序将在cpp!addSomeInt中执行8条语句之后,才开始调用cpp!ILT+855(__CheckForDebuggerJustMyCode),第二行的1则代表代码要在cpp!ILT+855(__CheckForDebuggerJustMyCode)中执行1条指令以后进入cpp!__CheckForDebuggerJustMyCode,最后一行的12代表代码进入cpp!__CheckForDebuggerJustMyCode后需要执行12条指令才返回cpp!addSomeInt。
下面列出代码:
cpp!addSomeInt:
00007ff6`8b521c80 89542410 mov dword ptr [rsp+10h], edx
00007ff6`8b521c84 894c2408 mov dword ptr [rsp+8], ecx
00007ff6`8b521c88 55 push rbp
00007ff6`8b521c89 57 push rdi
00007ff6`8b521c8a 4881ec28010000 sub rsp, 128h
00007ff6`8b521c91 488d6c2420 lea rbp, [rsp+20h]
00007ff6`8b521c96 488d0d63f30000 lea rcx, [cpp!__49DE7C48_cpp@cpp (7ff68b531000)]
00007ff6`8b521c9d e8baf6ffff call cpp!@ILT+855(__CheckForDebuggerJustMyCode) (7ff68b52135c)
00007ff6`8b521ca2 8b8528010000 mov eax, dword ptr [y (rbp+128h)]
00007ff6`8b521ca8 8b8d20010000 mov ecx, dword ptr [x (rbp+120h)]
00007ff6`8b521cae 03c8 add ecx, eax
00007ff6`8b521cb0 8bc1 mov eax, ecx
00007ff6`8b521cb2 894504 mov dword ptr [x1 (rbp+4)], eax
00007ff6`8b521cb5 8b4d04 mov ecx, dword ptr [x1 (rbp+4)]
00007ff6`8b521cb8 e803f7ffff call cpp!@ILT+955(MyReturn) (7ff68b5213c0)
00007ff6`8b521cbd 894524 mov dword ptr [x2 (rbp+24h)], eax
00007ff6`8b521cc0 8b4d24 mov ecx, dword ptr [x2 (rbp+24h)]
00007ff6`8b521cc3 e8f8f6ffff call cpp!@ILT+955(MyReturn) (7ff68b5213c0)
00007ff6`8b521cc8 894524 mov dword ptr [x2 (rbp+24h)], eax
00007ff6`8b521ccb 8b4d24 mov ecx, dword ptr [x2 (rbp+24h)]
00007ff6`8b521cce e8edf6ffff call cpp!@ILT+955(MyReturn) (7ff68b5213c0)
00007ff6`8b521cd3 894524 mov dword ptr [x2 (rbp+24h)], eax
00007ff6`8b521cd6 8b4524 mov eax, dword ptr [x2 (rbp+24h)]
00007ff6`8b521cd9 488da508010000 lea rsp, [rbp+108h]
00007ff6`8b521ce0 5f pop rdi
00007ff6`8b521ce1 5d pop rbp
00007ff6`8b521ce2 c3 ret
以上代码证明,程序进入cpp!addSomeInt以后,确实需要执行8条指令才到达call cpp!@ILT+855(__CheckForDebuggerJustMyCode)
进入cpp!ILT+855(__CheckForDebuggerJustMyCode)以后,只有一条指令就进入到cpp!__CheckForDebuggerJustMyCode:
cpp!ILT+855(__CheckForDebuggerJustMyCode):
00007ff6`8b52135c e9ef050000 jmp cpp!__CheckForDebuggerJustMyCode (7ff68b521950)
进入cpp!__CheckForDebuggerJustMyCode后,如果单步跟踪,会发现下图绿色叉掉的4条执行没有被执行,被bypass了,所以只执行了12条指令就返回了。
再贴一次wt输出,并删除其中部分行,仅保留所有的cpp!addSimeInt和紧跟其后的一行:
0:000> wt
Tracing cpp!addSomeInt to return address 00007ff6`8b52187e
8 0 [ 0] cpp!addSomeInt
1 0 [ 1] cpp!ILT+855(__CheckForDebuggerJustMyCode)
15 13 [ 0] cpp!addSomeInt
1 0 [ 1] cpp!ILT+955(MyReturn)
18 39 [ 0] cpp!addSomeInt
1 0 [ 1] cpp!ILT+955(MyReturn)
21 65 [ 0] cpp!addSomeInt
1 0 [ 1] cpp!ILT+955(MyReturn)
27 91 [ 0] cpp!addSomeInt
以上显示说明,程序进入到cpp!addSomeInt以后:
- 执行到第8行时call了cpp!ILT+855(__CheckForDebuggerJustMyCode)
- 执行到第15行时call了cpp!ILT+955(MyReturn)
- 执行到第18行时又call了cpp!ILT+955(MyReturn)
- 执行到第21行时又call了cpp!ILT+955(MyReturn)
- 执行到第27行时遇到了ret
通过仔细观察以上示例,第一列的数据已经清楚了,其数字代表在右侧方法中执行了多少条指令以后进入到下一个调用或返回。
第3列中括号中的数字代表入栈深度,初始深度为0,每进入一层调用,深度加1,每退出一级调用,深度减1。
中间列的数字代表本次程序从子程序返回时,比当前栈帧深度更深的所有子程序中完成的指令执行数量之和。如下图所示:
图中A处的13代表的是返回到cpp!addSomeInt时,深度超过当前级别的子程序总计执行了多少条指令。因为cpp!addSomeInt的级别为0,该点以上所有级别大于0的子程序执行指令数分别是:cpp!ILT+855(__CheckForDebuggerJustMyCode) 1条指令,cpp!__CheckForDebuggerJustMyCode是12条指令,总计13条指令,所以A处为13。
同理,B处的13代表的是之前子程序深度大于1的所有子程序指令执行条数之和,也就是cpp!ILT+855(__CheckForDebuggerJustMyCode) 1条 + cpp!__CheckForDebuggerJustMyCode 12条 = 13条;
C处的39代表的是这一行之前所有深度大于0的子程序执行指令条数之和,也就是 13 + 8 + 13 + 5 = 39;
D处的65和E处的91也是同样的道理。
下面再讲解一下汇总信息:
118 instructions were executed in 117 events (0 from other threads)
Function Name Invocations MinInst MaxInst AvgInst
cpp!ILT+855(__CheckForDebuggerJustMyCode) 4 1 1 1
cpp!ILT+955(MyReturn) 3 1 1 1
cpp!MyReturn 3 12 12 12
cpp!__CheckForDebuggerJustMyCode 4 12 12 12
cpp!addSomeInt 1 27 27 27
0 system calls were executed
cpp!main+0x4e:
00007ff6`8b52187e 894504 mov dword ptr [rbp+4],eax ss:00000000`0014fd14=0000000b
118 instructions : 总计执行了118条CPU指令;
cpp!ILT+855(__CheckForDebuggerJustMyCode) 总计被调用了4次,4次执行中被执行指令数最少是1条,最多是1条,平均是1条。同理,cpp!MyReturn执行了三次,因为没有分支,每次都是12条指令。
如果子程序存在分支,那么当参数不同时,可能会走不同的分支,则最少指令数、最多指令数及平均指令数就未必一致。比如以下列表取自.NET Framework应用程序的一个wt片段,就可以看到三个数据的不同:
Function Name Invocations MinInst MaxInst AvgInst
......
clr!MethodDesc::RequiresMethodDescCallingConven 4 16 35 30
clr!MethodDesc::SetStableEntryPointInterlocked 3 27 29 28
clr!MethodDesc::SizeOf 126 14 19 14
clr!MethodDescChunk::GetTemporaryEntryPoint 16 17 30 20
clr!MethodDescChunk::GetTemporaryEntryPoints 19 5 9 7
clr!MethodTable::CheckRunClassInitThrowing 4 11 42 32
clr!MethodTable::DoRunClassInitThrowing 3 110 171 130
......
带参数wt命令
最后,我们说一下带参数的wt命令。wt指令可以给参数和选项。选项很容易理解:
如果有 -l Depth,则仅trace Depth深度:
0:000> wt -l 1
2 0 [ 0] cpp!ILT+950(addSomeInt)
8 0 [ 0] cpp!addSomeInt
1 0 [ 1] cpp!ILT+855(__CheckForDebuggerJustMyCode)
12 0 [ 1] cpp!__CheckForDebuggerJustMyCode
15 13 [ 0] cpp!addSomeInt
1 0 [ 1] cpp!ILT+955(MyReturn)
12 0 [ 1] cpp!MyReturn
18 26 [ 0] cpp!addSomeInt
1 0 [ 1] cpp!ILT+955(MyReturn)
12 0 [ 1] cpp!MyReturn
21 39 [ 0] cpp!addSomeInt
1 0 [ 1] cpp!ILT+955(MyReturn)
12 0 [ 1] cpp!MyReturn
27 52 [ 0] cpp!addSomeInt
- -m:仅追踪指定模块
- -i :忽略指定模块
- -or :显示返回值寄存器数值
如果给定了开始地址,则当前IP到开始地址之间的代码会被略过,相当于直接修改了IP寄存器值以后再执行 wt。
如果给定了结束地址,则直接从当前IP执行到结束地址,期间所有的call都会被追踪。
下面进入第三个实战项目。
跟踪JIT本机代码生成
依旧使用第二个实战项目示例程序。
**目的:**我们知道,一个C#方法只有在第一次调用期间才会被CLR即时编译,第一次调用前,该方法通常处于未即时编译状态。如何查看未编译态和编译态?如何查到具体修改调用入口的clr指令?这就是本项目的目的,我们以查找Person类的Show()方法举例。
为了简化描述,以下主要使用命令列表进行说明,同时我在关键命令部分写入了注释(WinDbg可以用$$引入注释,但注释前必须用分号与命令隔开)。
0:000> bc*; $$清除所有断点
0:000> .restart; $$重新加载DotNetFramework.exe
CommandLine: E:\Add\DotNetFramework\DotNetFramework\bin\Debug\DotNetFramework.exe
************* Path validation summary **************
Response Time (ms) Location
Deferred srv*
Deferred srv*c:\Symbols*http://msdl.microsoft.com/download/symbols
Symbol search path is: srv*;srv*c:\Symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 00400000 00408000 DotNetFramework.exe
ModLoad: 77c40000 77df2000 ntdll.dll
ModLoad: 75840000 75895000 C:\Windows\SysWOW64\MSCOREE.DLL
ModLoad: 774a0000 77590000 C:\Windows\SysWOW64\KERNEL32.dll
ModLoad: 77200000 77479000 C:\Windows\SysWOW64\KERNELBASE.dll
(445c.784): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=fbb50000 edx=00000000 esi=005b2bc0 edi=0022e000
eip=77cf88b7 esp=0019f7f4 ebp=0019f820 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
ntdll!LdrpDoDebuggerBreak+0x2b:
77cf88b7 cc int 3
0:000> sxe ld:clrjit; $$当WinDbg完成clrjit.dll模块的加载后,断到WinDbg
0:000> sx; $$验证上面那条sxe命令是否已经生效(注意ld事件)
ct - Create thread - ignore
et - Exit thread - ignore
cpr - Create process - ignore
epr - Exit process - ignore
ld - Load module - break
(only break for clrjit)
ud - Unload module - ignore
ser - System error - ignore
ibp - Initial breakpoint - break
iml - Initial module load - ignore
out - Debuggee output - output
av - Access violation - break - not handled
asrt - Assertion failure - break - not handled
aph - Application hang - break - not handled
bpe - Break instruction exception - break
bpec - Break instruction exception continue - handled
eh - C++ EH exception - second-chance break - not handled
clr - CLR exception - second-chance break - not handled
clrn - CLR notification exception - break - handled
Command: "!HandleCLRN"
cce - Control-Break exception - break
cc - Control-Break exception continue - handled
cce - Control-C exception - break
cc - Control-C exception continue - handled
dm - Data misaligned - break - not handled
dbce - Debugger command exception - ignore - handled
gp - Guard page violation - break - not handled
ii - Illegal instruction - second-chance break - not handled
ip - In-page I/O error - break - not handled
dz - Integer divide-by-zero - break - not handled
iov - Integer overflow - break - not handled
ch - Invalid handle - break
hc - Invalid handle continue - not handled
lsq - Invalid lock sequence - break - not handled
isc - Invalid system call - break - not handled
3c - Port disconnected - second-chance break - not handled
svh - Service hang - break - not handled
sse - Single step exception - break
ssec - Single step exception continue - handled
sbo - Security check failure or stack buffer overrun - break - not handled
sov - Stack overflow - break - not handled
vs - Verifier stop - break - not handled
vcpp - Visual C++ exception - ignore - handled
wkd - Wake debugger - break - not handled
rto - Windows Runtime Originate Error - second-chance break - not handled
rtt - Windows Runtime Transform Error - second-chance break - not handled
wob - WOW64 breakpoint - break - handled
wos - WOW64 single step exception - break - handled
* - Other exception - second-chance break - not handled
0:000> g; $$运行程序,直到clrjit模块被加载
ModLoad: 75ee0000 75f5f000 C:\Windows\SysWOW64\ADVAPI32.dll
ModLoad: 759f0000 75ab4000 C:\Windows\SysWOW64\msvcrt.dll
ModLoad: 761a0000 76226000 C:\Windows\SysWOW64\sechost.dll
ModLoad: 77c10000 77c2a000 C:\Windows\SysWOW64\bcrypt.dll
ModLoad: 778b0000 7796a000 C:\Windows\SysWOW64\RPCRT4.dll
ModLoad: 74060000 740e8000 C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscoreei.dll
ModLoad: 76150000 7619b000 C:\Windows\SysWOW64\SHLWAPI.dll
ModLoad: 74040000 74053000 C:\Windows\SysWOW64\kernel.appcore.dll
ModLoad: 75830000 75838000 C:\Windows\SysWOW64\VERSION.dll
ModLoad: 73880000 7403c000 C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll
ModLoad: 75cc0000 75e6a000 C:\Windows\SysWOW64\USER32.dll
ModLoad: 76120000 7613a000 C:\Windows\SysWOW64\win32u.dll
ModLoad: 73680000 73733000 C:\Windows\SysWOW64\ucrtbase_clr0400.dll
ModLoad: 77590000 775b3000 C:\Windows\SysWOW64\GDI32.dll
ModLoad: 76900000 769e2000 C:\Windows\SysWOW64\gdi32full.dll
ModLoad: 73740000 73755000 C:\Windows\SysWOW64\VCRUNTIME140_CLR0400.dll
ModLoad: 75960000 759d9000 C:\Windows\SysWOW64\msvcp_win.dll
ModLoad: 775c0000 776d2000 C:\Windows\SysWOW64\ucrtbase.dll
ModLoad: 75ac0000 75ae5000 C:\Windows\SysWOW64\IMM32.DLL
ModLoad: 76670000 768ec000 C:\Windows\SysWOW64\combase.dll
(445c.784): Unknown exception - code 04242420 (first chance)
ModLoad: 71910000 72d58000 C:\Windows\assembly\NativeImages_v4.0.30319_32\mscorlib\3391cbd616b8ec57c9a32cb3266698ac\mscorlib.ni.dll
ModLoad: 75fc0000 76115000 C:\Windows\SysWOW64\ole32.dll
ModLoad: 76670000 768ec000 C:\Windows\SysWOW64\combase.dll
ModLoad: 76af0000 76b53000 C:\Windows\SysWOW64\bcryptPrimitives.dll
ModLoad: 71670000 716ee000 C:\Windows\Microsoft.NET\Framework\v4.0.30319\clrjit.dll
eax=c0000034 ebx=0019e848 ecx=00000000 edx=00000000 esi=000002f4 edi=0062d2c8
eip=77cb6e2c esp=0019e7fc ebp=0019e840 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
ntdll!NtMapViewOfSection+0xc:
77cb6e2c c22800 ret 28h
0:000> !bpmd DotNetFramework Program.Main; $$此时sos扩展已经加载,使用!bpmd给Main方法设入口断点
Found 1 methods in module 007d40b8...
MethodDesc = 007d4dc8
Adding pending breakpoints...
0:000> g; $$执行到Main方法入口
ModLoad: 769f0000 76a8c000 C:\Windows\SysWOW64\OLEAUT32.dll
(445c.784): CLR notification exception - code e0444143 (first chance)
JITTED DotNetFramework!DotNetFramework.Program.Main(System.String[])
Setting breakpoint: bp 00A70869 [DotNetFramework.Program.Main(System.String[])]
Breakpoint 0 hit
eax=00000000 ebx=0019f5ac ecx=025d2364 edx=00000000 esi=00000000 edi=0019f520
eip=00a70869 esp=0019f4f0 ebp=0019f508 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
DotNetFramework!COM+_Entry_Point <PERF> (DotNetFramework+0x670869):
00a70869 90 nop
此时,我们已经成功断到了Main方法的入口处。当前指令指针EIP就在Main方法之中,所以可以用 !u @eip 或 !u @$scopeip反汇编Main方法:
0:000> !u @eip
Normal JIT generated code
DotNetFramework.Program.Main(System.String[])
Begin 00a70848, size 82
E:\Add\DotNetFramework\DotNetFramework\Program.cs @ 9:
00a70848 55 push ebp
00a70849 8bec mov ebp,esp
00a7084b 83ec18 sub esp,18h
00a7084e 33c0 xor eax,eax
00a70850 8945e8 mov dword ptr [ebp-18h],eax
00a70853 894dfc mov dword ptr [ebp-4],ecx
00a70856 833d64437d0000 cmp dword ptr ds:[7D4364h],0
00a7085d 7405 je DotNetFramework!COM+_Entry_Point <PERF> (DotNetFramework+0x670864) (00a70864)
00a7085f e81c071573 call clr!JIT_DbgIsJustMyCode (73bc0f80)
00a70864 33d2 xor edx,edx
00a70866 8955ec mov dword ptr [ebp-14h],edx
>>> 00a70869 90 nop
E:\Add\DotNetFramework\DotNetFramework\Program.cs @ 11:
00a7086a b9804e7d00 mov ecx,7D4E80h (MT: DotNetFramework.Person)
00a7086f e88028d5ff call 007c30f4 (JitHelp: CORINFO_HELP_NEWSFAST)
00a70874 8945e8 mov dword ptr [ebp-18h],eax
00a70877 8b4de8 mov ecx,dword ptr [ebp-18h]
00a7087a ff15a04e7d00 call dword ptr ds:[7D4EA0h] (DotNetFramework.Person..ctor(), mdToken: 06000006)
00a70880 8b45e8 mov eax,dword ptr [ebp-18h]
00a70883 8945ec mov dword ptr [ebp-14h],eax
E:\Add\DotNetFramework\DotNetFramework\Program.cs @ 12:
00a70886 8b152c245d03 mov edx,dword ptr ds:[35D242Ch] ("Robert")
00a7088c 8b4dec mov ecx,dword ptr [ebp-14h]
00a7088f 3909 cmp dword ptr [ecx],ecx
00a70891 ff155c4e7d00 call dword ptr ds:[7D4E5Ch] (DotNetFramework.Person.set_Name(System.String), mdToken: 06000004)
00a70897 90 nop
E:\Add\DotNetFramework\DotNetFramework\Program.cs @ 13:
00a70898 8b4dec mov ecx,dword ptr [ebp-14h]
00a7089b 3909 cmp dword ptr [ecx],ecx
00a7089d ff15684e7d00 call dword ptr ds:[7D4E68h] (DotNetFramework.Person.Show(), mdToken: 06000005)
00a708a3 90 nop
E:\Add\DotNetFramework\DotNetFramework\Program.cs @ 14:
00a708a4 8b4dec mov ecx,dword ptr [ebp-14h]
00a708a7 3909 cmp dword ptr [ecx],ecx
00a708a9 ff15684e7d00 call dword ptr ds:[7D4E68h] (DotNetFramework.Person.Show(), mdToken: 06000005)
00a708af 90 nop
E:\Add\DotNetFramework\DotNetFramework\Program.cs @ 15:
00a708b0 8b4dec mov ecx,dword ptr [ebp-14h]
00a708b3 3909 cmp dword ptr [ecx],ecx
00a708b5 ff15684e7d00 call dword ptr ds:[7D4E68h] (DotNetFramework.Person.Show(), mdToken: 06000005)
00a708bb 90 nop
E:\Add\DotNetFramework\DotNetFramework\Program.cs @ 16:
00a708bc 8d4df0 lea ecx,[ebp-10h]
00a708bf e81cc8a271 call mscorlib_ni!System.Console.ReadKey (7249d0e0)
00a708c4 90 nop
E:\Add\DotNetFramework\DotNetFramework\Program.cs @ 17:
00a708c5 90 nop
00a708c6 8be5 mov esp,ebp
00a708c8 5d pop ebp
00a708c9 c3 ret
注意观察C#源码第11行的反汇编:
E:\Add\DotNetFramework\DotNetFramework\Program.cs @ 11:
00a7086a b9804e7d00 mov ecx,7D4E80h (MT: DotNetFramework.Person)
00a7086f e88028d5ff call 007c30f4 (JitHelp: CORINFO_HELP_NEWSFAST) ;快速给person分配空间
00a70874 8945e8 mov dword ptr [ebp-18h],eax
00a70877 8b4de8 mov ecx,dword ptr [ebp-18h]
00a7087a ff15a04e7d00 call dword ptr ds:[7D4EA0h] (DotNetFramework.Person..ctor(), mdToken: 06000006)
00a70880 8b45e8 mov eax,dword ptr [ebp-18h]
00a70883 8945ec mov dword ptr [ebp-14h],eax
第一行的 mov ecx,7D4E80h (MT: DotNetFramework.Person)说明,7D4E80h 是 DotNetFramework.Person 类的Method Table。有了Method Table,就可以使用 !dumpmt 扩展命令得到 Show() 方法入口地址,具体操作如下:
注意观察找到Person类的方法表:7D4E80h (MT: DotNetFramework.Person)
0:000> !dumpmt -md 7D4E80h; $$dumpmt以得到Show()方法的MethodDesc
EEClass: 007d1304
Module: 007d40b8
Name: DotNetFramework.Person
mdToken: 02000003
File: E:\Add\DotNetFramework\DotNetFramework\bin\Debug\DotNetFramework.exe
BaseSize: 0x10
ComponentSize: 0x0
Slots in VTable: 8
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
Entry MethodDe JIT Name
71cf9a98 71919e9c PreJIT System.Object.ToString()
71dba450 71a761d8 PreJIT System.Object.Equals(System.Object)
71ce8ea0 71a761f8 PreJIT System.Object.GetHashCode()
71dc37d0 71a76200 PreJIT System.Object.Finalize()
00a70459 007d4e6c NONE DotNetFramework.Person..ctor()
00a7044d 007d4e48 NONE DotNetFramework.Person.get_Name()
00a70451 007d4e54 NONE DotNetFramework.Person.set_Name(System.String)
00a70455 007d4e60 NONE DotNetFramework.Person.Show()
显然,从最后一行的JIT状态是NONE我们知道,此时DotNetFramework.Person.Show()尚未被即时编译,所以00a70455并不是最终的方法入口地址。此时可以使用dp命令查看一下保存方法入口地址的位置:
0:000> dp 007d4e60 L4; $$此时可以记下未即时编译的入口:00a70455
007d4e60 20060005 00080007 00a70455 20090006
可以发现,007d4e68保存的就是Show()的入口地址00a70455,一旦Show被JIT以后,这个地址中的00a70455会被修改为JIT后的本机代码地址。此时,就需要使用第一个实战项目中介绍的硬件断点ba了。
下面的命令设置硬件断点:
监控地址:007d4e68
事件:读写4字节长度数据(w4)
后续处理:显示 ‘Address is being visited!..’,然后执行 dp 007d4e68 L1,再然后执行k命令显示栈回朔,最后执行gc命令继续运行。
0:000> ba w4 007d4e68 ".echo 'Address is being visited!....';dp 007d4e68 L1; k; gc"
0:000> g; $$运行,观察栈回朔
'Address is being visited!....'
007d4e68 00a70958
# ChildEBP RetAddr
00 0019f2cc 739580ef clr!MethodDesc::SetStableEntryPointInterlocked+0x37
01 0019f2e0 7395717b clr!MethodDesc::SetNativeCodeInterlocked+0x69
02 0019f3dc 73956c01 clr!MethodDesc::MakeJitWorker+0x23c
03 0019f44c 738b9034 clr!MethodDesc::DoPrestub+0x596
04 0019f4c4 7389299b clr!PreStubWorker+0xef
05 0019f4e8 00a708a3 clr!ThePreStub+0x11
06 0019f508 73892516 DotNetFramework!COM+_Entry_Point <PERF> (DotNetFramework+0x6708a3) [E:\Add\DotNetFramework\DotNetFramework\Program.cs @ 13]
07 0019f514 7389e4b9 clr!CallDescrWorkerInternal+0x34
08 0019f568 7389f197 clr!CallDescrWorkerWithHandler+0x6b
09 0019f5d8 7392d773 clr!MethodDescCallSite::CallTargetWorker+0x170
0a 0019f6fc 7392d871 clr!RunMain+0x1c6
0b 0019f968 7392d6e9 clr!Assembly::ExecuteMainMethod+0xf7
0c 0019fe4c 7392daf4 clr!SystemDomain::ExecuteMainMethod+0x61c
0d 0019fea4 7392da3a clr!ExecuteEXE+0x4c
0e 0019fee4 7397e45c clr!_CorExeMainInternal+0xd8
0f 0019ff20 7406a38e clr!_CorExeMain+0x4d
10 0019ff5c 7584fb6e mscoreei!_CorExeMain+0x100
11 0019ff6c 75855708 MSCOREE!ShellShim__CorExeMain+0x9e
12 0019ff84 774b7ba9 MSCOREE!_CorExeMain_Exported+0x8
13 0019ff84 77cac10b KERNEL32!BaseThreadInitThunk+0x19
14 0019ffdc 77cac08f ntdll!__RtlUserThreadStart+0x2b
15 0019ffec 00000000 ntdll!_RtlUserThreadStart+0x1b
Breakpoint 2 hit
eax=00000001 ebx=0019f5ac ecx=025d4824 edx=00000000 esi=00000000 edi=0019f520
eip=00a708a4 esp=0019f4f0 ebp=0019f508 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
DotNetFramework!COM+_Entry_Point <PERF> (DotNetFramework+0x6708a4):
00a708a4 8b4dec mov ecx,dword ptr [ebp-14h] ss:002b:0019f4f4=025d238c
从以上栈回朔信息我们知道,线程执行到clr!MethodDesc::SetStableEntryPointInterlocked偏移0x37时,断点被命中。那么读或写007d4e68的具体指令一定就是该指令前一条。我们可以用uf命令反编译SetStableEntryPointInterlocked查到(备注:写本文由于忘记了记录反汇编,所以下面的uf结果是后补的,与其他部分存在地址不一致,特此澄清):
0:000> uf clr!MethodDesc::SetStableEntryPointInterlocked
clr!MethodDesc::SetStableEntryPointInterlocked:
738b9162 55 push ebp
738b9163 8bec mov ebp,esp
738b9165 53 push ebx
738b9166 56 push esi
738b9167 57 push edi
738b9168 8bf1 mov esi,ecx
738b916a e802faffff call clr!MethodDesc::GetTemporaryEntryPoint (738b8b71)
738b916f 8bce mov ecx,esi
738b9171 8bf8 mov edi,eax
738b9173 e832f9ffff call clr!MethodDesc::GetAddrOfSlot (738b8aaa)
738b9178 8bd8 mov ebx,eax
738b917a f6460608 test byte ptr [esi+6],8
738b917e 740d je clr!MethodDesc::SetStableEntryPointInterlocked+0x2e (738b918d) Branch
clr!MethodDesc::SetStableEntryPointInterlocked+0x1e:
738b9180 833d3c40fc7300 cmp dword ptr [clr!g_IBCLogger (73fc403c)],0
738b9187 0f852b722900 jne clr!MethodDesc::SetStableEntryPointInterlocked+0x27 (73b503b8) Branch
clr!MethodDesc::SetStableEntryPointInterlocked+0x2e:
738b918d 8b4d08 mov ecx,dword ptr [ebp+8]
738b9190 8bc7 mov eax,edi
738b9192 f00fb10b lock cmpxchg dword ptr [ebx],ecx
738b9196 8bc8 mov ecx,eax
738b9198 b800000001 mov eax,1000000h
738b919d f00906 lock or dword ptr [esi],eax
738b91a0 33c0 xor eax,eax
738b91a2 3bcf cmp ecx,edi
738b91a4 5f pop edi
738b91a5 5e pop esi
738b91a6 0f94c0 sete al
738b91a9 5b pop ebx
738b91aa 5d pop ebp
738b91ab c20400 ret 4
通过地址查找,我们知道就是下面这条指令完成了Show()方法入口地址的变更:
738b9192 f00fb10b lock cmpxchg dword ptr [ebx],ecx
也就是说,这之前的clr代码,已经完成了Show方法的即时编译。
此时如果再观察007d4e68,可以发现,原来的Show方法入口地址00a70455,现在已经改成了00a70958,此后再次执行Show时,就不会调用clrjit代码了,而是直接运行本机代码。
0:000> dp 007d4e60 L4
007d4e60 21060005 00080007 00a70958 21090006
当然,有了栈回朔以后,我们就可以综合利用ba断点及wt命令进行更精细的研究,可以做的事情很多,就看自己有哪些想法。
总结
本系列通过十篇文章,简单介绍了WinDbg调试Windows程序的方法。
调试工具的使用,是一个熟能生巧的过程,道理并不很难,但需要记忆的命令较多,所以鼓励大家多多实际操作。
软件调试真正的挑战,应该是计算机底层知识,比如对CPU、编译器、操作系统、汇编语言等,而这些知识又不是三言两语就能说清楚,所以,每当遇到困难时,我的建议是:不钻牛角尖,惹不起躲得起。说不定现在看来很难的问题,数月后便已不再是问题了。
行文匆忙,错误与疏漏在所难免,欢迎批评指正。
本系列文章CSDN首发,作者Login255。写作不易,转载务必注明出处:《WinDbg - 初学者系列》https://blog.csdn.net/login255/category_12686516.html