Formato DEX

referencia técnica · Revisado el 12 de agosto de 2026

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:

  1. Solo se usan las formas de uno, dos y tres bytes. Nunca cuatro.
  2. Los code points U+10000U+10ffff se codifican como par subrogado, cada mitad con su forma de tres bytes: seis bytes donde UTF-8 usaría cuatro.
  3. El code point U+0000 se codifica en dos bytes (c0 80), no en uno.
  4. 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 solo 00 y la cadena se habría cortado tras la a.
  • "ñ"c3 b1, idéntico a UTF-8: por debajo de U+0800 no hay diferencia.
  • "😀"ed a0 bd (U+D83D) + ed b8 80 (U+DE00), el par subrogado. UTF-8 estándar habría emitido f0 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 0x15proto_ids; VALUE_METHOD_HANDLE 0x16method_handles; VALUE_STRING 0x17string_ids; VALUE_TYPE 0x18type_ids; VALUE_FIELD 0x19 y VALUE_ENUM 0x1bfield_ids; VALUE_METHOD 0x1amethod_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 insns_size ushort[] El bytecode (Bytecode Dalvik)
padding 0 o 2 ushort = 0 Solo si tries_size != 0 y insns_size es impar
tries 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 0x00020x0004
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 0x0a0xff 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

  1. 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ódigos TYPE_*, tablas de índices, descriptores, LEB128 y MUTF-8, encoded_value, anotaciones, class_data_item, code_item, debug_info_item y access_flags (secciones 3 y 5 a 13).
  2. 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/jumbo y el detalle de invoke-custom de las secciones 6 y 15.
  3. 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, 22c y 31c que fijan el límite de la sección 15.
  4. 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í sale kDexMagicVersions con las seis versiones aceptadas y el motivo de que no exista la 036 (sección 4).
  5. 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í salen kDexEndianConstant, kDexNoIndex32, kSha1DigestSize, kDexContainerVersion = 41, los campos container_size_ y header_offset_ (sección 3) y el DCHECK_LT(result, 65536U) (sección 15).
  6. 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í sale kNumDexVersions = 6 (sección 4).
  7. 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.multidex para minSdkVersion ≤ 20 (sección 15).
  8. Mediciones sobre el corpus de verificación y el SDK — ejecutadas el 12 de agosto de 2026 con build-tools 37.0.0 (dexdump, d8 9.2.4-dev), xxd y Python 3.12. De aquí salen los volcados hexadecimales y las salidas de dexdump, la correspondencia --min-api → versión (sección 4), las distribuciones del corpus (secciones 4 y 15), la comprobación de checksum y signature (sección 14) y el experimento de MUTF-8 (sección 8.2).