总目录
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调试 DOTNET应用程序(如C#程序)的两个个小技巧。
- DOTNET应用程序的运行时离不开CLR与即时编译器的参与。如何使用WinDbg调试程序的同时,跟踪到即时编译器的动作?
- 调试DotNet程序时,如果遇到了clr代码,如何让WinDbg自动将源程序打开?
下图是我们希望实现的功能:在调试C#程序过程中,能把断点断到Clr即时编译器的某个方法中,直接调试 dotnet runtime源代码。
源代码
C#源代码定义了一个Person类,并且定义了一个GetAge方法。显然入口Main调用person.GetAge()时一定会调用clrjit模块对GetAge方法进行即时编译。
程序使用.NET 8.0编译。
using System.Diagnostics;
namespace BasicGrammar;
class Program
{
static void Main()
{
Person person = new Person();
person.age = 20;
int age = person.GetAge();
Console.WriteLine(age);
Debugger.Break();
}
public class Person
{
public int age;
public int GetAge()
{ return age; }
}
}
下面说明具体操作步骤。
让WinDbg加载clrjit.dll后暂停
第一步:使用WinDbg加载Core.exe。
第二步:使用sxe ld命令,让WinDbg加载完clrjit.dll模块后暂停:
0:000> sxe ld:clrjit
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 clrjit)
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
观察以上命令输出,会发现sxe命令已经起作用,将在Load clrjit这个module后break(证据就是下面两行输出):
ld - Load module - break
(only break for clrjit)
输入g命令运行程序,看看WinDbg能否按预期被断下:
0:000> g
ModLoad: 00007ff8`d1010000 00007ff8`d1041000 C:\Windows\System32\IMM32.DLL
ModLoad: 00007ff8`a3bb0000 00007ff8`a3c09000 C:\Program Files\dotnet\host\fxr\8.0.3\hostfxr.dll
ModLoad: 00007ff8`a3b40000 00007ff8`a3ba4000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\hostpolicy.dll
ModLoad: 00007ff8`a1a80000 00007ff8`a1f66000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\coreclr.dll
ModLoad: 00007ff8`d2410000 00007ff8`d25b5000 C:\Windows\System32\ole32.dll
ModLoad: 00007ff8`d1c80000 00007ff8`d2008000 C:\Windows\System32\combase.dll
ModLoad: 00007ff8`d1970000 00007ff8`d1a47000 C:\Windows\System32\OLEAUT32.dll
ModLoad: 00007ff8`d0f10000 00007ff8`d0f8b000 C:\Windows\System32\bcryptPrimitives.dll
(50c0.4e74): Unknown exception - code 04242420 (first chance)
ModLoad: 00007fff`c9d10000 00007fff`ca99c000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\System.Private.CoreLib.dll
ModLoad: 00007ff8`29ce0000 00007ff8`29e99000 C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.3\clrjit.dll
ntdll!NtMapViewOfSection+0x14:
00007ff8`d32f04a4 c3 ret
显然,此时clrjit已经被成功加载,且加载以后已被成功断下。不过此时clrjit的符号表还处于deferred状态(可以使用lm命令查看),所以必须用 ld 命令手动加载该模块的符号文件(pdb):
0:000> ld clrjit
当然,也可以直接用 x 命令查询符号,此时deferred符号会自动被WinDbg加载:
0:000> x clrjit!jitNativeCode
00007ff8`29d2b660 clrjit!jitNativeCode (struct CORINFO_METHOD_STRUCT_ *, struct CORINFO_MODULE_STRUCT_ *, class ICorJitInfo *, struct CORINFO_METHOD_INFO *, void **, unsigned int *, class JitFlags *, void *)
在这个命令中,我们使用x命令查询到clrjit拥有一个叫做jitNativeCode的方法。读者可能会问,你怎么知道要查这个方法?其实,如果对一个module不了解,可以使用类似于x clrjit!*的命令列出所有符号。
接下来,我们看一下线程栈,看看整个方法的调用链结构:
0:000> k
# Child-SP RetAddr Call Site
00 00000000`001cc958 00007ff8`d327c9ac ntdll!NtMapViewOfSection+0x14
01 00000000`001cc960 00007ff8`d327c5aa ntdll!LdrpMapViewOfSection+0x6c
02 00000000`001cc9d0 00007ff8`d327c734 ntdll!LdrpMinimalMapModule+0x116
03 00000000`001cca90 00007ff8`d3260b0f ntdll!LdrpMapDllWithSectionHandle+0x18
04 00000000`001ccb00 00007ff8`d32614f0 ntdll!LdrpMapDllNtFileName+0x19b
05 00000000`001ccc00 00007ff8`d32612bf ntdll!LdrpMapDllFullPath+0xe0
06 00000000`001ccd90 00007ff8`d3278db4 ntdll!LdrpProcessWork+0x77
07 00000000`001ccde0 00007ff8`d32690ac ntdll!LdrpLoadDllInternal+0x1a0
08 00000000`001cce80 00007ff8`d327a73a ntdll!LdrpLoadDll+0xb0
09 00000000`001cd040 00007ff8`d099b5a2 ntdll!LdrLoadDll+0xfa
0a 00000000`001cd130 00007ff8`a1a8c13f KERNELBASE!LoadLibraryExW+0x172
0b 00000000`001cd1a0 00007ff8`a1a8a581 coreclr!LoadLibraryExWrapper+0x157 [D:\a\_work\1\s\src\coreclr\utilcode\longfilepathwrappers.cpp @ 314]
0c (Inline Function) --------`-------- coreclr!CLRLoadLibraryWorker+0x24 [D:\a\_work\1\s\src\coreclr\vm\util.cpp @ 918]
0d 00000000`001cd470 00007ff8`a1a898fd coreclr!CLRLoadLibrary+0x31 [D:\a\_work\1\s\src\coreclr\vm\util.cpp @ 934]
0e 00000000`001cd4a0 00007ff8`a1b9f3a4 coreclr!LoadAndInitializeJIT+0x24d [D:\a\_work\1\s\src\coreclr\vm\codeman.cpp @ 1777]
0f 00000000`001cdb30 00007ff8`a1aa82e1 coreclr!EEJitManager::LoadJIT+0xe4 [D:\a\_work\1\s\src\coreclr\vm\codeman.cpp @ 1932]
10 00000000`001cdbb0 00007ff8`a1a95507 coreclr!UnsafeJitFunction+0xd1 [D:\a\_work\1\s\src\coreclr\vm\jitinterface.cpp @ 12716]
11 00000000`001ce150 00007ff8`a1a95327 coreclr!MethodDesc::JitCompileCodeLocked+0xef [D:\a\_work\1\s\src\coreclr\vm\prestub.cpp @ 939]
12 00000000`001ce2a0 00007ff8`a1a94ffc coreclr!MethodDesc::JitCompileCodeLockedEventWrapper+0x17b [D:\a\_work\1\s\src\coreclr\vm\prestub.cpp @ 820]
13 00000000`001ce430 00007ff8`a1aa56b4 coreclr!MethodDesc::JitCompileCode+0x2bc [D:\a\_work\1\s\src\coreclr\vm\prestub.cpp @ 707]
14 (Inline Function) --------`-------- coreclr!MethodDesc::PrepareILBasedCode+0x177 [D:\a\_work\1\s\src\coreclr\vm\prestub.cpp @ 433]
15 (Inline Function) --------`-------- coreclr!MethodDesc::PrepareCode+0x177 [D:\a\_work\1\s\src\coreclr\vm\prestub.cpp @ 321]
16 00000000`001ce4e0 00007ff8`a1aa4b38 coreclr!CodeVersionManager::PublishVersionableCodeIfNecessary+0x2b4 [D:\a\_work\1\s\src\coreclr\vm\codeversion.cpp @ 1734]
17 00000000`001ce940 00007ff8`a1b0cc4d coreclr!MethodDesc::DoPrestub+0x138 [D:\a\_work\1\s\src\coreclr\vm\prestub.cpp @ 2597]
18 00000000`001cea50 00007ff8`a1b0be63 coreclr!ECall::PopulateManagedCastHelpers+0x1a1 [D:\a\_work\1\s\src\coreclr\vm\ecall.cpp @ 153]
19 00000000`001cea80 00007ff8`a1b0cdc6 coreclr!SystemDomain::LoadBaseSystemClasses+0x4fb [D:\a\_work\1\s\src\coreclr\vm\appdomain.cpp @ 1361]
1a 00000000`001cf0b0 00007ff8`a1a85207 coreclr!SystemDomain::Init+0x126 [D:\a\_work\1\s\src\coreclr\vm\appdomain.cpp @ 1157]
1b 00000000`001cf120 00007ff8`a1ba2cdf coreclr!EEStartupHelper+0x657 [D:\a\_work\1\s\src\coreclr\vm\ceemain.cpp @ 922]
1c 00000000`001cf340 00007ff8`a1ba2c75 coreclr!EEStartup+0x27 [D:\a\_work\1\s\src\coreclr\vm\ceemain.cpp @ 1051]
1d 00000000`001cf390 00007ff8`a1ba2bc8 coreclr!EnsureEEStarted+0x7d [D:\a\_work\1\s\src\coreclr\vm\ceemain.cpp @ 300]
1e 00000000`001cf3e0 00007ff8`a1b9e659 coreclr!CorHost2::Start+0x58 [D:\a\_work\1\s\src\coreclr\vm\corhost.cpp @ 101]
1f 00000000`001cf420 00007ff8`a3b43630 coreclr!coreclr_initialize+0x179 [D:\a\_work\1\s\src\coreclr\dlls\mscoree\exports.cpp @ 320]
20 00000000`001cf4f0 00007ff8`a3b61aa8 hostpolicy!coreclr_t::create+0x2b0 [D:\a\_work\1\s\src\native\corehost\hostpolicy\coreclr.cpp @ 73]
21 00000000`001cf670 00007ff8`a3b63788 hostpolicy!`anonymous namespace'::create_coreclr+0x168 [D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp @ 82]
22 00000000`001cf6d0 00007ff8`a3bbb5c9 hostpolicy!corehost_main+0x148 [D:\a\_work\1\s\src\native\corehost\hostpolicy\hostpolicy.cpp @ 422]
23 00000000`001cf7d0 00007ff8`a3bbe066 hostfxr!execute_app+0x2e9 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 145]
24 00000000`001cf8d0 00007ff8`a3bc02ec hostfxr!`anonymous namespace'::read_config_and_execute+0xa6 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 532]
25 00000000`001cf9c0 00007ff8`a3bbe644 hostfxr!fx_muxer_t::handle_exec_host_command+0x16c [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 1007]
26 00000000`001cfa70 00007ff8`a3bb85a0 hostfxr!fx_muxer_t::execute+0x494 [D:\a\_work\1\s\src\native\corehost\fxr\fx_muxer.cpp @ 578]
27 00000000`001cfbb0 00007ff7`cb0ef998 hostfxr!hostfxr_main_startupinfo+0xa0 [D:\a\_work\1\s\src\native\corehost\fxr\hostfxr.cpp @ 62]
28 00000000`001cfcb0 00007ff7`cb0efda6 apphost!exe_start+0x878 [D:\a\_work\1\s\src\native\corehost\corehost.cpp @ 240]
29 00000000`001cfe80 00007ff7`cb0f12e8 apphost!wmain+0x146 [D:\a\_work\1\s\src\native\corehost\corehost.cpp @ 311]
2a (Inline Function) --------`-------- apphost!invoke_main+0x22 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 90]
2b 00000000`001cfef0 00007ff8`d1a6257d apphost!__scrt_common_main_seh+0x10c [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]
2c 00000000`001cff30 00007ff8`d32aaf28 KERNEL32!BaseThreadInitThunk+0x1d
2d 00000000`001cff60 00000000`00000000 ntdll!RtlUserThreadStart+0x28
如果反汇编一下clrjit!jitNativeCode,则可以找到该方法的源文件:
0:000> u clrjit!jitNativeCode
clrjit!jitNativeCode [D:\a\_work\1\s\src\coreclr\jit\compiler.cpp @ 7669]:
00007ff8`29d2b660 4c894c2420 mov qword ptr [rsp+20h],r9
00007ff8`29d2b665 4c89442418 mov qword ptr [rsp+18h],r8
00007ff8`29d2b66a 4889542410 mov qword ptr [rsp+10h],rdx
以上输出表明,clrjit!jitNativeCode方法位于** D:\a_work\1\s\src\coreclr\jit\compiler.cpp文件的7669行。**
** WinDbg如何知道的源文件名称及行号我们后面再讲,此时,我们希望先把这个源文件找到并打开。**
复制.NET Runtime源代码
我们知道,微软已经对.NET Runtime开源了,我们可以到GitHub上下载一份源代码到本地。
然后,我们可以在D盘创建一个如上所述的目录结构,即: D:\a_work\1\s\src。然后,将刚下载的 runtime-main\src 文件夹下的所有文件都拷贝到新建文件夹下。
我在调试Core.exe之前已经做完了以上两步。
让程序断在clrjit!jitNativeCode
有了以上的准备之后,我们就可以使用g命令,让程序进入到u clrjit!jitNativeCode方法后立即停止。
0:000> g clrjit!jitNativeCode
clrjit!jitNativeCode:
00007ff8`29d2b660 4c894c2420 mov qword ptr [rsp+20h],r9 ss:00000000`001cdb28=00007ff8a1b9f405
此时,你会发现,WinDbg自动打开了D:\a_work\1\s\src\coreclr\jit\compiler.cpp文件,并成功断在了7669行,如下图所示:
毫无疑问,此时你就可以在WinDbg中直接以源代码方式调试 jitNativeCode 了,是不是很方便?
至于能否读懂源代码,就只能看我们本身的C++功力了,已经超出本文的讨论范围。
源文件路径的秘密
你可能还有一个疑问,为什么clrjit模块的compiler.cpp文件要放到D:\a_work\1\s\src\coreclr\jit\文件夹下?放到其他路径不行吗?
答案是:如果你手动加载源文件,那么被加载文件在哪里并无所谓;但如果你想让WinDbg自动找到源文件并断下,就必须放到上述指定路径。下面说明具体原因:
符号文件
WinDbg之所以能找到模块的源文件地址,或者使用x命令之所以能查到模块的符号,都是因为符号文件(.PDB文件)的功劳。以下使用lm命令时,如果加载了符号文件,就会被列出来,如:C:\ProgramData\Dbg\sym\clrjit.pdb\8C6CD63E16C043C7B5EC7D38DCAC1E8B1\clrjit.pdb
0:000> lm m clrjit*
Browse full module list
start end module name
00007ff8`29ce0000 00007ff8`29e99000 clrjit (private pdb symbols) C:\ProgramData\Dbg\sym\clrjit.pdb\8C6CD63E16C043C7B5EC7D38DCAC1E8B1\clrjit.pdb
如果我们自己用Visual Studio编译程序,默认都会自动生成pdb文件的。但clr或clrjit模块是微软编译的,本来我们电脑中并没有对应的符号文件。不过微软为其产品提供了一个免费的符号服务器,所以上面列举的crljit.pdb就是WinDbg需要时从微软符号服务器下载的。
我们可以随便用一个文本编辑器打开该符号文件,虽然会发现有很多乱码,但也有很多可以阅读的字符串。我们搜一下compiler.cpp,就可以发现其保存位置恰好就是我们上面所说的路径。
所以,其实WinDbg首先自动从微软符号服务器下载了符号文件,然后又通过符号文件找到了所有方法的文件全路径及行号,最后才为我们提供了自动加载与调试支持。
版本对应
以上方法虽然基本可用,但要知道,自动下载的pdb文件是和我们调试时加载的clrjit.dll版本严格对应的,但我们从GitHub中下载的源代码可能是最新的。如果.NET RUNTIME团队修改了代码,其实两者之间就不是严格对应了,此时就只能使用汇编调试,或者必须找到对应版本的repository了。