Merry Christmas 2011

Introducción al lenguaje ensamblador IL de .NET [Parte 4]

[ <-- Introducción al lenguaje ensamblador IL de .NET [Parte 3] ]

Definiendo Métodos

Una vez hemos aprendido acerca de las condiciones (saltos o ramificaciones), bucles y variables, es hora de ver en profundidad cómo son declarados los métodos en ILAsm. La forma de declarar métodos en ILAsm es muy parecida a la usada para hacerlo en lenguajes como C# o C++, excepto algunos pequeños detalles. Bien, veamos entonces un código de ejemplo para analizar en detalle qué hemos hecho:

//Metodos.il
//Pruebas creando métodos

.assembly extern mscorlib {}

.assembly Metodos
{
    .ver 1:0:1:0
}
.module Metodos.exe

.method static void main() cil managed
{
    .maxstack 2
    .entrypoint

    ldc.i4 10
    ldc.i4 20
    call int32 RealizarSuma(int32, int32)
    call void EscribirSuma(int32)
    ret
}

.method public static int32 RealizarSuma(int32, int32) cil managed
{
    .maxstack 2    

    ldarg.0
    ldarg.1
    add

    ret
}
.method public static void EscribirSuma(int32) cil managed
{
    .maxstack 2
    ldstr "El resultado es: "
    call void [mscorlib]System.Console::Write(string)

    ldarg.0
    call void [mscorlib]System.Console::Write(int32)

    ret
}
Figura 1.8 – Metodos.il, Definir y llamar a nuestros propios métodos

Este simple programa sumará dos números (predefinidos, para simplificar el código) y mostrará el resultado. Hemos definido dos métodos aquí. Toma nota que ambos métodos son estáticos por lo que podemos acceder a ellos directamente sin crear ninguna instancia de ellos. Primero hemos cargado dos números al stack y llamado al primer método al que hemos denominado ‘RealizarSuma‘, el cual estará esperando dos valores int32 al principio del stack. Si miramos al cuerpo del método (RealizarSuma), veremos que su declaración luce igual que el método main tal y como hemos visto en ocasiones anteriores, de nuevo hemos definido la directiva .maxstack pero date cuenta que esta vez no hemos incluido la directiva .entrypoint ya que un programa únicamente puede tener un punto de entrada y, en el caso de arriba, hemos declarado ese punto en el método main (como se suele hacer). Las instrucciones ldarg.0 y ldarg.1 cargan los valores de los argumentos del método al stack. Entonces simplemente hemos usado la instrucción de suma (add) y por ultimo el método finaliza retornando al punto donde fue invocado. Date cuenta de que el método devuelve un valor de tipo int32, por lo que, ¿qué valor habrá devuelto?. Por supuesto, ese valor fue devuelto y ahora está disponible en el stack de evaluación una vez terminó el método su trabajo. A partir de aquí el método main vuelve a tener el control para llamar esta vez al método ‘EscribirSuma‘. Este método requiere también de un parámetro de tipo int32 y, ya que nuestra anterior llamada a ‘RealizarSuma‘ ya nos devolvió ese valor, ‘EscribirSuma‘ lo utilizará como parámetro. Una vez llamado al nuevo método, este escribirá en pantalla el resultado de la suma usando ldarg.0 para cargar el argumento y usar su valor.

El código analizado demuestra que crear y usar métodos no es difícil en ILAsm y bueno, de momento lo es, pero los métodos también pueden recibir valores por referencia, por lo que en la siguiente sección hablaremos de ello.

 

Pasando variables por Referencia

ILAsm soporta también el paso de parámetros por referencia ya que lo soportan también lenguajes de alto nivel en .NET. Cuando pasamos cualquier variable por referencia, no le pasamos su valor sino su dirección de memoria (puntero) para que el método pueda trabajar con ella directamente. Veamos un ejemplo de como funciona:

