Yet Another Hello World

Recently I heard there is a COOL programming language called C#, which runs on a popular environment called .NET platform (formally known as COMPLUS), so I decided to give it a try.

It took me some time to understand why I need to define a class and a static method in order to say hello to the world, but finally I managed to come up with the following piece of code:

 /* hello.cs */
class HelloWorld
{
  static void Main()
  {
    System.Console.WriteLine("Hello, world!");
  }
} 

It's quite straightforward then, I launched csc.exe /debug+ hello.cs from command prompt, after a little while a file named hello.exe was generated and everything just worked fine.

Then I launched hello.exe from WinDBG: 

 CommandLine: hello.exe
ModLoad: hello.exe
ModLoad: ntdll.dll
ModLoad: C:\Windows\SYSTEM32\MSCOREE.DLL 
ModLoad: C:\Windows\system32\KERNEL32.dll
ModLoad: C:\Windows\system32\KERNELBASE.dll
(780.b4): Break instruction exception - code 80000003 (first chance)

0:000> bp $exentry 
0:000> g
Breakpoint 0 hit
MSCOREE!ShellShim__CorExeMain:
70917cef 8bff            mov     edi,edi

As we can see, the entry point of our EXE falls into MSCOREE.DLL. By taking a look at the EXE image we can see the OEP actually points to ff250020cb00, which translates to jmp dword ptr [hello+0x2000] , it turned out that MSCOREE patched the EXE's entry point during DLL loading. A slightly different approach is used for 64bit process, the EXE is mapped as PAGE_READONLY instead of PAGE_EXECUTE_READ, and there is no OEP patching. The side effect is that you cannot use bp $exentry while debugging a 64bit managed application.

 0:000> wt -l 3 -oR 
Tracing MSCOREE!ShellShim__CorExeMain to return address 70914de3
    8     0 [  0] MSCOREE!ShellShim__CorExeMain
    4     0 [  1]   MSCOREE!GetShimImpl
   10     0 [  2]     MSCOREE!InitShimImpl
   17     0 [  3]       MSCOREE!_EH_prolog3 eax = 1ff5c4
   19    17 [  2]     MSCOREE!InitShimImpl
   14     0 [  3]       MSCOREE!BaseWrapper<unsigned short *,FunctionBase<unsigned short *,&DoNothing<unsigned short *>,&Delete<unsigned short>,2>,0,&CompareDefault<unsigned short *>,2>::operator& eax = 1ff5a8
   24    31 [  2]     MSCOREE!InitShimImpl
ModLoad: C:\Windows\system32\ADVAPI32.dll
ModLoad: C:\Windows\system32\msvcrt.dll
ModLoad: C:\Windows\SYSTEM32\sechost.dll
ModLoad: C:\Windows\system32\RPCRT4.dll
   35     0 [  3]       MSCOREE!FindLatestVersion eax = 0
   28    66 [  2]     MSCOREE!InitShimImpl
   11     0 [  3]       MSCOREE!BaseWrapper<unsigned short *,FunctionBase<unsigned short *,&DoNothing<unsigned short *>,&Delete<unsigned short>,2>,0,&CompareDefault<unsigned short *>,2>::TypedAddressInitHolder::~TypedAddressInitHolder eax = 1ff5b8
   37    77 [  2]     MSCOREE!InitShimImpl
  622     0 [  3]       MSCOREE!GetShimDllPathName eax = 0
   42   699 [  2]     MSCOREE!InitShimImpl
   23     0 [  3]       MSCOREE!FileExists eax = 1
   50   722 [  2]     MSCOREE!InitShimImpl
