Bytecode Dalvik

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

1. Qué es y por qué existe

El bytecode Dalvik es el juego de instrucciones que vive dentro del array insns de cada code_item de un fichero DEX (Formato DEX, sección 11). Es lo que ART interpreta o compila con dex2oat, y lo que un decompilador tiene que remontar hasta Java.

Su rasgo definitorio es que está basado en registros y no en pila. La JVM es una máquina de pila: cada operación saca operandos de una pila y empuja el resultado. Dalvik da a cada método un marco de registros de tamaño fijo, decidido en tiempo de compilación, y cada instrucción nombra explícitamente los registros que lee y el que escribe. La motivación es la de siempre en 2008: menos instrucciones que despachar significa menos ciclos por operación en un intérprete, que era la única forma de ejecutar código en los primeros teléfonos.

La diferencia se ve mejor en el mismo código. Este método:

public static int f(int a, int b) { return a + b * 2; }

compilado a .class y volcado con javap -c, y luego pasado por d8 y volcado con dexdump -d:

JVM (pila, 6 instrucciones)          Dalvik (registros, 3 instrucciones)
  0: iload_0                           0000: mul-int/lit8 v1, v1, #int 2
  1: iload_1                           0002: add-int/2addr v0, v1
  2: iconst_2                          0003: return v0
  3: imul
  4: iadd
  5: ireturn

Tres cosas que esto le cambia a quien decompila:

  • No hay pila que reconstruir, pero sí un grafo de definiciones y usos. El decompilador no tiene que simular una pila; tiene que averiguar qué valor lógico vive en cada registro en cada punto, lo que en la práctica significa reconstruir una forma SSA.
  • Los registros se reutilizan. v1 puede ser un int en una parte del método y una referencia a un objeto en otra. Sin debug_info_item no hay nombres ni tipos declarados (Formato DEX, sección 12), y el tipo de cada registro se infiere del uso, exactamente como hace el verificador de ART (sección 9).
  • Las constantes literales pequeñas se funden en la instrucción. iconst_2 + imul se convierten en un solo mul-int/lit8. La estructura del código fuente se pierde antes de lo que se pierde en la JVM.

El diseño se declara «pensado para imitar de forma aproximada arquitecturas reales y convenciones de llamada al estilo de C», y de ahí salen las reglas del modelo de registros.

2. Reglas generales del modelo de ejecución

  • La unidad de almacenamiento del flujo de instrucciones es una cantidad de 16 bits sin signo. Toda instrucción ocupa un número entero de esas unidades: 1, 2, 3 o 5.
  • Los registros son de 32 bits cuando llevan valores de bits. Los de 64 bits usan pares de registros adyacentes, y no hay requisito de alineación para esos pares: vN y vN+1 valen aunque N sea impar.
  • Cuando llevan referencias a objeto, un registro es lo bastante ancho para contener exactamente una. En representación de bits, (Object) null == (int) 0.
  • Los marcos son de tamaño fijo desde su creación: registers_size del code_item.
  • Hay pools de constantes separados e indexados por separado para cadenas, tipos, campos y métodos. Los datos literales de bits van en línea en el flujo de instrucciones.
  • Las instrucciones no están limitadas a un tipo sin necesidad: las que mueven 32 bits sin interpretarlos no distinguen int de float.
  • El orden de los argumentos es destino primero, fuentes después.
  • Existen pseudoinstrucciones que solo transportan datos de longitud variable (packed-switch-payload, sparse-switch-payload, fill-array-data-payload, sección 8). Nunca deben alcanzarse en el flujo normal de ejecución y deben estar alineadas a 4 bytes, para lo cual el generador inserta un nop de relleno si hiciera falta.

3. Modelo de registros

Un método dispone de registers_size registros, numerados v0 a v(registers_size − 1). La regla que lo gobierna todo es:

Los N argumentos de un método caen en los últimos N registros del marco, en orden. Los argumentos anchos consumen dos registros. A los métodos de instancia se les pasa una referencia this como primer argumento.

De ahí los tres campos del code_item:

Campo Qué cuenta
registers_size Total de registros del marco: locales más argumentos
ins_size Palabras de argumento de entrada. Incluye el this implícito y cuenta 2 por cada argumento ancho
outs_size Palabras de argumento de salida: el ancho del mayor invoke-* que aparece en el cuerpo. Es 0 si el método no invoca a nadie

Los registros de locales son entonces v0v(registers_size − ins_size − 1), y los de argumentos, los ins_size últimos.

La sintaxis smali da a esos últimos un segundo nombre: p0, p1, … pN, donde p0 es el primer argumento —el this en un método de instancia— y así sucesivamente. Es un alias, no otro registro:

