Formato DEX
1. Qué es y por qué existe
Un fichero DEX (Dalvik EXecutable) contiene todo el código compilado de una aplicación
Android: no una clase, sino cientos o miles. Dentro de un APK se llama classes.dex, y
con multidex (sección 15) le acompañan classes2.dex y sucesivos.
Existe porque el .class de la JVM no servía para un teléfono de 2008. En la JVM cada
clase es un fichero con su propio constant pool: si mil clases mencionan
java.lang.String, la cadena "java/lang/String" aparece mil veces. El DEX invierte el
reparto: un fichero, tablas compartidas. Todas las cadenas viven una vez en
string_ids; los tipos, en type_ids; los prototipos, en proto_ids; los campos y métodos
referenciados, en field_ids y method_ids. Una clase no guarda nombres, guarda
índices. El resultado se mapea en memoria (mmap) y se consulta por índice sin
desempaquetar nada, que es lo que hace ART al arrancar.
Se paga en dos monedas. Los índices son de 16 bits en el bytecode, de donde sale el
límite de 65.536 referencias y con él multidex. Y nada es local: renombrar un método
toca string_ids, que está ordenado, lo que desplaza offsets, lo que invalida map_list,
el checksum y la signature. Editar un DEX no es editar un fichero, es reconstruirlo.
Por eso existen baksmali y smali
(Desensambladores smali).
.class de la JVM |
DEX |
|
|---|---|---|
| Unidad de fichero | Una clase | Todas las clases de la aplicación |
| Tabla de constantes | Una por clase, con duplicados entre clases | Global, deduplicada, ordenada |
| Endianness | Big-endian | Little-endian |
| Modelo de ejecución | Pila de operandos | Registros (Bytecode Dalvik) |
| Cadenas | CONSTANT_Utf8 |
MUTF-8, esencialmente lo mismo (sección 8.2) |
┌────────────────────────────────────────────────────────────┐
│ header_item 112 bytes (0x70) │ offset 0
├────────────────────────────────────────────────────────────┤
│ string_ids string_id_item[] 4 bytes cada uno │ ┐
│ type_ids type_id_item[] 4 bytes │ │
│ proto_ids proto_id_item[] 12 bytes │ │ tablas de
│ field_ids field_id_item[] 8 bytes │ │ índices
│ method_ids method_id_item[] 8 bytes │ │ (registro fijo)
│ class_defs class_def_item[] 32 bytes │ ┘
├────────────────────────────────────────────────────────────┤
│ call_site_ids · method_handles (v038+; no en el header) │
├────────────────────────────────────────────────────────────┤
│ data todo lo de tamaño variable: │
│ string_data_item, type_list, class_data_item,│
│ code_item, debug_info_item, anotaciones… │
│ y el map_list, casi siempre al final │
├────────────────────────────────────────────────────────────┤
│ link_data vacío en todo APK distribuido │
└────────────────────────────────────────────────────────────┘
Ese orden es el habitual, no el obligatorio: lo único fijo es que el header_item va
primero y que las entradas del map_list van ordenadas por offset y sin solaparse. La
autoridad sobre dónde está cada cosa es el map_list (sección 5), no este dibujo.
2. Convenciones del formato
Endianness: little-endian en todo el documento. endian_tag lo declara con
ENDIAN_CONSTANT = 0x12345678; su reverso, REVERSE_ENDIAN_CONSTANT = 0x78563412, indica
un fichero con los bytes intercambiados y no aparece en APK distribuidos.
| Tipo | Tamaño | Significado |
|---|---|---|
byte / ubyte |
1 | Entero de 8 bits con y sin signo |
short / ushort |
2 | Entero de 16 bits con y sin signo |
int / uint |
4 | Entero de 32 bits con y sin signo |
long / ulong |
8 | Entero de 64 bits con y sin signo |
sleb128 / uleb128 |
var | LEB128 con y sin signo, de 1 a 5 bytes (sección 8.1) |
uleb128p1 |
var | LEB128 sin signo desplazado en uno |
Alineación, que no es uniforme y es fuente clásica de fallos: a 4 bytes el
header_item, las seis tablas de índices, map_list, type_list, code_item,
annotation_set_item, annotation_set_ref_list y annotations_directory_item. Sin
alineación (byte a byte) string_data_item, class_data_item, debug_info_item,
annotation_item, encoded_array_item y call_site_item.
3. header_item campo a campo
Offsets absolutos, porque la cabecera empieza en el byte 0. Los pares *_size/*_off van
en una sola fila: primero el tamaño, luego el offset.
| Campo | Offset | Tamaño | Tipo | Significado |
|---|---|---|---|---|
magic |
0x00 | 8 | ubyte[8] |
DEX_FILE_MAGIC: "dex\n" + tres dígitos de versión + "\0" (sección 4) |
checksum |
0x08 | 4 | uint |
Adler-32 de todo lo posterior al byte 12: el fichero menos magic y menos este campo |
signature |
0x0c | 20 | ubyte[20] |
SHA-1 de todo lo posterior al byte 32: el fichero menos magic, checksum y esta firma. Identifica el fichero; no autentica a nadie |
file_size |
0x20 | 4 | uint |
Tamaño del fichero entero, cabecera incluida (≤ v040). Desde v041, distancia hasta la siguiente cabecera o el fin del contenedor |
header_size |
0x24 | 4 | uint |
Debe valer 0x70 (112) hasta v040 y 0x78 (120) desde v041 |
endian_tag |
0x28 | 4 | uint |
ENDIAN_CONSTANT = 0x12345678 |
link_size / link_off |
0x2c / 0x30 | 4 + 4 | uint |
Sección link, o 0 si no hay enlazado estático. Formato sin especificar: hueco para el runtime |
map_off |
0x34 | 4 | uint |
Offset del map_list. Debe ser distinto de cero |
string_ids_size / _off |
0x38 / 0x3c | 4 + 4 | uint |
Número de cadenas y offset de la tabla, o 0 si el tamaño es 0 |
type_ids_size / _off |
0x40 / 0x44 | 4 + 4 | uint |
Número de tipos, como máximo 65.535, y offset |
proto_ids_size / _off |
0x48 / 0x4c | 4 + 4 | uint |
Número de prototipos, como máximo 65.535, y offset |
field_ids_size / _off |
0x50 / 0x54 | 4 + 4 | uint |
Número de campos referenciados y offset |
method_ids_size / _off |
0x58 / 0x5c | 4 + 4 | uint |
Número de métodos referenciados y offset |
class_defs_size / _off |
0x60 / 0x64 | 4 + 4 | uint |
Número de clases definidas aquí —no referenciadas— y offset |
data_size / data_off |
0x68 / 0x6c | 4 + 4 | uint |
Sección data; el tamaño es múltiplo de 4. Ambos sin uso desde v041 |
container_size |
0x70 | 4 | uint |
Solo v041+. Tamaño del contenedor completo |
header_offset |
0x74 | 4 | uint |
Solo v041+. Offset de esta cabecera dentro del contenedor |
Tres cosas que no están aquí y sorprenden. call_site_ids y method_handles no tienen
campos en la cabecera: llegaron en la versión 038 y no se podía crecer la estructura sin
romper la compatibilidad, así que viven solo en el map_list. Todos los offsets son
desde el inicio del fichero, no desde la estructura que los contiene. Y class_defs_size
cuenta clases definidas, siempre bastante menos que type_ids_size.
3.1 La cabecera de un DEX real
BT=~/Library/Android/sdk/build-tools/37.0.0
CORPUS=~/corpus-apk
DEXDIR=$(mktemp -d)
unzip -o -q $CORPUS/com.looker.droidify_710.apk 'classes*.dex' -d $DEXDIR
xxd -l 128 $DEXDIR/classes.dex
00000000: 6465 780a 3033 3500 3279 d7e5 d1dd fd39 dex.035.2y.....9
00000010: 4fe9 6f93 288b fcf9 ac38 a4e9 0c44 e660 O.o.(....8...D.`
00000020: 909b 5b00 7000 0000 7856 3412 0000 0000 ..[.p...xV4.....
00000030: 0000 0000 c09a 5b00 bb98 0000 7000 0000 ......[.....p...
00000040: ff1c 0000 5c63 0200 6820 0000 58d7 0200 ....\c..h ..X...
00000050: 9b54 0000 385c 0400 5588 0000 1001 0700 .T..8\..U.......
00000060: 5317 0000 b843 0b00 786d 4d00 182e 0e00 S....C..xmM.....
00000070: e08e 4500 e28e 4500 e68e 4500 3094 4500 ..E...E...E.0.E.
Anotado a mano sobre esos bytes (la cabecera acaba en 0x70, donde ya empieza string_ids):
| Offset | Bytes | Campo | Valor |
|---|---|---|---|
| 0x00 | 64 65 78 0a 30 33 35 00 |
magic |
"dex\n035\0" → versión 035 |
| 0x08 | 32 79 d7 e5 |
checksum |
0xe5d77932 |
| 0x0c | d1 dd … e6 60 |
signature |
d1ddfd39…0c44e660 (20 bytes) |
| 0x20 | 90 9b 5b 00 |
file_size |
6.003.600 |
| 0x24 | 70 00 00 00 |
header_size |
112 = 0x70 → v040 o anterior |
| 0x28 | 78 56 34 12 |
endian_tag |
0x12345678 → little-endian |
| 0x34 | c0 9a 5b 00 |
map_off |
6.003.392 = 0x5b9ac0 |
| 0x38 / 0x3c | bb 98 00 00 / 70 00 00 00 |
string_ids_size / _off |
39.099 / 112 |
| 0x40 / 0x44 | ff 1c 00 00 / 5c 63 02 00 |
type_ids_size / _off |
7.423 / 156.508 |
| 0x48 / 0x4c | 68 20 00 00 / 58 d7 02 00 |
proto_ids_size / _off |
8.296 / 186.200 |
| 0x50 / 0x54 | 9b 54 00 00 / 38 5c 04 00 |
field_ids_size / _off |
21.659 / 285.752 |
| 0x58 / 0x5c | 55 88 00 00 / 10 01 07 00 |
method_ids_size / _off |
34.901 / 459.024 |
| 0x60 / 0x64 | 53 17 00 00 / b8 43 0b 00 |
class_defs_size / _off |
5.971 / 738.232 |
| 0x68 / 0x6c | 78 6d 4d 00 / 18 2e 0e 00 |
data_size / data_off |
5.074.296 / 929.304 |
| 0x70 | e0 8e 45 00 |
string_ids[0] |
string_data_off = 4.558.560 |
La herramienta oficial da lo mismo salvo endian_tag y map_off, que no imprime:
$BT/dexdump -f $DEXDIR/classes.dex | head -24
Opened '…/classes.dex', DEX version '035'
DEX file header:
magic : 'dex\n035\0'
checksum : e5d77932
signature : d1dd...e660
file_size : 6003600
header_size : 112
…
method_ids_size : 34901
class_defs_size : 5971
data_off : 929304 (0x0e2e18)
-f es «display dex file header»; -h es «display all sections header» y vuelca la
cabecera de cada clase, que es otra cosa.
4. magic y la tabla de versiones
ubyte[8] DEX_FILE_MAGIC = { 0x64 0x65 0x78 0x0a 0x30 0x33 0x39 0x00 } = "dex\n039\0"
El salto de línea (0x0a) y el nulo (0x00) están puestos para detectar corrupciones de
transferencia en modo texto. Los tres dígitos decimales son la versión del formato y crecen
monótonamente. Esta tabla se copia mal a menudo; está contrastada contra las notas de la
especificación y contra el array kDexMagicVersions de AOSP.
| Versión | Se acepta desde | Qué introdujo |
|---|---|---|
009 |
Plataforma M3 (nov.–dic. de 2007) | Formato preliminar, muy distinto del actual |
013 |
Plataforma M5 (feb.–mar. de 2008) | Formato preliminar |
035 |
Línea base: casi todo lo anterior a 037 | — |
036 |
No existe | Saltada por un fallo antiguo de Dalvik que la aceptaba y ejecutaba erróneamente |
037 |
Android 7.0 | Métodos default de interfaz y el ajuste de invoke-super para invocarlos |
038 |
Android 8.0 | invoke-polymorphic, invoke-custom, method_handle_item y call_site_id_item |
039 |
Android 9.0 | const-method-handle y const-method-type. En Android 10 se amplía con información de hidden API, solo para los DEX del boot class path |
040 |
Android 10.0 | Amplía los caracteres de SimpleName: espacio, U+00a0, U+2000…U+200a, U+202f |
041 |
Ver nota | Formato contenedor: varios DEX lógicos en un fichero físico, compartiendo datos |
Nota sobre la 041. Las dos fuentes oficiales no coinciden. La especificación publicada
dice que su soporte «es experimental en la release de Android 16 para probar el formato
contenedor» y que «no debería usarse en código de producción». El comentario de AOSP en
standard_dex_file.cc dice // Dex version 041: Android "V" and beyond (aka Android 15).
⚠️ sin verificar: no se ha podido determinar cuál describe el comportamiento real del
cargador; se documentan ambas y se advierte de la discrepancia.
Qué versión emite D8 según --min-api. Medido compilando la misma clase trivial con
d8 de build-tools 37.0.0 (D8 9.2.4-dev) y leyendo el magic con dexdump -f:
min-api 1 21 23 → dex\n035\0 min-api 26 27 → dex\n038\0
min-api 24 25 → dex\n037\0 min-api 28 29 30 33 35 36 → dex\n039\0
Los cortes 24 / 26 / 28 coinciden exactamente con las versiones de la tabla. Pero D8 no
emite 040 ni 041 ni con --min-api 36: la versión del magic declara qué características
del formato necesita el fichero, no contra qué API se compiló, así que un APK moderno
puede llevar perfectamente un DEX 035. En el corpus, sobre los 140 ficheros
classes*.dex de los 58 APK standalone: 63 son 035, 24 037, 44 038 y 9 039; ni un
040 ni un 041. ⚠️ sin verificar: el corpus es mayoritariamente F-Droid y no representa
el catálogo de Play.
5. map_list: la vía fiable de recorrer el fichero
Es la lista de todo lo que contiene el fichero, en orden. Duplica parte de la cabecera a
propósito: la cabecera dice dónde están seis tablas, el map_list dice dónde está cada
sección, incluidas las que la cabecera no menciona. Es un uint size seguido de size
map_item de 12 bytes: ushort type con el código TYPE_*, ushort unused, uint size
con el número de elementos y uint offset desde el inicio del fichero. Reglas que lo hacen
utilizable como índice maestro: un tipo aparece como mucho una vez, las entradas van
ordenadas por offset y no se solapan.
| Estructura | Constante | Valor | Tamaño de cada elemento |
|---|---|---|---|
header_item |
TYPE_HEADER_ITEM |
0x0000 |
0x70 |
string_id_item |
TYPE_STRING_ID_ITEM |
0x0001 |
0x04 |
type_id_item |
TYPE_TYPE_ID_ITEM |
0x0002 |
0x04 |
proto_id_item |
TYPE_PROTO_ID_ITEM |
0x0003 |
0x0c |
field_id_item |
TYPE_FIELD_ID_ITEM |
0x0004 |
0x08 |
method_id_item |
TYPE_METHOD_ID_ITEM |
0x0005 |
0x08 |
class_def_item |
TYPE_CLASS_DEF_ITEM |
0x0006 |
0x20 |
call_site_id_item |
TYPE_CALL_SITE_ID_ITEM |
0x0007 |
0x04 |
method_handle_item |
TYPE_METHOD_HANDLE_ITEM |
0x0008 |
0x08 |
map_list |
TYPE_MAP_LIST |
0x1000 |
4 + (size * 12) |
type_list |
TYPE_TYPE_LIST |
0x1001 |
4 + (size * 2) |
annotation_set_ref_list |
TYPE_ANNOTATION_SET_REF_LIST |
0x1002 |
4 + (size * 4) |
annotation_set_item |
TYPE_ANNOTATION_SET_ITEM |
0x1003 |
4 + (size * 4) |
class_data_item |
TYPE_CLASS_DATA_ITEM |
0x2000 |
implícito: hay que analizarlo |
code_item |
TYPE_CODE_ITEM |
0x2001 |
implícito |
string_data_item |
TYPE_STRING_DATA_ITEM |
0x2002 |
implícito |
debug_info_item |
TYPE_DEBUG_INFO_ITEM |
0x2003 |
implícito |
annotation_item |
TYPE_ANNOTATION_ITEM |
0x2004 |
implícito |
encoded_array_item |
TYPE_ENCODED_ARRAY_ITEM |
0x2005 |
implícito |
annotations_directory_item |
TYPE_ANNOTATIONS_DIRECTORY_ITEM |
0x2006 |
implícito |
hiddenapi_class_data_item |
TYPE_HIDDENAPI_CLASS_DATA_ITEM |
0xF000 |
implícito |
El 0x70 de TYPE_HEADER_ITEM queda obsoleto en v041, donde la cabecera mide 0x78.
⚠️ sin verificar: la tabla oficial no se ha actualizado para el formato contenedor y no se
ha comprobado qué emiten las herramientas que sí generan v041.
Por qué es la vía fiable. Quien recorre el fichero por la cabecera tiene que adivinar
dónde empieza y acaba cada estructura de data, y no ve call_site_ids ni
method_handles. Quien recorre el map_list obtiene el inventario completo, con garantía
de orden y no solapamiento, detecta de inmediato huecos y solapes que delatan manipulación,
y puede saltarse lo que no entiende usando el offset de la entrada siguiente.
Decodificado sobre el mismo classes.dex, en el orden real del fichero:
map_off = 0x5b9ac0 entradas = 17
0x0000 HEADER_ITEM 1 0x000000 0x2003 DEBUG_INFO_ITEM 1113 0x43dae3
0x0001 STRING_ID_ITEM 39099 0x000070 0x1001 TYPE_LIST 5989 0x448d98
0x0002 TYPE_ID_ITEM 7423 0x02635c 0x2002 STRING_DATA_ITEM 39099 0x458ee0
0x0003 PROTO_ID_ITEM 8296 0x02d758 0x2004 ANNOTATION_ITEM 165 0x576052
0x0004 FIELD_ID_ITEM 21659 0x045c38 0x2000 CLASS_DATA_ITEM 5653 0x576ac3
0x0005 METHOD_ID_ITEM 34901 0x070110 0x2005 ENCODED_ARRAY 18 0x5b826a
0x0006 CLASS_DEF_ITEM 5971 0x0b43b8 0x1003 ANNOTATION_SET 149 0x5b8398
0x2001 CODE_ITEM 28006 0x0e2e18 0x2006 ANNOT_DIRECTORY 197 0x5b88a8
0x1000 MAP_LIST 1 0x5b9ac0
(tipo, nº de elementos, offset — en el orden real del fichero)
Se cierra solo: 0x5b9ac0 + 4 + 17 × 12 = 6.003.600 = file_size. Y el orden de tipos no
es el numérico: TYPE_CODE_ITEM (0x2001) va antes que TYPE_TYPE_LIST (0x1001).
Ordenar por código TYPE_* en vez de por offset es un error fácil de cometer y difícil de
detectar. Un DEX sin campos declarados simplemente no lleva entrada FIELD_ID_ITEM:
la ausencia de un tipo es legítima y significa cero elementos.
6. Las tablas de índices
Ninguna guarda texto: todas terminan apuntando a string_ids. Salvo method_handles, todas
están ordenadas y sin duplicados, lo que permite bisección y hace que renombrar un
identificador desplace media tabla.
| Estructura | Campo | Offset | Tamaño | Tipo | Significado |
|---|---|---|---|---|---|
string_id_item |
string_data_off |
0x00 | 4 | uint |
Offset del string_data_item. Sin requisito de alineación |
type_id_item |
descriptor_idx |
0x00 | 4 | uint |
Índice en string_ids del TypeDescriptor |
proto_id_item |
shorty_idx |
0x00 | 4 | uint |
Índice en string_ids del ShortyDescriptor |
return_type_idx |
0x04 | 4 | uint |
Índice en type_ids del tipo de retorno |
|
parameters_off |
0x08 | 4 | uint |
Offset a un type_list de parámetros, o 0. No puede contener void |
|
field_id_item |
class_idx |
0x00 | 2 | ushort |
Índice en type_ids del definidor. Debe ser una clase |
type_idx |
0x02 | 2 | ushort |
Índice en type_ids del tipo del campo |
|
name_idx |
0x04 | 4 | uint |
Índice en string_ids del nombre |
|
method_id_item |
class_idx |
0x00 | 2 | ushort |
Índice en type_ids. Clase o array, no primitivo |
proto_idx |
0x02 | 2 | ushort |
Índice en proto_ids |
|
name_idx |
0x04 | 4 | uint |
Índice en string_ids del nombre |
|
call_site_id_item |
call_site_off |
0x00 | 4 | uint |
Offset a un call_site_item, que es un encoded_array_item |
method_handle_item |
method_handle_type |
0x00 | 2 | ushort |
STATIC_PUT 0x00, STATIC_GET 0x01, INSTANCE_PUT 0x02, INSTANCE_GET 0x03, INVOKE_STATIC 0x04, INVOKE_INSTANCE 0x05, INVOKE_CONSTRUCTOR 0x06, INVOKE_DIRECT 0x07, INVOKE_INTERFACE 0x08 |
unused |
0x02 | 2 | ushort |
Sin usar | |
field_or_method_id |
0x04 | 2 | ushort |
Índice en field_ids o en method_ids, según el tipo |
|
unused |
0x06 | 2 | ushort |
Sin usar |
Criterios de ordenación: string_ids por contenido de la cadena, comparando valores de code
point UTF-16 y sin sensibilidad al locale; type_ids por índice en string_ids;
proto_ids por tipo de retorno y luego por lista de argumentos en orden lexicográfico;
field_ids y method_ids por definidor, luego nombre, luego tipo o prototipo; class_defs
de modo que superclase e interfaces aparezcan antes que la clase que las usa;
call_site_ids de forma ascendente por call_site_off. method_handles no está ordenado
y admite duplicados, que corresponden a instancias distintas de method handle.
NO_INDEX = 0xffffffff marca la ausencia de índice. No vale 0 porque 0 suele ser un índice
válido, y está elegido para que quepa en un solo byte codificado como uleb128p1.
Un call_site_item es un encoded_array_item cuyos tres primeros elementos son siempre el
method handle del bootstrap linker (VALUE_METHOD_HANDLE), el nombre del método a
resolver (VALUE_STRING) y su tipo (VALUE_METHOD_TYPE); lo que sigue son constantes que
se pasan al bootstrap, que debe devolver java.lang.invoke.CallSite y recibir como tres
primeros parámetros java.lang.invoke.Lookup, java.lang.String y
java.lang.invoke.MethodType. Un volcado real de estas dos tablas, con la instrucción
invoke-custom que las consume, está en
Bytecode Dalvik, sección 7.2.
class_def_item
├── class_idx ────────────► type_ids ──descriptor_idx──► string_ids
├── superclass_idx ───────► type_ids | o NO_INDEX si es java.lang.Object
├── interfaces_off ───────► type_list ──type_idx──► type_ids
├── source_file_idx ──────► string_ids | o NO_INDEX
├── annotations_off ──────► annotations_directory_item
├── static_values_off ────► encoded_array_item
└── class_data_off ──────► class_data_item
├── encoded_field[] ──field_idx_diff──► field_ids ──┬─class_idx─► type_ids
│ ├─type_idx──► type_ids
│ └─name_idx──► string_ids
└── encoded_method[] ──method_idx_diff─► method_ids ─┬─class_idx─► type_ids
└── code_off ──► code_item ├─proto_idx─► proto_ids
└── debug_info_off ──► … └─name_idx──► string_ids
7. Descriptores de tipo y shorty
| Sintaxis | Significado | Sintaxis | Significado | |
|---|---|---|---|---|
V |
void, solo como retorno |
J |
long |
|
Z |
boolean |
F |
float |
|
B |
byte |
D |
double |
|
S |
short |
Lnombre/completo/Clase; |
La clase nombre.completo.Clase |
|
C |
char |
[descriptor |
Array, recursivo. Máximo 255 dimensiones | |
I |
int |
El punto del paquete se escribe con barra: Ljava/lang/String;. Un array bidimensional de
enteros es [[I; uno de cadenas, [Ljava/lang/String;.
Los nombres admisibles los fija la gramática de SimpleName, mucho más permisiva que la
de Java: además de letras, dígitos, $, - y _, admite casi todos los code points no
ASCII que no sean de control ni especiales, y desde la versión 040 también el espacio.
Eso es lo que explotan algunos ofuscadores para generar identificadores que un decompilador
a Java no puede reproducir como código válido; ver
Ofuscadores comerciales.
Un shorty es la forma corta de un prototipo: tipo de retorno seguido de los tipos de
los parámetros, colapsando toda referencia en una única L, sin paréntesis ni punto y
coma. El runtime lo usa para decidir el paso de argumentos sin resolver los tipos.
| Firma Java | Prototipo | shorty |
|---|---|---|
void f() |
()V |
V |
int f(int, int) |
(II)I |
III |
String f(String, int) |
(Ljava/lang/String;I)Ljava/lang/String; |
LLI |
long[] f(double) |
(D)[J |
LD |
La última fila es la reveladora: [J es una referencia, así que en el shorty es L.
8. Codificaciones
8.1 LEB128
LEB128 («Little-Endian Base 128») viene de DWARF3. En un DEX solo codifica cantidades
de 32 bits, así que un valor ocupa de uno a cinco bytes. Cada byte lleva su bit más
significativo a 1 salvo el último, que lo lleva a 0; los otros siete son carga útil, con los
siete menos significativos del valor en el primer byte.
byte 0 byte 1
┌───┬───┬───┬───┬───┬───┬───┬───┐┌───┬────┬────┬────┬────┬───┬───┬───┐
│ 1 │b6 │b5 │b4 │b3 │b2 │b1 │b0 ││ 0 │b13 │b12 │b11 │b10 │b9 │b8 │b7 │
└───┴───┴───┴───┴───┴───┴───┴───┘└───┴────┴────┴────┴────┴───┴───┴───┘
↑ hay continuación ↑ último byte
uleb128 interpreta como 0 los bits no representados. sleb128 extiende con signo el bit de
carga más significativo del último byte. uleb128p1 codifica el valor más uno como
uleb128: sirve donde el número debe ser no negativo o exactamente -1 (el NO_INDEX),
y hace que ese -1 —y solo ese negativo— quepa en un byte.
| Secuencia | Como sleb128 |
Como uleb128 |
Como uleb128p1 |
|---|---|---|---|
00 |
0 | 0 | −1 |
01 |
1 | 1 | 0 |
7f |
−1 | 127 | 126 |
80 7f |
−128 | 16.256 | 16.255 |
Trabajado, el caso 80 7f. 0x80 = 1000 0000: bit alto a 1, hay continuación, carga
000 0000 = 0. 0x7f = 0111 1111: bit alto a 0, es el último, carga 111 1111 = 127. De
ahí uleb128 = 0 | (127 << 7) = 16.256, uleb128p1 = 16.255, y sleb128 = −128
porque el bit más significativo de la carga del último byte vale 1 y se extiende con signo.
Un caso real, del encoded_type_addr_pair de la sección 11.2, donde d7 15 codifica un
type_idx: 0xd7 = 1101 0111 continúa con carga 87; 0x15 = 0001 0101 es el último con
carga 21; valor = 87 | (21 << 7) = 2.775, que dexdump resuelve como
Ljava/io/IOException;.
El error clásico al implementar es no acotar la longitud. Un uleb128 legítimo nunca pasa de
cinco bytes; un fichero manipulado puede traer una secuencia infinita de bytes con el bit
alto puesto y colgar a un lector ingenuo.
8.2 MUTF-8
Las cadenas del DEX no están en UTF-8, sino en MUTF-8 (Modified UTF-8), que difiere
del estándar en cuatro puntos, los cuatro con consecuencias:
- Solo se usan las formas de uno, dos y tres bytes. Nunca cuatro.
- Los code points
U+10000–U+10ffffse codifican como par subrogado, cada mitad con su forma de tres bytes: seis bytes donde UTF-8 usaría cuatro. - El code point
U+0000se codifica en dos bytes (c0 80), no en uno. - Un byte nulo suelto marca el final de la cadena, como en C.
Los dos primeros se resumen en que MUTF-8 es una codificación de UTF-16, no de Unicode;
los dos últimos permiten a la vez incluir U+0000 dentro de una cadena y seguir tratándola
como cadena terminada en nulo. Formalmente está más cerca de CESU-8.
Comprobado compilando con d8 una clase con tres constantes —"a\0b", "😀" (U+1F600) y
"ñ" (U+00F1)— y volcando los string_data_item. El primer byte es el utf16_size en
uleb128; el último, el terminador:
idx 9 off=0x0178 utf16_size=3 bytes= 03 61 c0 80 62 00
idx 11 off=0x021d utf16_size=1 bytes= 01 c3 b1 00
idx 12 off=0x0221 utf16_size=2 bytes= 02 ed a0 bd ed b8 80 00
"a\0b"→61(a),c0 80(el nulo en dos bytes),62(b),00(fin). En UTF-8 estándar el nulo sería un solo00y la cadena se habría cortado tras laa."ñ"→c3 b1, idéntico a UTF-8: por debajo deU+0800no hay diferencia."😀"→ed a0 bd(U+D83D) +ed b8 80(U+DE00), el par subrogado. UTF-8 estándar habría emitidof0 9f 98 80, cuatro bytes.
Y el detalle que rompe más implementaciones: utf16_size cuenta unidades de código
UTF-16, no bytes ni caracteres. El emoji vale 2; "a\0b" vale 3 aunque ocupe 4 bytes.
Qué se rompe al confundirlos: un decodificador UTF-8 estricto falla ante c0 80 y ante
las subrogadas, que son secuencias inválidas en UTF-8 (String::from_utf8 en Rust devuelve
error; bytes.decode('utf-8') en Python lanza UnicodeDecodeError); uno permisivo produce
U+FFFD y pierde el contenido en silencio, que es peor; reservar utf16_size bytes
provoca lecturas cortas en cuanto hay un carácter no ASCII; y strcmp sobre MUTF-8 no da
el orden correcto en presencia de U+0000, cosa que la especificación advierte de forma
explícita. Además se admiten cadenas con subrogadas sueltas o desordenadas: rechazarlas
es responsabilidad de la capa superior.
9. La sección de datos
string_data_item (sin alineación): uleb128 utf16_size con la longitud decodificada
en unidades UTF-16, seguido de las unidades MUTF-8 y un byte 0. La longitud codificada está
implícita en la posición de ese nulo; los dos datos son redundantes a propósito y pueden
discrepar en un fichero manipulado.
type_list (alineación 4): uint size y size type_item, cada uno un ushort con un
índice en type_ids. Lo usan class_def_item.interfaces_off y
proto_id_item.parameters_off.
encoded_value: el primer byte lleva el tipo en los cinco bits bajos y un argumento en
los tres altos, (value_arg << 5) | value_type. En casi todos los casos value_arg es
tamaño − 1 del valor que sigue: 0 significa un byte, 7 significa ocho.
| Tipo | value_type |
value_arg |
Formato del valor |
|---|---|---|---|
VALUE_BYTE |
0x00 |
0 | ubyte[1] |
VALUE_SHORT / VALUE_CHAR |
0x02 / 0x03 |
tamaño−1 | Entero de 2 bytes, extendido con signo / con ceros |
VALUE_INT / VALUE_LONG |
0x04 / 0x06 |
tamaño−1 | Entero con signo, extendido con signo |
VALUE_FLOAT / VALUE_DOUBLE |
0x10 / 0x11 |
tamaño−1 | IEEE754, extendido con ceros a la derecha |
Índices: VALUE_METHOD_TYPE 0x15 → proto_ids; VALUE_METHOD_HANDLE 0x16 → method_handles; VALUE_STRING 0x17 → string_ids; VALUE_TYPE 0x18 → type_ids; VALUE_FIELD 0x19 y VALUE_ENUM 0x1b → field_ids; VALUE_METHOD 0x1a → method_ids |
tamaño−1 (0…3) | Entero sin signo, extendido con ceros | |
VALUE_ARRAY / VALUE_ANNOTATION |
0x1c / 0x1d |
0 | encoded_array / encoded_annotation; el tamaño va implícito |
VALUE_NULL |
0x1e |
0 | Ninguno |
VALUE_BOOLEAN |
0x1f |
el valor (0…1) | Ninguno: el bit viaja en value_arg |
Dos trampas: el «extendido con ceros a la derecha» de los flotantes —un float 1.0
cabe en un byte— y el VALUE_BOOLEAN, sin payload. Un encoded_array es un uleb128 con el
número de elementos seguido de esos elementos concatenados; el encoded_array_item de la
sección data no es más que eso, y es lo que apunta class_def_item.static_values_off.
Anotaciones. El annotations_directory_item (alineación 4) lleva
class_annotations_off, los contadores fields_size, annotated_methods_size y
annotated_parameters_size, y las tres listas correspondientes: field_annotations
(field_idx + annotations_off), method_annotations (method_idx + annotations_off) y
parameter_annotations (method_idx + offset a un annotation_set_ref_list). Las tres van
ordenadas de forma ascendente por su índice y todos los field_id/method_id deben
referirse a la misma clase definidora. Un annotation_set_item (alineación 4) es un
uint size y size offsets a annotation_item, ordenados por type_idx; un
annotation_item (sin alineación) es un ubyte visibility más un encoded_annotation.
| Constante | Valor | Significado | Equivalente Java |
|---|---|---|---|
VISIBILITY_BUILD |
0x00 |
Solo visible en tiempo de compilación | @Retention(CLASS) |
VISIBILITY_RUNTIME |
0x01 |
Visible en ejecución | @Retention(RUNTIME) |
VISIBILITY_SYSTEM |
0x02 |
Visible en ejecución, pero solo para el sistema | — |
VISIBILITY_SYSTEM es el que usan las anotaciones de sistema, donde el DEX guarda lo
que en un .class iría en atributos propios: dalvik.annotation.Signature (los genéricos,
como array de cadenas a concatenar), Throws, InnerClass, EnclosingClass,
EnclosingMethod, MemberClasses, MethodParameters y AnnotationDefault. Para quien
decompila esto importa: los genéricos sobreviven en dalvik.annotation.Signature, no en
la firma del método, y si el ofuscador los elimina —R8 lo hace salvo que se le pida
-keepattributes Signature— ya no se puede reconstruir List<String>, solo List. Ver
R8 y ProGuard.
10. class_def_item y class_data_item
class_def_item: 32 bytes, alineación 4.
| Campo | Offset | Tamaño | Tipo | Significado |
|---|---|---|---|---|
class_idx |
0x00 | 4 | uint |
Índice en type_ids de esta clase. Debe ser una clase |
access_flags |
0x04 | 4 | uint |
Ver sección 13 |
superclass_idx |
0x08 | 4 | uint |
Índice en type_ids de la superclase, o NO_INDEX si es raíz |
interfaces_off |
0x0c | 4 | uint |
Offset a un type_list con las interfaces, o 0. Sin duplicados |
source_file_idx |
0x10 | 4 | uint |
Índice en string_ids del fichero fuente, o NO_INDEX |
annotations_off |
0x14 | 4 | uint |
Offset a annotations_directory_item, o 0 |
class_data_off |
0x18 | 4 | uint |
Offset a class_data_item, o 0 (p. ej. una interfaz marcadora) |
static_values_off |
0x1c | 4 | uint |
Offset a encoded_array_item con los valores iniciales de los campos estáticos, o 0 |
El array de static_values_off puede ser más corto que el número de campos estáticos:
los que sobran se inicializan al cero o al null de su tipo. Truncarlo es una optimización
legítima que un lector debe tolerar.
class_data_item (sin alineación, todo uleb128) lleva primero los cuatro tamaños
—static_fields_size, instance_fields_size, direct_methods_size,
virtual_methods_size— y después las cuatro listas en ese mismo orden. Los métodos
directos son los estáticos, privados y constructores; los virtuales, ninguna de esas
tres cosas, y la lista no incluye los heredados salvo que la clase los sobrescriba.
| Elemento | Campo | Tipo | Significado |
|---|---|---|---|
encoded_field |
field_idx_diff |
uleb128 |
Diferencia respecto al índice del elemento anterior |
access_flags |
uleb128 |
Ver sección 13 | |
encoded_method |
method_idx_diff |
uleb128 |
Diferencia respecto al anterior |
access_flags |
uleb128 |
||
code_off |
uleb128 |
Offset al code_item, o 0 si el método es abstract o native |
La codificación delta es donde se equivocan los parsers. Los índices no son absolutos,
son incrementos: el primer elemento de cada una de las cuatro listas lleva su índice
directamente; cada siguiente lleva la diferencia respecto al anterior de su propia lista;
y el acumulador se reinicia en cada lista, de modo que instance_fields no continúa
desde donde acabó static_fields, ni virtual_methods desde direct_methods.
Los cuatro errores típicos, por frecuencia: llevar un solo acumulador para las cuatro
listas; llevar uno para los campos y otro para los métodos —el más sutil, porque produce
salidas plausibles—; tratar el primer elemento como delta sobre cero cuando su lista está
vacía y la siguiente no; e interpretar el delta como con signo, cuando es uleb128 y las
listas van en orden creciente, así que nunca es negativo. Como no hay repeticiones, un delta
de 0 solo puede aparecer en el primer elemento de una lista y solo si su índice es 0:
cualquier otro 0 es corrupción.
11. code_item
Alineación 4 bytes. Es el cuerpo de un método.
| Campo | Offset | Tamaño | Tipo | Significado |
|---|---|---|---|---|
registers_size |
0x00 | 2 | ushort |
Número total de registros del marco |
ins_size |
0x02 | 2 | ushort |
Número de palabras de argumentos de entrada |
outs_size |
0x04 | 2 | ushort |
Palabras de argumentos de salida que necesita para invocar |
tries_size |
0x06 | 2 | ushort |
Número de try_item |
debug_info_off |
0x08 | 4 | uint |
Offset al debug_info_item, o 0 si no hay información de depuración |
insns_size |
0x0c | 4 | uint |
Tamaño del bytecode en unidades de 16 bits, no en bytes |
insns |
0x10 | 2×insns_size |
ushort[] |
El bytecode (Bytecode Dalvik) |
padding |
— | 0 o 2 | ushort = 0 |
Solo si tries_size != 0 y insns_size es impar |
tries |
— | 8×tries_size |
try_item[] |
Solo si tries_size != 0 |
handlers |
— | var | encoded_catch_handler_list |
Solo si tries_size != 0 |
Los tres números de registros se explican en Bytecode Dalvik,
sección 3: registers_size es el total, los últimos ins_size registros contienen los
argumentos (incluido el this implícito) y outs_size es el ancho del mayor invoke-* del
cuerpo.
Sobre alineación hay dos reglas distintas que se confunden. El campo padding es el que
alinea tries a 4 bytes y solo existe si hay tries y el bytecode ocupa un número impar de
unidades. Aparte de eso, cada code_item empieza en múltiplo de 4, lo que obliga a rellenar
entre un code_item y el siguiente aunque no haya tries; ese relleno no es un campo de
la estructura y no aparece en la tabla.
try_item mide 8 bytes: uint start_addr (inicio del bloque cubierto, en unidades de 16
bits), ushort insn_count (unidades cubiertas; la última incluida es
start_addr + insn_count − 1) y ushort handler_off (offset en bytes desde el inicio
del encoded_catch_handler_list). Los try_item no se solapan y van de dirección baja a
alta; handler_off es un offset de bytes, no un índice, precisamente para que varios
try_item puedan compartir handler apuntando al mismo sitio.
El encoded_catch_handler_list es un uleb128 size seguido de esos encoded_catch_handler
concatenados. Cada uno lleva un sleb128 size, luego abs(size) pares
encoded_type_addr_pair (uleb128 type_idx + uleb128 addr) y, solo si size no es
positivo, un uleb128 catch_all_addr. El signo es la trampa: 0 significa «hay
catch-all y ningún tipo explícito», 2 significa «dos tipos y ningún catch-all» y −1
significa «un tipo y además catch-all». Quien use uleb128 en vez de sleb128
decodifica ese −1 como un número enorme y se pierde.
11.1 Un code_item con try/catch, byte a byte
androidx.core.graphics.TypefaceCompatUtil.closeQuietly:(Ljava/io/Closeable;)V, en
com.termux_1002.apk del corpus. Lo que dice dexdump -d:
0e2590: |[0e2590] …closeQuietly:(Ljava/io/Closeable;)V
0e25a0: 3800 0500 |0000: if-eqz v0, 0005 // +0005
0e25a4: 7210 cd4f 0000 |0002: invoke-interface {v0}, Ljava/io/Closeable;.close:()V
0e25aa: 0e00 |0005: return-void
catches : 1
0x0002 - 0x0005
Ljava/io/IOException; -> 0x0005
Y los bytes, con xxd -s $((0x0e2590)) -l 48:
000e2590: 0100 0100 0100 0100 cee4 1d00 0600 0000 ................
000e25a0: 3800 0500 7210 cd4f 0000 0e00 0200 0000 8...r..O........
000e25b0: 0300 0100 0101 d715 0500 0000 0500 0300 ................
| Offset | Bytes | Interpretación |
|---|---|---|
0x0e2590 |
0100 0100 0100 0100 |
registers_size = 1, ins_size = 1, outs_size = 1, tries_size = 1 |
0x0e2598 |
cee4 1d00 |
debug_info_off = 0x001de4ce |
0x0e259c |
0600 0000 |
insns_size = 6 → par, así que no hay campo padding |
0x0e25a0 |
3800 … 0e00 |
insns, 12 bytes |
0x0e25ac |
0200 0000 |
try_item.start_addr = 2 |
0x0e25b0 |
0300 |
try_item.insn_count = 3 → cubre 0x0002–0x0004 |
0x0e25b2 |
0100 |
try_item.handler_off = 1 byte dentro de la lista |
0x0e25b4 |
01 |
encoded_catch_handler_list.size = 1 |
0x0e25b5 |
01 |
encoded_catch_handler.size = +1 → un tipo, sin catch-all |
0x0e25b6 |
d7 15 |
type_idx = 2.775 → Ljava/io/IOException; (sección 8.1) |
0x0e25b8 |
05 |
addr = 5 → el handler está en 0x0005 |
0x0e25b9 |
00 00 00 |
Relleno hasta 0x0e25bc, donde empieza el siguiente code_item |
handler_off vale 1 porque salta el size de la lista. Y el rango que imprime dexdump
(0x0002 - 0x0005) es exclusivo por la derecha, mientras que la especificación define el
último cubierto como 0x0004: no es una contradicción, es una convención de impresión
distinta que confunde a quien compara salidas.
12. debug_info_item
Sin alineación. Es una máquina de estados byte a byte inspirada en DWARF3 que, al
interpretarla, emite la tabla de posiciones (dirección → número de línea) y la información de
variables locales de un code_item.
Tiene cinco registros: address (offset en unidades de 16 bits, arranca en 0 y solo puede
crecer), line (se inicializa en la cabecera, sube y baja pero nunca por debajo de 1),
source_file (inicializado con class_def_item.source_file_idx) y los booleanos
prologue_end y epilogue_begin, ambos a falso. La cabecera es uleb128 line_start,
uleb128 parameters_size (número de nombres de parámetro, excluyendo el this) y
uleb128p1 parameter_names[] (índice en string_ids, o NO_INDEX).
| Nombre | Valor | Argumentos | Efecto |
|---|---|---|---|
DBG_END_SEQUENCE |
0x00 |
— | Termina la secuencia |
DBG_ADVANCE_PC |
0x01 |
uleb128 addr_diff |
Avanza address sin emitir posición |
DBG_ADVANCE_LINE |
0x02 |
sleb128 line_diff |
Cambia line sin emitir posición |
DBG_START_LOCAL |
0x03 |
uleb128 register_num, uleb128p1 name_idx, uleb128p1 type_idx |
Declara una local viva en ese registro |
DBG_START_LOCAL_EXTENDED |
0x04 |
lo anterior más uleb128p1 sig_idx |
Ídem, con firma genérica |
DBG_END_LOCAL |
0x05 |
uleb128 register_num |
La local de ese registro sale de ámbito |
DBG_RESTART_LOCAL |
0x06 |
uleb128 register_num |
Reintroduce la última local de ese registro, con su nombre y tipo |
DBG_SET_PROLOGUE_END |
0x07 |
— | Marca el fin del prólogo |
DBG_SET_EPILOGUE_BEGIN |
0x08 |
— | Marca el inicio del epílogo |
DBG_SET_FILE |
0x09 |
uleb128p1 name_idx |
Cambia el fichero fuente de las posiciones siguientes |
| especiales | 0x0a…0xff |
— | Avanzan line y address, emiten posición y limpian los dos flags |
Los especiales cubren 246 valores y codifican en un byte los incrementos pequeños:
DBG_FIRST_SPECIAL = 0x0a DBG_LINE_BASE = -4 DBG_LINE_RANGE = 15
adjusted_opcode = opcode - DBG_FIRST_SPECIAL
line += DBG_LINE_BASE + (adjusted_opcode % DBG_LINE_RANGE)
address += (adjusted_opcode / DBG_LINE_RANGE)
Qué recupera un decompilador de aquí, y qué se pierde. De la tabla de posiciones salen
los números de línea, sin los cuales las trazas dicen (Unknown Source) en vez de
(Fichero.java:123) y retrace deja de poder mapear líneas. De DBG_START_LOCAL salen los
nombres y tipos de las variables locales, sin los cuales el decompilador inventa v0,
i, obj; de su variante extendida, los genéricos de esas locales; de parameter_names,
los nombres de los parámetros, que si faltan se convierten en arg0, arg1; y de
source_file_idx más DBG_SET_FILE, el nombre del fichero fuente.
Eliminarlo es barato y muy rentable para quien ofusca: basta con poner debug_info_off a 0 y
source_file_idx a NO_INDEX. El código sigue ejecutándose igual, porque nada de esto
participa en la semántica. Se ve en el corpus: en com.looker.droidify_710.apk la primera
clase trae source_file_idx : -1 (unknown) y positions : vacío, mientras que en
com.termux_1002.apk los métodos traen 0x0000 line=23 y
locals : … reg=3 this Lcom/termux/app/TermuxApplication;. Un dato de escala del fichero de
droidify: 28.006 code_item y solo 1.113 debug_info_item; el 96 % de los métodos no
lleva información de depuración alguna.
13. access_flags
Los mismos bits sirven en class_def_item, encoded_field, encoded_method y la anotación
InnerClass, con significados distintos según el contexto. Ojo a las colisiones en 0x40 y
0x80, que dependen de si el elemento es campo o método.
| Nombre | Valor | Clases | Campos | Métodos |
|---|---|---|---|---|
ACC_PUBLIC |
0x1 |
public |
public |
public |
ACC_PRIVATE |
0x2 |
private * |
private |
private |
ACC_PROTECTED |
0x4 |
protected * |
protected |
protected |
ACC_STATIC |
0x8 |
sin referencia this externa * |
global a la clase | sin argumento this |
ACC_FINAL |
0x10 |
no derivable | inmutable tras construir | no sobrescribible |
ACC_SYNCHRONIZED |
0x20 |
— | — | synchronized. Solo válido si también está ACC_NATIVE |
ACC_VOLATILE |
0x40 |
— | volatile |
— |
ACC_BRIDGE |
0x40 |
— | — | Método puente generado por el compilador |
ACC_TRANSIENT |
0x80 |
— | transient |
— |
ACC_VARARGS |
0x80 |
— | — | Último argumento variádico |
ACC_NATIVE |
0x100 |
— | — | native |
ACC_INTERFACE |
0x200 |
interfaz | — | — |
ACC_ABSTRACT |
0x400 |
abstract |
— | abstract |
ACC_STRICT |
0x800 |
— | — | strictfp |
ACC_SYNTHETIC |
0x1000 |
no está en el código fuente | ídem | ídem |
ACC_ANNOTATION |
0x2000 |
clase de anotación | — | — |
ACC_ENUM |
0x4000 |
tipo enumerado | valor enumerado | — |
| (sin usar) | 0x8000 |
— | — | — |
ACC_CONSTRUCTOR |
0x10000 |
— | — | Constructor de clase o de instancia |
ACC_DECLARED_SYNCHRONIZED |
0x20000 |
— | — | Declarado synchronized. No afecta a la ejecución, solo a la reflexión |
* ACC_PRIVATE, ACC_PROTECTED y ACC_STATIC solo se admiten en anotaciones InnerClass;
nunca deben aparecer en un class_def_item.
Los dos que más aparecen leyendo código real son ACC_SYNTHETIC, que marca todo lo que
generó el compilador —clases de lambda, puentes, accesores de clases internas—, y
ACC_CONSTRUCTOR, que acompaña siempre a <init> y <clinit>; ejemplo real de droidify:
access : 0x11008 (STATIC SYNTHETIC CONSTRUCTOR). Y los dos synchronized son cosas
distintas: ACC_SYNCHRONIZED hace que el runtime tome el monitor y solo vale con
ACC_NATIVE, mientras que ACC_DECLARED_SYNCHRONIZED es puramente informativo, porque en un
método no nativo el bloqueo se implementa con monitor-enter y monitor-exit explícitos.
14. Checksums: qué recalcular y en qué orden
| Campo | Offset | Algoritmo | Dominio |
|---|---|---|---|
checksum |
0x08 | Adler-32 | Bytes [12, file_size): el fichero menos magic y menos el propio checksum |
signature |
0x0c | SHA-1 | Bytes [32, file_size): el fichero menos magic, checksum y la propia signature |
La signature está dentro del dominio del checksum. De ahí el orden obligatorio:
1. Escribir todos los cambios estructurales del fichero.
2. Fijar file_size (y, en v041, container_size y header_offset).
3. Calcular SHA-1 sobre [32, file_size) y escribirlo en el offset 0x0c.
4. Calcular Adler-32 sobre [12, file_size) y escribirlo en el offset 0x08.
Hacerlo al revés produce un fichero cuyo checksum no cuadra, porque el paso 3 modifica
bytes que el paso 4 ya había resumido: es el fallo número uno de los scripts de parcheo
caseros. Y conviene insistir en que la signature no es una firma criptográfica de
aplicación, es un identificador de contenido; la firma real del APK vive en el contenedor
(ver Esquemas de firma).
Comprobado recalculando ambos valores con zlib.adler32 y hashlib.sha1 de Python:
classes.dex de com.looker.droidify_710.apk
checksum cabecera=0xe5d77932 recalculado=0xe5d77932 OK
signature cabecera=d1ddfd394fe96f93288bfcf9ac38a4e90c44e660
recalculado=d1ddfd394fe96f93288bfcf9ac38a4e90c44e660 OK
classes.dex de com.termux_1002.apk
checksum cabecera=0x18f8229a recalculado=0x18f8229a OK
signature cabecera=4cd2ed0ec3bc93f063ad695cb76835015ce580af
recalculado=4cd2ed0ec3bc93f063ad695cb76835015ce580af OK
La herramienta oficial hace la primera comprobación con dexdump -c, que responde
Checksum verified.
15. El límite de 65.536 referencias y multidex
El límite no viene de la cabecera. method_ids_size y field_ids_size son uint de 32
bits sin tope declarado. Viene del bytecode: invoke-virtual, invoke-super,
invoke-direct, invoke-static e invoke-interface usan el formato 35c, cuyo layout es
A|G|op BBBB F|E|D|C, donde BBBB es una única unidad de 16 bits con el índice de
método; sus variantes /range usan 3rc, AA|op BBBB CCCC, con el mismo BBBB; y los
accesos a campo (iget/iput en 22c, sget/sput en 21c) usan igualmente un índice de
16 bits. Eso direcciona los índices 0 a 65.535, es decir 65.536 referencias distintas;
como en informática kilo vale 1.024 y 65.536 = 64 × 1.024, se lo conoce como 64K reference
limit. Para tipos y prototipos el tope está además declarado en la cabecera —«como máximo
65535»— y reforzado en AOSP con un DCHECK_LT(result, 65536U) al construir un TypeIndex.
Las cadenas son la excepción: existe const-string/jumbo (formato 31c) con índice de 32
bits, precisamente para superar el tope. No hay equivalente jumbo para métodos, campos
ni tipos, y esa asimetría es la razón de que el límite se enuncie sobre referencias a
método.
multidex reparte las clases entre varios ficheros —classes.dex, classes2.dex,
classes3.dex…, sin huecos en la numeración—, cada uno con sus propias tablas y su
propio presupuesto de 65.536 referencias. Una clase solo puede definirse en uno; si aparece
en dos, gana la del primer DEX en orden de carga, lo que convierte el orden de los
ficheros en información semántica y es lo que hay que preservar al fusionar splits (ver
Fusión de splits).
Desde Android 5.0 (API 21) ART carga varios DEX de forma nativa y multidex está
activo por defecto sin biblioteca adicional. Con minSdkVersion 20 o inferior hace falta
androidx.multidex y heredar de MultiDexApplication o llamar a MultiDex.install(this);
ese legacy multidex instala los DEX secundarios en la partición de datos durante el
arranque, lo que puede provocar ANR si son grandes, y por debajo de Android 4.0 se puede
topar antes con el límite de linearalloc que con el de índices.
En el corpus, 37 de los 58 APK standalone llevan más de un classes*.dex: 21 llevan
uno, 20 llevan dos, 9 llevan tres y el resto entre cuatro y once —el máximo es
org.totschnig.myexpenses_858.apk—. Y un matiz que evita conclusiones erróneas: superar el
tope no es la única razón para partir. En com.looker.droidify_710.apk, classes.dex
declara 34.901 method_ids, muy por debajo del límite, y aun así hay un classes2.dex; al
mirarlo, 547 de sus 583 clases llevan el prefijo Lj$/ y el resto son
Ljava/util/function/*: es la biblioteca del núcleo reescrita que emite el core library
desugaring de D8/R8, colocada en su propio fichero.
16. Recetas
Verificadas el 12 de agosto de 2026 con build-tools 37.0.0 sobre macOS.
BT=~/Library/Android/sdk/build-tools/37.0.0
CORPUS=~/corpus-apk
DEXDIR=$(mktemp -d)
unzip -o -q $CORPUS/com.looker.droidify_710.apk 'classes*.dex' -d $DEXDIR
| Objetivo | Orden |
|---|---|
| Cabecera del fichero | $BT/dexdump -f $DEXDIR/classes.dex | head -24 |
| Cabecera de cada clase | $BT/dexdump -h $DEXDIR/classes.dex |
| Desensamblar el bytecode | $BT/dexdump -d $DEXDIR/classes.dex |
| Volcar todas las cadenas | $BT/dexdump -s $DEXDIR/classes.dex |
Verificar el checksum |
$BT/dexdump -c $DEXDIR/classes.dex |
| Omitir la información de depuración | $BT/dexdump -d -n $DEXDIR/classes.dex |
| Ver anotaciones | $BT/dexdump -a $DEXDIR/classes.dex |
| Los 128 primeros bytes en crudo | xxd -l 128 $DEXDIR/classes.dex |
Para el map_list no hay orden en las build-tools: se lee el uint del offset 0x34 y
desde ahí un uint de cuenta seguido de tuplas <HHII, que es lo que produce el volcado de
la sección 5. baksmali da una vista textual del mismo contenido, una clase por fichero:
java -jar baksmali.jar disassemble classes.dex -o out/. ⚠️ no ejecutado localmente:
baksmali no está instalado; la sintaxis procede de la documentación del proyecto smali y se
detalla en Desensambladores smali.
Fuentes
- Dalvik executable format — https://source.android.com/docs/core/runtime/dex-format
Consultado el 12 de agosto de 2026 («Last updated 2025-10-03 UTC»). Fuente primaria de
casi todo:
header_item, códigosTYPE_*, tablas de índices, descriptores, LEB128 yMUTF-8,encoded_value, anotaciones,class_data_item,code_item,debug_info_itemyaccess_flags(secciones 3 y 5 a 13). - Dalvik bytecode format — https://source.android.com/docs/core/runtime/dalvik-bytecode
Consultado el 12 de agosto de 2026 («Last updated 2025-01-07 UTC»). De aquí sale
const-string/jumboy el detalle deinvoke-customde las secciones 6 y 15. - Dalvik executable instruction formats —
https://source.android.com/docs/core/runtime/instruction-formats
Consultado el 12 de agosto de 2026 («Last updated 2024-08-26 UTC»). De aquí salen los
layouts
35c,3rc,21c,22cy31cque fijan el límite de la sección 15. - AOSP
art/libdexfile/dex/standard_dex_file.cc— https://android.googlesource.com/platform/art/+/refs/heads/main/libdexfile/dex/standard_dex_file.cc Consultado el 12 de agosto de 2026. De aquí salekDexMagicVersionscon las seis versiones aceptadas y el motivo de que no exista la 036 (sección 4). - AOSP
art/libdexfile/dex/dex_file.h— https://android.googlesource.com/platform/art/+/refs/heads/main/libdexfile/dex/dex_file.h Consultado el 12 de agosto de 2026. De aquí salenkDexEndianConstant,kDexNoIndex32,kSha1DigestSize,kDexContainerVersion = 41, los camposcontainer_size_yheader_offset_(sección 3) y elDCHECK_LT(result, 65536U)(sección 15). - AOSP
art/libdexfile/dex/standard_dex_file.h— https://android.googlesource.com/platform/art/+/refs/heads/main/libdexfile/dex/standard_dex_file.h Consultado el 12 de agosto de 2026. De aquí salekNumDexVersions = 6(sección 4). - Enable multidex for apps with over 64K methods — https://developer.android.com/build/multidex
Consultado el 12 de agosto de 2026. De aquí salen el enunciado del límite, el soporte
nativo desde API 21 y
androidx.multidexparaminSdkVersion≤ 20 (sección 15). - Mediciones sobre el corpus de verificación y el SDK — ejecutadas el 12 de agosto de 2026
con
build-tools37.0.0 (dexdump,d89.2.4-dev),xxdy Python 3.12. De aquí salen los volcados hexadecimales y las salidas dedexdump, la correspondencia--min-api→ versión (sección 4), las distribuciones del corpus (secciones 4 y 15), la comprobación dechecksumysignature(sección 14) y el experimento deMUTF-8(sección 8.2).