The JIT steps of compiling code in CLR look like this:

  1. When compiling code that contains a call to a particular method, a space in memory called a stub is allocated for this method. The stub contains a call to the internal undocumented compilation function and a pointer to the IL code of the method code.
  2. When the time comes to invoke a method, the compilation function is called, which compiles the IL code into machine commands, and also rewrites the contents of the stub to this code.
  3. The method is executing.

Since, as everyone knows, the CLR does not compile the same method every time for optimization purposes, you need to store the correspondences of the compiled code blocks and method names somewhere.

Consider the following example for clarity:

We have this code in C #:

void Main() { Console.ReadLine(); ReadLine(); } void ReadLine() { Console.ReadLine(); } 

Under methods Console.ReadLine and ReadLine will be allocated, respectively, Stub1 and Stub2. How the compiled code will look like:

 call stub1 call stub2 

When stub1 is called, the method Console.ReadLine will be compiled, its code will be written to stub1 and then executed. There is nothing to be asked.

But when you call stub2, a compilation of ReadLine will occur, of course, which will look like this:

 сall Console.ReadLine 

And the answer to the question of what will be caused: stub1 or a new stub3 containing the same code - the answer is obvious. But in order to call stub1, thus not compiling Console.ReadLine again, you need to remember the address of stub1 and put it in accordance with the method Console.ReadLine . How it's done?

    1 answer 1

    Initially, all the usual .NET methods perform call with the address of the adapter. The adapter itself contains a call to the PrecodeFixupThunk function, here is its code :

     ////////////////////////////////////////////////////////////////////////// // // PrecodeFixupThunk // // The call in fixup precode initally points to this function. // The pupose of this function is to load the MethodDesc and forward the call the prestub. // // EXTERN_C VOID __stdcall PrecodeFixupThunk() LEAF_ENTRY PrecodeFixupThunk, _TEXT pop rax // Pop the return address. It points right after the call instruction in the precode. // Inline computation done by FixupPrecode::GetMethodDesc() movzx r10,byte ptr [rax+2] // m_PrecodeChunkIndex movzx r11,byte ptr [rax+1] // m_MethodDescChunkIndex mov rax,qword ptr [rax+r10*8+3] lea METHODDESC_REGISTER,[rax+r11*8] // Tail call to prestub jmp C_FUNC(ThePreStub) LEAF_END PrecodeFixupThunk, _TEXT 

    As can be seen from the code, the adapter indexes ( m_PrecodeChunkIndex ) and the MethodDesc structure MethodDesc ( m_MethodDescChunkIndex ) are passed here. These indices are calculated from the address of the adapter. CLR specifically for this places them one by one in memory. ThePreStub function takes as an input the adapter index (to know where to replace the code), and the index of the MethodDesc structure, to know where to write the pointer to the compiled method.

    The documentation about Method Descriptor says:

    MethodDesc (method descriptor) is the internal representation of a managed method. It serves several purposes:

    • Provides a unique method handle, usable throughout the runtime. For normal methods, it is a unique handle for a triplet.
    • Caches frequently used information (for example if the method is static).
    • Captures the runtime state of the method (for example, the code has been generated for the method already). Owns the entry point of the method.

    Method Descriptor remembers whether there is a compiled version of the method, and where its address is.

    ThePreSub calls PreStubWorker , which calls MethodDesc::MakeJitWorker . Here is the documentation before the code of this method :

     // MakeJitWorker is the thread safe way to invoke the JIT compiler // If multiple threads get in here for the same pMD, ALL of them // MUST return the SAME value for pstub. // // This function creates a DeadlockAware list of methods being jitted // which prevents us from trying to JIT the same method more that once. 

    As you can see, this method is smart enough that even if one of the several threads are asked to compile one method, JIT will do it once, and the other threads will just wait.

    Before compiling the adapter looks like this:

     call PrecodeFixupThunk 

    After compilation, an unconditional jump with the address of the compiled method is inserted into place:

     jmp 000007FE92A60500 

    On Habré there is a very detailed article describing how everything works: https://habr.com/post/307088/

    • There are 4 precode adapters in the CLR. What is the environment guided by the choice of one of them? - D .Stark
    • MethodDesc structures are added into arrays (chunks). This is done to save space. But how is this savings achieved? And on what basis do these MethodDesc add up to one MethodDescChunk? By belonging to one class? - D .Stark
    • Is this whole thing created dynamically or does it exist even before the start of the entry point? In any case, the call of all methods is not predicted in advance (and it is also expensive as that) ... Ie Should MethodDescChunk be found for the required class and MethodDesc is added to it for the compiled method? - D .Stark
    • The high-order bits of MethodDesc contain its index in MethodDescChunk, and the low-order bits contain the index of the slot in MethodTable. There is one MethodTable per MethodDescChunk. The slot itself contains either the address of the compiled code or the address of the adapter, depending on the status of the method (compiled / highlighted). Can the address be in MethodDesc directly or is it always contained in the slot? - D .Stark
    • @ D.Stark The article describes the purpose of all adapters. Saving space is achieved due to the fact that addresses are not stored (which occupy 4/8 bytes per x86 / x64), and instead they are stored 2 indexes (1 byte in size), and 1 address for 256 adapters. This is all created when loading .NET dll. I do not know about the last question. If there are a lot of questions, it is better to ask them separately, comments are usually for small clarifications. - Zergatul