Quiz: What runs before Main()?

Quiz: What managed code runs before managed Main() in your program startup path?

 

 

Answers:

I note "managed code", because obviously the CLR startup code gets executed, as does other native startup code.

1) The common answer is static constructors referenced from Main().

2) The less common answer would be managed code in the CLR's startup path.  While much of the CLR is implemented in native code (mscorwks.dll), we try to migrate parts of the CLR itself into managed and into the BCL (mscorlib.dll).

You can verify this in MDbg. MDbg normally tries to identify the Main() method and set a breakpoint there and stop there. However, the 'ca nt' command tells Mdbg to stop when a thread first enters managed code. Here's an MDbg transcript:

C:\temp>mdbg
MDbg (Managed debugger) v2.0.50727.42 (RTM.050727-4200) started.
Copyright (C) Microsoft Corporation. All rights reserved.

For information about commands type "help";
to exit program type "quit".

mdbg> ca nt <-- stop when thread first hits managed code
mdbg> run x.exe
STOP: Thread Created
IP: 0 @ System.Security.PermissionSet..cctor - MAPPING_PROLOG
[p#:0, t#:0] mdbg> where
Thread [#:0]
*0. System.Security.PermissionSet..cctor (source line information unavailable)
[p#:0, t#:0] mdbg> go
STOP: Breakpoint Hit
22:    {
[p#:0, t#:0] mdbg> where
Thread [#:0]
*0. Program.Main (x.cs:22)
[p#:0, t#:0] mdbg>

So in this case, you can see that System.Security.PermissionSet() is being run before Main. When we continue past that, we hit the breakpoint that Mdbg set on the main() method.

 

3) And there bonus answer is: any code that the compiler injects before the call to Main().

The user's Main() method is not necessarily the real entry point for a module. That's actually why managed pdbs specify a "user entry point" method (see the <EntryPoint> tag in  https://blogs.msdn.com/jmstall/archive/2005/08/25/pdb2xml.aspx ).

The "real" entry point for the CLR has ".entrypoint" in the IL. The user's entry point is specified in the PDB.  Eg, in the ILDasm for the main method:

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
...

Why?  This gives languages additional flexibility to uphold language semantics and do setup before the user's Main() method is called.

 

4) And a very obscure answer: failure code, such as exception constructors. (I alluded to this here)

 

At this point, it should be clear that there's no reason that Main() has to be the first code that runs.  So we could brainstorm other strange cases.

 

A real example with MC++

C# is so clean that it's not an ideal language for demonstrating wonky features of the CLR. So let's turn our attention to MC++.

Compile the following MC++ app with /clr:pure (so it's 100% IL and no suspicious native stuff):

 // mcpp_console.cpp : main project file.

#include "stdafx.h"

using namespace System;

int main(array<System::String ^> ^args)
{
    Console::WriteLine(L"Hello World");
    return 0;
}

F10 into main, and you can see that Main() is not the first managed code on the stack:

>    mcpp_console.exe!main + 0x15 bytes    C++
     mcpp_console.exe!mainCRTStartupStrArray + 0xb8 bytes    C++

You can verify that mainCRTStartupStrArray in this case is indeed managed code.

This example should make it clear that that managed case of 'code running before main' can inherit much of the properties that the native case had.

 

A bonus experiment:

Here's a bonus experiment. Take a trivial C# app:

 // Test
using System;

class Foo
{
    static void Main()
    {
        Console.WriteLine("Main");
    }
    static void Test()
    {
        Console.WriteLine("Test");
    }
}

Compile and run it, and it prints "Main".

C:\temp>csc t.cs & t.exe
Microsoft (R) Visual C# 2005 Compiler version 8.00.50727.1378
for Microsoft (R) Windows (R) 2005 Framework version 2.0.50727
Copyright (C) Microsoft Corporation 2001-2005. All rights reserved.

Main

Now use ILasm/ildasm round-tripping to move the .entrypoint to Test.

C:\temp>ildasm t.exe /out=t.il

Edit t.il to move .entrypoint from 'main' to 'test'.

C:\temp>ilasm t.il

Microsoft (R) .NET Framework IL Assembler.  Version 2.0.50727.1378
Copyright (c) Microsoft Corporation.  All rights reserved.
Assembling 't.il'  to EXE --> 't.exe'
Source file is ANSI

Assembled method Foo::Main
Assembled method Foo::Test
Assembled method Foo::.ctor
Creating PE file

Emitting classes:
Class 1:        Foo

Emitting fields and methods:
Global
Class 1 Methods: 3;

Emitting events and properties:
Global
Class 1
Writing PE file
Operation completed successfully

 

And run it. You see you've changed the entry point and Main() doesn't execute now.

C:\temp>t.exe
Test