Dalvik VM: Ficheros .dex

En esta entrada vamos a hablar de la estructura interna de los ficheros .dex. Lo primero, decir que, los ficheros .dex se encuentran empaquetados dentro los archivos .apk (Android Package). A continuación podemos ver una imagen con la estructura interna de estos ficheros:

dex_structure

Como podemos apreciar, un fichero .dex esta divido en distintas secciones llamadas “pools”. El pool de strings contiene todos los Strings que las clases dentro del .dex usan. En pool de tipos (type_ids) se guardan los distintos tipos datos usados en la aplicación, etc. Además de las secciones mostradas en la imagen, en la estructura actual de los ficheros dex existe una sección más por debajo de la de datos (data) llamada link_data o enlace de datos. En este momento dicha sección no está documentada. Todo lo que dice la documentación oficial es que en ella se guarda información sobre los ficheros enlazados estáticamente. Si el fichero no es enlazado esta sección permanece vacía y dicha documentación concluye diciendo que dicha sección se use como mejor se adecue a nuestra implementación.

Veamos la estructura de cada sección.

Antes de nada vamos a ver los distintos tipos de datos y su longitud.

Name Description
byte 8-bit signed int
ubyte 8-bit unsigned int
short 16-bit signed int, little-endian
ushort 16-bit unsigned int, little-endian
int 32-bit signed int, little-endian
uint 32-bit unsigned int, little-endian
long 64-bit signed int, little-endian
ulong 64-bit unsigned int, little-endian
sleb128 signed LEB128, variable-length (see below)
uleb128 unsigned LEB128, variable-length (see below)
uleb128p1 unsigned LEB128 plus 1, variable-length (see below)
Header o Cabecera La cabecera es quizás unas de las partes más importantes de un fichero y no sólo de los .dex, sino que cualquier otro formato, como: PDF, doc. rtf, etc. En este caso, las estructura de la cabecera es como sigue:

Name Format Description
magic ubyte[8] magic value. Este valor es el que identifica el tipo de fichero. En este caso: { 0x64 0x65 0x78 0x0a 0x30 0x33 0x35 0x00 }= "dex\n035\0"
checksum uint adler32 checksum. Típico valor para identificar cualquier tipo de corrupción. Este valor se calcula en base a todo el fichero, menos magic y checksum.
signature ubyte[20] Hash SHA-1 de todo el fichero menos magic, checksum y signature. Se usa para la identificación de ficheros.
file_size uint Tamaño de todo el fichero incluida la cabecera en bytes.
header_size uint = 0x70 Tamaño de la cabecera en bytes. Se usa para guardar la compatibilidad.
endian_tag uint = ENDIAN_CONSTANT Etiqueta endian. Nos indica que tipo de formato endian usa el fichero. Puede tener los siguientes valores:

uint ENDIAN_CONSTANT = 0x12345678;

uint REVERSE_ENDIAN__CONSTANT.