ModLoad: C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscoreei.dll 
   32     0 [  3]       MSCOREE!LoadLibraryShim eax = 0
   59   754 [  2]     MSCOREE!InitShimImpl
    5     0 [  3]       KERNEL32!GetProcAddressStub
    1     0 [  3]       KERNEL32!GetProcAddress
   37     0 [  3]       KERNELBASE!GetProcAddress eax = 70346975
   63   797 [  2]     MSCOREE!InitShimImpl

    5     0 [  3]       KERNEL32!GetProcAddressStub
    1     0 [  3]       KERNEL32!GetProcAddress
   35     0 [  3]       KERNELBASE!GetProcAddress eax = 0
   68   838 [  2]     MSCOREE!InitShimImpl
    5     0 [  3]       KERNEL32!GetProcAddressStub
    1     0 [  3]       KERNEL32!GetProcAddress
   35     0 [  3]       KERNELBASE!GetProcAddress eax = 0
   74   879 [  2]     MSCOREE!InitShimImpl
    5     0 [  3]       KERNEL32!GetProcAddressStub
    1     0 [  3]       KERNEL32!GetProcAddress
   37     0 [  3]       KERNELBASE!GetProcAddress eax = 703443ef
   80   922 [  2]     MSCOREE!InitShimImpl
   21     0 [  3]       ntdll!RtlEnterCriticalSection eax = 0
   91   943 [  2]     MSCOREE!InitShimImpl
   16     0 [  3]       mscoreei!RegisterShimImplCallback eax = 0
   97   959 [  2]     MSCOREE!InitShimImpl
  613     0 [  3]       MSCOREE!wcscpy_s eax = 0
  108  1572 [  2]     MSCOREE!InitShimImpl
   23     0 [  3]       ntdll!RtlLeaveCriticalSection eax = 0
  114  1595 [  2]     MSCOREE!InitShimImpl
    8     0 [  3]       MSCOREE!NewArrayHolder<unsigned char,Wrapper<unsigned char *,&DoNothing<unsigned char *>,&DeleteArray<unsigned char>,0,&CompareDefault<unsigned char *>,2> >::~NewArrayHolder<unsigned char,Wrapper<unsigned char *,&DoNothing<unsigned char *>,&DeleteArray<un eax = 7f
  123  1603 [  2]     MSCOREE!InitShimImpl
    3     0 [  3]       MSCOREE!__security_check_cookie eax = 7f
  127  1606 [  2]     MSCOREE!InitShimImpl eax = 7f
   15  1733 [  1]   MSCOREE!GetShimImpl eax = 2
   23  1748 [  0] MSCOREE!ShellShim__CorExeMain
    5     0 [  1]   KERNEL32!GetProcAddressStub
    1     0 [  1]   KERNEL32!GetProcAddress
   15     0 [  1]   KERNELBASE!GetProcAddress
   37     0 [  2]     ntdll!RtlInitString eax = 0
   23    37 [  1]   KERNELBASE!GetProcAddress
   11     0 [  2]     KERNELBASE!BasepMapModuleHandle eax = 70340000
   25    48 [  1]   KERNELBASE!GetProcAddress
    9     0 [  2]     ntdll!LdrGetProcedureAddress
   97     0 [  3]       ntdll!LdrGetProcedureAddressEx eax = ffffffff`c0000139
   11    97 [  2]     ntdll!LdrGetProcedureAddress eax = ffffffff`c0000139
   29   156 [  1]   KERNELBASE!GetProcAddress
    6     0 [  2]     KERNELBASE!BaseSetLastNTError
   14     0 [  3]       ntdll!RtlNtStatusToDosError eax = 7f
    9    14 [  2]     KERNELBASE!BaseSetLastNTError
   12     0 [  3]       ntdll!RtlSetLastWin32Error eax = 0
   13    26 [  2]     KERNELBASE!BaseSetLastNTError eax = 7f
   35   195 [  1]   KERNELBASE!GetProcAddress eax = 0
   33  1984 [  0] MSCOREE!ShellShim__CorExeMain
    5     0 [  1]   KERNEL32!GetProcAddressStub
    1     0 [  1]   KERNEL32!GetProcAddress
   15     0 [  1]   KERNELBASE!GetProcAddress
   29     0 [  2]     ntdll!RtlInitString eax = 0
   23    29 [  1]   KERNELBASE!GetProcAddress
   11     0 [  2]     KERNELBASE!BasepMapModuleHandle eax = 70340000
   25    40 [  1]   KERNELBASE!GetProcAddress
    9     0 [  2]     ntdll!LdrGetProcedureAddress
  109     0 [  3]       ntdll!LdrGetProcedureAddressEx eax = 0
   11   109 [  2]     ntdll!LdrGetProcedureAddress eax = 0
   30   160 [  1]   KERNELBASE!GetProcAddress
   11     0 [  2]     KERNELBASE!BasepMapModuleHandle eax = 70340000
   37   171 [  1]   KERNELBASE!GetProcAddress eax = 70345573
   39  2198 [  0] MSCOREE!ShellShim__CorExeMain
    7     0 [  1]   mscoreei!_CorExeMain
   18     0 [  2]     mscoreei!ShimLog::Log
    3     0 [  3]       mscoreei!__security_check_cookie eax = ffffffff`cf9f4bc6
   20     3 [  2]     mscoreei!ShimLog::Log eax = ffffffff`cf9f4bc6
   15    23 [  1]   mscoreei!_CorExeMain 
    3     0 [  2]     mscoreei!GetCorExeMainEntrypoint
   17     0 [  3]       mscoreei!_EH_prolog3 eax = 1ff7bc
   13    17 [  2]     mscoreei!GetCorExeMainEntrypoint
   14     0 [  3]       mscoreei!BaseWrapper<ICLRMetaHostPolicy *,FunctionBase<ICLRMetaHostPolicy *,&DoNothing<ICLRMetaHostPolicy *>,&DoTheRelease<ICLRMetaHostPolicy>,2>,0,&CompareDefault<ICLRMetaHostPolicy *>,2>::operator& eax = 1ff7d8
   18    31 [  2]     mscoreei!GetCorExeMainEntrypoint
  135     0 [  3]       mscoreei!CLRCreateInstance eax = 0
   34   166 [  2]     mscoreei!GetCorExeMainEntrypoint
   14     0 [  3]       mscoreei!BaseWrapper<ICLRMetaHostPolicy *,FunctionBase<ICLRMetaHostPolicy *,&DoNothing<ICLRMetaHostPolicy *>,&DoTheRelease<ICLRMetaHostPolicy>,2>,0,&CompareDefault<ICLRMetaHostPolicy *>,2>::operator& eax = 1ff7d8
   55   180 [  2]     mscoreei!GetCorExeMainEntrypoint
ModLoad: C:\Windows\system32\SHLWAPI.dll
ModLoad: C:\Windows\system32\GDI32.dll
ModLoad: C:\Windows\system32\USER32.dll
ModLoad: C:\Windows\system32\LPK.dll
ModLoad: C:\Windows\system32\USP10.dll
ModLoad: C:\Windows\system32\IMM32.DLL
ModLoad: C:\Windows\system32\MSCTF.dll
   54     0 [  3]       mscoreei!CLRMetaHostPolicyImpl::GetRequestedRuntime eax = 0
   73   234 [  2]     mscoreei!GetCorExeMainEntrypoint