.method static void main() cil managed
{
    .maxstack 2
    .entrypoint
    .locals init (int32, int32)

    ldc.i4 10
    stloc.0
    ldc.i4 20
    stloc.1

    //Cargamos la dirección de memoria de la 1era variable
    ldloca 0
    //Cargamos el valor de la 2ª variable
    ldloc.1
    call void RealizarSuma(int32 &, int32)
    ldloc.0
    //Cargamos la 1era variable de nuevo, pero esta vez su valor
    call void [mscorlib]System.Console::WriteLine(int32)
    ret 
} 
.method    public static void RealizarSuma(int32 &, int32) cil managed 
{ 
    .maxstack 2 
    .locals    init (int32)    
    //Buscamos en memoria y copiamos su valor 
    ldarg.0 
    ldind.i4 
    stloc.0
    ldloc.0
    //Realizamos la suma
    ldarg.1
    add
    stloc.0

    ldarg.0
    ldloc.0
    stind.i4

    ret
}
Figura 1.9 – VariablesPorRef.il, Pasando variables por referencia

El punto interesante en el código es el uso de algunas nuevas instrucciones como ldloca, la cual carga la dirección de memoria de una variable, en vez de su valor, al stack. En el método principal (main), hemos declarado dos variables (locales) y asignado unos valores (10 y 20 respectivamente). Entonces hemos cargado la dirección de memoria de la primera variable en la memoria y el valor de la segunda, para luego invocar el método ‘RealizarSuma‘. Notarás que se ha usado ‘&’ con el primer parametro int32, lo cual significa que el valor contendrá su referencia en memoria y no su valor para poder pasar tal variable por referencia. De la misma manera, en la definición del método ‘RealizarSuma’ encontraremos también el mismo símbolo ‘&’. Por lo que, tenemos una variable que es pasada por referencia y una segunda variable por valor. Ahora el problema está en que tenemos que acceder a esa dirección de memoria para poder acceder a su valor y modificarlo si fuese necesario. Para solucionar esto, hemos almacenado al stack el primer argumento (el cual contiene la dirección de memoria) y utilizado la instrucción ldind.i4, la cual carga el valor de un entero (32 bit) al stack utilizando una dirección en memoria dada. Hemos almacenado ese valor en una variable local para que podamos reutilizarla fácilmente (o tendríamos que hacer este paso varias veces). Entonces hemos cargado esa variable local y el segundo argumento (el cual ya va por valor) al stack, sumados ambos y guardados en la misma variable local. Por ultimo tenemos algo interesante, y es modificar el valor de esa variable pasada por referencia con el valor del resultado. Para ello hemos usado la instrucción ldind.i4, la cual modifica el valor de una variable en la dirección de memoria indicada en el stack (es decir, lee qué dirección de memoria tiene que acceder y escribe un valor). Una vez hecho esto, para testear que todo ha ido bien, no devolveremos el resultado con el método, en vez de esto, ya que hemos almacenado el resultado en la variable pasada por referencia, usaremos en el método main esa misma variable para escribir el resultado utilizando el método ‘WriteLine‘.

Esta es la forma en la que en ILAsm se utilizan variables por referencia. Hasta aquí, hemos visto formas de declarar variables, condiciones, bucles y por último métodos (con ambos tipos de parámetros). En la próxima parte entraremos de lleno con los espacios de nombre (namespaces a partir de ahora) y las clases, es decir, con la parte orientada a objetos de ILAsm.

¡Hasta la próxima entrega!

Introducción al lenguaje ensamblador IL de .NET [Parte 3]

[ <-- Introducción al lenguaje ensamblador IL de .NET [Parte 2] ]

Declaración de Variables

Las variables es uno de los pilares de cualquier lenguaje de programación y por tanto, ILAsm nos permite declararlas y usarlas. Aunque no es una tarea tan sencilla como en los lenguajes de alto nivel, usando la directiva .locals podremos definir variables. Esta directiva debería normalmente usarse al principio de cualquier método, aunque puedes poner la declaración en cualquier sitio, obviamente, antes de usarlas. Aquí tenemos un ejemplo en el que declaramos una variable, guardamos un valor en ella y luego la usamos para imprimir su valor.

.locals init (int32, string)
ldc.i4 34
stloc.0
ldstr "Texto de ejemplo para probar la variable local"
stloc.1
ldloc.0
call void [mscorlib]System.Console::WriteLine(int32)
ldloc.1
call void [mscorlib]System.Console::WriteLine(string)
Figura 1.5 – Variables Locales

