Home Random Page


CATEGORIES:

BiologyChemistryConstructionCultureEcologyEconomyElectronicsFinanceGeographyHistoryInformaticsLawMathematicsMechanicsMedicineOtherPedagogyPhilosophyPhysicsPolicyPsychologySociologySportTourism






Nbsp;   How Things Relate at Run Time

In this section, I’m going to explain the relationship at run time between types, objects, a thread’s stack, and the managed heap. Furthermore, I will also explain the difference between calling static methods, instance methods, and virtual methods. Let’s start off with some fundamentals of computers.


What I’m about to describe is not specific to the CLR at all, but I’m going to describe it so that we have a working foundation, and then I’ll modify the discussion to incorporate CLR-specific information.

Figure 4-2 shows a single Windows process that has the CLR loaded into it. In this process there may be many threads. When a thread is created, it is allocated a 1-MB stack. This stack space is used for passing arguments to a method and for local variables defined within a method. In Figure 4-2, the memory for one thread’s stack is shown (on the right). Stacks build from high-memory addresses to low-memory addresses. In the figure, this thread has been executing some code, and its stack has some data on it already (shown as the shaded area at the top of the stack). Now, imagine that the thread has executed some code that calls the M1 method.

 

       
 
   
 

Thread Stack

FIGURE 4-2A thread’s stack with the M1 method about to be called.

 

All but the simplest of methods contain some prologue code, which initializes a method before it can start doing its work. These methods also contain epilogue code, which cleans up a method after it has performed its work so that it can return to its caller. When the M1 method starts to execute, its prologue code allocates memory for the local name variable from the thread’s stack (see Figure 4-3).

 

Thread Stack

 
 

 

 
 

} M1 Locals

 

 

   
name (String)
 

 

FIGURE 4-3Allocating M1’s local variable on the thread’s stack.

 

Then, M1 calls the M2 method, passing in the name local variable as an argument. This causes the address in the name local variable to be pushed on the stack (see Figure 4-4). Inside the M2 method, the stack location will be identified using the parameter variable named s. (Note that some architec- tures pass arguments via registers to improve performance, but this distinction is not important for


this discussion.) Also, when a method is called, the address indicating where the called method should return to in the calling method is pushed on the stack (also shown in Figure 4-4).

 

Thread Stack

 
 

 

 
 

} M1 Locals

   
name (String)
s (String)
[return address]
 

 

} M2 Params

 

 
 

FIGURE 4-4M1 pushes arguments and the return address on the thread’s stack when calling M2.

 

When the M2 method starts to execute, its prologue code allocates memory for the local length and tally variables from the thread’s stack (see Figure 4-5). Then the code inside method M2 exe- cutes. Eventually, M2 gets to its return statement, which causes the CPU’s instruction pointer to be set to the return address in the stack, and M2’s stack frame is unwound so that it looks the way it did in Figure 4-3. At this point, M1 is continuing to execute its code that immediately follows the call to M2, and its stack frame accurately reflects the state needed by M1.



Eventually, M1 will return back to its caller by setting the CPU’s instruction pointer to be set to the return address (not shown on the figures, but it would be just above the name argument on the stack), and M1’s stack frame is unwound so that it looks the way it did in Figure 4-2. At this point, the method that called M1 continues to execute its code that immediately follows the call to M1, and its stack frame accurately reflects the state needed by that method.

 

Thread Stack

 
 

 

 
 

} M1 Locals

   
name (String)
s (String)
[return address]
length (Int32)
tally (Int32)
 

 

} M2 Params

 

M2 Locals

 

FIGURE 4-5Allocating M2’s local variables on the thread’s stack.


Now, let’s start gearing the discussion toward the CLR. Let’s say that we have these two class

definitions.

 

internal class Employee {

public Int32 GetYearsEmployed() { ... }
public virtual String GetProgressReport() { ... }
public static Employee Lookup(String name) { ... }
}    

 

internal sealed class Manager : Employee {

public override String GetProgressReport() { ... }

}

 

Our Windows process has started, the CLR is loaded into it, the managed heap is initialized, and a thread has been created (along with its 1 MB of stack space). This thread has already executed some code, and this code has decided to call the M3 method. All of this is shown in Figure 4-6. The M3 method contains code that demonstrates how the CLR works; this is not code that you would nor- mally write, because it doesn’t actually do anything useful.

 

Thread Stack

       
 
   
 

Heap

FIGURE 4-6The CLR loaded in a process, its heap initialized, and a thread’s stack with the M3 method about to be called.

 