ModLoad: C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll 
ModLoad: C:\Windows\system32\MSVCR100_CLR0400.dll 
   32     0 [  3]       mscoreei!CLRRuntimeInfoImpl::GetProcAddress eax = 0
   79   266 [  2]     mscoreei!GetCorExeMainEntrypoint
   17     0 [  3]       mscoreei!BaseWrapper<ICLRMetaHostPolicy *,FunctionBase<ICLRMetaHostPolicy *,&DoNothing<ICLRMetaHostPolicy *>,&DoTheRelease<ICLRMetaHostPolicy>,2>,0,&CompareDefault<ICLRMetaHostPolicy *>,2>::~BaseWrapper<ICLRMetaHostPolicy *,FunctionBase<ICLRMetaHostPolicy eax = 0
   82   283 [  2]     mscoreei!GetCorExeMainEntrypoint
   17     0 [  3]       mscoreei!BaseWrapper<ICLRMetaHostPolicy *,FunctionBase<ICLRMetaHostPolicy *,&DoNothing<ICLRMetaHostPolicy *>,&DoTheRelease<ICLRMetaHostPolicy>,2>,0,&CompareDefault<ICLRMetaHostPolicy *>,2>::~BaseWrapper<ICLRMetaHostPolicy *,FunctionBase<ICLRMetaHostPolicy eax = 0
   84   300 [  2]     mscoreei!GetCorExeMainEntrypoint
   11     0 [  3]       mscoreei!_EH_epilog3 eax = 0
   85   311 [  2]     mscoreei!GetCorExeMainEntrypoint eax = 0
   20   419 [  1]   mscoreei!_CorExeMain
    3     0 [  2]     clr!_CorExeMain
   21     0 [  3]       clr!_SEH_prolog4 eax = 1ff7c4
    8    21 [  2]     clr!_CorExeMain
(780.b4): Unknown exception - code 04242420 (first chance)
ModLoad: C:\Windows\assembly\NativeImages_v4.0.30319_32\mscorlib\mscorlib.ni.dll 
ModLoad: C:\Windows\Microsoft.NET\Framework\v4.0.30319\nlssorting.dll
ModLoad: C:\Windows\system32\ole32.dll
ModLoad: C:\Windows\system32\CRYPTBASE.dll
ModLoad: C:\Windows\Microsoft.NET\Framework\v4.0.30319\clrjit.dll 
   70     0 [  3]       clr!_CorExeMainInternal

A quick debugging on clr!_CorExeMainInternal function showed many interesting function invocations, such as clr!DoAdditionalPEChecks, clr!CorCommandLine::SetArgvW, clr!EnsureEEStarted, clr!IsStackProbingEnabled, and eventually clr!ExecuteEXE.

 clr!ExecuteEXE:
657080dc 6a24            push    24h
657080de 6888817065      push    offset clr! ?? ::FNODOBFM::`string'+0x6ee2c (65708188)
657080e3 e8f898edff      call    clr!_SEH_prolog4 (655e19e0)
657080e8 8b7508          mov     esi,dword ptr [ebp+8]
657080eb 33ff            xor     edi,edi
657080ed 3bf7            cmp     esi,edi
657080ef 0f84be250b00    je      clr!ExecuteEXE+0x15 (657ba6b3)

clr!ExecuteEXE+0x1c:
657080f5 683c796165      push    offset clr!StartupId (6561793c)
657080fa 6868817065      push    offset clr!ExecExe_V1 (65708168)
657080ff ff359ca9bc65    push    dword ptr [clr!Microsoft_Windows_DotNETRuntimePrivateHandle+0x4 (65bca99c)]
65708105 ff3598a9bc65    push    dword ptr [clr!Microsoft_Windows_DotNETRuntimePrivateHandle (65bca998)]
6570810b e804f8f0ff      call    clr!ETWTraceStartup::StartupTraceEvent (65617914)
65708110 897508          mov     dword ptr [ebp+8],esi
65708113 897de4          mov     dword ptr [ebp-1Ch],edi
65708116 897dfc          mov     dword ptr [ebp-4],edi
65708119 8d4dcc          lea     ecx,[ebp-34h]
6570811c e837b4edff      call    clr!CLRException::HandlerState::HandlerState (655e3558)
65708121 c745fc01000000  mov     dword ptr [ebp-4],1
65708128 57              push    edi
65708129 ff7508          push    dword ptr [ebp+8]
6570812c e8a2f7ffff      call    clr!SystemDomain::ExecuteMainMethod (657078d3)
65708131 897dfc          mov     dword ptr [ebp-4],edi
65708134 e8c1380000      call    clr!ExecuteEXE+0x64 (6570b9fa)
65708139 c745fcfeffffff  mov     dword ptr [ebp-4],0FFFFFFFEh
65708140 683c796165      push    offset clr!StartupId (6561793c)
65708145 6878817065      push    offset clr!ExecExeEnd_V1 (65708178)
6570814a ff359ca9bc65    push    dword ptr [clr!Microsoft_Windows_DotNETRuntimePrivateHandle+0x4 (65bca99c)]
65708150 ff3598a9bc65    push    dword ptr [clr!Microsoft_Windows_DotNETRuntimePrivateHandle (65bca998)]
65708156 e8b9f7f0ff      call    clr!ETWTraceStartup::StartupTraceEvent (65617914)
6570815b 33c0            xor     eax,eax
6570815d 40              inc     eax

clr!ExecuteEXE+0xcb:
6570815e e8c298edff      call    clr!_SEH_epilog4 (655e1a25)
65708163 c20400          ret     4

clr!ExecuteEXE+0x15:
657ba6b3 33c0            xor     eax,eax
657ba6b5 e9a4daf4ff      jmp     clr!ExecuteEXE+0xcb (6570815e)

After a few steps into and over, I got an interesting callstack:

 0:000> k 
ChildEBP RetAddr  
001af6fc 657079b4 clr!PEFile::GetEntryPointToken
001afbdc 65708131 clr!SystemDomain::ExecuteMainMethod+0xe1
001afc30 65708032 clr!ExecuteEXE+0x58
001afc7c 657468b0 clr!_CorExeMainInternal+0x19f
001afcb4 703455ab clr!_CorExeMain+0x4e
001afcc0 70917f16 mscoreei!_CorExeMain+0x38
001afcd0 70914de3 MSCOREE!ShellShim__CorExeMain+0x99
001afcd8 7677ed6c MSCOREE!_CorExeMain_Exported+0x8
001afce4 773d37f5 KERNEL32!BaseThreadInitThunk+0xe
001afd24 773d37c8 ntdll!__RtlUserThreadStart+0x70
001afd3c 00000000 ntdll!_RtlUserThreadStart+0x1b

0:000> uf eip 
clr!PEFile::GetEntryPointToken:
65726c97 6a08            push    8
65726c99 b8c08fad65      mov     eax,offset clr! ?? ::FNODOBFM::`string'+0x6359 (65ad8fc0)
65726c9e e8f5abebff      call    clr!_EH_prolog3 (655e1898)
65726ca3 8bf1            mov     esi,ecx
65726ca5 e8b0ffeeff      call    clr!PEFile::IsResource (65616c5a)
65726caa 85c0            test    eax,eax
65726cac 0f858ac10a00    jne     clr!PEFile::GetEntryPointToken+0x91 (657d2e3c)

