The 68 things the CLR does before executing a single line of your code (*)

Because the CLR is a managed environment there are several components within the runtime that need to be initialised before any of your code can be executed. This post will take a look at the EE (Execution Engine) start-up routine and examine the initialisation process in detail.

(*) 68 is only a rough guide, it depends on which version of the runtime you are using, which features are enabled and a few other things


‘Hello World’

Imagine you have the simplest possible C# program, what has to happen before the CLR prints ‘Hello World’ out to the console?

using System;

namespace ConsoleApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

The code path into the EE (Execution Engine)

When a .NET executable runs, control gets into the EE via the following code path:

  1. _CorExeMain() (the external entry point)
  2. _CorExeMainInternal()
  3. EnsureEEStarted()
  4. EEStartup()
  5. EEStartupHelper()

(if you’re interested in what happens before this, i.e. how a CLR Host can start-up the runtime, see my previous post ‘How the dotnet CLI tooling runs your code’)

And so we end up in EEStartupHelper(), which at a high-level does the following (from a comment in ceemain.cpp):

EEStartup is responsible for all the one time initialization of the runtime.
Some of the highlights of what it does include

  • Creates the default and shared, appdomains.
  • Loads mscorlib.dll and loads up the fundamental types (System.Object …)

The main phases in EE (Execution Engine) start-up routine

But let’s look at what it does in detail, the lists below contain all the individual function calls made from EEStartupHelper() (~500 L.O.C). To make them easier to understand, we’ll split them up into separate phases:

  • Phase 1 - Set-up the infrastructure that needs to be in place before anything else can run
  • Phase 2 - Initialise the core, low-level components
  • Phase 3 - Start-up the low-level components, i.e. error handling, profiling API, debugging
  • Phase 4 - Start the main components, i.e. Garbage Collector (GC), AppDomains, Security
  • Phase 5 - Final setup and then notify other components that the EE has started

Note some items in the list below are only included if a particular feature is defined at build-time, these are indicated by the inclusion on an ifdef statement. Also note that the links take you to the code for the function being called, not the line of code within EEStartupHelper().