Hemos declarado dos variables usando la directiva .locals, una del tipo int32 y la otra del tipo string. Luego hemos cargado el número 34 del tipo int32 en memoria y se la hemos asignado a la variable local 0, la cual es nuestra primera variable local. Toma nota que en ILAsm los índices empiezan por cero. Luego hemos cargado en memoria una segunda variable y se la hemos asignado a esa segunda variable local. Finalmente hemos mostrado por pantalla ambas variables. Os preguntareis para qué sirve la instrucción ldloc, pues bien, puede ser usada para cargar cualquier tipo de dato en memoria (enteros, coma flotante, objetos, etc…).

En este caso no hemos usado nombres para nuestras variables. Ya que son locales y no tenemos intención de utilizarlas fuera de nuestro método. Eso no significa que no se puedan declarar variables por su nombre. Para declarar esas variables, simplemente hazlo al igual que en otro lenguaje de alto nivel como por ejemplo .locals init (int32 x, int32 y). Después podrás leer o guardar valores en esas variables usando las mismas instrucciones, pero usando sus nombres como ‘stloc x‘ y ‘ldloc y‘. Aun así, aunque tengan sus propios nombres, podrás seguir accediendo a ellas mediante su índice. Nota: A lo largo de todos los tutoriales usaré variables sin nombre, para que te acostumbres a pensar en qué indice están guardadas tus variables.

Bien, ahora tienes una idea de cómo manipular variables y stacks. Por favor revisa los códigos si aún no tienes muy claro algún concepto y que, a partir de ahora, estaremos manipulando ambos con bastante frecuencia. Por ello es necesario que te familiarices con este procedimiento (definir/inicializar, guardar y leer sus valores).

 

Condiciones en ILAsm

Las condiciones son otros de los pilares de cualquier lenguaje de programación. En los lenguajes de bajo nivel, como cualquier otro lenguaje ensamblador, las condiciones están hechas usando saltos (o ramificaciones). Tomemos un ejemplo de esto:

br SaltarAqui //También puedes usar br.s si lo deseas

//El código que haya aquí será saltado después de la instrucción anterior

SaltarAqui:
//En cambio, estas instrucciones serán ejecutadas

Compara esa instrucción con la instrucción ‘goto‘ de un lenguaje de alto nivel. ¿ Funciona igual verdad ? La instrucción saltará el punto de ejecución a la etiqueta indicada. Aquí, en ILAsm, en vez de usar goto usaremos br. Es instrucción también puede ser usada utilizando br.s si estás seguro de que el destino está entre los -128 y +127 bytes anteriores-posteriores a este, ya que usará un int8 en vez de un int32 para el offset. El ejemplo de arriba ha sido una ramificación (salto) incondicional ya que no hubo ninguna condición anterior de la instrucción que indicase lo contrario. Tomemos pues un ejemplo en el que sí contemplaremos un ejemplo con un salto condicional:

//Saltando.il
.method static void main() cil managed
{
    .maxstack 2
    .entrypoint
    //Cogemos el primer valor del usuario
    ldstr "Introduce el Primer número: "
    call void [mscorlib]System.Console::WriteLine (string)

    call string [mscorlib]System.Console::ReadLine ()
    call int32 [mscorlib]System.Int32::Parse(string)

    //Cogemos el segundo valor del usuario
    ldstr "Introduce el Segundo número: "
    call void [mscorlib]System.Console::WriteLine (string)

    call string [mscorlib]System.Console::ReadLine ()
    call int32 [mscorlib]System.Int32::Parse(string)

    ble Menor
        ldstr "El segundo número es menor que el primero."
        call void [mscorlib]System.Console::WriteLine (string)

    br Salida

Menor:
    ldstr "El primer número es menor que el segundo."
    call void [mscorlib]System.Console::WriteLine (string)
Salida:
    ret
}
Figura 1.6 – Saltando.il (solamente el método main)

El código de arriba toma dos valores del usuario y entonces consulta cual de ellos es el más pequeño. La instrucción la cual requiere nuestra atención es “ble Menor“, la cual indica al JIT a mirar si el primer valor en el stack es menor o igual que el segundo, si así es el caso, entonces saltará a la etiqueta ‘Menor‘. En el caso contrario la ejecución del programa continuará por la siguiente instrucción, sin realizar el salto, sin embargo luego se encontrará con un salto incondicional, ya que si no estuviera ahí, el programa continuaría y ejecutaría las instrucciones contenidas en la etiqueta ‘Menor‘. Por ello hemos creado una nueva etiqueta llamada ‘Salida‘ y hemos llamado a la instrucción ‘br Salida‘ la cual obligará al programa a saltar a la etiqueta ‘Salida‘ y ejecutar la instrucción ret, para finalizar el método.

