Programmiamo in IL

Data

by Quake2

 

27/07/2004

UIC's Home Page

Published by Quequero


"That is not dead which can eternal lie / And with strange aeons even death may die." H.P. Lovecraft

Grazie quake,specie perche' con la tua illuminazione di sicuro non iniziero' mai in tutta la vita a programmare in C#, W i framework della M$ e il ruby....ihihihi ero ironico ovviamente ;p. Ah il tute e' bello non mi fraintendere ;p e' il resto che non mi piace ;p

"But as, in ethics, evil is a consequence of good, so, in fact, out of joy is sorrow born" E.A. Poe

....

Home page se presente: http://pmode.cjb.net
E-mail: [email protected]
Nick: Quake2^AM, UIN: 51184823, AzzurraNet: #gameprog-ita, #crack-it, #asm, #pmode, #c#, #programmazione, #beos

....

Difficoltà

( )NewBies (x)Intermedio (x)Avanzato ( )Master

 

Beh penso che dal titolo si capisce che tratteremo della programmazione in IL (Intermediate Language), ma in modo molto inusuale :)


Programmazione in IL
(come rendere semplici le cose difficili :))
Written by Quake2

Introduzione

In questo tutorial parleremo di come scrivere un semplice programmino direttamente in IL, e nel contempo sarà anche un tutorial sul C# (o meglio, su .NET), dato che parleremo in modo esteso del namespace System.Reflection, e mostrerò alcune tecniche carine per generare codice a runtime in modo semplicissimo (più semplice di come state immaginando ora! :)). I requisiti necessari per questo tutorial sono:
  • Conoscenza del C#
  • Conoscenza di .NET
  • Un framework compatibile con .NET, quindi o Mono o .NET Framework, il primo lo trovate su http://www.go-mono.com, per il secondo, uhm... indovinate? Sul sito della Microsoft!! :) Vabbe so che siete pigri, ecco il link per il .NET Framework SDK (è GROSSO): .NET Framework SDK, in alternativa potete scaricare il .NET Redistributable Package (un po' più piccolo) da qua: .NET Redistributable Package, però vi avverto che non sono sicuro che ci sia il compilatore, teoricamente dovrebbe esserci, altrimenti le classi che usano System.CodeDom.Compiler non funzionerebbero, comunque in ogni caso, c'è sempre Mono che è abbastanza piccolo.
    NOTA: Platform.NET non è supportato, non ho potuto provare il codice su questo ambiente .NET, quindi non vi assicuro che qua funziona, poi c'è Mono che (secondo me) è molto meglio di Platform.NET, quindi usate quello!!! :)
NOTONA (ovvero più importante della NOTA): Ovviamente presuppongo che state usando .NET 1.1, il codice non l'ho provato sulla versione 1.0 ne voglio farlo :)

Tools usati

Beh ve l'ho detto prima, mi sembrava più una cosa da introduzione, vabbe dai, qua vi do il link diretto pure per Mono:

URL o FTP del programma

Oh, il programma ce lo facciamo noi!

Notizie sul programma

Questo programma usa una super protezione con codice polimorfico decriptato da 40-1 (come le frustate) thread in parallelo che funziona solo su un'architettura smp con 512 processori R12000, gestiti dalla marmotta che confeziona la cioccolata. (Vabbe avete capito che voglio solo riempire un po' di spazio :))  

Essay