método LMyObject;->callMe(II)V   con .registers 5

  v0   ──────────  primera local
  v1   ──────────  segunda local
  v2   ═══ p0 ═══  referencia this
  v3   ═══ p1 ═══  primer parámetro int
  v4   ═══ p2 ═══  segundo parámetro int

Las dos directivas de smali que declaran el tamaño del marco cuentan cosas distintas: .registers da el total y .locals da solo los no parámetros. Confundirlas es el error clásico al editar smali a mano, y produce un code_item con registers_size insuficiente que la verificación de ART rechaza (sección 9).

3.1 Verificado sobre el corpus

com.termux.app.TermuxApplication.setLogLevel:()V es un método de instancia sin parámetros, y dexdump da registers: 4, ins: 1, outs: 3. Es decir: v0, v1 y v2 son locales; v3 es el único argumento de entrada, o sea p0, o sea this. Y en efecto la primera instrucción del cuerpo es invoke-virtual {v3}, …getApplicationContext().

androidx.core.graphics.TypefaceCompatUtil.closeQuietly:(Ljava/io/Closeable;)V es estático con un parámetro: registers: 1, ins: 1, outs: 1. No hay locales; v0 es el único registro y es el parámetro, p0.

Sum.f(II)I, estático con dos parámetros, da registers: 2, ins: 2, outs: 0. v0 = a = p0, v1 = b = p1, y outs vale 0 porque el método no invoca a nadie.

3.2 Por qué existen las variantes /from16, /16 y /range

Como en la práctica es raro que un método necesite más de 16 registros, y bastante común que necesite más de ocho, muchas instrucciones solo pueden direccionar los 16 primeros registros (campos de 4 bits). Cuando es razonable, el formato admite hasta 256 (campos de 8 bits). Y unas pocas variantes llegan al rango completo v0v65535. Si una operación necesita un registro alto y su instrucción no lo alcanza, el compilador emite un move de copia antes o después:

0x1865e0: 6e10 f000 0300   invoke-virtual {v3}, …   ← 35c: registro de 4 bits
0x08ceca: 0509 1400        move-wide/from16 v9, v20 ← 22x: destino 8 bits, origen 16

4. Formatos de instrucción

4.1 Cómo se lee el identificador

Un identificador de formato tiene tres caracteres —dos dígitos y una letra— y se lee así:

  • Primer dígito: número de unidades de 16 bits que ocupa la instrucción.
  • Segundo dígito: número máximo de registros que el formato contiene. La letra r en su lugar significa que se codifica un rango de registros.
  • Letra final: de forma semimnemónica, el tipo de dato extra que lleva.

Así, 21t es de dos unidades, contiene una referencia a registro y lleva además un destino de salto. Los identificadores de cuatro caracteres que acaban en s o i son formatos sugeridos de enlazado estático o inline que el runtime puede generar al instalar; no aparecen en un DEX distribuido, y su implementación no es obligatoria.

Letra Tamaño en bits Significado
b 8 Byte inmediato con signo
c 16, 32 Índice de pool de constantes
f 16 Constantes de interfaz (solo en formatos enlazados estáticamente)
h 16 «Hat» inmediato con signo: los bits altos de un valor de 32 o 64 bits, con los bajos a 0
i 32 Entero inmediato con signo, o float de 32 bits
l 64 long inmediato con signo, o double de 64 bits
m 16 Constantes de método (solo en formatos enlazados estáticamente)
n 4 Nibble inmediato con signo
s 16 short inmediato con signo
t 8, 16, 32 Destino de salto
x 0 Sin datos adicionales

En la sintaxis, vX es un registro, #+X un literal, +X un desplazamiento relativo de dirección y kind@X un índice de pool, donde kind es string, type, field, meth, proto, method_handle o site.

4.2 Layout de bits