Las otras instrucciones condicionales que podemos encontrar son beq (==), bne (!= ), bge (>= ), bgt (>), ble (<= ), blt (<), además de brfalse y brtrue (para comprobar si el primer elemento del stack es cero o no respectivamente). Puedes usar cualquiera de ellas para ejecutar alguna parte de tu código y evitar (saltar) otro. Como dije antes, no tenemos la facilidad que tenemos en los lenguajes de alto nivel, por ello todo debe de ser realizado por ti mismo si tienes pensado escribir o modificar código en ILAsm.

 

Bucles en ILAsm

Los bucles son otra parte fundamental de un lenguaje de programación. Un bucle no es otra cosa que la repetición del mismo bloque de código una vez y otra vez. Para ello la repetición del bloque dependerá de una variable a la que llamaremos índice del bucle. En el siguiente fragmento de código mostraré un ejemplo de cómo funciona un bucle básico, así que dedicale un poco de tiempo a entender cómo funcionan los bucles:

 .method static void main() cil managed
{
    //Definimos dos variables locales
    variables .locals init (int32, int32)
    .maxstack 2
    .entrypoint

    ldc.i4 4
    stloc.0        //Tope, el límite del bucle. Haremos 5 repeticiones
    ldc.i4 0
    stloc.1        //Inicializamos el índice a cero 

Comienzo:
    //Comprobamos si el contador del índice ha superado el límite
    ldloc.1
    ldloc.0
    bgt Salida //Si la segunda variable supera a la primera, entonces saltamos

    ldloc.1
    call void [mscorlib]System.Console::WriteLine(int32)

    //Incrementamos el contador
    ldc.i4 1
    ldloc.1
    add
    stloc.1
    br Comienzo
Salida:
    ret
}
Figura 1.7 – Bucles.il (solamente el método main)

En C#, este código se vería de esta manera:

for (temp = 0; temp < 5; temp++)
            System.Console.WriteLine (temp);

Bien, examinemos el código. Primero, hemos declarado dos variables locales e inicializado la primera con un valor de 4 y la segunda con valor 0. El bucle real empieza con la etiqueta ‘Comienzo‘, desde donde primero comprobaremos si el contador del indice del bucle (la variable nº2, ldloc 1) sobrepasa el límite impuesto por nuestra primera variable (ldloc 0), si ese fuera el caso, el programa saltaría a la etiqueta ‘Salida‘ con lo cual nuestro programa terminaría. Si no es el caso, entonces el valor será escrito por pantalla e incrementaremos ese valor para luego saltar de nuevo a la etiqueta ‘Comienzo‘ para comprobar de nuevo si el contador ha excedido o no el límite impuesto. Así funcionan los bucles en ILAsm (y en la mayoría de los otros lenguajes ensamblador).

En la próxima entrega veremos cómo definir nuestros propios métodos y utilizarlos, además de ver los dos tipos de parámetros soportados. Por valor y por referencia.

 

[ Introducción al lenguaje ensamblador IL de .NET [Parte 4] –> ]

Introducción al lenguaje ensamblador IL de .NET [Parte 2]

[ <-- Introducción al lenguaje ensamblador IL de .NET [Parte 1] ]

El Stack de evaluación

El stack de evaluación puede ser considerado como un stack normal como otro cualquiera, sin embargo este stack es únicamente usado para guardar información justo antes de ejecutar una instrucción. Sabemos que la información guardada en memoria cuando podemos tener acceso a realizar alguna operación sobre ella. Lo mismo pasa si movemos esa información a los registros en un lenguaje ensamblador antes de invocar alguna instrucción o interrupción. De la misma manera que tenemos que mover la información (en el caso del primer tutorial fue una cadena de texto) al stack antes de procesarla (para mostrar esa cadena en pantalla). Al principio de nuestro método ‘main’ (véase figura 1.1 en la anterior entrada), nos damos cuenta que tuvimos que almacenar información a lo largo de nuestro método. Por ello usamos la directiva .maxstack, si hubiéramos indicado la directiva con un número tres (.maxstack 3), entonces el JIT hubiera creado un stack con 3 celdas para su uso posterior en el método. Ojo, esto no significa que únicamente podemos cargar 3 valores en la vida de nuestro método, simplemente significa que podemos albergar 3 valores a la vez como máximo. Una vez que el método finaliza, el stack y sus valores se eliminan. Así es como funciona el recolector de Basura en .NET. Además, no hay limitación en cuanto al tipo de dato que podemos usar para almacenar en el stack. Podremos mover cualquier tipo de dato (cadenas, enteros, objetos, etc) al stack siempre que queramos.

