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:
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.
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
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:
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
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:
- The lock statement is translated to try/finally block by the compiler, and System.Threading.Monitor is used to do the actual synchronization.
- 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...)