Claudio Brotto

luglio 2005 - Posts

SOS ... che non e' una richiesta di aiuto ...

In questo caso SOS sta per Son Of Strike.

Alcuni debugger (tra gli altri CDB e WinDBG) espongono un meccanismo di "estensione" tramite il quale DLL di terze parti, purchè ovviamente scritte in maniera opportuna, possono aggiungere funzionalità a quelle native del debugger stesso. Una sorta di meccanismo a plug-in, insomma.

SOS è, infatti, una dll di estensione ai debugger sopra citati che viene distribuita con l'SDK del .NET Framework e che consente di visualizzare alcune strutture interne del Common Language Runtime: informazioni su tipi, GC, thread e via discorrendo.

Personalmente non sono un amante dei debugger a linea di comando (come CDB) e non mi trovo spesso ad utilizzare WinDBG (forse perchè mi è capitato raramente di dover fare debug su una macchina di produzione, dove VisualStudio non è quasi mai installato, nè tanto meno installabile).

In realtà SOS è utilizzabile in modo estremamente semplice anche da VS.NET.

E' sufficiente "attaccarsi" ad un processo col debugger in modalità nativa (almeno), sospendere l'esecuzione, aprire la finestra Immediate (Debug->Windows->Immediate) e passare (se già non è attiva) alla modalità immediata digitando immed al prompt.

Non resta che caricare la dll tramite:

.load sos.dll