Cada «palabra» separada por espacios es una unidad de 16 bits. Cada carácter representa cuatro bits, leídos de los altos a los bajos; op marca el opcode de ocho bits y ØØ un nibble que debe valer cero. Las letras nombran los campos.

 layout                            id     sintaxis
 ─────────────────────────────────────────────────────────────────────────────
 (ninguno)                         00x    pseudoformato de los opcodes sin usar
 ØØ|op                             10x    op
 B|A|op                            12x    op vA, vB
 B|A|op                            11n    op vA, #+B
 AA|op                             11x    op vAA
 AA|op                             10t    op +AA
 ØØ|op AAAA                        20t    op +AAAA
 AA|op BBBB                        22x    op vAA, vBBBB
 AA|op BBBB                        21t    op vAA, +BBBB
 AA|op BBBB                        21s    op vAA, #+BBBB
 AA|op BBBB                        21h    op vAA, #+BBBB0000  |  #+BBBB000000000000
 AA|op BBBB                        21c    op vAA, type@BBBB · field@BBBB · string@BBBB
                                          · proto@BBBB · method_handle@BBBB
 AA|op CC|BB                       23x    op vAA, vBB, vCC
 AA|op CC|BB                       22b    op vAA, vBB, #+CC
 B|A|op CCCC                       22t    op vA, vB, +CCCC
 B|A|op CCCC                       22s    op vA, vB, #+CCCC
 B|A|op CCCC                       22c    op vA, vB, type@CCCC · field@CCCC
 ØØ|op AAAAlo AAAAhi               30t    op +AAAAAAAA
 ØØ|op AAAA BBBB                   32x    op vAAAA, vBBBB
 AA|op BBBBlo BBBBhi               31i    op vAA, #+BBBBBBBB
 AA|op BBBBlo BBBBhi               31t    op vAA, +BBBBBBBB
 AA|op BBBBlo BBBBhi               31c    op vAA, string@BBBBBBBB
 A|G|op BBBB F|E|D|C               35c    [A=5] op {vC,vD,vE,vF,vG}, meth@BBBB · site@BBBB
                                          · type@BBBB;  A=0..5 fija cuántos se usan
 AA|op BBBB CCCC                   3rc    op {vCCCC .. vNNNN}, meth@BBBB
                                          NNNN = CCCC+AA-1; AA es la cuenta (0..255)
 A|G|op BBBB F|E|D|C HHHH          45cc   op {vC..vG}, meth@BBBB, proto@HHHH
 AA|op BBBB CCCC HHHH              4rcc   op {vCCCC..vNNNN}, meth@BBBB, proto@HHHH
 AA|op BBBBlo BBBB BBBB BBBBhi     51l    op vAA, #+BBBBBBBBBBBBBBBB

El resto de identificadores que aparecen en la especificación —20bc, 22cs, 35ms, 35mi, 3rms, 3rmi— son los formatos sugeridos de enlazado que se mencionan en 4.1.

La rareza de 35c merece explicación: las letras no van en orden de bits. A es la cuenta de palabras de argumento y está en el nibble alto de la primera unidad; G es el quinto registro y está en el nibble bajo; y C, D, E y F van en la tercera unidad, de los bits bajos a los altos. Se hizo así para que la cuenta y el índice de referencia lleven la misma letra que en 3rc.

4.3 Cómo se empaquetan, con instrucciones reales del corpus

11x   0f00                  →  return v0
      byte0 = 0f = opcode           byte1 = 00 = AA = 0     → vAA = v0

11n   1201                  →  const/4 v1, #int 0
      byte0 = 12 = opcode           byte1 = 01: A = nibble bajo = 1 → vA = v1
                                              B = nibble alto = 0 → #+0

12x   b010                  →  add-int/2addr v0, v1
      byte0 = b0 = opcode           byte1 = 10: A = 0 → v0    B = 1 → v1

21t   3900 0300             →  if-nez v0, +0003
      unidad0 = 0x0039: op = 39, AA = 00 → v0
      unidad1 = 0x0003: BBBB = +3

21c   1f01 ea01             →  check-cast v1, type@01ea
      unidad0 = 0x011f: op = 1f, AA = 01 → v1
      unidad1 = 0x01ea: BBBB = índice 0x01ea en type_ids

22b   da01 0102             →  mul-int/lit8 v1, v1, #int 2
      unidad0 = 0x01da: op = da, AA = 01 → vAA = v1
      unidad1 = 0x0201: BB = byte bajo = 01 → vBB = v1
                        CC = byte alto = 02 → #+2

22c   5b01 941f             →  iput-object v1, v0, field@1f94
      unidad0 = 0x015b: op = 5b, byte1 = 01: A = 1 → v1, B = 0 → v0
      unidad1 = 0x1f94: CCCC = índice 0x1f94 en field_ids

35c   6e30 0b4b 1002        →  invoke-virtual {v0, v1, v2}, meth@4b0b
      unidad0 = 0x306e: op = 6e, A = 3 (tres palabras), G = 0
      unidad1 = 0x4b0b: BBBB = meth@4b0b
      unidad2 = 0x0210: C = 0, D = 1, E = 2, F = 0  → se usan C, D y E

3rc   7406 210f 0100        →  invoke-virtual/range {v1 .. v6}, meth@0f21
      unidad0 = 0x0674: op = 74, AA = 06 → seis registros
      unidad1 = 0x0f21: BBBB = meth@0f21
      unidad2 = 0x0001: CCCC = 1 → v1 .. v(1+6-1) = v6

51l   1801 00da 626d dc00 0000  →  const-wide v1, #000000dc6d62da00
      unidad0 = 0x0118: op = 18, AA = 01 → v1
      unidades 1..4, de la baja a la alta: 0xda00 0x6d62 0x00dc 0x0000

5. Sufijos de las instrucciones