clr!PEFile::GetEntryPointToken+0x17:
65726cb2 394604          cmp     dword ptr [esi+4],eax
65726cb5 0f8481c10a00    je      clr!PEFile::GetEntryPointToken+0x91 (657d2e3c)

clr!PEFile::GetEntryPointToken+0x1c:
65726cbb 394608          cmp     dword ptr [esi+8],eax
65726cbe 7461            je      clr!PEFile::GetEntryPointToken+0x87 (65726d21)

clr!PEFile::GetEntryPointToken+0x21:
65726cc0 8bce            mov     ecx,esi
65726cc2 e8468befff      call    clr!PEFile::IsNativeLoaded (6561f80d)
65726cc7 85c0            test    eax,eax
65726cc9 7556            jne     clr!PEFile::GetEntryPointToken+0x87 (65726d21)

clr!PEFile::GetEntryPointToken+0x2c:
65726ccb 8b4e08          mov     ecx,dword ptr [esi+8]
65726cce e82a78f4ff      call    clr!PEImage::IsOpened (6566e4fd)
65726cd3 85c0            test    eax,eax
65726cd5 0f840fc10a00    je      clr!PEFile::GetEntryPointToken+0x38 (657d2dea)

clr!PEFile::GetEntryPointToken+0x82:
65726cdb 8b4e08          mov     ecx,dword ptr [esi+8]

clr!PEFile::GetEntryPointToken+0x8a:
65726cde e806000000      call    clr!PEImage::GetEntryPointToken (65726ce9)

clr!PEFile::GetEntryPointToken+0x93:
65726ce3 e854aaebff      call    clr!_EH_epilog3 (655e173c)
65726ce8 c3              ret

clr!PEFile::GetEntryPointToken+0x87:
65726d21 8b4e0c          mov     ecx,dword ptr [esi+0Ch]
65726d24 ebb8            jmp     clr!PEFile::GetEntryPointToken+0x8a (65726cde)

clr!PEFile::GetEntryPointToken+0x38:
657d2dea 8bce            mov     ecx,esi
657d2dec e8a61be8ff      call    clr!PEFile::GetNativeImageWithRef (65654997)
657d2df1 8365f000        and     dword ptr [ebp-10h],0
657d2df5 8945ec          mov     dword ptr [ebp-14h],eax
657d2df8 85c0            test    eax,eax
657d2dfa 7407            je      clr!PEFile::GetEntryPointToken+0x51 (657d2e03)

clr!PEFile::GetEntryPointToken+0x4a:
657d2dfc c745f001000000  mov     dword ptr [ebp-10h],1

clr!PEFile::GetEntryPointToken+0x51:
657d2e03 c745fc03000000  mov     dword ptr [ebp-4],3
657d2e0a 8b4dec          mov     ecx,dword ptr [ebp-14h]
657d2e0d 85c9            test    ecx,ecx
657d2e0f 741a            je      clr!PEFile::GetEntryPointToken+0x76 (657d2e2b)

clr!PEFile::GetEntryPointToken+0x5f:
657d2e11 e8d33ef5ff      call    clr!PEImage::GetEntryPointToken (65726ce9)
657d2e16 8bf0            mov     esi,eax
657d2e18 834dfcff        or      dword ptr [ebp-4],0FFFFFFFFh
657d2e1c 8d4dec          lea     ecx,[ebp-14h]
657d2e1f e8e91be8ff      call    clr!BaseWrapper<PEImage *,FunctionBase<PEImage *,&DoNothing<PEImage *>,&DoTheRelease<PEImage>,2>,0,&CompareDefault<PEImage *>,2>::~BaseWrapper<PEImage *,FunctionBase<PEImage *,&DoNothing<PEImage *>,&DoTheRelease<PEImage>,2>,0,&CompareDefault<PEImage *>,2> (65654a0d)
657d2e24 8bc6            mov     eax,esi
657d2e26 e9b83ef5ff      jmp     clr!PEFile::GetEntryPointToken+0x93 (65726ce3)