Tomemos otro ejemplo que nos aclarará el concepto del stack de evaluación:

//Sumar.il
//Suma dos números

.assembly extern mscorlib {}

.assembly Sumar
{
    .ver 1:0:1:0
}
.module Sumar.exe

.method static void main() cil managed
{
    .maxstack 2
    .entrypoint

    ldstr "La suma de 50 y 30 es = "
    call void [mscorlib]System.Console::Write (string)

    ldc.i4.s 50
    ldc.i4 30
    add
    call void [mscorlib]System.Console::Write (int32)
    ret
}
Figura 1.3 – Sumar.il, sumando dos números predefinidos

El trozo de código encima de nuestro método main es el mismo que en el ejemplo anterior, el cual ya explicamos. Únicamente ha cambiado el nombre del módulo. En este código lo importante a discutir es el .maxstack 2, el cual indica al JIT a preparar suficiente espacio en memoria para que podamos guardar dos valores. Entonces cargamos la cadena en el stack y la imprimimos. Lo próximo que hacemos es cargar dos enteros en memoria a la vez (usando las instrucciones ldc.i4.s y ldc.i4), usar la instrucción de suma y finalmente mostrar por pantalla el resultado entero que hemos obtenido. La instrucción de suma usada (add) tomará del stack de evaluación dos valores (siempre que sean encontrados), los sumará y guardará el resultado al comienzo del stack (es decir, un ‘push’). Justo después de la instrucción de suma, encontramos otra instrucción llamada Write que escribirá algo por pantalla. Este método requiere que haya un valor guardado en el stack. En este caso requerirá que sea del tipo entero. Si lo encuentra, entonces escribirá su valor por pantalla o en caso contrario levantará un error.

No os liéis con ldc.i4.s y ldc.i4, ambos representan un tipo de dato entero (integer), pero el primero es un único byte y el segundo son cuatro bytes.

Espero que hayáis entendido la forma de usar el stack de evaluación y como funciona. Ahora veremos los tipos de datos del lenguaje ILAsm como una parte fundamental del mismo.

Tipos de Datos ILAsm

 

Al igual que cuando aprendes cualquier otro lenguaje, hay que hablar sobre sus tipos de datos soportados. Este caso es igual. Tómate un tiempo para observar la tabla de abajo (figura 1.4) para aprender cuales son los tipos de datos más comunes en este lenguaje, pero antes, me gustaría añadir una cosa. No hay consistencia en la definición de los tipos de datos en los diferentes lenguajes de .NET. Por ejemplo para indicar un tipo de dato entero (32 bit) en VB.NET usamos ‘Integer‘ pero en C# y en VC++ no es así, aunque luego ambos casos se representen usando System.Int32. Además, necesitamos tener en cuenta si es un tipo básico con la especificación del Lenguaje común (CLS, Common Language Specification) o no. Como por ejemplo el tipo de dato UInt32 no está reconocido por VB.NET por ello no es básico CLS.

Bien, empecemos por aprender la tabla que nos proporciona los nuevos nombres para los tipos de datos de ILAsm:

Nombre en IL Tipo Base en .NET Significado (Descripción) Básico CLS
Void Sin valor, solo usado como tipo de retorno No
Bool System.Boolean Booleano No
Char System.Char Carácter (16-bit unicode) No
int8 System.SByte Entero 1-byte (con signo) No
int16 System.Int16 Entero 2-byte (con signo) No
int32 System.Int32 Entero 4-byte (con signo) Si
int64 System.64 Entero 8-byte (con signo) Si
native int System.IntPtr Entero (con signo) Si
unsigned int8 System.Byte Entero 1-byte (sin signo) Si
unsigned int16 System.UInt16 Entero 2-byte (sin signo) Si
unsigned int32 System.UInt32 Entero 4-byte (sin signo) No
unsigned int64 System.UInt64 Entero 8-byte (sin signo) Si
native unsigned int System.UIntPtr Entero (sin signo) Si
Float32 System.Single Coma flotante de 4-byte No
Float64 System.Double Coma flotante de 8-byte No
object System.Object Tipo objeto Si
& Puntero administrado Si
* System.IntPtr Puntero no administrado Si
typedef System.Typed Reference Tipo especial que contiene datos y explícitamente indica su tipo Si
Array System.Array Array Si
string System.String Cadena de texto Si
Figura 1.4 – Tipos de Datos en ILAsm