Hay dos clases de sufijo y se distinguen por el separador.

Sufijos de tipo, unidos con guion. Marcan sobre qué opera la instrucción:

Sufijo Significado
(ninguno) Opcode genérico de 32 bits: no distingue int de float
-wide Opcode genérico de 64 bits: usa pares de registros
-object Referencia a objeto
-boolean, -byte, -char, -short, -int, -long, -float, -double El tipo concreto
-string, -class, -void El tipo concreto, en las instrucciones donde aplica

Que move y move-object sean instrucciones distintas aunque muevan los mismos 32 bits no es redundancia: el verificador necesita saber si el registro destino pasa a contener una referencia, y el recolector de basura necesita saber qué registros escanear.

Sufijos de variante, unidos con barra. No cambian la semántica, cambian la codificación: /from16 y /16 amplían el ancho de una referencia a registro, /32 y /jumbo amplían el de un literal o un índice, /range cambia una lista de registros por un rango, /lit8 y /lit16 fijan el ancho de una constante inmediata, y /2addr indica que el destino es también el primer operando fuente. Existen «sobre todo para que haya una correspondencia uno a uno con las constantes estáticas del código que genera e interpreta ejecutables».

6. La tabla de opcodes

Los 256 valores del opcode, agrupados por familia. (sin usar) marca los rangos reservados.

Opcodes Formato Mnemónicos Qué hace la familia
00 10x nop Gasta ciclos. También etiqueta las pseudoinstrucciones de datos (sección 8)
0109 12x, 22x, 32x move, move/from16, move/16, move-wide, move-wide/from16, move-wide/16, move-object, move-object/from16, move-object/16 Copia entre registros; tres anchos de referencia a registro por cada tipo
0a0d 11x move-result, move-result-wide, move-result-object, move-exception Recogen el resultado del invoke-* o del filled-new-array inmediatamente anterior, o la excepción capturada. Solo son válidos en esa posición exacta
0e11 10x, 11x return-void, return, return-wide, return-object Retorno
1219 11n, 21s, 31i, 21h, 51l const/4, const/16, const, const/high16, const-wide/16, const-wide/32, const-wide, const-wide/high16 Constantes numéricas. /high16 carga los 16 bits altos y rellena el resto de ceros
1a1c 21c, 31c const-string, const-string/jumbo, const-class Referencias a cadena y a clase. jumbo es el único índice de 32 bits del juego
1d1e 11x monitor-enter, monitor-exit Bloqueo. Es como se implementa synchronized en un método no nativo
1f20 21c, 22c check-cast, instance-of Comprobación de tipo
21, 2326 12x, 22c, 35c, 3rc, 31t array-length, new-array, filled-new-array, filled-new-array/range, fill-array-data Arrays: longitud, creación y relleno
22 21c new-instance Reserva la instancia; el constructor se llama aparte con invoke-direct
27 11x throw Lanza la excepción del registro
282a 10t, 20t, 30t goto, goto/16, goto/32 Salto incondicional. El desplazamiento no puede ser 0 salvo en goto/32
2b2c 31t packed-switch, sparse-switch Salto por tabla; los datos van en un payload aparte (sección 8)
2d31 23x cmpl-float, cmpg-float, cmpl-double, cmpg-double, cmp-long Comparación de tres valores: 0, 1 o −1. El sesgo l/g fija qué devuelven ante NaN
3237 22t if-eq, if-ne, if-lt, if-ge, if-gt, if-le Salto condicional comparando dos registros
383d 21t if-eqz, if-nez, if-ltz, if-gez, if-gtz, if-lez Salto condicional comparando con cero
3e43 (sin usar)
4451 23x aget, aget-wide, aget-object, aget-boolean, aget-byte, aget-char, aget-short, y los ocho aput-* correspondientes Lectura y escritura de elemento de array: valor, array, índice
525f 22c igetiget-short, iputiput-short Campo de instancia; el operando es field@CCCC
606d 21c sgetsget-short, sputsput-short Campo estático; el operando es field@BBBB
6e72 35c invoke-virtual, invoke-super, invoke-direct, invoke-static, invoke-interface Invocación con lista de hasta cinco registros (sección 7)
73 (sin usar)
7478 3rc Los cinco invoke-*/range Invocación con rango de hasta 256 registros
797a (sin usar)
7b8f 12x neg-int, not-int, neg-long, not-long, neg-float, neg-double, y las quince conversiones int-to-longint-to-short Operaciones unarias y conversión de tipo
90af 23x add/sub/mul/div/rem/and/or/xor/shl/shr/ushr-int, ídem -long, y add/sub/mul/div/rem -float y -double Binarias con tres registros
b0cf 12x Las mismas con sufijo /2addr Binarias donde el destino es el primer operando
d0d7 22s add-int/lit16, rsub-int, mul-int/lit16, div-int/lit16, rem-int/lit16, and-int/lit16, or-int/lit16, xor-int/lit16 Binarias contra literal de 16 bits
d8e2 22b add-int/lit8xor-int/lit8, shl-int/lit8, shr-int/lit8, ushr-int/lit8 Binarias contra literal de 8 bits
e3f9 (sin usar)
fafd 45cc, 4rcc, 35c, 3rc invoke-polymorphic, invoke-polymorphic/range, invoke-custom, invoke-custom/range Desde v038 (sección 7.2)
feff 21c const-method-handle, const-method-type Desde v039