Beh, iniziamo a fare su serio va. In questo tutorial vedremo come sfruttare i potenti mezzi messi a disposizione dal namespace System.Reflection, per generare un eseguibile
(si avete capito bene, un eseguibile intero e funzionante) a runtime, ma senza impazzire con indirizzi,rilocazione, gestione della memoria, ecc... Vi starete chiedendo come
sia possibile una cosa del genere, beh è semplice, prima di tutto chiudete ilasm.exe che sicuramente avete aperto :), perché NOI saremo sia l'assemblatore sia il linker :).
Ho deciso di usare il C# come linguaggio, primo perché lo conosco meglio, secondo perché con VB.NET mi impiccio :), comunque il codice dovrebbe essere facilmente
convertibile in VB.NET.
Per prima cosa, vediamo un po' di classi importanti dei namespace System.Reflection e System.Reflection.Emit:
  • System.Reflection.Assembly: Permette di gestire un assembly, ovvero un pezzo di codice completamente funzionante che può interagire con altrocodice del CLR (Common Language Runtime)
  • System.Reflection.AssemblyName: Descrive il nome di un assembly in modo univoco, può essere sia un simple name che uno strong name, di cui comunque parlerò dopo.
  • System.Reflection.Emit.AssemblyBuilder: Serve per definire e rappresentare un assembly dinamico
  • System.Reflection.Emit.ConstructorBuilder: Definisce il costruttore di una classe dinamica
  • System.Reflection.Emit.EnumBuilder: Definisce e descrive una enum
  • System.Reflection.Emit.EventBuilder: Permette di definire degli eventi per una classe
  • System.Reflection.Emit.FieldBuilder: Permette di definire i campi di un oggetto, questa classe non può essere istanziata
  • System.Reflection.Emit.ILGenerator: Il cuore :) Permette di generare codice IL
  • System.Reflection.Emit.LocalBuilder: Permette di definire una variabile locale in un metodo o in un costruttore
  • System.Reflection.Emit.MethodBuilder: Permette di definire un metodo in una classe
  • System.Reflection.Emit.ModuleBuilder: Permette di definire un modulo che conterrà l'assembly
  • System.Reflection.Emit.OpCodes: Contiene tutte le istruzioni del linguaggio IL
  • System.Reflection.Emit.ParameterBuilder: Permette di creare o associare dei parametri di un metodo
  • System.Reflection.Emit.PropertyBuilder: Permette di definire le proprietà di una classe
  • System.Reflection.Emit.TypeBuilder: Il cervello :) Permette di costruire classi a runtime

Bene, queste sono alcune delle classi più importanti, alcune le useremo altre no, però è importante conoscerle. Per prima cosa, dobbiamo pensare al programma
che vogliamo creare in IL, e se vogliamo fare un eseguibile o una dll, che in .NET sono la stessa identica cosa, l'unica differenza è che un eseguibile ha un entry point,
mentre una dll no. Direi che possiamo iniziare con una dll e un semplice programma che ha una funzione che accetta due interi e li somma, stampando nella console la loro somma e ritornandola come valore di ritorno (useremo degli int per semplificare le cose), il cui codice C# è il seguente:

using System;
namespace OurMath {
  public class MathOp
  {
    public MathOp()
    {
      Console.WriteLine("Costruttore");
    }
    public int Sum(int a, int b)
    {
      int s = a+b;
      Console.WriteLine("La somma dei numeri è {0}", s);
      return s;
    }
  }
}