clr!PEFile::GetEntryPointToken+0x76:
657d2e2b 834dfcff        or      dword ptr [ebp-4],0FFFFFFFFh
657d2e2f 8d4dec          lea     ecx,[ebp-14h]
657d2e32 e8d61be8ff      call    clr!BaseWrapper<PEImage *,FunctionBase<PEImage *,&DoNothing<PEImage *>,&DoTheRelease<PEImage>,2>,0,&CompareDefault<PEImage *>,2>::~BaseWrapper<PEImage *,FunctionBase<PEImage *,&DoNothing<PEImage *>,&DoTheRelease<PEImage>,2>,0,&CompareDefault<PEImage *>,2> (65654a0d)
657d2e37 e99f3ef5ff      jmp     clr!PEFile::GetEntryPointToken+0x82 (65726cdb)

clr!PEFile::GetEntryPointToken+0x91:
657d2e3c 33c0            xor     eax,eax
657d2e3e e9a03ef5ff      jmp     clr!PEFile::GetEntryPointToken+0x93 (65726ce3)

This function would retrieve the EntryPointToken from the COR Header, which in turn is a part of the PE32/PE32+ header. Record down the return value of clr!PEFile::GetEntryPointToken for later use.

As soon as clr.dll was loaded, we are ready to ask Son of Strike for help:

 0:000>  .loadby sos clr

Continue tracing until clr!AppDomain::LoadDomainAssembly, now the hello.exe assembly was loaded into the AppDomain:

 0:000>  !EEVersion
4.0.30319.237 free
Workstation mode
SOS Version: 4.0.30319.237 retail build
 0:000>  !DumpDomain
--------------------------------------
System Domain:      65bd3478
LowFrequencyHeap:   65bd3784
HighFrequencyHeap:  65bd37d0
StubHeap:           65bd381c
Stage:              OPEN
Name:               None
--------------------------------------
Shared Domain:      65bd3140
LowFrequencyHeap:   65bd3784
HighFrequencyHeap:  65bd37d0
StubHeap:           65bd381c
Stage:              OPEN
Name:               None
Assembly:           003e7988 [C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\mscorlib.dll]
ClassLoader:        003e7a28
  Module Name
64811000            C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\mscorlib.dll

--------------------------------------
Domain 1:           003a24d0
LowFrequencyHeap:   003a284c
HighFrequencyHeap:  003a2898
StubHeap:           003a28e4
Stage:              OPEN
SecurityDescriptor: 003a3c48
Name:               DefaultDomain
Assembly:           003e7988 [C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\mscorlib.dll]
ClassLoader:        003e7a28
SecurityDescriptor: 003e7870
  Module Name
64811000            C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\mscorlib.dll

Assembly:           003efd40 [hello.exe]
ClassLoader:        003eb2a0
SecurityDescriptor: 003efc68
  Module Name
00232e9c            hello.exe

0:000>  !DumpAssembly 003efd40
Parent Domain:      003a24d0
Name:               hello.exe
ClassLoader:        003eb2a0
  Module Name
00232e9c            hello.exe

0:000>  !DumpModule 00232e9c
Name:       hello.exe
Attributes: PEFile 
Assembly:   003efd40
LoaderHeap:              00000000
TypeDefToMethodTableMap: 002300c4
TypeRefToMethodTableMap: 002300d0
MethodDefToDescMap:      002300ec
FieldDefToDescMap:       002300f8
MemberRefToDescMap:      002300fc
FileReferencesMap:       00230118
AssemblyReferencesMap:   0023011c
MetaData start address:  00272068 (732 bytes)

0:000> k
ChildEBP RetAddr  
001af4a0 65707f0b clr!PEFile::GetEntryPointToken+0x98
001af6f8 65707d3f clr!Assembly::ExecuteMainMethod+0xa4
001afbdc 65708131 clr!SystemDomain::ExecuteMainMethod+0x4ec
001afc30 65708032 clr!ExecuteEXE+0x58
001afc7c 657468b0 clr!_CorExeMainInternal+0x19f
001afcb4 703455ab clr!_CorExeMain+0x4e
001afcc0 70917f16 mscoreei!_CorExeMain+0x38
001afcd0 70914de3 MSCOREE!ShellShim__CorExeMain+0x99
001afcd8 7677ed6c MSCOREE!_CorExeMain_Exported+0x8
001afce4 773d37f5 KERNEL32!BaseThreadInitThunk+0xe
001afd24 773d37c8 ntdll!__RtlUserThreadStart+0x70
001afd3c 00000000 ntdll!_RtlUserThreadStart+0x1b

0:000> pt
eax=06000001 ebx=00000000 ecx=65726ce8 edx=fffffdff esi=003efd40 edi=00000000
eip=65726ce8 esp=001af034 ebp=001af4a0 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
clr!PEFile::GetEntryPointToken+0x98:
65726ce8 c3              ret

0:000>  !Token2EE hello.exe 6000001
Module:      00232e9c
Assembly:    hello.exe
Token:       06000001
MethodDesc:  <not loaded yet>
Name:        HelloWorld.Main 
Not JITTED yet.