Dos detalles que se olvidan y producen bugs. shl-long, shr-long y ushr-long, así como sus variantes /2addr, toman un par de registros para el valor a desplazar pero un solo registro para la distancia de desplazamiento, al contrario que el resto de operaciones -long. Y rsub-int no lleva sufijo porque es el opcode principal de su familia: resta al revés, resultado = literal − registro.

7. Las instrucciones de invocación

7.1 Las cinco clásicas

Todas comparten el formato 35c, donde A es el número de palabras de argumento —no de argumentos: uno ancho cuenta dos— y BBBB es el índice en method_ids. La variante /range (3rc) cambia la lista de hasta cinco registros por un rango vCCCCvNNNN con NNNN = CCCC + AA − 1, lo que permite hasta 256 palabras. El resultado, si lo hay, se recoge con un move-result* inmediatamente después.

Instrucción Cuándo se usa
invoke-virtual Método virtual normal: ni estático, ni privado, ni constructor
invoke-super La versión de la superclase más cercana, en vez de la del method_id de la clase llamante. Desde la versión 037 del DEX, si el method_id apunta a un método de interfaz, invoca la versión más específica no sobrescrita de esa interfaz —el mecanismo de los métodos default—. Antes de 037 un method_id de interfaz aquí era ilegal e indefinido
invoke-direct Método directo no estático: instancia privada o constructor. Es el único que puede llamar a un método cuyo nombre empieza por <
invoke-static Método estático, que siempre se considera directo
invoke-interface Método de interfaz sobre un objeto cuya clase concreta no se conoce

El par new-instance + invoke-direct <init> es la huella de un new de Java, y aparece separado porque la reserva y la construcción son dos pasos distintos en el bytecode.

7.2 invoke-polymorphic e invoke-custom

invoke-polymorphic (fa, formato 45cc) y su /range (fb, 4rcc) invocan un método polimórfico de firma, típicamente MethodHandle.invoke o MethodHandle.invokeExact. Llevan dos índices: meth@BBBB al método e proto@HHHH al prototipo que describe los tipos de los argumentos realmente pasados y el retorno esperado. El primer registro es el receptor. Presentes desde la versión 038 del DEX.

invoke-custom (fc, formato 35c) y su /range (fd, 3rc) son el equivalente de invokedynamic de la JVM. Su operando es call_site@BBBB, un índice en call_site_ids. Se ejecuta en dos fases: resolución, en la que se comprueba si el call site ya tiene un java.lang.invoke.CallSite asociado y, si no, se invoca el método bootstrap linker con los argumentos que trae el call_site_item del DEX; e invocación, que se hace sobre el MethodHandle objetivo del CallSite resuelto, como si fuera un invoke-polymorphic. Si el bootstrap falla, devuelve null, o el MethodHandle no es del tipo pedido, se lanza java.lang.BootstrapMethodError.

En un APK real esto es raro de ver: D8 desazucara las lambdas a clases sintéticas por defecto, incluso con --min-api 26. En los dos DEX del corpus examinados a fondo —com.termux_1002.apk y com.looker.droidify_710.apkno hay ni una sola invoke-custom. Se puede producir una a propósito:

BT=~/Library/Android/sdk/build-tools/37.0.0
# Demo.run() devuelve s.getAsInt() sobre un IntSupplier definido como lambda
$BT/d8 --min-api 26 --no-desugaring --output . Demo.class
$BT/dexdump -d classes.dex
0001a8: fc00 0000 0000    |0000: invoke-custom {}, call_site@0000
0001ae: 0c00              |0003: move-result-object v0
0001b0: 7210 0500 0000    |0004: invoke-interface {v0}, L…/IntSupplier;.getAsInt:()I
0001b6: 0a00              |0007: move-result v0
0001b8: 0f00              |0008: return v0
…
Method handle #0:  type: invoke-static  target: LDemo; lambda$run$0     target_type: ()I
Method handle #1:  type: invoke-static  target: LambdaMetafactory metafactory
Call site #0:
  link_argument[0] : 1 (MethodHandle)   ← el bootstrap: LambdaMetafactory.metafactory
  link_argument[1] : getAsInt (String)  ← el nombre a resolver
  link_argument[2] : ()Ljava/util/function/IntSupplier; (MethodType)
  link_argument[3] : ()I (MethodType)
  link_argument[4] : 0 (MethodHandle)   ← la implementación: Demo.lambda$run$0
  link_argument[5] : ()I (MethodType)