Come vedete il codice è semplicissimo, ma stiamo appunto iniziando :) Anche con cose più complicate, il concetto è lo stesso, cambia solo il tempo
necessario a scriverlo :). Ok, adesso è tempo di programmare in IL :). Se state usando il Visual Studio.NET create un nuovo progetto Visual C#, e scegliete
Console Application, se state usando Mono, prendete un editor di testo e create un nuovo file .cs :), chiamate il progetto, il namespace e la classe come vi pare,
e poi nel Main mettete il seguente codice (il codice presenta gia dei commenti, ma dopo averlo riportato darò più spiegazioni):
public void Generate()
{
    //crea un nome per l'assembly, abbiamo scelto di usare un simple name invece di uno strong name
    //per motivi di semplicità, la differenza tra single name e strong name è che uno strong name
    //può contenere una chiave pubblica e una firma digitale per motivi di sicurezza, in più uno strong name
    //ha un nome unico garantito (cosa che non accade con i simple name)
    AssemblyName an = new AssemblyName();

    //la versione di questo assembly, mettetene una a caso, io ho scelto 1.0.0.0 :)
    an.Version = new Version(1,0,0,0);

    //il nome dell'assembly, con molta fantasia OurMath
    an.Name = "OurMath";

    //creiamo una classe AssemblyBuilder, che ci permetterà di costruire il nostro assembly

    //tra i parametri specifichiamo l'AssemblyName che vogliamo suare, e il tipo di accesso
    //mettendo AssemblyBuilderAccess.Save possiamo salvare l'assembly su disco una volta creato
    //gli altri tipi di accesso sono AssemblyBuilderAccess.Run che permette di usare l'assembly ma senza salvarlo su disco e
    //AssemblyBuilderAccess.RunAndSave che permette di salvarlo su disco e di usarlo subito
    AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Save);

    //creiamo il ModuleBuilder, che ci permetterà di aggiungere tipi al nostro assembly, il primo parametro è il nome del modulo

    //(deve essere minore di 260 caratteri), il secondo è il nome del file che vogliamo creare
    ModuleBuilder mb = ab.DefineDynamicModule("OurMath", "OurMath.dll");

    //ed eccoci al TypeBuilder :), quello che facciamo è creare una nuova classe,
    //usando il metodo DefineType della classe ModuleBuilder

    //il primo parametro è il nome del tiponel formato namespace.tipo, il secondo sono le proprietà del tipo,
    //in questo caso vogliamo una classe (TypeAttributes.Class) publica (TypeAttributes.Public)
    TypeBuilder tb = mb.DefineType("OurMath.MathOp", TypeAttributes.Class | TypeAttributes.Public);
   
    //questo è un passo obbligatorio, ovvero dobbiamo definire il costruttore della classe
    //il primo parametro indica l'accesso, scegliamo public per poter istanziare la classe, il secondo parametro
    //indica la calling convention che vogliamo usare (standard in questo caso), e il terzo indica gli argomenti del costruttore
    //in questo caso siccome non vogliamo niente, mettiamo new Type[0], ovvero un costruttore senza argomenti
    //se avremmo voluto un costruttore con 2 argomenti, uno di tipo string e uno di tipo int, avremmo fatto:
    //new Type[] { typeof(string), typeof(int) }
    ConstructorBuilder cb = tb.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[0]);

    //ogni metodo (il costruttore è un caso particolare di un metodo) ha un ILGenerator associato ad esso
    //che cos'è un ILGenerator? Semplice, è una classe che permette di generare il codice direttamente in IL,
    //senza impazzire con indirizzi o altro, ma pensa a tutto la classe, noi dobbiamo solo dirgli cosa generare

    ILGenerator ilgen = cb.GetILGenerator();

    //il codice del costruttore come prima cosa DEVE chiamare il costruttore della classe base, ovvero Object,
    //in C# tutte le classi derivano implicitamente da Object, questo è un passo obbligatorio
    //per chiamare il costruttore di Object, dobbiamo passargli un riferimento a chi lo sta chiamando
    //dovete sapere che in C#, come in C++, il primo argomento di qualsiasi funzione è sempre implicitamente
    //un riferimento all'oggetto che sta chiamando quella funzione (in altre parole è il puntatore this,
    //che in C# è un riferimento)
    //l'istruzione IL per pushare un argomento nello stack è Ldarg_x, dove x è l'argomento da pushare,
    //siccome vogliamo pushare this, facciamo Ldarg_0, usando il metodo Emit di ILGenerator,
    //e passandogli come argomento l'opcode da generare, OpCodes è una classe che contiene tutti gli opcode del linguaggio IL

    ilgen.Emit(OpCodes.Ldarg_0);

    //ora siccome vogliamo chiamare il costruttore, generiamo l'opcode Call, seguito dalla funzione che vogliamo chiamare

    //per prendere la funzione da chiamare, tramite il metodo GetConstructor della classe System.Type
    //(che viene ritornata dall'operatore typeof(oggetto)), prendiamo un riferimento ad un oggetto ConstructorInfo,
    //che è appunto ciò che ritorna GetConstructor
    //il costruttore che scegliamo è quello senza argomenti, che tra l'altro è l'unico presente nella classe Object

    ilgen.Emit(OpCodes.Call, typeof(Object).GetConstructor(new Type[0]));

    //qui usiamo un trucchetto :), non generiamo codice IL a mano, ma tramite ILGenerator ci facciamo generare il codice necessario
    //ad eseguire una call a System.Console.WriteLine, se non dobbiamo passare argomenti alla funzione,
    //questo è il modo più semplice per farlo, più avanti comunque vedremo come passare argomenti a System.Console.WriteLine

    ilgen.EmitWriteLine("Costruttore");

    //il lavoro del costruttore è finito, quindi generiamo l'opcode Ret, che esce dalla funzione e torna a chi l'ha chiamata

    //da notare che se è presente qualcosa sullo stack, Ret prende quel valore e lo ritorna alla funzione chiamante
    ilgen.Emit(OpCodes.Ret);

    //ora creiamo un metodo, come potete vedere, invece di usare ConstructorBuilder, usiamo MethodBuilder,
    //questo perché ora vogliamo creare un metodo e non un costruttore, il primo parametro è il nome del metodo,
    //in questo caso Sum, il secondo il tipo di accesso, anche in questo caso Public, il terzo rappresenta la
    //calling convention da utiilzzare, sempre standard, il quarto e il quinto definiscono rispettivamente
    //il valore di ritorno e gli argomenti del metodo, siccome avevamo definito Sum come: int Sum(int a, int b),
    //il valore di ritorno è un int (typeof(int)) e gli argomenti sono due int, siccome la funzione richiede un array di Type,
    //lo generiamo con new Type[] {typeof(int), typeof(int)}

    MethodBuilder sumMethod = tb.DefineMethod("Sum", MethodAttributes.Public, CallingConventions.Standard, typeof(int),
                                                new Type[] {typeof(int), typeof(int)});

    //prendiamo la classe ILGenerator di questo metodo
    ilgen = sumMethod.GetILGenerator();

    //tramite il metodo DeclareLocal di ILGenerator, definiamo una variabile locale, ovvero una variabile
    //che verrà allocata nello stack frame