También tenemos abreviaciones para los tipos de datos en ILAsm como por ejemplo .i4, .i4.s, .u4, etc como usamos en el ejemplo anterior. Los tipos de datos mencionados en la tabla de arriba muestran los tipos de datos son reconocidos en ILAsm y además menciona cuales son tipos básicos CLS y cuales no. Entonces, teniendo en mente esa información, podremos llamar a cualquier función como esta:

call int32 FuncionEjemplo (string, int32, float64) 

Lo cual significa, que la función ‘FuncionEjemplo‘ devuelve un valor del tipo int32 (System.Int32) y toma tres valores como parámetros que son del tipo string (System.String), int32 (System.Int32) y float64 (System.Double) respectivamente. Toma nota que estos son datos básicos en el CLR y en ILAsm. Cuando estés interesado en tratar tipos de datos que no son básicos (definidos por el usuario) entonces el código podrá lucir como esto:

//Código en C#
ColorTranslator.ToHtml(Color);

//Código en ILAsm
call instance string [System.Drawing]System.Drawing.ColorTranslator::ToHtml(valuetype [System.Drawing]System.Drawing.Color)

Toma nota que hemos definido explícitamente los tipos de dato en los parámetros y incluyendo además el espacio de nombre (namespace) donde el tipo de dato reside y una palabra clave que indicará al JIT que estamos referenciando a un tipo de dato no básico (valuetype).

Esto lo dejaré más claro en la próxima entrega del tutorial, donde trataremos más intensamente con los tipos de datos. También veremos una de las bases de cualquier lenguaje: Condiciones y bucles.

 

[ Introducción al lenguaje ensamblador IL de .NET [Parte 3] –> ]

Introducción al lenguaje ensamblador IL de .NET [Parte 1]

Introducción a los Tutoriales

Lo que pretendo con esto es realizar una serie de tutoriales consecutivos sobre este tema, por lo que los próximos tutoriales se basaran en los anteriores. No pretendo que lleguéis a ser maestros escribiendo código ILAsm, más bien que obtengáis la base necesaria para que podáis desenvolveros sin mucha dificultad con el y sigáis aprendiendo por vuestra cuenta.

