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:
Registers the
rotor_palrt
library usingPAL_RegisterLibrary
. Therotor_pal
androtor_palrt
libraries combine to provide the PAL implementation that is needed to run the SSCLI.Obtains the assembly name to feed to the CLI as the executing assembly. Within
clix
, this is obtained from the command line.Obtains the name of the execution engine to be loaded. In the case of
clix
, this is obtained by working from the full path toclix.exe
and stripping out the program name.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.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.