Claudio Brotto

agosto 2005 - Posts

Sapore di antico

Da non crederci, ma oggi ho lanciato l'installazione Windows 98 (dopo almeno 5 anni di polvere nel mobile).

Ovviamente non sono stato preso da un raptus di follia, nè di amore per l'antiquariato (sì, lo so che ce ne sono ancora molti in giro), ma mi è stato necessario per effettuare alcuni test di un'applicazione che sto sviluppando in questi giorni.

Lanciato il setup sotto Virtual Pc, ho continuato a farmi gli affari miei, salvo notare due piccoli particolari che mi hanno fatto pensare a come sono cambiate le cose (che non vuol dire migliorate).

Primo. Durante la richiesta del numero di serie, la maschera indicava esplicitamente qualcosa del tipo "Inserire il numero di serie scritto sul retro del CD senza includere i trattini".

Secondo.Durante la fase finale (registrazione e salvataggio delle impostazioni) la didascalia a sinistra indicava esplicitamente qualcosa del tipo "Durante questo procedimento il computer potrebbe riavviarsi. Se il computer rimane bloccato a lungo, spegnere e riaccendere.

Onestamente non so se ridere, o pensare che riboostrappare un computer bloccato ed inserire numeri di serie è ormai diventato un'abitudine che non ha bisogno di ulteriori, esplicite spiegazioni ...

Jit compilation e differenze tra CPU

David Notario illustra in un recente post alcune strategie seguite per adattare il codice generato dal Jitter (e tramite ngen) a diverse CPU target.

Interessante, tra le altre, questa affermazione:

We don’t take advantage of other things, such as knowing code cache sizes, etc… One of the reasons for this is that we don’t want different code on every single machine out there. As usual, there is a trade off if we did this, we may get some extra speed in some situations, but on the other hand, in  a realistic world, it’s more likely for us to produce bugs that only repro in machines that meet n conditions, so introducing more processor specific optimizations has to be done carefully

Link: http://blogs.msdn.com/davidnotario/archive/2005/08/15/451845.aspx

Posted: ago 16 2005, 01:10 by devlizard | with no comments
Filed under:
Nullable types: cambiamenti dopo la beta 2

Tra ieri e oggi sono usciti due post (Joe Duffy e Somasegar) a proposito di un cambiamento nella gestione dei Nullable Types.

Le modifiche, che dovrebbero essere presenti già nelle prossime CTP (si parla di Agosto), coinvolgono principalmente le situazioni di boxing/unboxing.

Riassumendo, dovrebbe accadere quanto segue (tutti gli esempi riguardano un Nullable<Int32>, ma il tutto si applica ad ogni Nullable<T> where T : ValueType ):

  • Un Nullable<Int32> x sottoposto ad operazione di boxing genera:
    • null se x == null (x.HasValue == false)
    • un boxed Int32 in caso contrario
  • E' possibile effettuare unboxing da un boxed int ad un Nullable<Int32> , ottenendo un tipo nullabile con valore interno corrispondente al valore del boxed int.

A corollario di quanto detto, il codice seguente :

Nullable<Int32> i = null;

Object o = i; // boxing here

Console.WriteLine(o == null);

scriverà true a console (con la Beta2 che ho installato il risultato è false).

Analogamente, il codice che segue:

Int32 i = 78;

Object o = i; // boxing here

Nullable<Int32> j = (Nullable<Int32>) o; // unboxing here.

sarà perfettamente valido, producendo un Nullable<Int32> , al contrario di quanto accade oggi con la Beta2, che genera una InvalidCastException alla terza riga.

Citando direttamente dalla fonte, infatti (sottolineatura mia):

The outcome is that the Nullable type is now a new basic runtime intrinsic .  It is still declared as a generic value-type, yet the runtime treats it special .  One of the foremost changes is that boxing now honors the null state.  A Nullabe int now boxes to become not a boxed Nullable int but a boxed int (or a null reference as the null state may indicate.)  Likewise, it is now possible to unbox any kind of boxed value-type into its Nullable type equivalent.

