Introducción al lenguaje ensamblador IL de .NET [Parte 4]
18 diciembre 2011 1 comentario
[ <-- 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!
Pingback: Introducción al lenguaje ensamblador IL de .NET [Parte 3] « El Blog de JuDelCo