di questa funzione, il tipo che abbiamo scelto è int,
    //infatti specifichiamo typeof(int)

    ilgen.DeclareLocal(typeof(int));

    //da come avevamo definito Sum, faceva la somma dei suoi argomenti (int a ed int b),
    //quindi quello che facciamo con queste istruzioni è generare l'opcode per Ldarg_1 e Ldarg_2,
    //che rispettivamente pushano il primo ed il secondo argomento nello stack

    //come penso avrete capito, si parte da 1 perché Ldarg_0 è per il riferimento this, che stavolta non ci serve
    ilgen.Emit(OpCodes.Ldarg_1);
    ilgen.Emit(OpCodes.Ldarg_2);

    //tramite l'istruzione Add aggiungiamo i due valori presenti sullo stack, quello che fa l'istruzione
    //Add è prendere i due valori nello stack, fare un pop di entrambi, sommarli, e poi pushare nello stack
    //il risultato della loro somma

    //ci sono due cose da notare, la prima che Add non fa controlli di overflow (Add_Ovf si), la seconda è che
    //Add fa la somma di due interi con segno, per fare la somma di due interi senza segno bisogna usare Add_Ovf_Un

    ilgen.Emit(OpCodes.Add);

    //come ho detto, Add pusha il risultato nello stack, quindi con Stloc_0, prendiamo il risultato dallo stack
    //e lo mettiamo nella prima variabile

    //locale definita
    ilgen.Emit(OpCodes.Stloc_0);

    //la stringa che stamperà il risultato della somma
    String resStr = "La somma dei numeri è {0}";

    //i parametri che passeremo alla funzione WriteLine, qui bisogna stare attenti,
    //infatti come sapete WriteLine accetta un numero di parametri variabile ed è qui che dobbiamo
    //scegliere quanti parametri passare, in questo caso ne passiamo uno string, e uno object
    //perché object e non int? Semplice, WriteLine non può sapere cosa gli passiamo, dato che nella stringa
    //noi diciamo solo dove mettere l'argomento tramite {0}, {1}, ecc..., ma non gli diciamo cosa mettere,
    //al contrario di ciò che avviene con la printf del C ad esempio dove gli diciamo cosa mettere,
    //quindi la WriteLine definisce la lista di argomenti come un array di tipo object (se prende più di 2 parametri, il primo di tipo string)

    //o come un singolo object (se prende solo due parametri, di cui il primo sempre di tipo string, come in questo caso)
    //prendendo un array (o un singolo elemento) di tipo object, siccome tutte le classi del C# derivano da object,
    //possiamo passare alla WriteLine qualsiasi classe

    Type[] wlParams = new Type[] {typeof(string), typeof(object)};

    //prendiamo le informazioni necessarie sul metodo WriteLine di System.Console, sempre tramite l'operatore typeof
    //passando wlParams come secondo argomento, diciam alla funzione che stiamo scegliendo
    //l'overload WriteLine(string format, object arg0)

    MethodInfo wlMInfo = typeof(Console).GetMethod("WriteLine", wlParams);

    //iniziamo a passare gli argomenti sullo stack, siccome il primo argomento di WriteLine è una stringa,
    //pushamo sullo stack la stringa resStr, tramite l'istruzione Ldstr

    ilgen.Emit(OpCodes.Ldstr, resStr);

    //pushamo nello stack la prima variabile locale, risultato della somma degli argomenti del metodo
    ilgen.Emit(OpCodes.Ldloc_0);

    //facciamo un boxing della variabile locale, questo perché nello stack noi non abbiamo un riferimento, ma un valore
    //a WriteLine dobbiamo passare un riferimento, quindi tramite il boxing convertiamo una variabile che contiene un valore,
    //in un riferimento, il secondo argomento dice il tipo della variabile che vogliamo convertire
    //l'istruzione Box prende il valore corrente nello stack, lo tira fuori, lo converte, e pusha nello stack il nuovo valore
    //così ora al posto della variabile locale, avremo un oggetto che rappresenta un riferimento a questa variabile
    ilgen.Emit(OpCodes.Box, typeof(int));

    //tramite il metodo EmitCall, diciamo a ILGenerator di generare l'opcode necessario per chiamare la nostra funzione
    //il terzo parametro indica gli argomenti opzionali da passare, per una funzione vararg, ma in questo caso non ce ne sono
    //quindi specifichiamo null
    ilgen.EmitCall(OpCodes.Call, wlMInfo, null);

    //pushamo il risultato della somma nello stack (la variabile locale NON è stata modificata dall'operazione di boxing)
    ilgen.Emit(OpCodes.Ldloc_0);

    //e ritorniamo alla funzione chiamante, come ho detto qualche riga più sopra,
    //se Ret trova un valore nello stack lo prende e lo ritorna alla funzione chiamante

    ilgen.Emit(OpCodes.Ret);

    //ci siamo!!! CreateType() crea la classe che abbiamo generato in modo definitivo
    tb.CreateType();

    //salviamo il file su disco, dandogli il nome OurMath.dll
    ab.Save("OurMath.dll");
}