Il tutto risulta, a mio parere, estremamente più intuitivo.

Comunque, va sottolineato come evidentemente il feedback ricevuto da Microsoft sia stato molto influente e abbastanza pressante, tanto da indurre ad un cambiamento decisamente tardivo, se consideriamo che l'uscita della RTM è ormai prossima e che le modifiche effettuate (leggete i link) non siano state proprio minimali.

Posted: ago 13 2005, 01:21 by devlizard | with no comments
Filed under:
LUA: difficile diffondere l'abitudine.

Ho letto il post di Paolo relativo al (non)funzionamento di alcuni programmi sotto utente non amministratore, e non posso che concordare con lui.

Ormai sono alcuni mesi che ho adottato l'abitudine di utilizzare un utente User sui miei computer.

Dovendo tirare le somme, adesso, della mia esperienza, devo dire che è stata meno traumatica di come me l'ero prefigurata e che indubbiamente i vantaggi superano di molto lo sforzo iniziale, dovuto magari proprio al malfunzionamento di alcuni programmi.

Il discorso cambia quando si parla di diffondere questa pratica.

Per la mia esperienza (lo sottolineo due volte, perchè magari la realtà è differente) ci sono in giro diverse situazioni di informatizzazione forzata: intendo dire posti in cui l'utilizzo dello strumento informatico è in qualche modo subìto in modo passivo, e manca un'adeguata preparazione del personale che lo utilizza.

Manca, in quei casi, la competenza necessaria per capire l'importanza di questa abitudine: talvolta il concetto di privilegi di un utente non è assolutamente noto, la password è quella cosa che mi permette di accendere il pc , e via dicendo.

Però, ammesso e non concesso di porre rimedio a questa disinformazione, il problema è risolto ?

Giusto qualche giorno fa mi è capitato di installare un'applicazione che ho scritto presso un ufficio.

Tra una parola e l'altra, ho proposto di adottare la pratica di utilizzare utenti a bassi privilegi.

Domanda: Perchè ?

Risposta: bla bla bla.

Domanda: Ma poi funziona tutto come prima ?

Risposta: bla bla bla ... Dipende ... Facciamo una prova ?

Prova: il programma di contabilità che stanno usando (pagato diverse migliaia di euro) si pianta inesorabilmente.

Risultato: ovviamente come non detto, si lascia tutto così come sta, non è un problema mio (semmai di chi ha prodotto il software).

Sono due le cose da evidenziare, secondo me.

Primo. Se l'utente non è informato, difficilmente adotterà una pratica "fuori default". E quindi difficilmente i difetti del software che compra verranno a galla.

Secondo. In alcuni casi non c'è quasi la possibilità di scelta sull'acquisto del software. Non stiamo parlando di programmi "general purpose", ma di applicazioni molto, molto specifiche, spesso sviluppate ad hoc, talvolta scelte obbligate addirittura per motivi pseudo-legali. Quello che hai, te lo tieni, perchè mancano le alternative.

Se sei costretto ad utilizzare un programma, per forza di cose non hai troppa libertà nel modificare il suo ambiente di esecuzione. In pratica sei costretto a fare in modo che il programma funzioni. Non la software house che l'ha prodotto (che ha una sorta di monopolio), ma tu che lo usi.

Sigh.

Posted: ago 07 2005, 03:26 by devlizard | with no comments
Filed under:
Menu.bat

Non so come mi è ritornata in mente questa storia, ma tant'è ... la racconto lo stesso.

Da piccolo (credo più o meno 8 o 9 anni) la mia esperienza informatica era con un vecchio 8088 che mio padre aveva comprato, per motivi di lavoro immagino, ed aveva portato a casa.

Al 90% mi davo ai giochi che avevo a disposizione (rigorosamente su floppy da 5 pollici e 1 quarto).

