Tutorial de Ildasm.exe

Este tutorial ofrece una introducción al Desensamblador MSIL (Ildasm.exe) incluido en .NET Framework SDK. La herramienta Ildasm.exe analiza cualquier ensamblado .exe o .dll de .NET Framework, además de mostrar la información en un formato de lenguaje natural. Ildasm.exe no sólo muestra el código MSIL (Microsoft intermediate language) sino también espacios de nombres y tipos, incluidas sus interfaces. Se puede usar Ildasm.exe para examinar ensamblados nativos de .NET Framework, como Mscorlib.dll, y ensamblados de .NET Framework proporcionados por otros o creados por el usuario. La mayoría de los programadores de .NET Framework opinan que Ildasm.exe es indispensable.

Para este tutorial, utilice la versión Visual C# del ejemplo de WordCount incluido en SDK. También podrá utilizar la versión Visual Basic, pero el MSIL generado será diferente para los dos lenguajes y las imágenes de pantalla no serán idénticas. WordCount está ubicado en el directorio <FrameworkSDK>\Samples\Applications\WordCount\. Para generar y ejecutar el ejemplo, siga las instrucciones que se recogen en el archivo Readme.htm. Este tutorial utiliza Ildasm.exe para examinar el ensamblado WordCount.exe.

Para comenzar a trabajar, genere el ejemplo de WordCount y cárguelo en Ildasm.exe mediante la siguiente línea de comandos:

ildasm WordCount.exe

De este modo, aparece la ventana de Ildasm.exe, tal como se muestra en la siguiente ilustración.

El árbol en la ventana de Ildasm.exe muestra la información del manifiesto incluida en WordCount.exe así como los cuatro tipos de clase global: App, ArgParser, WordCountArgParser y WordCounter.

Al hacer doble clic en cualquiera de los tipos en el árbol, aparece más información sobre dicho tipo. En la siguiente ilustración, está expandido el tipo de clase WordCounter.

En la ilustración anterior, se pueden ver todos los miembros de WordCounter. En la siguiente tabla se explica el significado de cada símbolo gráfico.

Símbolo Significado
Más información
Espacio de nombres
Clase
Interfaz
Clase de valor
Enumeración
Método
Método estático
Campo
Campo estático
Evento
Propiedad
Manifiesto o un elemento de información de clase

Al hacer doble clic en la entrada .class public auto ansi beforefieldinit, aparece la siguiente información:

En la anterior ilustración, se puede ver fácilmente que el tipo WordCounter se deriva del tipo System.Object.

El tipo WordCounter contiene otro tipo, denominado WordOccurrence. El tipo WordOccurrence se puede expandir para ver sus miembros, tal como se muestra en la siguiente ilustración.

Si mira el árbol, verá que WordOccurrence implementa la interfaz System.IComparable y, en concreto, el método CompareTo. Sin embargo, el resto de esta conversación omitirá el tipo WordOccurrence y se concentrará en el tipo WordCounter.

Como puede comprobar, el tipo WordCounter contiene cinco campos privados: totalBytes, totalChars, totalLines, totalWords y wordCounter. Los primeros cuatro de estos campos son instancias del tipo int64, mientras que el campo wordCounter es una referencia a un tipo System.Collections.SortedList.

Después de los campos, se pueden ver los métodos. El primer método, .ctor, es un constructor. Este tipo concreto sólo tiene un constructor, pero otros tipos pueden tener varios constructores, cada uno con una firma distinta. El constructor de WordCounter tiene un tipo de valor devuelto de void (al igual que todos los constructores) y no acepta ningún parámetro. Si hace doble clic en el método del constructor, aparecerá una nueva ventana que muestre el código MSIL incluido en el método, tal como se muestra en la siguiente ilustración.