(il path completo non dovrebbe essere necessario, poichè l'installazione di Visual Studio ha messo a posto le variabili di ambiente in modo opportuno).

SOS mette a disposizione una serie piuttosto ampia (e ampliata col procedere delle versioni del CLR, anche in modo notevole con la 2.0) di comandi.

Non ne riporto la lista poiché basta digitare !help per ottenere l'elenco completo. L'ultima release, che accompagna la beta 2, consente anche di ottenere alcune righe di informazione su ogni singolo comando, semplicemente digitando !help <nomecomando>.

L'utilità di questa "cosa" ?

Poca dal punto di vista meramente pratico (se dobbiamo sviluppare un sito in ASP.NET o un'applicazione Windows probabilmente riusciamo a farne a meno senza troppa difficoltà ...).

Molta dal punto di vista "accademico": perchè, appunto, permette di catturare informazioni sulle strutture interne al runtime e, quindi, sul reale funzionamento di alcune delle sue componenti.

Per rendere la cosa un po' meno astratta, riporto un esempio che mostra come sia possibile visualizzare il codice x86 emesso dal Jitter nella compilazione di un metodo.

Innanzitutto, scriviamoci un programma "scratch", anche banale, come questo:

using System;
using System.Runtime.CompilerServices;

namespace SOS {

  class Program {
        
    
static void Main() {
      Console.Write("Aggiungo 10 a 5. Il risultato è: ");
      Console.WriteLine(Add(5, 10).ToString());
      Console.ReadLine();
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    
static Int32 Add(Int32 first, Int32 second) { 
      
return first + second; 
    }
  }
}

Il metodo Add, che è quello che andremo ad esaminare, è volutamente marcato come "non-inlinabile", in modo tale che il Jitter non ne inserisca il corpo sostituendolo alla chiamata in Main e, di fatto, evitandone la compilazione ... dedicata.

Compiliamo in Release ed eseguiamo. Attacchiamo il debugger al processo e sospendiamone l'esecuzione.Non resta che caricare SOS all'interno del processo ed iniziare ad utilizzarne le funzionalità (.load sos.dll).

Il percorso che dovremo seguire per scoprire il codice macchina generato dal Jitter per il metodo Add è un po' lungo, benchè piuttosto lineare:

  1. si parte dalla struttura che rappresenta il tipo Program in memoria ...
  2. dalla quale si ottiene l'elenco dei metodi definiti o ridefiniti ...
  3. nel quale si cerca il metodo Add ...
  4. che infine si decompila !

(1) Per visualizzare le informazioni su un tipo al runtime si può utilizzare il comando !DumpMT <indirizzo MethodTable> . L'indirizzo di memoria della MethodTable relativa al tipo Program non è ovviamente noto a priori: esistono diversi modi per ottenerlo, ma il più semplice consiste nel chiedere ad SOS di indicarcelo, a partire dal nome del tipo e del modulo che lo contiene, tramite il comando !Name2EE <nome modulo> <nome tipo>. Quindi:

!Name2EE SOSTest.exe SOS.Program

--------------------------------------

MethodTable: 009350a8

EEClass: 00c233a4

Name: SOS.Program

(2) Poi ci facciamo elencare i metodi definiti da Sos.Program. Lo switch MD (MethodDesc) indica a SOS la richiesta di un elenco completo con le informazioni su ciascuna entry della tabella dei metodi:

!DumpMT -MD 009350a8

EEClass : 00c233a4

Module : 00161f20

Name: SOS.Program

mdToken: 02000002 (D:\Test\.NET\SOSTest\bin\Release\SOSTest.exe)

MethodTable Flags : 80000

Number of IFaces in IFaceMap : 0

Interface Map : 009350f0

Slots in VTable : 7

--------------------------------------

MethodDesc Table

Entry MethodDesc JIT Name

79b9300b 79b93010 None [DEFAULT] [hasThis] String System.Object.ToString()

79b9301b 79b93020 None [DEFAULT] [hasThis] Boolean System.Object.Equals(Object)

79b9304b 79b93050 None [DEFAULT] [hasThis] I4 System.Object.GetHashCode()

79b9306b 79b93070 None [DEFAULT] [hasThis] Void System.Object.Finalize()

00c30058 00935080 JIT [DEFAULT] Void SOS.Program.Main()

00c300c8 00935090 JIT [DEFAULT] I4 SOS.Program.Add(I4,I4)

0093509b 009350a0 None [DEFAULT] [hasThis] Void SOS.Program..ctor()

 

(3) Le informazioni disponibili sono diverse: la locazione di memoria che contiene il codice del metodo (o il rimando al thunk di compilazione), l'indirizzo della relativa MethodDesc, il fatto che il metodo sia già stato compilato o meno dal Jitter (JIT/None), che sia un metodo statico o di istanza ([hasThis]). Si può ottenere un dettaglio ulteriore tramite:

!DumpMD 00935090

Method Name : [DEFAULT] I4 SOS.Program.Add(I4,I4)

MethodTable 9350a8

Module: 161f20

mdToken: 06000002 (D:\Test\.NET\SOSTest\bin\Release\SOSTest.exe)

Flags : 30

Method VA : 00c300c8

 

(4) In fondo si nota l'indirizzo (Virtual Address) del codice del metodo in memoria. Non resta che decompilarlo:

!u 00c300c8

Normal JIT generated code

[DEFAULT] I4 SOS.Program.Add(I4,I4)

Begin 00c300c8, size 5

00C300C8 add ecx,edx

00C300CA mov eax,ecx

00C300CC ret

 

Voila, gioco finito :-)

Se vi interessa, vi lascio un paio di link con qualche approfondimento e qualche ulteriore esempio d'uso.

http://blogs.msdn.com/yunjin/archive/2005/05/15/417569.aspx

http://blogs.msdn.com/mvstanton/archive/2004/04/05/108023.aspx

Posted: lug 28 2005, 12.08 by devlizard | with no comments
Filed under:
MethodDesc: come ti passo un parametro ...

Quando il loader del CLR carica un tipo, va a leggere dal modulo che lo contiene i metadati relativi e prepara una struttura in memoria, che altro non è se non una rappresentazione del tipo (quindi dei suoi metadati) ottimizzata per l'esecuzione.

Questa struttura di memoria è complessa, piuttosto distribuita e non documentata, per quanto molte informazioni si riescano ad ottenere con l'ausilio dei sorgenti di Rotor e di un buon debugger.

La struttura chiave, che contiene le informazioni a più frequente accesso è chiamata MethodTable. Per inciso, vi si accede da codice gestito tramite la classe RuntimeTypeHandle, esposta da Type tramite la property TypeHandle.

Ogni riferimento ad un oggetto di tipo reference punta ad una locazione di memoria nello heap gestito dal garbage collector, la quale è a sua volta un puntatore all'inizio di tale struttura.

La MethodTable di un tipo contiene diverse informazioni vitali affinchè il runtime possa gestire gli oggetti in fase di esecuzione: dimensioni del componente, riferimento alla mappa delle interfacce implementate e, alla fine della struttura, un vettore di puntatori ai metodi che il tipo (ri)definisce.

Ora, poichè i metodi non vengono compilati dal Jitter se non in occasione della prima esecuzione, è necessario un meccanismo che:

  • scateni la compilazione just-in-time quando necessario ...
  • salvi in memoria la versione compilata del metodo ...
  • e infine richiami quest'ultima, se presente.

Il CLR ottiene questo scopo inserendo un'istruzione JMP in ogni slot della tabella dei metodi. In fase iniziale, ossia quando ancora il metodo non è stato compilato in codice nativo, il jump porta il percorso di codice attraverso i meandri del Jitter, in modo tale che questo possa effettuare la compilazione. Una volta compilato, il codice viene salvato in un'area di memoria privata del runtime, e l'istruzione JMP viene patchata in modo tale da puntare direttamente alla versione eseguibile del metodo in questione. Ad ogni esecuzione successiva, la fase di compilazione verrà saltata a piè pari in maniera del tutto automatica e trasparente, e il metodo sarà eseguito senza ulteriori overhead, a parte quello (minimo) dell'indirezione dovuta all'istruzione di salto.

Una domanda interessante, a questo punto, potrebbe essere: come vengono passate al Jitter le informazioni sul metodo da compilare (in primis la locazione dello stream di codice IL) ?

La risposta è altrettanto interessante e, a mio avviso, anche piuttosto sorprendente.

Innanzitutto una breve premessa: come affermato in precedenza, le informazioni sul tipo in memoria sono numerose e "ramificate", nel senso che non sono necessariamente presenti in zone di memoria contigue e, quindi, "staticamente" accessibili le une dalle altre tramite offset fisso. Ogni metodo, ad esempio, è identificato da uno slot nella tabella dei metodi (di cui abbiamo appena parlato), ma è altresì descritto da una struttura dati denominata MethodDesc, che non è accessibile a partire dalla MethodTable se non attraverso diverse indirezioni.

Ogni MethodDesc, come il nome fa presupporre, contiene informazioni sul metodo in questione, tra le quali spicca l'indirizzo del codice IL che lo rappresenta (quando è ancora nella forma pre-jit).

Facendola semplice, il runtime dovrebbe indicare al Jitter l'indirizzo di memoria che contiene la MethodDesc, affinchè questo ne possa ricavare le informazioni necessarie e procedere alla compilazione. Tutto questo a partire da un JUMP dalla MethodTable.

Quindi ... soluzione 1 (quella che se me lo avessero chiesto in un colloquio avrei proposto, nda): ogni volta che viene invocato un metodo, il runtime passa sullo stack l'indirizzo della MethodTable (o dello slot del metodo, la scelta è indifferente poichè la distanza di questo dall'inizio della MethodTable è nota al runtime), quindi effettua il JMP. Se il salto porta alla compilazione JIT, il JMP punterà ad un (unico) helper che darà inizio alla compilazione, ricavando l'indirizzo della MethodDesc a partire dalla MethodTable tramite una serie di indirezioni. E il gioco è fatto (per modo di dire, giacchè spero che domande di questo genere non siano la quotidianità di un colloquio per un posto da programmatore).

In realtà, la soluzione adottata è un po' più perversa e geniale.

Innanzitutto, ogni elemento della tabella dei metodi contiene un'istruzione JMP ad una diversa locazione di memoria: 10 metodi definiti, 10 slot nella tabella dei metodi, 10 istruzioni JMP con operandi differenti.

Il target di ogni istruzione di salto è una locazione di memoria molto particolare: esattamente 5 byte prima dell'indirizzo della rispettiva MethodDesc.

???

Stiamo saltando ad un'area dati, non dovremmo saltare ad un'area di memoria contenente codice eseguibile ?!?

Sì e no !

In quei 5 byte, il loader ha emesso il codice x86 che implementa un'istruzione call (E8 + <indirizzo della funzione che inizia la compilazione JIT>).

L'istruzione call in assembler è in pratica un comando di salto incondizionato "con memoria": è del tutto equivalente ad un JMP, ma in aggiunta l'indirizzo successivo al corrente (quello contenuto in ogni momento nel registro EIP) viene posto sullo stack. E' il funzionamento di una chiamata a funzione in assembler: l'indirizzo di ritorno viene salvato, e l'istruzione ret che conclude la procedura lo preleva, lo salva in EIP e fa in modo di riprendere l'esecuzione dal punto di chiamata (per la gioia degli attacchi da buffer overflow, ma questa è un'altra storia).

Il trucco sta nel porre il codice per l'istruzione call esattamente prima dell'indirizzo della struttura MethodDesc. In tal modo non facciamo che ingannare l'assembler, che crede di trovarsi di fronte ad un'istruzione di chiamata e memorizza l'indirizzo di memoria successivo al corrente sullo stack. Peccato che tale indirizzo sia proprio quello della MethodDesc ! Non ci sarà nessuna istruzione ret che ci riporterà a questa locazione: è stato solo sfruttato un side-effect dell'istruzione x86 call, cioè il passaggio dell'indirizzo di esecuzione successivo sullo stack.

Il vantaggio di una soluzione di questo tipo sta nell'aver evitato tutte le ricerche necessarie a ricavare l'indirizzo di una struttura MethodDesc a partire da quello di una struttura MethodTable: come detto, anche se logicamente correlate, le informazioni sui tipi sono disposte in maniera piuttosto ramificata nella memoria del CLR, tanto che a volte sono necessarie alcune indirezioni (dereferenziazioni, offset su vettori, ricerche su liste concatenate) per ottenere le une dalle altre.

In uno scenario (come questo) dove va ricercata anche la minima ottimizzazione, un "trucco" del genere vien giusto bene !

Posted: lug 25 2005, 07.09 by devlizard | with 1 comment(s)
Filed under:
Blogging Again

Sono stato assente dal mio blog per un po' di tempo, sigh.

Potrei addurre a mia scusante un po' di impegni che ho avuto nell'ultimo periodo, ma in realtà ci sono stati momenti più densi di attività che non mi hanno impedito di scrivere con maggiore regolarità.

Vabbè.

Nel mentre sono successe un po' di cosine: un motore ed un look rinnovato per il sito ed i blog Devleap, due TechEd con qualche novità (no, non ne ho frequentato nemmeno uno, ma per fortuna c'è chi scrive sempre rapporti completi !), un nuovo nome per Longhorn (che brutto !), strepitose imprese di Valentino Rossi (scusate l'OT, ma quando ci vuole ci vuole, ragazzi che fenomeno !).

Sto finendo di scrivere un post abbastanza lungo (che dovrebbe seguire a breve), ma nel frattempo ecco il mio personale welcome back a me stesso, sperando di essere uno scrittore un po' più costante in futuro.