Bootstrapping the Assembly Load Process

Executing the code stored within an assembly is a chicken-and-egg scenario. The assembly cannot execute until it has been resolved, loaded into the CLI, verified, and JIT-compiled. The CLI itself is simply a body of code, contained in assemblies that must be loaded into the process space and run. Fortunately, this is a classic bootstrapping problem, and implementation solutions abound. For the SSCLI implementation, a special entry point into the primary assembly is all that is needed, along with some initial security conditions, which are attached to the assembly as data.

The bootstrap API makes hosting the CLI a simple thing to do, as evinced by Rotor’s program launcher, clix.exe, whose code can be found in sscli/clr/src/tools/clix, and whose main function, Launch, appears without error handling in Example 4-5.

Example 4-5. The Launch function of clix.exe

DWORD Launch(WCHAR* pRunTime, WCHAR* pFileName, WCHAR* pCmdLine)
{
  HANDLE hFile = NULL;
  HANDLE hMapFile = NULL;
  PVOID pModule = NULL;
  HINSTANCE hRuntime = NULL;
  DWORD nExitCode = 1;
  DWORD dwSize;
  DWORD dwSizeHigh;
  IMAGE_DOS_HEADER* pdosHeader;
  IMAGE_NT_HEADERS32* pNtHeaders;
  IMAGE_SECTION_HEADER*   pSectionHeader;
  WCHAR exeFileName[MAX_PATH + 1];

  // open the file & map it
  hFile = ::CreateFile(pFileName, GENERIC_READ, FILE_SHARE_READ,
                       0, OPEN_EXISTING, 0, 0);
  hMapFile = ::CreateFileMapping(hFile, NULL, PAGE_WRITECOPY, 0, 0, NULL);
  pModule = ::MapViewOfFile(hMapFile, FILE_MAP_COPY, 0, 0, 0);
  dwSize = GetFileSize(hFile, &dwSizeHigh);

  // check the DOS headers
  pdosHeader = (IMAGE_DOS_HEADER*) pModule;
  if (pdosHeader->e_magic != IMAGE_DOS_SIGNATURE ||
      pdosHeader->e_lfanew <= 0 ||
      dwSize <= pdosHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS32)) {
    // Error logic here
  }

  // check the NT headers
  pNtHeaders = (IMAGE_NT_HEADERS32*) ((BYTE*)pModule + pdosHeader->e_lfanew);
  if ((pNtHeaders->Signature != IMAGE_NT_SIGNATURE) ||
      (pNtHeaders->FileHeader.SizeOfOptionalHeader !=
          IMAGE_SIZEOF_NT_OPTIONAL32_HEADER) ||
      (pNtHeaders->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR32_MAGIC)) {
    // Error logic here
  }

  // check the COR headers
  pSectionHeader = (PIMAGE_SECTION_HEADER)
      Cor_RtlImageRvaToVa(pNtHeaders, (PBYTE)pModule,
                          pNtHeaders->OptionalHeader
                               .DataDirectory[IMAGE_DIRECTORY_ENTRY_COMHEADER]
                               .VirtualAddress,
                          dwSize);
  if (pSectionHeader == NULL) {
    // Error logic here
  }

  // load the runtime and go
  hRuntime = ::LoadLibrary(pRunTime);

  _  _int32 (STDMETHODCALLTYPE * pCorExeMain2)(
           PBYTE   pUnmappedPE,                // -> memory mapped code
           DWORD   cUnmappedPE,                // Size of memory mapped code
           LPWSTR  pImageNameIn,               // -> Executable Name
           LPWSTR  pLoadersFileName,           // -> Loaders Name
           LPWSTR  pCmdLine);                  // -> Command Line

  *((VOID**)&pCorExeMain2) = ::GetProcAddress(hRuntime, "_CorExeMain2");
  nExitCode = (int)pCorExeMain2((PBYTE)pModule, dwSize,
                           pFileName,                  // -> Executable Name
                           NULL,                       // -> Loaders Name
                           pCmdLine);                  // -> Command Line

With this code, an assembly is loaded, fed to the CLI, executed, and the return code is fed back to the operating system. To be fair, much of the code in clix is error-handling and message-display, which has been edited out, as well as some memory-management and string-parsing, but these sections of code make for uninteresting reading.

Note

Why is clix necessary? On Windows, the commercial .NET Framework uses a tiny executable entrypoint to launch managed executables directly, without the need for a helper program. This executable stub consists of a jmp instruction that transfers control to _CorExeMain and is defined as part of the image’s file format.