0:000>  !Token2EE hello.exe 6000002
Module:      00232e9c
Assembly:    hello.exe
Token:       06000001
MethodDesc:  <not loaded yet>
Name:        HelloWorld..ctor 
Not JITTED yet.

Now that we know where the CLR entry point is, set a break point:

 0:000>  !bpmd hello.exe HelloWorld.Main
Adding pending breakpoints...
 0:000> go

It turns out that the simple hello world application has 3 threads at least (far better than Notepad, though):

 0:000>  ~*k 
   0  Id: a18.90c Suspend: 1 Teb: 7ffde000 Unfrozen
ChildEBP RetAddr  
0023f1e4 651821bb hello!HelloWorld.Main()
0023f1e4 651a4be2 clr!CallDescrWorker+0x33
0023f260 651a4d84 clr!CallDescrWorkerWithHandler+0x8e
0023f39c 651a4db9 clr!MethodDesc::CallDescr+0x194
0023f3b8 651a4dd9 clr!MethodDesc::CallTargetWorker+0x21
0023f3d0 652a7e1d clr!MethodDescCallSite::Call+0x1c
0023f534 652a7f28 clr!ClassLoader::RunMain+0x24c
0023f79c 652a7d3f clr!Assembly::ExecuteMainMethod+0xc1
0023fc80 652a8131 clr!SystemDomain::ExecuteMainMethod+0x4ec
0023fcd4 652a8032 clr!ExecuteEXE+0x58
0023fd20 652e68b0 clr!_CorExeMainInternal+0x19f
0023fd58 6f1e55ab clr!_CorExeMain+0x4e
0023fd64 70377f16 mscoreei!_CorExeMain+0x38
0023fd74 70374de3 MSCOREE!ShellShim__CorExeMain+0x99
0023fd7c 76f8ed6c MSCOREE!_CorExeMain_Exported+0x8
0023fd88 76e637f5 KERNEL32!BaseThreadInitThunk+0xe
0023fdc8 76e637c8 ntdll!__RtlUserThreadStart+0x70
0023fde0 00000000 ntdll!_RtlUserThreadStart+0x1b

   1  Id: a18.b54 Suspend: 1 Teb: 7ffdd000 Unfrozen
ChildEBP RetAddr  
009cfb4c 76e46a04 ntdll!KiFastSystemCallRet
009cfb50 75256a36 ntdll!NtWaitForMultipleObjects+0xc
009cfbec 76f8bd1e KERNELBASE!WaitForMultipleObjectsEx+0x100
009cfc34 76f8bd8c KERNEL32!WaitForMultipleObjectsExImplementation+0xe0
009cfc50 652dd300 KERNEL32!WaitForMultipleObjects+0x18
009cfcb4 652dd23e clr!DebuggerRCThread::MainLoop+0xd9
009cfce4 652dd179 clr!DebuggerRCThread::ThreadProc+0xca
009cfd10 76f8ed6c clr!DebuggerRCThread::ThreadProcStatic+0x83
009cfd1c 76e637f5 KERNEL32!BaseThreadInitThunk+0xe
009cfd5c 76e637c8 ntdll!__RtlUserThreadStart+0x70
009cfd74 00000000 ntdll!_RtlUserThreadStart+0x1b

   2  Id: a18.e1c Suspend: 1 Teb: 7ffdc000 Unfrozen
ChildEBP RetAddr  
00c5f688 76e46a04 ntdll!KiFastSystemCallRet
00c5f68c 75256a36 ntdll!NtWaitForMultipleObjects+0xc
00c5f728 76f8bd1e KERNELBASE!WaitForMultipleObjectsEx+0x100
00c5f770 76f8bd8c KERNEL32!WaitForMultipleObjectsExImplementation+0xe0
00c5f78c 6520d4de KERNEL32!WaitForMultipleObjects+0x18
00c5f7ac 6520d542 clr!WKS::WaitForFinalizerEvent+0xa6
00c5f7c4 652c453a clr!WKS::GCHeap::FinalizerThreadWorker+0x4a
00c5f7d8 652c45bc clr!Thread::DoExtraWorkForFinalizer+0x114
00c5f888 652c4677 clr!Thread::ShouldChangeAbortToUnload+0x101
00c5f8e8 65337ae7 clr!Thread::ShouldChangeAbortToUnload+0x399
00c5f90c 65337afa clr!ManagedThreadBase_NoADTransition+0x35
00c5f91c 652a82ab clr!ManagedThreadBase::FinalizerBase+0xf
00c5f954 65286578 clr!WKS::GCHeap::FinalizerThreadStart+0x10c
00c5f9ec 76f8ed6c clr!Thread::intermediateThreadProc+0x4b
00c5f9f8 76e637f5 KERNEL32!BaseThreadInitThunk+0xe
00c5fa38 76e637c8 ntdll!__RtlUserThreadStart+0x70
00c5fa50 00000000 ntdll!_RtlUserThreadStart+0x1b

Why are we getting extra threads? The answer is one for finalizer and another for debug helper. Another finding is that we were given workstation GC (WKS::GCHeap) by default (and dd clr!g_IGCConcurrent showed that concurrent GC mode was enabled). The  !Threads -special command would show these extra threads as Finalizer and DbgHelper. Sometimes, there can be extra threads with the ntdll!TppWorkerThread frame, these are threads created by the operating system kernel, which can be verified by placing a breakpoint on ntdll!TpAllocPool. On my Win7 x64 machine the callstack would look like:

 ntdll!TpAllocPool
