9. 让WinDbg自动打开DotNet Runtime源程序

总目录

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了。

  • 22
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值