There are two reasons that Rotor doesn’t do this. First, such a mechanism cannot be done portably (although platform-specific code could certainly be written for the purpose). Second, and more importantly, to enable many versions of the CLI to easily run side-by-side, the Rotor team opted use a simple and configurable helper program that is tied to the version being run, rather than more complex launch mechanisms.

clix performs the following steps when hosting the runtime:

  1. Registers the rotor_palrt library using PAL_RegisterLibrary. The rotor_pal and rotor_palrt libraries combine to provide the PAL implementation that is needed to run the SSCLI.

  2. Obtains the assembly name to feed to the CLI as the executing assembly. Within clix, this is obtained from the command line.

  3. Obtains the name of the execution engine to be loaded. In the case of clix, this is obtained by working from the full path to clix.exe and stripping out the program name.

  4. Loads the sscoree library and obtains the function pointer for _CorExeMain2. A host could of course choose to bind directly against the CLI library but would then be unable to take advantage of running against newer versions of the CLI.

  5. Call_CorExeMain2 with the mapped file for the assembly to be loaded, and let the CLI execution engine take over.

Having loaded the CLI into the process space, the call from clix to _CorExeMain2 will cause the CLI to initialize itself, through a call to CorEEInitialize, to create the system and default domains and other necessary internal bookkeeping constructs, and ultimately to call ExecuteMainMethod on the ClassLoader instance for the assembly.

Example 4-6. Bootstrap assembly loading

HRESULT ClassLoader::ExecuteMainMethod(Module *pModule, PTRARRAYREF *stringArgs)
{
  MethodDesc             *pFD = NULL;
  Thread *                pThread = NULL;
  BOOL                    fWasGCDisabled;
  IMAGE_COR20_HEADER *    Header;
  mdToken                 ptkParent;

  // error handling and HRESULT return calculation omitted

  Header = pModule->GetCORHeader(  );

  // Disable GC if not already disabled
  pThread = GetThread(  );
  fWasGCDisabled = pThread->PreemptiveGCDisabled(  );
  if (fWasGCDisabled == FALSE)
    pThread->DisablePreemptiveGC(  );

  // This thread keeps the process alive, so it can't be a background thread
  pThread->SetBackground(FALSE);

  // Must have a method def token for the entry point.
  if (TypeFromToken(Header->EntryPointToken) != mdtMethodDef) {
    // bail out if not
  }

  // Get properties and the class token for MethodDef
  pModule->GetMDImport(  )->GetParentToken(Header->EntryPointToken,&ptkParent);

  if (ptkParent != COR_GLOBAL_PARENT_TOKEN) {
    EEClass* InitialClass;
    OBJECTREF pThrowable = NULL;

    NameHandle name;
    name.SetTypeToken(pModule, ptkParent);
    InitialClass = LoadTypeHandle(&name,&pThrowable).GetClass(  );

    pFD =  InitialClass->FindMethod((mdMethodDef)Header->EntryPointToken);
  } else {
    pFD =  pModule->FindFunction((mdToken)Header->EntryPointToken);
  }

  RunMain(pFD, 1, stringArgs);

  // more code follows

Notice how the code in Example 4-6 demonstrates (which summarized from clr/src/vm/clsload.cpp) the use of metadata tokens. The entrypoint for an executable assembly is stored as a method metadata token. Because method tokens can be one of several types, however, this means effectively that the entrypoint can be either a method of a type or a global method, and so this is tested by the if (ptkParent != COR_GLOBAL_PARENT_TOKEN) statement. If this token has a parent token, in this case an enclosing type token, that equals the constant token value COR_GLOBAL_PARENT_TOKEN (which represents the containing module instead of a specific type, in essence making this a global function), the enclosing type doesn’t need to be loaded. Either way, the ClassLoader pulls out the MethodDesc for the method and calls it by passing it to RunMain, which checks to make sure that the entrypoint signature conforms to CLI conventions, and then calls into the MethodDesc directly:

    RetVal = (_  _int32)(pFD->Call(&stackVar));

Note the use of the IMAGE_COR20_HEADER in Example 4-6 to retrieve the entrypoint token. As we saw in Example 4-1, this struct is a map that yields the location of important data about disk layout. Also note the operational details: a garbage collection pass should not be happening when the main entrypoint is called, and the thread that the code will be running on must not be marked as “background,” since background threads do not keep the execution engine alive.

Get Shared Source CLI Essentials now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.