La codificación de la instrucción es fc00 0000 0000: op = fc, A = 0 (cero argumentos), BBBB = 0x0000 (call_site@0000) y la tercera unidad a cero porque no hay registros. Se ve también que el desugaring de D8 es lo que hace desaparecer el call_site_ids de un APK normal: sin --no-desugaring, el mismo fuente produce una clase sintética LDemo$0; que implementa IntSupplier y un invoke-static corriente.

8. Los payloads

Tres pseudoinstrucciones transportan datos de longitud variable. Se identifican por una primera unidad de 16 bits cuyo byte bajo es 00 —el opcode nop— y cuyo byte alto indica de qué payload se trata. Deben estar alineadas a 4 bytes y nunca deben alcanzarse en el flujo normal de ejecución; en la práctica los generadores las colocan al final del método.

packed-switch-payload            sparse-switch-payload         fill-array-data-payload
├── ident   ushort = 0x0100      ├── ident  ushort = 0x0200    ├── ident  ushort = 0x0300
├── size    ushort               ├── size   ushort             ├── element_width ushort
├── first_key int                ├── keys   int[size]          ├── size   uint
└── targets int[size]            └── targets int[size]         └── data   ubyte[]

unidades = (size * 2) + 4        unidades = (size * 4) + 2     unidades =
                                                                (size*element_width+1)/2+4

En packed-switch las claves son consecutivas desde first_key; en sparse-switch van explícitas y ordenadas de menor a mayor. En ambos, los destinos son relativos a la dirección del opcode del switch, no a la de la tabla, que es el error más común al implementar el desensamblador.

Un packed-switch-payload real de androidx.appcompat.widget.DrawableUtils.parseTintMode, en com.termux_1002.apk:

0a8110: 2b01 1600 0000    |000a: packed-switch v1, 00000020 // +00000016
…
0a813c: 0001 0300 0e00 0000 0a00 0000 0700 0000 0400 0000
        ident=0x0100  size=3  first_key=14  targets = 10, 7, 4

Diez unidades de 16 bits, que es 3 × 2 + 4. Los destinos se suman a 0x000a, la dirección del packed-switch: la clave 14 salta a 0x0014, la 15 a 0x0011 y la 16 a 0x000e, que es exactamente donde el desensamblado coloca los sget-object de MULTIPLY, SCREEN y ADD.

9. Los descriptores en las instrucciones

