Un ensamblado es una agrupación lógica de uno o más módulos o ficheros
de recursos (ficheros .GIF, .HTML, etc.) que se engloban bajo un nombre común.
Un programa puede acceder a información o código almacenados en un ensamblado
sin tener porqué sabe cuál es el fichero en concreto donde se encuentran, por
lo que los ensamblados nos permiten abstraernos de la ubicación física del
código que ejecutemos o de los recursos que usemos. Por ejemplo, podemos
incluir todos los tipos de una aplicación en un mismo ensamblado pero colocando
los más frecuentemente usados en un cierto módulo y los menos usados en otro,
de modo que sólo se descarguen de Internet los últimos si es que se van a usar.
Todo ensamblado contiene un manifiesto, que son metadatos con
información sobre las características del ensamblado. Este manifiesto puede
almacenarse cualquiera de los módulos que formen el ensamblado o en uno
específicamente creado para ello, caso éste último necesario cuando es un ensamblado satélite (sólo contiene
recursos)
Las principales tablas incluidas
en los manifiestos son las siguientes:
|
Tabla
|
Descripción
|
|
AssemblyDef
|
Define las características del
ensamblado. Consta de un único elemento que almacena el nombre del ensamblado
sin extensión, versión, idioma, clave pública y tipo de algoritmo de
dispersión usado para hallar los valores de dispersión de la tabla FileDef.
|
|
FileDef
|
Define cuáles son los archivos
que forman el ensamblado. De cada uno se da su nombre y valor de dispersión.
Nótese que sólo el módulo que contiene el manifiesto sabrá qué ficheros que
forman el ensamblado, pero el resto de ficheros del mismo no sabrán si
pertenecen o no a un ensamblado (no contienen metadatos que les indique si
pertenecen a un ensamblado)
|
|
ManifestResourceDef
|
Define las características de
los recursos incluidos en el módulo. De cada uno se indica su nombre y
modificadores de acceso. Si es un recurso incrustado se indica dónde empieza
dentro del PE que lo contiene, y si es un fichero independiente se indica
cuál es el elemento de la tabla FileDef correspondiente a dicho fichero.
|
|
ExportedTypesDef
|
Indica cuáles son los tipos
definidos en el ensamblado y accesibles desde fuera del mismo. Para ahorrar
espacio sólo recogen los que no pertenezcan al módulo donde se incluye el
manifiesto, y de cada uno se indica su nombre, la posición en la tabla
FileDef del fichero donde se ha implementado y la posición en la tabla
TypeDef correspondiente a su definición.
|
|
AssemblyProccesorDef
|
Indica en qué procesadores se
puede ejecutar el ensamblado, lo que puede ser útil saberlo si el ensamblado
contiene módulos con código nativo (podría hacerse usando C++ con extensiones gestionadas) Suele estar vacía, lo
que indica que se puede ejecutar en cualquier procesador; pero si estuviese
llena, cada elemento indicaría un tipo de procesador admitido según el
formato de identificadores de procesador del fichero WinNT.h incluido con
Visual Studio.NET (por ejemplo, 586 =
Pentium, 2200 = Arquitectura IA64, etc.)
|
|
AssemblyOSDef
|
Indica bajo qué sistemas
operativos se puede ejecutar el ensamblado, lo que puede ser útil si contiene
módulos con tipos o métodos disponibles sólo en ciertos sistemas. Suele estar
vacía, lo que indica que se puede ejecutar en cualquier procesador; pero si
estuviese llena, indicaría el identificador de cada uno de los sistemas
admitidos siguiendo el formato del WinNT.h de Visual Studio.NET (por ejemplo,
0 = familia Windows 9X, 1 = familia Windows NT, etc.) y el número de la
versión del mismo a partir de la que se admite.
|
Tabla 2: Principales
tablas de un manifiesto
Para asegurar que no se haya alterado la información de ningún
ensamblado se usa el criptosistema de clave pública RSA. Lo que se hace es
calcular el código de dispersión SHA-1 del módulo que contenga el manifiesto e
incluir tanto este valor cifrado con RSA (firma
digital) como la clave pública necesaria para descifrarlo en algún lugar
del módulo que se indicará en la cabecera de CLR. Cada vez que se vaya a cargar
en memoria el ensamblado se calculará su valor de dispersión de nuevo y se
comprobará que es igual al resultado de descifrar el original usando su clave
pública. Si no fuese así se detectaría que se ha adulterado su contenido.
Para asegurar también que los
contenidos del resto de ficheros que formen un ensamblado no hayan sido
alterados lo que se hace es calcular el código de dispersión de éstos antes de
cifrar el ensamblado y guardarlo en el elemento correspondiente a cada fichero
en la tabla FileDef del manifiesto. El algoritmo de cifrado usado por defecto
es SHA-1, aunque en este caso también se da la posibilidad de usar MD5. En
ambos casos, cada vez que se accede al fichero para acceder a un tipo o recurso se calculará de nuevo su
valor de dispersión y se comprobará que coincida con el almacenado en FileDef.
Dado que las claves públicas son valores
que ocupan muchos bytes (2048 bits), lo que se hace para evitar que los
metadatos sean excesivamente grandes es no incluir en las referencias a
ensamblados externos de la tabla AssemblyRef las claves públicas de dichos
ensamblados, sino sólo los 64 últimos bits resultantes de aplicar un algoritmo
de dispersión a dichas claves. A este valor recortado se le llama marca de clave pública.
Hay dos tipos de ensamblados: ensamblados privados y ensamblados compartidos. Los privados
se almacenan en el mismo directorio que la aplicación que los usa y sólo puede
usarlos ésta, mientras que los compartidos se almacenan en un caché de ensamblado global (GAC) y
pueden usarlos cualquiera que haya sido compilada referenciándolos.
Los compartidos han
de cifrase con RSA ya que lo que los identifica es en el GAC es su
nombre (sin extensión) más su clave pública, lo que permite que en el GAC
puedan instalarse varios ensamblados con el mismo nombre y diferentes claves
públicas. Es decir, es como si la clave pública formase parte del nombre del ensamblado, razón por la que a los
ensamblados así cifrados se les llama ensamblados
de nombre fuerte. Esta política permite resolver los conflictos derivados
de que se intente instalar en un mismo equipo varios ensamblados compartidos
con el mismo nombre pero procedentes de distintas empresas, pues éstas tendrán
distintas claves públicas.
También para evitar problemas, en
el GAC se pueden mantener múltiples versiones de un mismo ensamblado. Así, si
una aplicación fue compilada usando una cierta versión de un determinado
ensamblado compartido, cuando se ejecute sólo podrá hacer uso de esa versión
del ensamblado y no de alguna otra más moderna que se hubiese instalado en el
GAC. De esta forma se soluciona el problema del infierno de las DLL comentado al principio del tema.
En realidad es posible modificar
tanto las políticas de búsqueda de ensamblados (por ejemplo, para buscar
ensamblados privados fuera del
directorio de la aplicación) como la política
de aceptación de ensamblados compartidos (por ejemplo, para que se haga
automáticamente uso de las nuevas versiones que se instalen de DLLs
compartidas) incluyendo en el directorio de instalación de la aplicación un
fichero de configuración en formato XML con las nuevas reglas para las mismas.
Este fichero ha de llamarse igual que el ejecutable de la aplicación pero ha de
tener extensión .cfg.