As the just-in-time (JIT) compiler converts M3’s Intermediate Language (IL) code into native CPU instructions, it notices all of the types that are referred to inside M3: Employee, Int32, Manager, and String (because of "Joe"). At this time, the CLR ensures that the assemblies that define these types are loaded. Then, using the assembly’s metadata, the CLR extracts information about these types and creates some data structures to represent the types themselves. The data structures for the Employee


and Manager type objects are shown in Figure 4-7. Because this thread already executed some code prior to calling M3, let’s assume that the Int32 and String type objects have already been created (which is likely because these are commonly used types), and so I won’t show them in the figure.

 

Thread Stack

       
 
   
 

Heap

FIGURE 4-7The Employee and Manager type objects are created just as M3 is being called.

 

Let’s take a moment to discuss these type objects. As discussed earlier in this chapter, all objects on the heap contain two overhead members: the type object pointer and the sync block index. As you can see, the Employee and Manager type objects have both of these members. When you define a type, you can define static data fields within it. The bytes that back these static data fields are al- located within the type objects themselves. Finally, inside each type object is a method table with one entry per method defined within the type. This is the method table that was discussed in Chapter 1, “The CLR’s Execution Model.” Because the Employee type defines three methods (GetYears­ Employed, GetProgressReport, and Lookup), there are three entries in Employee’s method table. Because the Manager type defines one method (an override of GetProgressReport), there is just one entry in Manager’s method table.

Now, after the CLR has ensured that all of the type objects required by the method are created and the code for M3 has been compiled, the CLR allows the thread to execute M3’s native code. When M3’s prologue code executes, memory for the local variables must be allocated from the thread’s stack, as shown in Figure 4-8. By the way, the CLR automatically initializes all local variables to null or 0 (zero) as part of the method’s prologue code. However, the C# compiler issues a Use of unassigned local variable error message if you write code that attempts to read from a local variable that you have not explicitly initialized in your source code.


Thread Stack

Heap

 

null 0

 

 
 

FIGURE 4-8Allocating M3’s local variables on the thread’s stack.

 

Then, M3 executes its code to construct a Manager object. This causes an instance of the Manager type, a Manager object, to be created in the managed heap, as shown in Figure 4-9. As you can see, the Manager object—as do all objects—has a type object pointer and sync block index. This object also contains the bytes necessary to hold all of the instance data fields defined by the Manager type, as well as any instance fields defined by any base classes of the Manager type (in this case, Employee and Object). Whenever a new object is created on the heap, the CLR automatically initializes the internal type object pointer member to refer to the object’s corresponding type object (in this case, the Manager type object). Furthermore, the CLR initializes the sync block index and sets all of the object’s instance fields to null or 0 (zero) prior to calling the type’s constructor, a method that will likely modify some of the instance data fields. The new operator returns the memory address of the Manager object, which is saved in the variable e (on the thread’s stack).

The next line of code in M3 calls Employee’s static Lookup method. When calling a static method, the JIT compiler locates the type object that corresponds to the type that defines the static method. Then, the JIT compiler locates the entry in the type object’s method table that refers to the method being called, JITs the method (if necessary), and calls the JITted code. For our discussion, let’s say that Employee’s Lookup method queries a database to find Joe. Let’s also say that the database indicates that Joe is a manager at the company, and therefore, internally, the Lookup method constructs a new Manager object on the heap, initializes it for Joe, and returns the address of this object. The address is saved in the local variable e. The result of this operation is shown in Figure 4-10.


Thread Stack

Heap

 


Manager Object

Type object ptr

= 0 Sync block index Instance fields


Manager Type Object

 

Employee Type Object


 

 

FIGURE 4-9Allocating and initializing a Manager object.

 

Thread Stack

Heap

 


Manager Object

Type object ptr

= 0 Sync block index Instance fields

 

Manager Object

Type object ptr Sync block index Instance fields


Manager Type Object

 

Employee Type Object


 

JITted code
GetYearsEmployed GetProgressReport Lookup

 

 

FIGURE 4-10Employee’s static Lookup method allocates and initializes a Manager object for Joe.


Note that e no longer refers to the first Manager object that was created. In fact, because no vari- able refers to this object, it is a prime candidate for being garbage collected in the future, which will reclaim (free) the memory used by this object.