Phase 1 - Set-up the infrastructure that needs to be in place before anything else can run

  1. Wire-up console handling - SetConsoleCtrlHandler(..) (ifndef FEATURE_PAL)
  2. Initialise the internal SString class (everything uses strings!) - SString::Startup()
  3. Make sure the configuration is set-up, so settings that control run-time options can be accessed - EEConfig::Set-up() and InitializeHostConfigFile() (#if !defined(CROSSGEN_COMPILE))
  4. Initialize Numa and CPU group information - NumaNodeInfo::InitNumaNodeInfo() and CPUGroupInfo::EnsureInitialized() (#ifndef CROSSGEN_COMPILE)
  5. Initialize global configuration settings based on startup flags - InitializeStartupFlags()
  6. Set-up the Thread Manager that gives the runtime access to the OS threading functionality (StartThread(), Join(), SetThreadPriority() etc) - InitThreadManager()
  7. Initialize Event Tracing (ETW) and fire off the CLR startup events - InitializeEventTracing() and ETWFireEvent(EEStartupStart_V1) (#ifdef FEATURE_EVENT_TRACE)
  8. Set-up the GS Cookie (Buffer Security Check) to help prevent buffer overruns - InitGSCookie()
  9. Create the data-structures needed to hold the ‘frames’ used for stack-traces - Frame::Init()
  10. Ensure initialization of Apphacks environment variables - GetGlobalCompatibilityFlags() (#ifndef FEATURE_CORECLR)
  11. Create the diagnostic and performance logs used by the runtime - InitializeLogging() (#ifdef LOGGING) and PerfLog::PerfLogInitialize() (#ifdef ENABLE_PERF_LOG)

Phase 2 - Initialise the core, low-level components

  1. Write to the log ===================EEStartup Starting===================
  2. Ensure that the Runtime Library functions (that interact with ntdll.dll) are enabled - EnsureRtlFunctions() (#ifndef FEATURE_PAL)
  3. Set-up the global store for events (mutexes, semaphores) used for synchronisation within the runtime - InitEventStore()
  4. Create the Assembly Binding logging mechanism a.k.a Fusion - InitializeFusion() (#ifdef FEATURE_FUSION)
  5. Then initialize the actual Assembly Binder infrastructure - CCoreCLRBinderHelper::Init() which in turn calls AssemblyBinder::Startup() (#ifdef FEATURE_FUSION is NOT defined)
  6. Set-up the heuristics used to control Monitors, Crsts, and SimpleRWLocks - InitializeSpinConstants()
  7. Initialize the InterProcess Communication with COM (IPC) - InitializeIPCManager() (#ifdef FEATURE_IPCMAN)
  8. Set-up and enable Performance Counters - PerfCounters::Init() (#ifdef ENABLE_PERF_COUNTERS)
  9. Set-up the CLR interpreter - Interpreter::Initialize() (#ifdef FEATURE_INTERPRETER), turns out that the CLR has a mode where your code is interpreted instead of compiled!
  10. Initialise the stubs that are used by the CLR for calling methods and triggering the JIT - StubManager::InitializeStubManagers(), also Stub::Init() and StubLinkerCPU::Init()
  11. Set up the core handle map, used to load assemblies into memory - PEImage::Startup()
  12. Startup the access checks options, used for granting/denying security demands on method calls - AccessCheckOptions::Startup()
  13. Startup the mscorlib binder (used for loading “known” types from mscorlib.dll) - MscorlibBinder::Startup()
  14. Initialize remoting, which allows out-of-process communication - CRemotingServices::Initialize() (#ifdef FEATURE_REMOTING)
  15. Set-up the data structures used by the GC for weak, strong and no-pin references - Ref_Initialize()
  16. Set-up the contexts used to proxy method calls across App Domains - Context::Initialize()
  17. Wire-up events that allow the EE to synchronise shut-down - g_pEEShutDownEvent->CreateManualEvent(FALSE)
  18. Initialise the process-wide data structures used for reader-writer lock implementation - CRWLock::ProcessInit() (#ifdef FEATURE_RWLOCK)
  19. Initialize the debugger manager - CCLRDebugManager::ProcessInit() (#ifdef FEATURE_INCLUDE_ALL_INTERFACES)
  20. Initialize the CLR Security Attribute Manager - CCLRSecurityAttributeManager::ProcessInit() (#ifdef FEATURE_IPCMAN)
  21. Set-up the manager for Virtual call stubs - VirtualCallStubManager::InitStatic()
  22. Initialise the lock that that GC uses when controlling memory pressure - GCInterface::m_MemoryPressureLock.Init(CrstGCMemoryPressure)
  23. Initialize Assembly Usage Logger - InitAssemblyUsageLogManager() (#ifndef FEATURE_CORECLR)

Phase 3 - Start-up the low-level components, i.e. error handling, profiling API, debugging

  1. Set-up the App Domains used by the CLR - SystemDomain::Attach() (also creates the DefaultDomain and the SharedDomain by calling SystemDomain::CreateDefaultDomain() and SharedDomain::Attach())
  2. Start up the ECall interface, a private native calling interface used within the CLR - ECall::Init()
  3. Set-up the caches for the stubs used by delegates - COMDelegate::Init()
  4. Set-up all the global/static variables used by the EE itself - ExecutionManager::Init()
  5. Initialise Watson, for windows error reporting - InitializeWatson(fFlags) (#ifndef FEATURE_PAL)
  6. Initialize the debugging services, this must be done before any EE thread objects are created, and before any classes or modules are loaded - InitializeDebugger() (#ifdef DEBUGGING_SUPPORTED)
  7. Activate the Managed Debugging Assistants that the CLR provides - ManagedDebuggingAssistants::EEStartupActivation() (ifdef MDA_SUPPORTED)
  8. Initialise the Profiling API - ProfilingAPIUtility::InitializeProfiling() (#ifdef PROFILING_SUPPORTED)
  9. Initialise the exception handling mechanism - InitializeExceptionHandling()
  10. Install the CLR global exception filter - InstallUnhandledExceptionFilter()
  11. Ensure that the initial runtime thread is created - SetupThread() in turn calls SetupThread(..)
  12. Initialise the PreStub manager (PreStub’s trigger the JIT) - InitPreStubManager() and the corresponding helpers StubHelpers::Init()
  13. Initialise the COM Interop layer - InitializeComInterop() (#ifdef FEATURE_COMINTEROP)
  14. Initialise NDirect method calls (lazy binding of unmanaged P/Invoke targets) - NDirect::Init()
  15. Set-up the JIT Helper functions, so they are in place before the execution manager runs - InitJITHelpers1() and InitJITHelpers2()
  16. Initialise and set-up the SyncBlock cache - SyncBlockCache::Attach() and SyncBlockCache::Start()
  17. Create the cache used when walking/unwinding the stack - StackwalkCache::Init()

Phase 4 - Start the main components, i.e. Garbage Collector (GC), AppDomains, Security

  1. Start up security system, that handles Code Access Security (CAS) - Security::Start() which in turn calls SecurityPolicy::Start()
  2. Wire-up an event to allow synchronisation of AppDomain unloads - AppDomain::CreateADUnloadStartEvent()
  3. Initialise the ‘Stack Probes’ used to setup stack guards InitStackProbes() (#ifdef FEATURE_STACK_PROBE)
  4. Initialise the GC and create the heaps that it uses - InitializeGarbageCollector()
  5. Initialise the tables used to hold the locations of pinned objects - InitializePinHandleTable()
  6. Inform the debugger about the DefaultDomain, so it can interact with it - SystemDomain::System()->PublishAppDomainAndInformDebugger(..) (#ifdef DEBUGGING_SUPPORTED)
  7. Initialise the existing OOB Assembly List (no idea?) - ExistingOobAssemblyList::Init() (#ifndef FEATURE_CORECLR)
  8. Actually initialise the System Domain (which contains mscorlib), so that it can start executing - SystemDomain::System()->Init()

Phase 5 Final setup and then notify other components that the EE has started

  1. Tell the profiler we’ve stated up - SystemDomain::NotifyProfilerStartup() (#ifdef PROFILING_SUPPORTED)
  2. Pre-create a thread to handle AppDomain unloads - AppDomain::CreateADUnloadWorker() (#ifndef CROSSGEN_COMPILE)
  3. Set a flag to confirm that ‘initialisation’ of the EE succeeded - g_fEEInit = false
  4. Load the System Assemblies (‘mscorlib’) into the Default Domain - SystemDomain::System()->DefaultDomain()->LoadSystemAssemblies()
  5. Set-up all the shared static variables (and String.Empty) in the Default Domain - SystemDomain::System()->DefaultDomain()->SetupSharedStatics(), they are all contained in the internal class SharedStatics.cs
  6. Set-up the stack sampler feature, that identifies ‘hot’ methods in your code - StackSampler::Init() (#ifdef FEATURE_STACK_SAMPLING)
  7. Perform any once-only SafeHandle initialization - SafeHandle::Init() (#ifndef CROSSGEN_COMPILE)
  8. Set flags to indicate that the CLR has successfully started - g_fEEStarted = TRUE, g_EEStartupStatus = S_OK and hr = S_OK
  9. Write to the log ===================EEStartup Completed===================

Once this is all done, the CLR is now ready to execute your code!!


Executing your code

Your code will be executed (after first being ‘JITted’) via the following code flow:

  1. CorHost2::ExecuteAssembly()
  2. Assembly::ExecuteMainMethod()
  3. RunMain() (in assembly.cpp)

Discuss this post on Hacker News and /r/programming


Further information

The CLR provides a huge amount of log information if you create a debug build and then enable the right environment variables. The links below take you to the various logs produced when running a simple ‘hello world’ program (shown at the top of this post), they give you an pretty good idea of the different things that the CLR is doing behind-the-scenes.