CLR : Inside memory model (Parte 2)

Introduzione

Nell’articolo precedente, abbiamo iniziato ad approfondire cosa accade nel momento in cui il CLR alloca un oggetto sulla memoria Heap e come vengono distribuite  le informazioni contenute nell’oggetto stesso, sia in termini di dati propri dell’oggetto che di dati aggiunti dal CLR.

In questo articolo, vedremo nel dettaglio come avviene l’invocazione dei metodi sugli oggetti con e senza polimorfismo.

La Method Table

Ciascun Type Object allocato sull’Heap ed associato ad uno specifico tipo, contiene al suo interno la Method Table, ossia la tabella dei metodi esposti dal tipo stesso che viene utilizzata dal CLR per l’invocazione di questi ultimi sugli oggetti.

Utilizzando il debugger avanzato SOS di Visual Studio, è possibile visualizzare la Method Table di un tipo. Eseguiamo in primo luogo il dump dell’oggetto bClass in modo da poter ricavare l’indirizzo della Method Table all’interno del corrispondete Type ObjectBaseClass Type Obj.

!DumpObj 00c2c3b0
PDB symbol for clr.dll not loaded
Name:        ConsoleApplication1.BaseClass
MethodTable: 009b391c
EEClass:     009b14d8
Size:        16(0x10) bytes
File:        C:\Documents and Settings\Developer\My Documents\Visual Studio 2010\Projects\ConsoleApplication1\ConsoleApplication1\bin\Debug\ConsoleApplication1.exe
Fields:
MT    Field   Offset                 Type VT     Attr    Value Name
79b9f9ac  4000001        4        System.String  0 instance 00c2c3c0 instanceString
79ba2978  4000002        8         System.Int32  1 instance        1 instanceInt
79ba2978  4000003       24         System.Int32  1   static        3 staticInt

Dall’esecuzione del comando di DumpObj, si evince che la Method Table si trova all’indirizzo 0x009b391c. Su tale indirizzo, possiamo eseguire il comando DumpMT :

!DumpMT -MD 009b391c
EEClass:      009b14d8
Module:       009b2e9c
Name:         ConsoleApplication1.BaseClass
mdToken:      4a0ec1b602000002
File:         C:\Documents and Settings\Developer\My Documents\Visual Studio 2010\Projects\ConsoleApplication1\ConsoleApplication1\bin\Debug\ConsoleApplication1.exe
BaseSize:        0x10
ComponentSize:   0x0
Slots in VTable: 9
Number of IFaces in IFaceMap: 0
————————————–
MethodDesc Table
Entry MethodDesc      JIT Name
79aaa7e0   79884934   PreJIT System.Object.ToString()
79aae2e0   7988493c   PreJIT System.Object.Equals(System.Object)
79aae1f0   7988495c   PreJIT System.Object.GetHashCode()
79b31600   79884970   PreJIT System.Object.Finalize()
009bc048   009b38e8     NONE ConsoleApplication1.BaseClass.VirtualMethod()
009bc038   009b38cc      JIT ConsoleApplication1.BaseClass..ctor()
009bc058   009b3904      JIT ConsoleApplication1.BaseClass..cctor()
009bc040   009b38d8     NONE ConsoleApplication1.BaseClass.InstanceMethod()
009bc050   009b38f4     NONE ConsoleApplication1.BaseClass.StaticMethod()

In primo luogo, osserviamo che il numero di slot nella tabella sono 9 (quanti sono i metodi invocabili sul tipo BaseClass). I primi quattro metodi sono ereditati dalla classe Object, che come sappiamo è la classe base per un qualsiasi oggetto nel .Net Framework. Si evince che essi hanno l’entry point (colonna Entry) ad un indirizzo di memoria completamente diverso dagli altri, in quanto l’oggetto Object Type Object, viene istanziato dal CLR all’avvio in una propria zona della memoria Heap ed il codice viene subito compilato dal JIT compiler (da notare la voce PreJIT).

Subito dopo, ci sono i tre metodi della classe BaseClass, che non essendo stati ancora invocati, non sono stati compilati dal JIT compiler (valore NONE). Viceversa, i due costruttori.ctor e .cctor (di istanza e di tipo, rispettivamente) sono stati eseguiti al momento della creazione dell’oggetto bClass e quindi compilati dal JIT compiler.

Invocazione di un metodo di istanza non virtuale…

A questo punto, eseguiamo la seguente riga di codice :

bClass.InstanceMethod();
Il metodo in questione è un metodo di istanza non virtuale, per cui il JIT compiler deve semplicemente individuare il tipo con cui è stato dichiarato l’oggetto bClassBaseClass, ed accedere alla Method Table del BaseClass Type Object corrispondente. Individuato il metodo, lo compila e lo esegue.
3730.mem_alloc_4_4DBEDD07

Figura 1

Se rieseguiamo il dump della tabella dei metodi :