link_size uint Indica el tamaño de la sección de enlace (link section) o 0, si el fichero es enlazado de forma dinámica.
link_off uint Desplazamiento al comienzo de la sección de enlace desde el inicio del fichero o 0 si link_size == 0.
map_off uint Desplazamiento al map_list en caso que éste exista o 0 en caso contrario. El map_list es una lista con todo el contenido del fichero. Esta estructura de datos puede contener datos redundantes, pero la intención de la misma es el poder recorrer el contenido del fichero de una forma más cómoda. Esta lista está ordenada.
string_ids_size uint Número de elementos en la lista de strings.
string_ids_off uint Desplazamiento a la lista de strings o 0 en caso que dicha lista este vacía, circunstancia que raramente se va a dar.
type_ids_size uint Número de elementos en la lista de tipos (type).
type_ids_off uint Desplazamiento a la lista de tipos o 0 en caso que dicha lista este vacía. Caso que raramente también se dará.
proto_ids_size uint Número de elementos en la lista de prototipos.
proto_ids_off uint Desplazamiento a la lista de prototipos. 0 en caso que dicha lista este vacía. De nuevo, situación que raramente se dará.
field_ids_size uint Número de elementos en la lista de campos.
field_ids_off uint Desplazamiento a la lista de campos o 0 en caso que dicha lista esté vacía.
method_ids_size uint Número de elementos en la lista de métodos.
method_ids_off uint Desplazamiento a la lista de métodos. 0 si la lista está vacía.
class_defs_size uint Número de elementos en la lista de clases.
class_defs_off uint Desplazamiento a la lista de clases. 0 en caso dicha lista este vacía, situación poco probable.
data_size uint Tamaño en bytes de la sección de datos. Debe ser un número par múltiplo del tamaño de un uint (sizeof(uint)).
data_off uint Desplazamiento a la sección de datos.
Sección de Strings Esta sección está compuesta por una lista de referencias a todas las Strings o cadenas de texto de la aplicación.
Name Format Description
string_data_off uint Desplazamiento desde el inicio del fichero hasta la cadena de texto en sí. La estructura de datos a la que este puntero apunta debe de estar en la sección de datos.
String_data_item es la estructura de datos referenciada por la lista de punteros anterior. Como dije en la descripción, estos datos deben estar situados en la sección de datos.
Name Format Description
utf16_size uleb128 Tamaño del string en formato UTF-16 (2 bytes por caracter).
data ubyte[] Array de bytes en formato MUTF-8. Aunque también acepta la codificación UTF-16.
Sección de tipos Aquí nos encontramos con la lista de los tipos de datos usados en el fichero.
Name Format Description
descriptor_idx uint Indica el tipo (clase, arrays o tipos primitivos) de cada String especificada en el sección de Strings. Debe estar en el mismo orden que la sección de strings.
Estos son los valores que puede tomar:
TypeDescriptor
'V'
| FieldTypeDescriptor
FieldTypeDescriptor
NonArrayFieldTypeDescriptor
| ('[' * 1…255) NonArrayFieldTypeDescriptor
NonArrayFieldTypeDescriptor
'Z'
| 'B'
| 'S'
| 'C'
| 'I'
| 'J'
| 'F'
| 'D'
| 'L' FullClassName ';'
Tipos de datos correspondientes a los valores anteriores:
Syntax Meaning
V void; sólo válido para valores de retorno
Z boolean
B byte
S short
C char
I int
J long
F float
D double
Lfully/qualified/Name; Nombre completo de la clase, incluido los paquetes.
[descriptor array of descriptor, usable recursively for arrays-of-arrays, though it is invalid to have more than 255 dimensions.
Sección de prototipo de métodos Esta sección contiene la lista de los prototipos de métodos de nuestras clases. Lo que se conoce también como el signature (firma) del método.
Name Format Description
shorty_idx uint Índice dentro de la sección de Strings que contiene la cadena de texto con el nombre del método
return_type_idx uint Índice dentro de la cadena de tipos correspondiente al tipo de dato devuelto por este método.
parameters_off uint Desplazamiento a la lista de tipos de parámetros o 0 en case de que el método no espere ningún parámetro. Dicha sección se encuentra en la sección de datos.
Sección de campos (fields) Lista de todos los campos referenciados por este fichero, estén en este mismo fichero o no, es decir, si referenciamos un campo que pertenece a alguna libraría, aunque éste no se encuentre en nuestro fichero, de cualquier forma aparecerá en esta lista.
Name Format Description
class_idx ushort Índice correspondiente a la clase a la que pertenece dicho campo. Este elemento se encuentra en la lista de tipos. Debe referenciar un tipo clase.
type_idx ushort Índice en la lista de tipos que se corresponde con el tipo de este campo
name_idx uint Índice dentro de la lista de Strings que contiene el nombre de dicho campo.
Sección de métodos Ésta contiene la lista de métodos referenciados en nuestro fichero y al igual que la lista de campos, aquí también aparecen los métodos que no se encuentran en este fichero.
Name Format Description
class_idx ushort Índice dentro de la lista de tipos que define este método. Éste debe ser del tipo clase o array, pero un tipo primitivo.
proto_idx ushort Índice que define el prototipo de este método en la sección de prototipos.
name_idx uint Índice en la sección de strings que contiene el nombre de este método.
Sección de clases Esta sección compone la lista de las clases en nuestro fichero. Esta lista está ordenada por clases de forma que un clase que herede o implemente otra, ésta/s deben aparecer antes en dicha lista.
Name Format Description
class_idx uint Índice en la lista de tipos. Debe ser del tipo clase y no array o primitivo.
access_flags uint Tipo de acceso (public, final, etc)
superclass_idx uint Índice en la sección de tipos del tipo de la superclase o clase que hereda. NO_INDEX en caso de que no tenga superclase. Por ejemplo Object
interfaces_off uint Desplazamiento desde el inicio del fichero hasta la o las interfaces que esta clase implementa o 0 caso que no implemente ninguna. Dicha lista de interfaces deber aparecer en la sección de datos.
source_file_idx uint Índice en la lista de Strings con el nombre del fichero que contiene la fuente original o la constante NO_INDEX en caso de que se carezca de dicha información.
annotations_off uint Desplazamiento a la estructura de anotaciones de esta clase o 0 en caso de que esta clase no contenga anotaciones. La estructura de anotaciones aparece en la sección de datos.
class_data_off uint Desplazamiento a los datos asociados con esta clase. Esos datos deben estar definidos en la sección de datos.
static_values_off uint Desplazamiento a la sección de datos donde se guardan los valores iniciales de los campos estáticos o 0 si la clase no tiene valores estáticos.
Con esto hemos visto las estructuras de datos que compone cada sección. Como podréis apreciar, la mayoría de los datos referenciados desde estas secciones, se encuentran en la sección de datos. Todavía existen más estruturas de datos que no hemos visto aquí, pero que siempre podéis consultar en la documentación oficial.

Sólo he intentado repasar las estructuras, que desde mi punto de vista son más atractivas. También podemos apreciar el papel que juega la sección de Strings y quizás ahora se vea un poco más claro el proceso de optimización de los ficheros .dex.

Para tener una idea un poco más clara de la diferencia entre ficheros .jar y ficheros vamos a ver unas imágenes de ejemplo donde se puede ver la diferencia entre ambos.

En esta primera imagen podemos ver como se relacionan, o como se van a distribuir los datos con generemos el fichero .dex.

java_dex

A continuación veremos una imagen de como estarían distribuidas las clases y sus datos dentro de un .jar.

jar_structure

Aquí vemos como cada clase contiene su propia información. No se comparte nada. Ahora veremos como se distribuyen los datos dentro de un fichero .dex.

dex_data_link

Como podemos ver no existen elementos repetidos y todo está enlazado y compartido. Como ya hemos dicho esta una de las formas en que Dalvik reduce el tamaño de los ficheros y además acelera el acceso a los datos.

La estructura de los ficheros .jar es mucho más clara y ordenada, pero recordemos que cuando hablamos de Dalvik hablamos de dispositivos móviles y ligeros, dónde el ahorro de espacio y proceso es crítico.

Enlaces anteriores de esta serie: Dalvik VM: Introducción Dalvik VM: Optimización

Fuente: http://www.netmite.com/android/mydroid/dalvik/docs/dex-format.html