En realidad, el código MSIL es bastante fácil de leer y comprender. (Para obtener toda la información, vea la especificación CIL Instruction Set Specification, ubicada en el archivo Partition III CIL.doc en la carpeta <FrameworkSDK>\Tool Developers Guide\Docs\.) En la parte superior, se puede ver que este constructor requiere 50 bytes de código MSIL. Tomando como base este número, no se puede conocer en realidad la cantidad de código nativo que será emitida por el compilador JIT, ya que el tamaño depende de la CPU host y del compilador que se utilice para generar el código.

Common Language Runtime está basado en pilas. De este modo, para realizar cualquier operación, el código MSIL inserta primero los operandos en una pila virtual y, a continuación, ejecuta el operador. El operador recoge los operandos de la pila, realiza la operación requerida y, a continuación, coloca el resultado en la pila. En ningún momento este método tiene más de ocho operandos insertados en la pila virtual. Para identificar este número, haga clic en el atributo .maxstack que aparece justo delante del código MSIL.

Examine ahora las primeras instrucciones MSIL, que se reproducen en las siguientes cuatro líneas:

IL_0000: ldarg.0 ; Load the object's 'this' pointer on the stack
IL_0001: ldc.i4.0 ; Load the constant 4-byte value of 0 on the stack
IL_0002: conv.i8 ; Convert the 4-byte 0 to an 8-byte 0
IL_0003: stfld int64 WordCounter::totalLines

La instrucción máquina en IL_0000 carga el primer parámetro que se ha pasado al método en la pila virtual. A cada método de instancia se pasa siempre la dirección de la memoria del objeto. Este argumento se denomina Argument Zero y jamás se muestra explícitamente en la firma del método. Por consiguiente, incluso si parece que el método .ctor recibe cero argumentos, en realidad recibe un argumento. A continuación, la instrucción máquina en IL_0000 carga el puntero a este objeto en la pila virtual.

La instrucción máquina en IL_0001 carga un valor cero constante de 4 bytes en la pila virtual.

La instrucción máquina en IL_0002 toma el valor de la parte superior de la pila (cero de 4 bytes) y lo convierte a un cero de 8 bytes, colocándolo por lo tanto en la parte superior de la pila.

En ese momento, la pila contiene dos operandos: el cero de 8 bytes y el puntero a este objeto. La instrucción máquina en IL_0003 usa ambos operandos para almacenar el valor de la parte superior de la pila (cero de 8 bytes) en el campo totalLines del objeto identificado en la pila.

Se repite la misma secuencia de instrucciones MSIL para los campos totalChars, totalBytes y totalWords.

La inicialización del campo wordCounter comienza con la instrucción máquina IL_0020, tal y como se muestra a continuación:

IL_0020: ldarg.0
IL_0021: newobj instance void [mscorlib]System.Collections.SortedList::.ctor()
IL_0026: stfld class [mscorlib]System.Collections.SortedList WordCounter::wordCounter

La instrucción máquina en IL_0020 inserta el puntero this correspondiente a WordCounter en la pila virtual. Este operando no lo utiliza la instrucción máquina newobj pero sí la instrucción máquina stfld en IL_0026.

La instrucción máquina en IL_0021 indica al motor de tiempo de ejecución que cree un nuevo objeto System.Collections.SortedList y llame a su constructor sin argumentos. Cuando se devuelva newobj, la dirección del objeto SortedList estará en la pila. En ese momento, la instrucción máquina stfld en IL_0026 almacena el puntero al objeto SortedList en el campo wordCounter del objeto WordCounter.

Tras inicializarse todos los campos del objeto WordCounter, la instrucción máquina en IL_002b inserta el puntero this en la pila virtual e IL_002c llama al constructor en el tipo base (System.Object).

Naturalmente, la última instrucción máquina en IL_0031 es la instrucción máquina de devolución que hace que el constructor de WordCounter vuelva al código que lo creó. Los constructores deben devolver void, de modo que no se coloque nada en la pila antes de que vuelva el constructor.

A continuación figura otro ejemplo. Haga doble clic en el método GetWordsByOccurranceEnumerator para ver su código MSIL, que se muestra en la siguiente ilustración.