Il 10% rimanente smanettavo col DOS: crea una directory di qua, copia i file di là, le prime prove con autoexec e poco altro.

Finchè non decisi che accendere il computer e trovarsi di fronte uno scarno prompt (C:\>) non mi soddisfaceva.

Così creai il mio menu personale.

Il file principale era menu.bat: una serie di echo uno dopo l'altro, che mostravano a video le varie voci previste, precedute da un numero progressivo (1, 2 ... n). E basta.

Poi c'era una serie di file batch denominati 1.bat, 2.bat ... n.bat, ciascuno corrispondente ad una voce del menu.

All'avvio del computer, lanciavo menu.bat che mi riempiva lo schermo di caratteri di formattazione (80 righe x 25 colonne tutte più o meno piene). E ritornavo al prompt del DOS.

Quando volevo eseguire un comando dal menu, beh, semplicissimo: bastava scrivere 1 + <ENTER>.

Il file 1.bat poteva essere qualcosa del tipo:

@echo off

cd pctools

pctools

e, manco a dirlo, lanciava il PcTools della Norton.

Insomma ... un piccolo programmatore in erba !!!

Posted: ago 05 2005, 05:47 by devlizard | with no comments
Filed under:
Un VIRT di troppo

L'ambiente di esecuzione virtuale del Common Language Runtime effettua le chiamate di metodo attraverso la MethodTable. Questo sottintende che non solo i metodi virtuali, ma anche quelli non virtuali (statici compresi) hanno una loro identità precisa, rappresentata (anche) da un'entry nella tabella dei metodi.

Al runtime spetta la scelta di quale MethodTable utilizzare. Se, per un metodo non virtuale, questa può essere effettuata in maniera "statica" in fase di compilazione, i metodi virtuali richiedono, per la loro stessa natura, un meccanismo di scelta rimandato alla fase di esecuzione.

L'Intermediate Language consente di selezionare il comportamento richiesto tramite le due istruzioni call e callvirt.

A farla breve, si potrebbe quindi dire che i compilatori generano l'istruzione call per chiamate a metodi non virtuali (e statici) e l'istruzione callvirt per chiamate virtuali.

Non si impiega molto a verificare la validità di questa affermazione: buttiamo giù due righe di codice !

class Esempio {
  
public static void 
MetodoStatico() {}
  
public void 
MetodoNonVirtuale() {}
  
public virtual void 
MetodoVirtuale() {}
}

class 
Chiamante {
  
static Esempio
DammiUnEsempio() { ... }
  
static void 
Main() {
      Esempio esempio = DammiUnEsempio();
      Esempio.MetodoStatico();
      esempio.MetodoNonVirtuale();
      esempio.MetodoVirtuale();
  }
}

Ildasm alla mano, andiamo ad analizzare il codice IL del chiamante (Chiamante::Main()):

IL_0006:  call       void Esempio::MetodoStatico()
IL_000b:  ldloc.0
IL_000c:  callvirt   instance void Esempio::MetodoNonVirtuale()
IL_0011:  ldloc.0
IL_0012:  callvirt   instance void Esempio::MetodoVirtuale()

Mmmhhh ... Non ci siamo !

Abbiamo (IL_000c) una chiamata non virtuale effettuata tramite l'istruzione callvirt. Il che smentisce quanto assunto in precedenza. Dove sta l'errore ?

Un piccolo (!!) prologo è d'obbligo.

Virtual Dispatch.

Quando un metodo non è virtuale (questo vale, quindi, anche per i metodi statici) il compilatore ha la garanzia (per definizione) che la sua implementazione è fornita dal tipo del riferimento sul quale il metodo stesso è invocato.

Se scrivo:

 

Esempio esempio = DammiUnEsempio();

esempio.MetodoNonVirtuale();

 

il compilatore è in grado di risolvere direttamente l'indirizzo del metodo invocato.