Ok, ora mette quella funzione nel file C# che avete creato, e chiamatela dal Main, dopo aver eseguito il tutto (se usate Mono, fate: mono file.exe), otterrete nella stessa
cartella del file .exe un file .dll, che sarà appunto il nostro modulo. Ora per testare questo modulo potete (se usate il visual studio), fare Project->Add Reference, scegliere la
dll e poi utilizzarla come se fosse una classe normale del namespace. Però non c'è niente di dinamico in tutto ciò, quindi quello che faremo è caricare l'assembly a runtime :)
Il codice necessario è il seguente:

public void LoadAndExec()
{
    //carichiamo l'assembly con il metodo LoadFrom, che permette di specificare il file dal quale caricarlo
    Assembly a = Assembly.LoadFrom("OurMath.dll");

    //prendiamo un riferimento alla classe MathOp, che sarebbe la classe che vogliamo istanziare, come sempre bisogna specificare
    //il namespace oltre al nome della classe
    Type MathOp = a.GetType("OurMath.MathOp");

    //a questo punto prendiamo il metodo che vogliamo creare, usando la classe MethodInfo, come al solito usiamo il metodo GetMethod di type
    //per prendere il metodo desiderato
    MethodInfo Sum = MathOp.GetMethod("Sum");

    //istanziamo la classe MathOp tramite CreateInstance, questa funzione ritorna un Object, che dobbiamo usare quando chiamiamo il metodo
    Object obj = Activator.CreateInstance(MathOp);


      //e infatti tramite il metodo Invoke di MethodInfo chiamiamo il metodo Sum, notate che come primo oggetto viene
    //passata l'istanza della quale vogliamo che venga chiamato
il metodo, bisogna fare un cast ad int, perché Invoke ritorna un Object
    //che dobbiamo unboxare per poterlo usare, anche se dovendolo passare a WriteLine, avremmo potuto passarlo così com'è, considerando che
    //poi bisogna fare il box di nuovo, ansi, vi consiglio di evitare di far fare troppi box-unbox ai vostri programmi, quando potete evitatelo :)
    int res = (int)Sum.Invoke(obj, new object[] {4, 5});


    //stampiamo il risultato ottenuto dal metodo Sum di MathOp
    Console.WriteLine("{0}", res);
}