KERNELBASE!CreateThreadpool+0x12
RPCRT4!RPC_THREAD_POOL::InitializeCallbackEnvironmentIfNecessary+0x94
RPCRT4!RPC_THREAD_POOL::CreateTimer+0x1d
RPCRT4!GarbageCollectionNeeded+0xc3
RPCRT4!LRPC_CASSOCIATION::RemoveReference+0x14c
RPCRT4!LRPC_FAST_BINDING_HANDLE::ResetBindState+0x81
RPCRT4!LRPC_BASE_BINDING_HANDLE::FreeObject+0x17
RPCRT4!RpcBindingFree+0x48
RPCRT4!NDRCContextUnmarshallInternal+0xb7
RPCRT4!Ndr64UnmarshallHandle+0xad
RPCRT4!Ndr64pClientUnMarshal+0x122
RPCRT4!NdrpClientCall3+0x2a6
RPCRT4!NdrClientCall3+0xf2
ADVAPI32!LsaClose+0x2f
ADVAPI32!InitializeSidLookupTable+0x279
ADVAPI32!LocalConvertStringSDToSD_Rev1+0xb2
ADVAPI32!ConvertStringSecurityDescriptorToSecurityDescriptorW+0x32
clr!ProfilingAPIAttachDetach::GetSecurityDescriptor+0x172
clr!ProfilingAPIAttachDetach::InitSecurityAttributes+0x19
clr!ProfilingAPIAttachDetach::InitializeForOnDemandMode+0x58
clr!ProfilingAPIAttachDetach::GetAttachEvent+0x1a
clr!WKS::GCHeap::FinalizerThreadStart+0x6b
clr!Thread::intermediateThreadProc+0x7d
KERNEL32!BaseThreadInitThunk+0xd
ntdll!RtlUserThreadStart+0x1d

Now let's take a look at the JITted code:

 0:000>  !u eip
Normal JIT generated code
HelloWorld.Main()
Begin 003e0070, size 21

003e0070 55              push    ebp
003e0071 8bec            mov     ebp,esp
003e0073 833d3c311c0000  cmp     dword ptr ds:[1C313Ch],0
003e007a 7405            je      hello!HelloWorld.Main()+0x11 (003e0081)
003e007c e8c6671b67      call    clr!JIT_DbgIsJustMyCode (67596847)
003e0081 90              nop

003e0082 8b0d30209f02    mov     ecx,dword ptr ds:[29F2030h] ("Hello, world!")
003e0088 e81f701364      call    mscorlib_ni!System.Console.WriteLine(System.String) (645170ac)
003e008d 90              nop

003e008e 90              nop
003e008f 5d              pop     ebp
003e0090 c3              ret

The disassembly looks interesting at least in three ways:

  1. The code is already JITted, otherwise the entry point in MethodDesc table would point to clr!PrecodeFixupThunk instead of the emitted machine instructions, this can be verified by placing a data breakpoint on the entry point field of MethodDesc, and take a look at clr!MethodDesc::SetStableEntryPointInterlocked and clr!MethodDesc::SetNativeCodeInterlocked from the callstack. One thing to keep in mind is that the breakpoint itself relies on JIT compiler to tell where the machine instruction was emitted.

  2. The generated code is not optimized in two ways. First, the CIL (MSIL) itself is not optimized as the code is compiled under debug mode with optimization turned off (/optimize-) by default, which also implied EnC (Edit and Continue). This can be verified using the !DumpIL command:

     IL_0000: nop 
    IL_0001: ldstr "Hello, world!"
    IL_0006: call System.Console::WriteLine 
    IL_000b: nop 
    IL_000c: ret 
    

    Second, the code generation engine is working under debugging mode, this is one of the Side Effects of Debugger. However, there are several ways to enable the optimization, such as hijacking the clrjit!CLRConfig::GetConfigValue. One cheap way is to have a configuration file and save it as hello.ini (one chicken and egg question: why are we using .ini file instead of the modern & popular XML?) under the same folder as hello.exe:

     [.NET Framework Debugging Control]
    GenerateTrackingInfo=0
    AllowOptimize=1
    
  3. There is a special function, which is direct related to JMC (Just My Code).

The next thing I noticed is that the C:\Windows\assembly folder looks strange from Windows Explorer, so I tried to rename the Desktop.ini file:

 cd /d %WINDIR%\assembly
 attrib -r -h -s Desktop.ini
 ren Desktop.ini Desktop.ini.bak

I also tried to dump the folder structure using tree /f command under C:\Windows\assembly, and find the folder GAC, GAC_32, GAC_64, GAC_MSIL as well as a number of folders whose name started with NativeImages_ . There are two other folders called tmp and temp which I haven't spend time investigating. A quick search on MSDN showed there is a tool comes with the .NET SDK called corflags.exe, so I gave it a try using the following command:

 for /r C:\Windows\assembly %f in (*.dll) do corflags.exe /nologo "%f" 

When I used corflags.exe on my hello.exe application, I got the following output:

 Version   : v4.0.30319
CLR Header: 2.5
PE        : PE32
CorFlags  : 1
ILONLY    : 1
32BIT     : 0
Signed    : 0

The loader that comes with NT 5.1 and later versions would acknowledge the CLR header with help from the MsCorEE.dll shim. On a 64bit system (either x64 or IA64) the loader would dynamically construct an in memory PE32+ header based on the original PE32 header, given that the module has a IMAGE_COR20_HEADER with MajorRuntimeVersion >= 2 and the module targets ANYCPU.