Questo vale per un compilatore C++ che genera codice x86 da sorgenti C++, tanto quanto per il compilatore JIT del CLR che genera codice x86 da sorgenti IL.

Un'istruzione IL come:

 

IL_000c:  call       void Esempio::MetodoNonVirtuale()

 

verrà compilata just in time dando origine a codice macchina simile al seguente (a meno dei valori reali degli offset, ovviamente):

 

mov    ecx, esi

call   dword ptr ds:[02BF0D88h]

 

Innanzitutto il riferimento all’oggetto viene copiato nel registro ecx. La modalità di passaggio dei parametri dipende dalla convenzione di chiamata che il Jitter utilizza: il Jitter del CLR, almeno nelle versioni distribuite sino ad ora, utilizza la convenzione fastcall, che prevede, se possibile, il passaggio dei primi due parametri in ecx ed edx.

Successivamente, l'indirizzo del metodo (o del thunk di precompilazione, in ogni caso l'indirizzo target dell'istruzione x86 call) viene inserito direttamente all'interno dello stream di codice generato. Il compilatore lo conosce a priori. Punto.

 

Se un metodo è virtuale, le cose cambiano.

Dichiarare un metodo virtuale significa, più o meno, affermare: "Questo metodo può essere ridefinito (sovrascritto) da un tipo derivato".

E, quindi, significa che un’istruzione di chiamata come:

 

Esempio esempio = DammiUnEsempioOUnSuoDerivato();

esempio.MetodoVirtuale();

 

non è risolvibile in fase di compilazione in maniera diretta, come nel caso precedente.

Questo perchè non basta il tipo del riferimento (Esempio) per determinare quale sia l'implementazione corretta da invocare. La variabile esempio, per quanto acceduta tramite un riferimento alla classe base, potrebbe essere di tipo EsempioDerivato (class EsempioDerivato : Esempio { ... }), e se la classe EsempioDerivato ridefinisce il metodo MetodoVirtuale, allora sarà quello a dover essere utilizzato.

 

L'implementazione più comune del meccanismo di dispatch virtuale prevede l'utilizzo di  una tabella di indirizzi di metodi specifica per ciascun tipo (classicamente chiamata v-table) che sia accessibile a partire dal riferimento ad un'istanza.

In tal modo, benchè non sia noto a priori l'indirizzo del metodo da chiamare (poichè non è noto il tipo reale dell'oggetto), il compilatore conosce l'offset del metodo in tale tabella. Il "trucco", ovviamente, è assegnare ad un metodo virtuale lo stesso offset in tutte le v-table di tutti i tipi che lo definiscono: in esecuzione, il riferimento all'istanza chiamata consentirà di risalire alla tabella corretta, e l'offset (staticamente inserito dal compilatore) consentirà di invocare il metodo appropriato.

Come abbiamo visto, il sistema dei tipi del CLR non sfugge a questo disegno, anzi lo estende mantenendo informazioni a livello dei tipi che vanno ben al di là di un elenco di puntatori a funzione.

 

Ecco spiegato come mai la chiamata ad un metodo virtuale si trasforma nel seguente codice x86:

 

mov    ecx, esi

mov    eax, dword ptr [ecx]

call   dword ptr [eax + 38h]

 

Nel registro ecx è memorizzato l'indirizzo in memoria dell'istanza in questione (di tipo Esempio o derivato).

La seconda istruzione mov copia nel registro eax il contenuto di tale indirizzo di memoria. In pratica dereferenzia il puntatore all’oggetto.

Infine il metodo viene invocato indirizzando in modo indiretto la tabella dei metodi tramite un offset predeterminato in compilazione.

Il risultato è che c'è un livello di indirezione ulteriore, nella chiamata ai metodi virtuali, che risulta necessario proprio per la definizione stessa di "metodo virtuale".

  

Effetti Collaterali.

 

Ritorniamo al caso di una chiamata ad un metodo di istanza non virtuale.