Come potete vedere, il codice è di una semplicità assurda, ma vi starete chiedendo a cosa può servire creare eseguibili a runtime, beh la risposta è semplice, il namespace
System.Reflection.Emit è stato creato apposta per permettere agli sviluppatori di creare il loro compilatore per .NET, rendendo questo ambiente di una flessibilità estrema,
praticamente è possibile fare qualsiasi cosa.

Ok questo tutorial è praticamente finito (so che è stato corto, ma forse ne scriverò un altro più lungo e con cose ancora più carine, sempre riguardanti i namespace System.Reflection
e System.Reflection.Emit), come ultima cosa però, voglio discutere un po' il modo in cui vengono trattati le variabili locali e gli argomenti delle funzioni in IL.
Dunque, dando un occhiata alla classe System.Reflection.Emit.OpCodes, noteremo i seguenti opcode per gestire le variabili locali (tra parentesi c'è l'opcode effettivamente generato
da ILGenerator):

Ldloc (ldloc)
Ldloca (ldloca)
Ldloca_S (ldloca.s)
Ldloc_0 (ldloc.0)
Ldloc_1 (ldloc.1)
Ldloc_2 (ldloc.2)
Ldloc_3 (ldloc.3)
Ldloc_S (ldloc.s)

Perché tanti opcode per fare la stessa cosa? Beh per rendere le cose più semplici al programma, ad esempio perché usare lo stesso opcode se tipo nel programma ci sono
solo 2 variabili? Ricordiamoci che stiamo su una virtual machine, su un processore codificare un opcode di 3 byte o 10 byte non cambia niente, su una vm può infierire, per questo
esistono diversi tipi di opcode a seconda di quello che bisogna fare, ad esempio Ldloc_0, Ldloc_1, Ldloc_2 ed Ldloc_3 servono per caricare sullo stack le primo 4 variabili locali,
e se ne abbiamo più di 4? Allora c'è Ldloca_S, che permette di caricare fino a 255 variabili, usando un intero a 8bit come indice, se ce ne sono più di 255? Beh allora si usa Ldloc, che
permette un massimo di 65535 (da 0 a 65534) variabili locali, ma ve ne serviranno mai così tante? Se come ad ogni essere umano al massimo ve ne serviranno 10-15, verrà usato l'opcode Ldloc_S (ldloc.s), che è più efficiente da decodificare, ovviamente Ldloc_0, Ldloc_1, Ldloc_2 ed Ldloc_3 sono ancora più efficienti,
quindi USATE quelli quando dovete caricare le prime variabili, ricordatevi sempre che state su una vm, per quanto ci sia il JIT o altro a velocizzare le cose, è una vm, non complicategli
il lavoro, gia di per se complicato :). Gli opcode Ldloca e Ldloca_S fanno la stessa cosa ma caricano l'indirizzo della variabile locale.
Il discorso è analogo per gli opcode relativi agli argomenti di una funzione, infatti anche la abbiamo Ldarg_0 Ldarg_1, Ldarg_2, Ldarg_3 ecc... .
Ovviamente il discorso vale pure per i rispettivi Stloc, Stloc_0, Stloc_1, ecc... (no, non vale per Starg_0, Starg_1, ecc... perché non esistono :), ma abbiamo solo Starg ed Starg_S,
una cosa importante, in caso di una funzione vararg, Starg_S può essere usata solo per gli argomenti "fissi", non per la parte variabile).