The next line of code in M3 calls Employee’s nonvirtual instance GetYearsEmployed method. When calling a nonvirtual instance method, the JIT compiler locates the type object that corre- sponds to the type of the variable being used to make the call. In this case, the variable e is defined as an Employee. (If the Employee type didn’t define the method being called, the JIT compiler walks down the class hierarchy toward Object looking for this method. It can do this because each type object has a field in it that refers to its base type; this information is not shown in the figures.) Then, the JIT compiler locates the entry in the type object’s method table that refers to the method being called, JITs the method (if necessary), and then calls the JITted code. For our discussion, let’s say that Employee’s GetYearsEmployed method returns 5 because Joe has been employed at the company for five years. The integer is saved in the local variable year. The result of this operation is shown in Figure 4-11.

 

Thread Stack

Heap

 


Manager Object

Type object ptr

= 5 Sync block index Instance fields

 

Manager Object

Type object ptr Sync block index Instance fields


Manager Type Object

 

Employee Type Object


 

JITted code
JITted code
GetYearsEmployed GetProgressReport Lookup

 

 

FIGURE 4-11Employee’s nonvirtual instance GetYearsEmployed method is called, returning 5.

 

The next line of code in M3 calls Employee’s virtual instance GetProgressReport method. When calling a virtual instance method, the JIT compiler produces some additional code in the method, which will be executed each time the method is invoked. This code will first look in the variable be- ing used to make the call and then follow the address to the calling object. In this case, the variable e points to the Manager object representing “Joe.” Then, the code will examine the object’s internal type object pointer member; this member refers to the actual type of the object. The code then locates the entry in the type object’s method table that refers to the method being called, JITs the method (if necessary), and calls the JITted code. For our discussion, Manager’s GetProgressReport


implementation is called because e refers to a Manager object. The result of this operation is shown in Figure 4-12.

Note that if Employee’s Lookup method had discovered that Joe was just an Employee and not a Manager, Lookup would have internally constructed an Employee object whose type object pointer member would have referred to the Employee type object, causing Employee’s implementation of GetProgressReport to execute instead of Manager’s implementation.

 

Thread Stack

Heap

 


Manager Object

Type object ptr

= 5 Sync block index Instance fields

 

Manager Object

Type object ptr Sync block index Instance fields


Manager Type Object

 

 

JITted code
GetProgressReport Employee Type Object


 

JITted code
JITted code
GetYearsEmployed GetProgressReport Lookup

 

 

FIGURE 4-12Employee’s virtual instance GetProgressReport method is called, causing Manager’s override of this method to execute.

 

At this point, we have discussed the relationship between source code, IL, and JITted code. We have also discussed the thread’s stack, arguments, local variables, and how these arguments and vari- ables refer to objects on the managed heap. You also see how objects contain a pointer to their type object (containing the static fields and method table). We have also discussed how the JIT compiler determines how to call static methods, nonvirtual instance methods, and virtual instance methods. All of this should give you great insight into how the CLR works, and this insight should help you when architecting and implementing your types, components, and applications. Before ending this chapter, I’d like to give you just a little more insight as to what is going on inside the CLR.

You’ll notice that the Employee and Manager type objects both contain type object pointer members. This is because type objects are actually objects themselves. When the CLR creates type objects, the CLR must initialize these members. “To what?” you might ask. Well, when the CLR starts running in a process, it immediately creates a special type object for the System.Type type (defined in MSCorLib.dll). The Employee and Manager type objects are “instances” of this type, and therefore, their type object pointer members are initialized to refer to the System.Type type object, as shown in Figure 4-13.


Thread Stack

Heap

 


Manager Object

Type object ptr

= 5 Sync block index Instance fields

 

Manager Object

Type object ptr Sync block index Instance fields

 

Type Type Object

Type object ptr Sync block index Static fields


Manager Type Object

 

 

JITted code
GetProgressReport Employee Type Object

 

 

JITted code
JITted code
GetYearsEmployed GetProgressReport Lookup


 

 

FIGURE 4-13The Employee and Manager type objects are instances of the System.Type type.

 

Of course, the System.Type type object is an object itself and therefore also has a type object pointer member in it, and it is logical to ask what this member refers to. It refers to itself because the System.Type type object is itself an “instance” of a type object. And now you should understand the CLR’s complete type system and how it works. By the way, System.Object’s GetType method simply returns the address stored in the specified object’s type object pointer member. In other words, the GetType method returns a pointer to an object’s type object, and this is how you can determine the true type of any object in the system (including type objects).


C HA P T E R 5


Date: 2016-03-03; view: 715


<== previous page | next page ==>
Nbsp;   Namespaces and Assemblies | Nbsp;   Programming Language Primitive Types
doclecture.net - lectures - 2014-2024 year. Copyright infringement or personal data (0.015 sec.)