Non abbiamo detto che il compilatore è in grado di determinare a priori l'indirizzo in memoria del metodo ed emettere la corrispondente istruzione call ?

Sicuramente sì, tanto è vero che l'assembler generato è:

 

mov    ecx, esi

call   dword ptr ds:[02BF0D88h]

 

Ma, ciò nonostante, chi dà origine a questa istruzione è l'opcode IL callvirt associato ad un token dei metadati che indica un metodo di istanza non virtuale.

Come mai ?

 

La spiegazione va ricercata in un side-effect del dispatch virtuale: la dereferenziazione del riferimento all'oggetto, c he risulta necessaria per accedere alla v-table.

E' un aspetto importante, poichè consente di verificare la validità del puntatore al nostro oggetto di tipo Esempio.

Se il puntatore è invalido (per esempio nullo), la chiamata non avviene poichè prima che questa sia possibile il sistema genera un'eccezione di Access Violation e il programma va in crash.

Nel caso di una chiamata ad un metodo statico, non c'è nessun riferimento da derefereniziare (niente this, per intenderci). E il compilatore C# è felicissimo di utilizzare l'istruzione IL call, che il Jitter saprà interpretare in modo appropriato.

Ma nel caso di una chiamata ad un metodo di istanza non virtuale, la funzione chiamata deve ricevere come (primo) parametro il riferimento all’oggetto sul quale è invocata.

Semplicemente che in questo caso la validità del riferimento (cioè il fatto che sia correttamente dereferenziabile) non si può verificare tramite il meccanismo di indirezione del dispatch virtuale: il metodo, in fondo, virtuale non lo è affatto, e il meccanismo di dispatch virtuale non può (non deve) intervenire.

 

Il Jitter, allora, incontrando un'istruzione callvirt su un metodo non virtuale, lo invoca in modo diretto ma fa precedere l'invocazione da un test sulla validità del riferimento all'oggetto. Lo stesso meccanismo che era implicito per i metodi virtuali diventa ora esplicito:

 

mov    ecx, esi

cmp    dword ptr [ecx], ecx

call   dword ptr ds:[02BF0D88h]

 

L’istruzione x86 cmp, per farla breve, effettua il confronto per sottrazione di due valori ed imposta in modo conseguente il registro di flag (EFLAGS); nella maggior parte degli utilizzi la si trova in congiunzione con qualche istruzione di salto condizionato.

Nel nostro caso, in realtà, il suo comportamento è del tutto inutile (o, per meglio dire, inutilizzato): non c’è alcun interesse a verificare la differenza fra l’indirizzo di memoria in ecx (puntatore all’oggetto) con il valore dword in esso contenuto (l’oggetto stesso).

A parte, appunto, quello di fallire se il riferimento in ecx è invalido.

Qualsiasi istruzione che avesse effettuato una dereferenziazione sarebbe stata adatta allo scopo.

La scelta di cmp credo sia dovuta principalmente ad un paio di fattori.

Primo: cmp non fa altro che settare flag. Non modifica registri, non modifica locazioni di memoria, tutti “effetti collaterali” sicuramente indesiderati in un caso come questo.

Secondo: prestazioni. Su un Pentium la versione di cmp che confronta un registro con un indirizzo di memoria porta via solamente due cicli di clock.

 

Ecco, alla fine, la spiegazione: in definitiva, l'utilizzo di callvirt su metodi non virtuali non modifica la modalità di chiamata, che è e resta non virtuale, ma serve esclusivamente ad indicare al Jitter di aggiungere un controllo ulteriore prima della chiamata stessa.

 

La prova del 9.

 

In altre parole, cosa succederebbe se il Jitter ignorasse la necessità di questo controllo ed emettesse solamente l’istruzione di chiamata ?

