During my preparation for an interview, I was revisiting the concept of structures and classes. It seems like the concept is well understood and even diagrams of the representation of those types on memory can be found. But when I decided to actually look at the memory of the .NET process, I found that there is no article to explain how to do that. This article is a compilation of different articles and my findings in one place.
I will concentrate on the Microsoft Console Debugger (cdb.exe) tool and how to use it to see stack memory of managed methods. The same should be possible to achieve via WinDbg.exe, but I didn’t use it. Both are available only on Windows, but for Unix there are alternatives which are not covered here.
I’ll be using .NET 5 and the application will be built in Debug configuration. The code will be executed on Windows 10, 64 bit. Hence addresses in memory will take 8 bytes (e.g. 000000dd`9b17e828).
If you want to reproduce steps from this article, you need to install cdb tool and SOS extension. Cdb is a part of Debugging Tools for Windows in SDK, follow the guide in documentation to set it up - Debugging Tools for Windows (WinDbg, KD, CDB, NTSD) - Windows drivers.
For SOS extension, run dotnettool install -g dotnet-sos. It will install SOS to your user profile and will give you a command to load SOS at the end of the installation: dotnet-sos - .NET Core
We've set up the scene. Now, let’s dive in.
Hello world and beyond
Let’s start with a basic C# program:
- class Program {
- static void Main() {
- Console.WriteLine("Hello World!");
- Debugger.Break();
- }
- }
We need to build it and then run Console Debugger via command line. First, I will set the current directory to my project output folder to omit the path to my binaries.
> “C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe” HelloWorld.exe
Microsoft (R) Windows Debugger Version 10.0.19041.1 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
[...]
Microsoft (R) Windows Debugger Version 10.0.19041.1 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
[...]
Debugger will load the necessary modules and stop. At that point CLR is not loaded. We need to use g command to run it until break:
0:000> g
ModLoad: 00007ff8`584d0000 00007ff8`584fe000 C:\Windows\System32\IMM32.DLL
[...]
Hello World!
(3f28.2c64): Break instruction exception - code 80000003 (first chance)
KERNELBASE!wil::details::DebugBreak+0x2:
00007ff8`572e1a52 cc int 3
ModLoad: 00007ff8`584d0000 00007ff8`584fe000 C:\Windows\System32\IMM32.DLL
[...]
Hello World!
(3f28.2c64): Break instruction exception - code 80000003 (first chance)
KERNELBASE!wil::details::DebugBreak+0x2:
00007ff8`572e1a52 cc int 3
As you can see it displayed Hello World and stopped at Debugger.Break(). Now we can load the SOS extension (it’s possible only after CLR has loaded) and we are ready to explore our little application memory.
0:000>.load C:\Users\eugen\.dotnet\sos\sos.dll
Call stack and stack memory
With k command we can explore unmanaged call stack (note 0x00007ff7`b2685f01 in call site for our managed function)
0:000> k
Child-SP RetAddr Call Site
000000dd`9b17e828 00007ff8`12297c89 KERNELBASE!wil::details::DebugBreak+0x2
000000dd`9b17e830 00007ff8`00989e3a coreclr!DebugDebugger::Break+0x149
000000dd`9b17e9b0 00007ff7`b2685f01 System_Private_CoreLib!System.Diagnostics.Debugger.Break()$##6005841+0xa
000000dd`9b17e9e0 00007ff8`121c9e33 0x00007ff7`b2685f01
000000dd`9b17ea10 00007ff8`1210869c coreclr!CallDescrWorkerInternal+0x83
000000dd`9b17ea50 00007ff8`121112ab coreclr!MethodDescCallSite::CallTargetWorker+0x268
(Inline Function) --------`-------- coreclr!MethodDescCallSite::Call+0xb
000000dd`9b17eb90 00007ff8`12111076 coreclr!RunMainInternal+0x11f
000000dd`9b17ecc0 00007ff8`12110bd9 coreclr!RunMain+0xd2
000000dd`9b17ed70 00007ff8`121515c8 coreclr!Assembly::ExecuteMainMethod+0x1cd
[...]
Child-SP RetAddr Call Site
000000dd`9b17e828 00007ff8`12297c89 KERNELBASE!wil::details::DebugBreak+0x2
000000dd`9b17e830 00007ff8`00989e3a coreclr!DebugDebugger::Break+0x149
000000dd`9b17e9b0 00007ff7`b2685f01 System_Private_CoreLib!System.Diagnostics.Debugger.Break()$##6005841+0xa
000000dd`9b17e9e0 00007ff8`121c9e33 0x00007ff7`b2685f01
000000dd`9b17ea10 00007ff8`1210869c coreclr!CallDescrWorkerInternal+0x83
000000dd`9b17ea50 00007ff8`121112ab coreclr!MethodDescCallSite::CallTargetWorker+0x268
(Inline Function) --------`-------- coreclr!MethodDescCallSite::Call+0xb
000000dd`9b17eb90 00007ff8`12111076 coreclr!RunMainInternal+0x11f
000000dd`9b17ecc0 00007ff8`12110bd9 coreclr!RunMain+0xd2
000000dd`9b17ed70 00007ff8`121515c8 coreclr!Assembly::ExecuteMainMethod+0x1cd
[...]
We can explore managed stack with !clrstack command from SOS extension (or !clrstack -a -i if you want also see parameters and locals)
0:000> !clrstack
OS Thread Id: 0x2c64 (0)
Child SP IP Call Site
000000DD9B17E8A8 00007ff8572e1a52 [HelperMethodFrame: 000000dd9b17e8a8] System.Diagnostics.Debugger.BreakInternal()
000000DD9B17E9B0 00007FF800989E3A System.Diagnostics.Debugger.Break()
000000DD9B17E9E0 00007FF7B2685F01 HelloWorld.Program.Main()
We see here Child SP (Stack Pointer) and IP (Instruction Pointer) for HelloWorld.Program.Main() method. Those are pointers to the top of the stack and the next instruction address in method context. It’s important to note that managed stack is a subset of unmanaged stack, hence we can use commands not only from the SOS extension to explore .NET application.
Now we have an SP that points to the top of the stack memory for the Main() function. But we don’t know where it starts. One way to find that out is to rely on cdb tool and use .frame /r command to unwind stack frames and show us registers for specific calls (you can use kn to show frame number),
0:000> .frame /r 3
03 000000dd`9b17e9e0 00007ff8`121c9e33 0x00007ff7`b2685f01
rax=000000000002a020 rbx=000000dd9b17eb08 rcx=0000000000000010
rdx=000000000000000b rsi=000000dd9b17eab8 rdi=000000dd9b17ec68
rip=00007ff7b2685f01 rsp=000000dd9b17e9e0 rbp=000000dd9b17ea00
r8=0000000000000010 r9=000002740000ebb0 r10=0000000000000000
r11=000000dd9b17e8d0 r12=000000dd9b17ecc8 r13=000000dd9b17ebe0
r14=0000000000000000 r15=0000000000000004
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
00007ff7`b2685f01 90 nop
03 000000dd`9b17e9e0 00007ff8`121c9e33 0x00007ff7`b2685f01
rax=000000000002a020 rbx=000000dd9b17eb08 rcx=0000000000000010
rdx=000000000000000b rsi=000000dd9b17eab8 rdi=000000dd9b17ec68
rip=00007ff7b2685f01 rsp=000000dd9b17e9e0 rbp=000000dd9b17ea00
r8=0000000000000010 r9=000002740000ebb0 r10=0000000000000000
r11=000000dd9b17e8d0 r12=000000dd9b17ecc8 r13=000000dd9b17ebe0
r14=0000000000000000 r15=0000000000000004
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
00007ff7`b2685f01 90 nop

Join the conversation! Your thoughts help the community grow.