Beh, per ora è tutto, alla prossima!! (che spero ci sarà :))


Note finali


Beh spero che questo tutorial vi sia piaciuto, forse un po' troppo corto, ma odio i tutorial troppo lunghi (vi starete chiedendo perché quello su safedisc2 è enorme, ma sono due argomenti diversi, qua si parla di programmazione :)), come come ho detto durante il tutorial, forse ne scriverò qualcun altro sempre su IL, System.Reflection e System.Reflection.Emit .
Comunque, spero che questo tutorial vi abbia fatto comprendere l'ENORME potenza di .NET, se non l'avete compreso, beh rileggetevi il tutorial 4-5 volte :), e poi trovate un ambiente
in cui potete fare qualcosa del genere in modo così semplice :) (sto scherzando, non è una sfida eh :))

Passiamo ai ringraziamenti e saluti di rito :)
Ringraziamenti:
Microsoft: per aver creato .NET, un capolavoro assoluto, forse la cosa migliore che hanno fatto negli ultimi anni :) (molti diranno l'unica cosa buona che ha fatto negli ultimi anni :))
Mono: per aver permesso la diffusione di .NET su piattaforme diverse da Windows.
Winamp: per aver suonato ininterrottamente dalla mattina alla sera le canzoni che ascolto senza lamentarsi
I gruppi che ascolto: per creare la migliore musica esistente nell'universo, quindi un ringraziamento a: Iron Maiden, Iced Earth, Manowar, Virgin Steele, Helloween, Opeth, My Dying Bride,
Agalloch, Dio, Dark Tranquillity, Running Wild, Atheist, Cynic, Death e basta sennò que poi dice che facevo prima ad incollare la track list del winamp :)
Be Inc. : per aver creato il miglior sistema operativo (BeOS) della storia, peccato sia finita male :(
Haiku (OpenBeOS): per lo sforzo che stanno facendo per ricreare BeOS, avanti così!!!! :)
Il mio monitor: perché dopo 10 anni onorevoli di servizio ancora riesce a reggere una risoluzione di 1280x1024x32 a 60hz senza esplodere (beh più o meno), e i miei occhi che ancora
lo sopportano :)
Edgar Allan Poe: beh, il mio preferito :)
H.P. Lovecraft: per non avermi fatto dormire la notte con i suoi racconti :), soprattutto Dagon e The Nameless City
Frank Herbert: per avere creato il più bel romanzo della storia

Veniamo ai saluti:
#gameprog-ita: in particolare Rhymes che mi ha fatto conoscere .NET e Python, ovvero due capolavori degli anni '90-'00, elevator2 che risponde sempre alle mie insistenti domande sul 3D, Lexiw per avermi fatto conoscere gli Agalloch, e tutti gli altri :)
#crack-it: beh se mi metto a scrivere tutti quanti qua non finisco più, saluto tutti quanti in particolare que che pubblicherà questo tutorial :)
#asm: tutti quanti pure qua :)
#programmazione: come sopra :)
#beos: tutti chi? siamo in 2 :)
#c#: ecco il tutorial che avevo promesso :)

Beh con questo è tutto (si vabbe l'avevo detto pure sopra, ma siamo in un contesto diverso :)).
Ciao!
                                                                                                                                                                                    Quake2

Disclaimer


Non mi assumo nessuna responsabilità in caso il codice qui riportato arrechi danni permamenti al vostro compilatore, ovvero se inizia a bestemmiare, prendervi in giro, rifituarsi di compilare i vostri file, modificarvi il codice per dispetto, crearvi i bug per farvi impazzire, non è colpa mia. Ricordate che i compilatori non vanno maltrattati, ma vanno trattati con rispetto, non fategli compilare milioni di righe di codice al giorno, imparate a compilarvi i vostri programmi a mano, è ora di smetterla con questo sfruttamento.
Ricordate che il codice non va copiato o rubato, ma va scritto, quindi che lo leggete a fare sto tutorial? :)

Noi programmiamo al solo scopo di perdere tempo e peggiorare la nostra vista.