009bc048   009b38e8     NONE ConsoleApplication1.BaseClass.VirtualMethod()
009bc038   009b38cc      JIT ConsoleApplication1.BaseClass..ctor()
009bc058   009b3904      JIT ConsoleApplication1.BaseClass..cctor()
009bc040   009b38d8      JIT ConsoleApplication1.BaseClass.InstanceMethod()
009bc050   009b38f4     NONE ConsoleApplication1.BaseClass.StaticMethod()

Osserviamo che il metodo InstanceMethod() risulta essere stato compilato dal JIT compiler.

… di un metodo virtuale …

Passiamo adesso ad invocare il metodo virtuale :

bClass.VirtualMethod();
In questo caso, avendo a che fare con un metodo virtuale, non basta al JIT compiler sapere con quale tipo è stata dichiarata la variabile bClass, perchè utilizzando l’ereditarietà, potremmo anche far puntare la variabile bClass (dichiarata come BaseClass) ad una istanza della classe derivata DerivedClass. In questo caso, il tipo utilizzato per la dichiarazione della variabile non coinciderebbe con il tipo dell’effettivo oggetto puntato (come vedremo successivamente) e la tecnica utilizzata per l’invocazione di un metodo di istanza non virtuale non funzionerebbe.
In una situazione del genere, il JIT compiler deve accedere all’oggetto effettivamente puntato dalla variabile bClass, cioè nel nostro caso il BaseClass Obj, e deve analizzare il campo Type Object Pointer per poter ricavare l’effettivo Type Object. In questo caso, esso è ancora ilBaseClass Type Obj. A questo punto, il JIT accede alla Method Table, individua il metodo, lo compila e lo esegue.
8802.mem_alloc_5_35C7D014

Figura 2

… infine di un metodo statico

Infine, vediamo cosa accade per l’invocazione del metodo statico :

BaseClass.StaticMethod();

Questa situazione, è molto simile all’invocazione del metodo di istanza non virtuale, con la differenze che il JIT compiler conosce immediatamente la classe a cui appartiene il metodo (considerato il modo in cui viene eseguita l’invocazione) e quindi conosce subito il Type Object (in questo caso il BaseClass Type Obj) nel quale cercare il metodo nella tabella.

Allocazione di un oggetto della classe derivata …

Passiamo ora a vedere cosa accade utilizzando un riferimento del tipo BaseClass, per puntare ad un oggetto della classe derivata DerivedClass e quindi invocare su di essa il metodo virtuale sfruttando a tutti gli effetti il concetto di polimorfismo.

In primo luogo, un’istanza della classe DerivedClass deve essere allocata :

bClass = new DerivedClass();

Con l’istruzione precedente, il CLR deve allocare in memoria Heap un’istanza della classeDerivedClass e fare in modo che il suo Type Object Pointer punti, ovviamente, alDerivedClass Type Obj. L’indirizzo assegnato al nuovo oggetto allocato verrà restituito nella variabile bClass.

3858.mem_alloc_6_5B4127A8

Figura 3

Osservando la Figura 3, si evince che il nuovo oggetto DerivedClass Obj è stato allocato sull’Heap e che la variabile bClass punta ad esso. Non c’è più alcun riferimento all’oggettoBaseClass Obj che quindi sarà sottoposto alla pulizia da parte del Garbage Collector.

Analizziamo nel dettaglio l’area di memoria in cui è stato allocato il nuovo oggetto :

5353.mem_alloc_7_15C8FAF2

Figura 4

In riferimento alla Figura 4, di seguito c’è il dettaglio dei valori contenuti in memoria :

  • Offset –4 (val. 00000000) : è il Sync Block Index;
  • Offset 0 (val. 009b39a4) : è il Type Object Pointer;
  • Offset +4 (val. 00c2c3c0) : campo instanceString, ereditato dalla classe baseBaseClass. Da osservare che l’indirizzo è ovviamente il medesimo;
  • Offset +8 (val. 00000001) : campo instanceInt, ereditato dalla classe base BaseClass;

… ed invocazione di un metodo virtuale overridden

Per concludere, vediamo cosa accade durante l’invocazione del metodo virtuale di cui la classe derivata ne fa l’override :

bClass.VirtualMethod();

In questo caso, il JIT compiler parte dalla variabile bClass che punta all’oggetto DerivedClass Obj sulla memoria Heap. A partire da tale oggetto, analizza il suo Type Object Pointer che punterà al DerivedClass Type Obj. In questo modo, il JIT compiler arriva alla Method Tabledella DerivedClass, compila ed esegue il metodo virtuale giusto e non quello della classe base.

0702.mem_alloc_8_27C518B2

Figura 5

Con questa seconda parte, si conclude l’approfondimento relativo al CLR memory model mettendo in evidenza che nonostante le notevoli semplificazioni che il .Net Framework ci fornisce per sviluppare le nostre applicazione nel modo più rapido possibile, l’architettura sottostante del sistema non è assolutamente semplice.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s