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

Comments

  • Anonymous
    October 14, 2007
    PingBack from http://www.artofbam.com/wordpress/?p=8306

  • Anonymous
    October 17, 2007
    for Nr 2 (managed Code in the CLR's startup path) I cannot get the point. In theroy (that the CLR injects some managed code) it sounds ok. Maybe just the example is misleading, because System.Security.PermissionSet..cctor is just the class constructor (static constructor) of PermissionSet. So a do not see a difference from Nr 1. Or did you meen that the cctor is called without a direct reference from Main()? If yes: Where is this call issued? It is hardcoded within the (unmanged) CLR Code? Or is it caused by the initalization of the AppDomain Instance?

  • Anonymous
    October 17, 2007
    GP - #2 as a cctor is less distinct than #1. In previous CLR versions, it used to be something like "AppDomain::Setup". Regardless, it's still issued without a direct reference by Main(). These sort of calls can occur from the CLR.

  • Anonymous
    January 06, 2008
    MC++? Is that really C++/CLI?

  • Anonymous
    January 07, 2008
    Yuhong - essentially yes. See http://msdn.microsoft.com/msdnmag/issues/05/02/PureC/ for more.