The PE checksum (NT_HEADERS.OptionalHeader.CheckSum) is zero for the hello.exe module, no matter which flags I used for csc.exe (C# Compiler) or al.exe (Assembly Linker). It doesn't take much time to find the reason from ECMA-335 [25.2.3.2] PE header Windows NT-specific fields:

 File Checksum: should be 0

Even many assemblies which are part of the .NET Framework have zero in their PE checksum. WinDBG would complain because PE checksum is used for symbol matching. Currently there are two workarounds on top of my head:

  1. Use link.exe instead of al.exe and csc.exe, with /RELEASE turned on. (link.exe would always merge modules and produce single module assembly)

     csc.exe /nologo /debug+ /t:module hello.cs
    link.exe /LTCG /DEBUG /RELEASE /ENTRY:HelloWorld.Main /PDB:hello.exe.pdb /SUBSYSTEM:CONSOLE hello.netmodule
    
  2. Use some PE editing utility to fix the PE checksum. (the associated PDB should be tweaked to reflect the PE checksum changes as well)

To answer why File Checksum "should be 0", I guess the original team that designed the assembly format encountered chicken and egg problem while trying to put a digital sign into the assembly itself.

Synchronization is always one of my favorites, so I tried the following code:

 class HelloWorld
{
  static void Main()
  {
    lock("")
    {
      System.Console.WriteLine("Hello, world!");
    }
  }
}

When I tried to debug, I got the following thing:

 0:000>  !Name2EE hello!HelloWorld.Main 
Module:      009b2e9c
Assembly:    hello.exe
Token:       06000001
MethodDesc:  009b33f0
Name:        HelloWorld.Main()
JITTED Code Address: 035f2670

0:000>  !DumpIL 009b33f0 
ilAddr = 00402050
IL_0000: ldstr ""
IL_0005: dup
IL_0006: stloc.0
IL_0007: call System.Threading.Monitor::Enter
.try 
{
  IL_000c: ldstr "Hello, world!"
  IL_0011: call System.Console::WriteLine
  IL_0016: leave.s IL_001f
} // end .try
.finally 
{
  IL_0018: ldloc.0
  IL_0019: call System.Threading.Monitor::Exit 
  IL_001e: endfinally

} // end .finally
IL_001f: ret

0:000>  !U 035f2670
Normal JIT generated code
HelloWorld.Main()
Begin 035f2670, size 71
035f2670 55              push    ebp
035f2671 8bec            mov     ebp,esp
035f2673 57              push    edi
035f2674 56              push    esi
035f2675 53              push    ebx
035f2676 83ec18          sub     esp,18h
035f2679 8d7ddc          lea     edi,[ebp-24h]
035f267c b905000000      mov     ecx,5
035f2681 33c0            xor     eax,eax
035f2683 f3ab            rep stos dword ptr es:[edi]
035f2685 33c0            xor     eax,eax
035f2687 8945e8          mov     dword ptr [ebp-18h],eax
035f268a 8b056020b201    mov     eax,dword ptr ds:[1B22060h] ("")
035f2690 8945dc          mov     dword ptr [ebp-24h],eax
035f2693 8bc8            mov     ecx,eax
035f2695 e82004b575      call    clr!JIT_MonEnterWorker (79142aba)
035f269a ff153c705c03    call    dword ptr ds:[35C703Ch] (System.Console.get_Out(), mdToken: 060008fd)
035f26a0 8bc8            mov     ecx,eax
035f26a2 8b152c37b201    mov     edx,dword ptr ds:[1B2372Ch] ("Hello, world!")
035f26a8 8b01            mov     eax,dword ptr [ecx]
035f26aa 8b403c          mov     eax,dword ptr [eax+3Ch]
035f26ad ff5010          call    dword ptr [eax+10h]
035f26b0 c745e400000000  mov     dword ptr [ebp-1Ch],0
035f26b7 c745e8fc000000  mov     dword ptr [ebp-18h],0FCh
035f26be 68d8265f03      push    35F26D8h
035f26c3 eb00            jmp     035f26c5
035f26c5 8b4ddc          mov     ecx,dword ptr [ebp-24h]
035f26c8 e8bb07b575      call    clr!JIT_MonExitWorker (79142e88)
035f26cd 58              pop     eax
035f26ce ffe0            jmp     eax
035f26d0 8d65f4          lea     esp,[ebp-0Ch]
035f26d3 5b              pop     ebx
035f26d4 5e              pop     esi
035f26d5 5f              pop     edi
035f26d6 5d              pop     ebp
035f26d7 c3              ret
035f26d8 c745e800000000  mov     dword ptr [ebp-18h],0
035f26df ebef            jmp     035f26d0

Two things seem interesting:

  1. The lock statement is translated to try/finally block by the compiler, and System.Threading.Monitor is used to do the actual synchronization.
  2. The Enter and Exit method of System.Threading.Monitor got translated to JIT_MonEnterWorker and JIT_MonExitWorker implemented inside the clr.dll. By looking at the metadata, it looks that System.Threading.Monitor::Enter is decorated by [MethodImpl(MethodImplOptions.InternalCall)], and there is no real CIL code. This is known as ECall (SSCLI vm/ecall.cpp), the JIT compiler would treat this in a special way, and instead of generating native code from CIL, it would look up from a table of native functions implemented by the CLR, and generate related thunk code.

(to be continued...)