Con estos tutoriales aprenderéis la base del lenguaje ensamblador IL (tambien llamado CIL, o Common Intermediate Language) para que podáis debuguear código en bajo nivel .NET ya compilado (desde cualquier lenguaje de alto nivel, como VB .NET, C# y otros) y así corregir fallos, añadir, quitar o cambiar características y más. Es decir, podrás editar libremente cualquier programa o librería una vez compilada de .NET. Además, usando estos conocimientos, tendrás la base para poder empezar a escribir tu propio compilador para crear un nuevo lenguaje .NET (aunque no vamos a llegar tan lejos).

Siempre que compilas tu código en .NET, independientemente del lenguaje que estés usando, será convertido al Lenguaje Intermedio (IL, Intermediate Language) el cual es también conocido como Lenguaje Intermedio de Microsoft (MSIL) o Lenguaje Intermedio Común (CIL). Puedes pensar que IL es como el Byte Code generado por el lenguaje Java. Si estás interesado en entender cómo .NET funciona con sus tipos de datos y cómo el código que escribes es convertido a código IL, entonces estos tutoriales sobre ILAsm te van a resultar muy útiles dándote grandes ventajas.

Entre estas ventajas encontramos entender qué es lo que devuelve el compilador .NET cuando compila, por lo tanto, podrás examinar el codigo traducido por el compilador y realizar los cambios que sean necesarios (aunque no es necesario en la mayoría de los casos). Además, puedes cambiar el código IL para hacer cambios que tu lenguaje de alto nivel no permitiría para aumentar el rendimiento de tu código, y también te ayudará a debuggear tu código en bajo nivel. Y como dije antes, si estás planeando escribir un compilador para .NET, entonces necesitarás entender el código ILAsm.

IL es el formato binario el cual el compilador devuelve una vez termina su tarea, es decir, no puede ser leido por humanos, pero al igual que otros binarios (ejecutables) tienen su lenguaje ensamblador, IL tiene tambien el suyo propio conocido como lenguaje ensamblador IL (ILAsm). El lenguaje ensamblador IL (ILAsm a partir de ahora) tiene sus propias instrucciones de la misma manera que cualquier otro lenguaje ensamblador nativo pueda tener. Por ejemplo, para sumar dos números, tienes la instrucción ‘add’, para restar dos números, tienes la instrucción ‘sub’, etc. Es obvio que el compilador en tiempo de ejecución de .NET (el JIT) no puede ejecutar código ILAsm directamente. Si tu tienes escrito código en ILAsm entonces tendrás primero que compilarlo a código IL y entonces el JIT podrá ejecutar el código.

NOTA: Por favor ten en cuenta que IL y ILAsm son cosas distintas. Siempre que hablaremos sobre IL, nos referiremos al código binario devuelto por el compilador de .NET mientras que cuando referenciemos a ILAsm, nos referiremos al lenguaje ensamblador de este código el cual no está en su forma binaria.

NOTA2: Ten en cuenta que espero que estés bastante familiarizado con .NET (incluyendo algún lenguaje de alto nivel como C# o VB.NET). En estos tutoriales no profundizaremos en todos los detalles, sino únicamente en aquellos que realmente necesitan ser explicados. Si algo te confunde, por favor házmelo saber para aclarártelo.

 

Introducción al Lenguaje Ensamblador IL

Bien, empecemos con el propósito de estos tutoriales, la introducción al lenguaje ensamblador IL. Ten en cuenta que puedes escribir código en ILAsm en cualquier editor de texto (como notepad) y luego usar la linea de comandos (cmd) para compilar el código. El compilador en este caso es el ILAsm.exe, el cual está distribuido junto con el SDK de .NET Framework. ILAsm es un lenguaje algo duro de aprender para aquellos que siempre han programado en lenguajes de alto nivel, pero para aquellos que han programado en C o en C++ no les debería resultar demasiado complicado. En ILAsm necesitamos hacer todas las cosas manualmente, como añadir un valor al stack, administrar la memoria, etc. Piensa en ILAsm de la misma manera que cualquier otro lenguaje ensamblador con la diferencia de que en otro lenguaje estarás tratando con ejecutables nativos para Windows y con este lenguaje (ILAsm) estarás tratando con ejecutables .NET y además, este lenguaje es un poco más fácil (que otros lenguajes ensamblador) y además está orientado a objetos.

Empecemos con este lenguaje con nuestro primer programa de ejemplo que escribirá una frase en pantalla (en la consola). Es una tradición que en el comienzo de cada lenguaje se use un “hola mundo” así que vamos a hacer lo mismo:

//Ejemplo.IL 
//Un simple programa que imprime una cadena de texto por consola

.assembly extern mscorlib {}

.assembly Ejemplo
{
    .ver 1:0:1:0
}
.module ejemplo.exe

.method static void main() cil managed
{
    .maxstack 1
    .entrypoint

    ldstr "Hola mundo! Also, The Game !!!"

    call void [mscorlib]System.Console::WriteLine (string)
    ret
}
 Figura 1.1 – Un programa de ejemplo en ILAsm

 

Una vez escrito el codigo de arriba (o figura 1.1) en un simple editor de texto como notepad, guárdalo como “Ejemplo.il“. Ahora realizaremos nuestra primera compilación y ejecutaremos nuestro codigo. Una vez hecho esto entraremos en detalles y explicaré qué es lo que hace cada instrucción. Para compilar el código, escribe lo siguiente en una ventana de comandos (cmd):

 Figura 1.2 – Salida del programa de ejemplo. Aqui puedes ver el comando usado para compilar el código

 

ILAsm.exe es una herramienta utilizada por linea de comando la cual está incluida con el Framework SDK de .NET y puede ser encontrada en la carpeta “<carpeta_windows>\Microsoft.NET\Framework\<version>“. Si quieres puedes incluir esta ruta a la variable de entorno Path de tu SO (por ello no he tenido que escribir la ruta completa en la figura 1.2 como podéis ver). Cuando has terminado de compilar tu archivo .IL, tendrás listo tu ejecutable (.exe) con el mismo nombre que tu archivo .IL. Puedes especificar el nombre de salida utilizando “/OutPut=<filename>” (sin comillas). Para ejecutar tu recien compilado programa, simplemente escribe el nombre de tu progama y presiona Enter. La salida del programa aparecerá ante ti. Bien, ahora nos tomaremos un tiempo para entender que es lo que hemos escrito en el código. Ten en mente que me estoy refiriendo al código de la figura 1.1

  • Las dos primeras lineas (que empiezan por //) son comentarios. En ILAsm puedes escribir comentarios de la misma manera que en C# o C++. Para comentar múltiples líneas o un trozo de línea, puedes usar /* … */
  • Lo siguiente que hemos hecho a sido indicar al compilador que importe la librería llamada mscorlib (.assembly extern mscorlib {}). En ILAsm, cada linea comenzada por un punto (.) indica que la línea es una instrucción especial o una directiva. Entonces la directiva .assembly que tenemos aquí dice que vamos a usar una librería externa (la cual no está escrita por nosotros en este código, está pre-compilada).
  • La siguiente directiva .assembly define la información de nuestro archivo (.assembly Ejemplo ….). En nuestro caso, hemos incluido “Ejemplo” como el nombre del ensamblado y entre llaves hemos incluido informacion sobre el ensamblado de salida (nuestro .exe), en este caso hemos incluido la información de versión de nuestro programa. Podemos incluir más información sobre el ensamblado en este bloque, como la clave pública, etc.
  • La siguiente directiva indica el nombre del módulo de nuestro ensamblado (.module Ejemplo.exe). Como ya sabemos debe haber al menos un módulo en cada ensamblado.
  • Si seguimos hacia adelante, nos encontramos con (.method static void main() cil managed), la directiva .method indica que estamos definiendo un método, el cual es estático (usando la misma definición que en C#) y no retorna nada (void). Además, el nombre del método es ‘main’ y no toma ningún parámetro (ya que no hay nada dentro de los paréntesis). La instrucción final ‘cil managed‘ indica al compilador que el código que vamos a compilar estará gestionado por nosotros (nuestro código).
  • Moviéndonos dentro del método, la primera directiva es .maxstack (.maxstack 1). Esta directiva es importante la cual anuncia el número máximo de objetos que podremos cargar en memoria (en el stack de evaluación en realidad). Discutiremos sobre esto extensamente en el próximo capítulo, ignóralo por el momento.
  • La directiva .entrypoint le indica al compilador que este método será el punto de entrada para nuestra aplicación, es decir, la primera función que será llamada una vez el programa se ejecute (que suele ser el método main).
  • La siguiente línea (ldstr “Hola mundo! Also, The Game !!!”) contiene la instrucción ldstr. La instrucción ldstr es usada para cargar una cadena en la memoria (o stack de evaluación). Es necesario escribir valores en el stack de evaluación antes de que puedan ser utilizados. Como he dicho antes, discutiremos sobre ello extensamente en el siguiente capítulo.
  • La siguiente instrucción (call void [mscorlib]System.Console::WriteLine (string)) llama (invoca) a un método que reside en la librería mscorlib. Toma nota que hemos indicado la ruta completa de este método incluyendo el tipo de retorno, los tipos de parámetros y en qué librería reside. Con esto hemos pasado la cadena de texto como parámetro, la cual no es una variable, es un tipo de dato. La instrucción anterior (ldstr “Hola m….”) cargó la cadena al stack y este método la está usando para imprimirla.
  • La instrucción final (ret), aunque no debería ser necesario explicarla, implica retornar (terminar) el método.

Leyendo las lineas de arriba, ahora deberías tener una idea de como escribir código en ILAsm, además, deberías darte cuenta de que ILAsm no es como un lenguaje .NET de alto nivel (como C#, VB, etc). De todas formas, todo el codigo que tu escribas, lo harás siguiendo una estructura de este estilo (o con pequeños cambios, cuando trabajemos con clases).

Para la siguiente parte del tutorial hablaremos sobre el stack de evaluación y los tipos de datos más comunes que nos encontraremos.

[ Introducción al lenguaje ensamblador IL de .NET [Parte 2] –> ]

Seguir

Get every new post delivered to your Inbox.

Únete a otros 228 seguidores