Como puede comprobar, el código de este método tiene un tamaño de 69 bytes y el método requiere cuatro ranuras en la pila virtual. Además, este método tiene tres variables locales: una es del tipo System.Collection.SortedList y las otras dos son del tipo System.Collections.IDictionaryEnumerator. Observe que los nombres de variable mencionados en el código fuente no se emiten al código MSIL, a menos que se compile el ensamblado con la opción /debug. Si no se utiliza /debug, se usan los nombres de variable V_0, V_1 y V_2 en lugar de sl, de y CS$00000003$00000000, respectivamente.

Cuando este método inicia la ejecución, lo primero que hace es ejecutar la instrucción máquina newobj, creando así un nuevo objeto System.Collections.SortedList y llamando al constructor predeterminado de este objeto. Cuando se devuelva newobj, la dirección del objeto creado estará en la pila virtual. La instrucción máquina stloc.0 (en IL_0005) almacena este valor en la variable local 0 ó sl (V_0 sin /debug) (que es del tipo System.Collections.SortedList).

En las instrucciones IL_0006 e IL_0007, se carga el puntero this del objeto WordCounter (en el argumento Argument Zero pasado al método) en la pila y se llama al método GetWordsAlphabeticallyEnumerator. Cuando se devuelva la instrucción máquina call, la dirección del enumerador estará en la pila. La instrucción máquina stloc.1 (en IL_000c) guarda esta dirección en la variable local 1 o de (V_1 sin /debug), que es del tipo System.Collections.IDictionaryEnumerator.

La instrucción máquina br.s en IL_000d causa una bifurcación incondicional en la condición de prueba IL de la instrucción while. Esta condición de prueba IL comienza en la instrucción máquina IL_0032. En IL_0032, se inserta la dirección de de (o V_1) (IDictionaryEnumerator) en la pila y, en IL_0033, se llama a su método MoveNext. Si MoveNext devuelve true, existe una entrada que ha de ser enumerada y la instrucción máquina brtrue.s salta hasta la instrucción máquina en IL_000f.

En las instrucciones máquina IL_000f e IL_0010, se insertan en la pila las direcciones de los objetos en sl (o V_0) y de (o V_1). A continuación, se llama al método de propiedad get_Value del objeto IdictionaryEnumerator para obtener el número de repeticiones de la actual entrada. Este número es un valor de 32 bits que está almacenado en System.Int32. El código convierte este objeto Int32 a un tipo de valor int. Para convertir un tipo de referencia a un tipo de valor, se requiere la instrucción máquina unbox en IL_0016. Cuando se devuelva unbox, la dirección del valor al que se ha aplicado la conversión unboxing estará en la pila. La instrucción máquina ldind.i4 (en IL_001b) carga en la pila un valor de 4 bytes, que apunta a la dirección actualmente en la pila. En otras palabras, se coloca en la pila el entero de 4 bytes al que se ha aplicado la conversión unboxing.

En la instrucción máquina IL_001c, se inserta en la pila el valor de sl (o V_1) (la dirección de IDictionaryEnumerator) y se llama a su método de propiedad get_Key. Cuando se devuelva get_Key, la dirección de System.Object estará en la pila. El código sabe que el diccionario contiene cadenas, de modo que el compilador convierte este Objeto a una Cadena mediante la instrucción máquina castclass en IL_0022.

Las siguientes instrucciones máquina (desde IL_0027 hasta IL_002d) crean un nuevo objeto WordOccurrence y pasan la dirección del objeto al método Add de SortedLists.

En la instrucción máquina IL_0032, se vuelve a evaluar la condición de prueba de la instrucción while. Si MoveNext devuelve true, el bucle ejecuta otra iteración. Sin embargo, si MoveNext devuelve false, la ejecución no puede pasar del bucle y acaba en la instrucción máquina IL_003a. Las instrucciones máquina de IL_003a a IL_0040 llaman al método GetEnumerator del objeto SortLists. El valor que se devuelve es un System.Collections.IDictionaryEnumerator, que permanece en la pila para convertirse en el valor devuelto GetWordsByOccurrenceEnumerator.