Los operandos type@, field@, meth@ y string@ son índices, no texto: la instrucción solo lleva el número, y el desensamblador lo resuelve contra las tablas del DEX. Los descriptores son los mismos que documenta Formato DEX, sección 7 —I para int, [I para un array de int, Lcom/foo/Bar; para una clase—, y dexdump los imprime ya resueltos detrás de un comentario con el índice original:

1f01 ea01        check-cast v1, Landroidx/activity/Cancellable;         // type@01ea
5b01 941f        iput-object v1, v0, L…$LocalBinder;.this$0:L…Service;  // field@1f94
6e10 f000 0300   invoke-virtual {v3}, L…;.getApplicationContext:()L…;   // method@00f0
1a00 2024        const-string v0, "Starting Application"                // string@2420

Esa doble salida —forma legible más índice— es lo que hace de dexdump un buen oráculo: se puede comprobar la resolución de índices de un parser propio comparando ambas columnas.

10. La verificación de bytecode de ART

Antes de ejecutar, ART verifica. La verificación ocurre al cargar la clase, no al llegar a la instrucción problemática, y por eso un DEX editado a mano que no la supera produce un fallo de carga —típicamente un VerifyError o un rechazo en la instalación— y no un comportamiento erróneo en ejecución. Es una diferencia importante para diagnosticar: si la aplicación arranca y el método falla al llamarlo, el problema no es de verificación.

El verificador trabaja en dos pasadas, tal como las describe el propio código de AOSP.

Estática, instrucción a instrucción, con las comprobaciones heredadas de la especificación de la JVM: que el destino de cada salto y de cada switch sea válido; que los operandos que referencian entradas del pool de constantes sean válidos; que los operandos de instanceof, checkcast y new sean válidos; que new-array no pase de 255 dimensiones; que no se use new sobre una clase de array; que los índices de registro caigan dentro del rango asignado; que los saltos caigan dentro del array de código; que todo destino de control de flujo sea el principio de una instrucción; que el código no acabe en mitad de una instrucción; que la ejecución no se salga por el final; y que, para cada handler de excepción, el área try empiece y acabe en el principio de una instrucción y el handler empiece en una instrucción válida.

De flujo de datos, que es la cara: se marca la primera instrucción como «cambiada», se procesa, se propagan los tipos a sus sucesores, se fusionan en los puntos de confluencia y se repite hasta que no queda nada cambiado. Lo que se comprueba es que los registros contengan los tipos correctos de valores, que los métodos se invoquen con los argumentos adecuados, que a los campos se les asignen valores del tipo apropiado y que los opcodes reciban en sus registros de operando los tipos que esperan. Además, ART verifica de forma específica que sea imposible ejecutar new-instance cuando un registro ya contiene una instancia sin inicializar creada por esa misma instrucción, que es su forma de resolver el problema que la especificación de la JVM ataja prohibiendo los saltos hacia atrás con referencias sin inicializar.

De ahí salen los errores típicos de un DEX editado a mano, todos con la misma causa —romper una invariante que el compilador mantenía sin que el editor se diera cuenta—:

Síntoma Causa habitual
«register index out of range» Se añadieron instrucciones que usan un registro nuevo sin subir .registers
«wide register index out of range» Se usó vN como par de 64 bits siendo N+1 el último registro
Tipo incorrecto en un registro Se reutilizó un registro que en esa rama traía una referencia, o al revés
«branch offset of zero not allowed» Se dejó un salto a sí mismo al borrar instrucciones
Destino de salto que no es inicio de instrucción Se editaron bytes sin recalcular offsets, o se olvidó que los desplazamientos van en unidades de 16 bits
«unaligned table» Un payload dejó de estar alineado a 4 bytes al insertar o quitar una instrucción y falta el nop de relleno
move-result fuera de sitio Se insertó algo entre el invoke-* y su move-result

⚠️ sin verificar: los textos de la columna izquierda son los mensajes que emite method_verifier.cc de AOSP, no se han reproducido en un dispositivo porque adb no está disponible en esta máquina y el corpus no contiene ningún DEX inválido.

11. Correspondencia con smali

smali es la sintaxis de texto del bytecode Dalvik y baksmali el desensamblador que la produce; ver Desensambladores smali. Los tres niveles del mismo método, Sum.f(II)I:

hexadecimal          dexdump -d                              smali
─────────────────────────────────────────────────────────────────────────────────
da01 0102            0000: mul-int/lit8 v1, v1, #int 2       mul-int/lit8 v1, v1, 0x2
b010                 0002: add-int/2addr v0, v1              add-int/2addr v0, v1
0f00                 0003: return v0                         return v0

Y la envoltura que baksmali pone alrededor:

.class public LSum;
.super Ljava/lang/Object;
.source "Sum.java"

.method public static f(II)I
    .registers 2

    mul-int/lit8 v1, v1, 0x2
    add-int/2addr v0, v1
    return v0
.end method

⚠️ no ejecutado localmente: baksmali no está instalado, así que el bloque smali está reconstruido a partir de la sintaxis documentada del proyecto y de la salida real de dexdump. En particular, la elección entre .registers y .locals y entre notación v y p depende de las opciones con que se invoque a baksmali y puede no coincidir con lo mostrado.

Las tres diferencias sistemáticas entre las dos vistas:

  • dexdump imprime la dirección de cada instrucción en unidades de 16 bits al principio de la línea; smali no, y usa etiquetas (:cond_0, :goto_0, :try_start_0) como destinos simbólicos de salto, lo que permite insertar y borrar instrucciones sin recalcular desplazamientos a mano. Es la razón principal por la que se edita en smali y no en hexadecimal.
  • dexdump imprime los literales en decimal con el hexadecimal en un comentario (#int 2 // #02); smali los escribe en hexadecimal (0x2).
  • Los bloques try/catch son estructura en smali (:try_start_0:try_end_0 más una directiva .catch), mientras que en el DEX son la tabla try_item de fuera del array de instrucciones (Formato DEX, sección 11).

12. Receta: dexdump -d comentado instrucción a instrucción

BT=~/Library/Android/sdk/build-tools/37.0.0
CORPUS=~/corpus-apk
DEXDIR=$(mktemp -d)
unzip -o -q $CORPUS/com.termux_1002.apk classes.dex -d $DEXDIR
$BT/dexdump -d $DEXDIR/classes.dex

Salida real para com.termux.app.TermuxApplication.setLogLevel:()V, recortada a lo esencial:

      registers     : 4
      ins           : 1
      outs          : 3
      insns size    : 25 16-bit code units
1865d0:                |[1865d0] com.termux.app.TermuxApplication.setLogLevel:()V
1865e0: 6e10 f000 0300 |0000: invoke-virtual {v3}, L…;.getApplicationContext:()L…;
1865e6: 0c00           |0003: move-result-object v0
1865e8: 7110 f84a 0000 |0004: invoke-static {v0}, L…/TermuxAppSharedPreferences;.build:(L…;)L…;
1865ee: 0c00           |0007: move-result-object v0
1865f0: 3900 0300      |0008: if-nez v0, 000b // +0003
1865f4: 0e00           |000a: return-void
1865f6: 1201           |000b: const/4 v1, #int 0 // #0
1865f8: 6e10 ff4a 0000 |000c: invoke-virtual {v0}, L…;.getLogLevel:()I
1865fe: 0a02           |000f: move-result v2
186600: 6e30 0b4b 1002 |0010: invoke-virtual {v0, v1, v2}, L…;.setLogLevel:(L…/Context;I)V
186606: 1a00 2024      |0013: const-string v0, "Starting Application" // string@2420
18660a: 7110 1b4a 0000 |0015: invoke-static {v0}, L…/Logger;.logDebug:(L…/String;)V
186610: 0e00           |0018: return-void
      positions     :
        0x0000 line=23
        0x000c line=25
        0x0015 line=26

Lectura instrucción a instrucción:

Dir. Instrucción Qué ocurre
0000 invoke-virtual {v3}, …getApplicationContext() registers 4 e ins 1v3 es el único argumento, o sea p0, o sea this. Formato 35c con A = 1
0003 move-result-object v0 Recoge la referencia devuelta. Tiene que ir aquí: entre un invoke-* y su move-result no cabe nada
0004 invoke-static {v0}, …build(Context) Pasa el Context recién obtenido
0007 move-result-object v0 Sobrescribe v0, que pasa de Context a TermuxAppSharedPreferences. Reutilizar el registro con otro tipo es legal; es el verificador quien lleva la cuenta
0008 if-nez v0, 000b Formato 21t: AA = 00v0, BBBB = +3. Salto relativo a esta dirección: 0x0008 + 3 = 0x000b
000a return-void La rama del null
000b const/4 v1, #int 0 Formato 11n, una sola unidad: cabe un literal de 4 bits. Este 0 es el null que se pasa como Context
000c invoke-virtual {v0}, …getLogLevel()I
000f move-result v2 Sin -object: el resultado es un int
0010 invoke-virtual {v0, v1, v2}, …setLogLevel(Context, I)V Tres palabras de argumento. De aquí sale outs 3: es la mayor llamada del método
0013 const-string v0, "Starting Application" Formato 21c, índice string@2420 en string_ids
0015 invoke-static {v0}, …logDebug(String)V
0018 return-void

Y de la tabla positions sale la correspondencia con el fuente: 0x0000 es la línea 23, 0x000c la 25 y 0x0015 la 26. Sin debug_info_item esa tabla estaría vacía y el decompilador no podría dar números de línea (Formato DEX, sección 12).

Fuentes

  1. 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í salen el diseño general del modelo de ejecución (sección 2), los sufijos (sección 5), la tabla completa de opcodes (sección 6), la descripción de las instrucciones de invocación (sección 7) y los tres formatos de payload (sección 8).
  2. 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 la nomenclatura de los identificadores de formato, la tabla de letras de tipo y la tabla completa de layouts de bits (sección 4).
  3. 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»). De aquí salen los campos registers_size, ins_size y outs_size del code_item (sección 3) y los descriptores de tipo (sección 9).
  4. AOSP art/runtime/verifier/method_verifier.cc — https://android.googlesource.com/platform/art/+/refs/heads/main/runtime/verifier/method_verifier.cc Consultado el 12 de agosto de 2026. De aquí sale íntegra la sección 10: los comentarios de VerifyInstruction y CodeFlowVerifyMethod enumeran las comprobaciones estáticas y de flujo de datos, y los mensajes de Fail(VERIFY_ERROR_BAD_CLASS_HARD) son los de la tabla de errores típicos.
  5. Registers · wiki del proyecto smali — https://github.com/JesusFreke/smali/wiki/Registers Consultado el 12 de agosto de 2026. De aquí salen la notación p, la correspondencia pNvN del diagrama de la sección 3 y la diferencia entre .registers y .locals.
  6. TypesMethodsAndFields · wiki del proyecto smali — https://github.com/JesusFreke/smali/wiki/TypesMethodsAndFields Consultado el 12 de agosto de 2026. De aquí sale la sintaxis de descriptores, métodos y campos usada en el bloque smali de la sección 11.
  7. 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), javac de OpenJDK 21.0.10 y javap. De aquí salen la comparación JVM/Dalvik de la sección 1, los tamaños de marco de la sección 3.1, todas las decodificaciones de bits de la sección 4.3, el ejemplo de invoke-custom de la sección 7.2, el payload de la sección 8 y el desensamblado comentado de la sección 12.