Quali problemi porta con sé un codice come il seguente (che è, nè più nè meno, quello generato dall'istruzione IL call) ?

 

mov    ecx, esi

; Questo non viene eseguito:

; cmp    dword ptr [ecx], ecx

call   dword ptr ds:[02BF0D88h]

 

Come detto, se il riferimento all’oggetto è valido l’istruzione cmp è del tutto superflua, e il codice scritto sopra è perfettamente legale e corretto.

Ma poniamo il caso che, per un errore o semplicemente per costruzione, il metodo DammiUnEsempio() sia:

 

static Esempio DammiUnEsempio() {

   return null;

}

 

Il chiamante si ritrova con un riferimento nullo, che viene salvato nel registro ecx come primo parametro per il MetodoNonVirtuale. Senza istruzione cmp, questa situazione non provoca errori e il controllo viene passato al metodo stesso.

 

Eccoci così con un metodo di istanza pronto ad essere eseguito su ... nessuna istanza !

Il metodo inizierà la propria esecuzione con la garanzia (implicita nel suo essere “metodo di istanza”) di avere a disposizione un puntatore this valido in ecx. Alla prima occasione in cui tale puntatore andrà dereferenziato (per esempio al primo accesso ad un campo di istanza) il programma genererà la famigerata eccezione di violazione di accesso alla memoria (il codice di stato dell’eccezione in SEH me lo ricordo a memoria, 0xC0000005, chissà come mai ... ).

 

E giusto che venga generata una NullReferenceException/Access Violation ?

Sicuramente sì, perchè tentiamo di dereferenziare un riferimento invalido.

E’ giusto che questa sia generata dal MetodoNonVirtuale() ?

Sicuramente no ! Non è compito del metodo di istanza verificare la validità dell’istanza, è compito del suo chiamante !

In questo caso, eliminando l’istruzione cmp, abbiamo sollevato il chiamante da una sua precisa responsabilità, e l’effetto che se ne ottiene è che abbiamo un’eccezione originata nel posto sbagliato.

Addirittura, la cosa si fa curiosa se, ad esempio, il metodo di istanza è:

 

void MetodoNonVirtuale() {

  Console.WriteLine("Ciao");

}

 

Se il metodo non “tocca” il riferimento this, non viene generata alcuna eccezione ed il codice è (o meglio, sembra) esente da errori. Siamo riusciti a chiamare un metodo di istanza su un’istanza nulla, e l’esecuzione procede senza intoppi.

 

In definitiva: l’istruzione cmp, che funge da “dereferenziatore” è necessaria per garantire che un metodo non virtuale riceva un riferimento all’istanza valido, e che l’esecuzione vada in exception nel punto di chiamata se questa condizione, al contrario, non si verifica.

 

Considerazioni.

 

Come sempre, andando ad indagare il funzionamento del CLR, ci sono diversi attori in gioco, e diversi livelli dai quali si possono osservare i fenomeni: linguaggio macchina, Linguaggio Intermedio, linguaggio ad alto livello (C# ecc.).

Il tutto diventa interessante quando si deve scegliere in quale ambito implementare una funzionalità.

E' una caratteristica del linguaggio ? Del motore di esecuzione ? Chi deve garantire cosa ?

Nella situazione discussa sinora, la funzionalità da realizzare è: "garantire che un metodo di istanza sia invocato su un'istanza non nulla, e fallire in caso contrario".

IL, in questa occasione, mostra il suo lato di linguaggio a basso livello e lascia ai linguaggi "applicativi" l'onere del controllo: se le specifiche di un linguaggio lo consentono, il compilatore relativo è libero di violare questa invarianza ed effettuare chiamate su riferimenti nulli (bontà sua ...) emettendo un opcode call.

Le specifiche di C#, però, indicano con chiarezza che una chiamata su un riferimento nullo deve generare una NullReferenceException. L'utilizzo di callvirt su metodi non virtuali può sembrare una forzatura, ma il compilatore C# deve ... materializzare le sue specifiche, sfruttando le funzionalità ed i meccanismi che il runtime mette a disposizione.

 

 

 

 

Posted: ago 04 2005, 03:46 by devlizard | with no comments
Filed under: