Código nativo y ELF

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

1. Qué es y por qué existe

No todo el código de una aplicación Android es DEX. Bajo lib/<abi>/ viven bibliotecas compartidas en formato ELF —el mismo formato de los binarios de Linux— compiladas con el NDK y llamadas desde Java o Kotlin a través de JNI. Existen por rendimiento, por reutilización de bibliotecas en C y C++ que nadie va a reescribir (ffmpeg, sqlite, motores de juego), y por ocultación.

Ese tercer motivo es el que importa aquí. Un método movido a .so desaparece del bytecode Dalvik: lo que queda en el DEX es una declaración native sin cuerpo. Un decompilador a Java lo enseña vacío y el análisis se para ahí, salvo que se cambie de herramienta y de nivel.

Este documento cubre lo que hace falta para inventariar, identificar y decidir: qué ABIs trae un APK, cómo se enlaza cada método nativo, qué exige la regla de las páginas de 16 KB y qué se ha perdido al hacer stripping. El análisis del código nativo en sí —Ghidra, radare2, IDA— está en Análisis binario y estático.

Endianness: el ELF la declara en su propia cabecera, en el byte EI_DATA. Las cuatro ABIs vigentes de Android son little-endian (ELFDATA2LSB), y todos los offsets de las tablas de este documento son relativos al inicio de la estructura descrita.

2. lib/<abi>/ y las ABIs vigentes

El empaquetado es una convención sobre rutas del ZIP: un directorio por ABI bajo lib/, y dentro los .so planos, sin subdirectorios. El instalador elige el directorio que corresponde a la arquitectura del dispositivo e ignora el resto.

2.1 Las cuatro vigentes

ABI CPU Nota de la documentación del NDK
arm64-v8a ARM de 64 bits «Armv8.0 only.» long double de 128 bits
armeabi-v7a ARM de 32 bits «Incompatible with ARMv5/v6 devices.» Incluye Thumb-2 y Neon
x86_64 x86 de 64 bits «Full x86-64-v2»
x86 x86 de 32 bits «No support for MOVBE or SSE4.»

2.2 Las eliminadas

«Historically the NDK supported ARMv5 (armeabi), and 32-bit and 64-bit MIPS, but support for these ABIs was removed in NDK r17.»

Es decir, armeabi, mips y mips64 ya no se pueden compilar con un NDK moderno. Pero siguen apareciendo en APK de 2026, porque una biblioteca Java de terceros puede traer sus .so precompilados de hace diez años dentro del .jar y el empaquetador los copia sin preguntar. En com.x8bit.bitwarden_2026.5.0.apk del corpus, con targetSdk 36:

Directorio Entradas Bytes
arm64-v8a 5 14.544.672
armeabi-v7a 5 9.437.328
x86_64 5 15.964.120
x86 5 15.481.932
armeabi 1 126.980
mips 1 130.556
mips64 1 150.256

Los tres huérfanos son el mismo fichero, libjnidispatch.so, de JNA, y aapt2 dump badging los declara como ABIs soportadas: native-code: 'arm64-v8a' 'armeabi' 'armeabi-v7a' 'mips' 'mips64' 'x86' 'x86_64'.

Consecuencia para un inventario: contar directorios bajo lib/ no da las ABIs que la aplicación soporta de verdad, da las que alguien empaquetó.

⚠️ sin verificar: no se ha localizado documentación oficial sobre riscv64 como ABI de producción de Android; la página de ABIs del NDK consultada no lo menciona.

3. Estructura mínima de un ELF compartido

Un .so de Android es un ELF de tipo ET_DYN (3, «Shared object file»). Interesan tres capas: la cabecera, la tabla de program headers —lo que el cargador usa— y la tabla de secciones —lo que el analista usa—.

┌─────────────────────────────────────────┐  0x00
│ Elf64_Ehdr   (64 bytes)                 │
├─────────────────────────────────────────┤  e_phoff  (típicamente 0x40)
│ Elf64_Phdr[e_phnum]  (56 bytes cada uno)│  ← lo que mapea el cargador
├─────────────────────────────────────────┤
│ .dynsym .dynstr .text .rodata .init_array│  segmentos PT_LOAD
│ .data.rel.ro .dynamic .got               │
├─────────────────────────────────────────┤  e_shoff
│ Elf64_Shdr[e_shnum]  (64 bytes cada uno)│  ← opcional para ejecutar
└─────────────────────────────────────────┘

3.1 Cabecera ELF64

Campo Offset Tamaño Tipo Significado
e_ident 0x00 16 unsigned char[16] Identificación. Ver 3.2
e_type 0x10 2 Elf64_Half ET_DYN = 3 en toda biblioteca compartida
e_machine 0x12 2 Elf64_Half Arquitectura. Ver 3.3
e_version 0x14 4 Elf64_Word 1
e_entry 0x18 8 Elf64_Addr Punto de entrada. 0 o irrelevante en un .so
e_phoff 0x20 8 Elf64_Off Offset de la tabla de program headers. 0x40
e_shoff 0x28 8 Elf64_Off Offset de la tabla de secciones. 0 si se han eliminado
e_flags 0x30 4 Elf64_Word Banderas específicas de la arquitectura
e_ehsize 0x34 2 Elf64_Half Tamaño de esta cabecera: 0x40 = 64
e_phentsize 0x36 2 Elf64_Half Tamaño de un program header: 0x38 = 56
e_phnum 0x38 2 Elf64_Half Número de program headers
e_shentsize 0x3A 2 Elf64_Half Tamaño de una cabecera de sección: 0x40 = 64
e_shnum 0x3C 2 Elf64_Half Número de secciones
e_shstrndx 0x3E 2 Elf64_Half Índice de la sección con los nombres de sección

En ELF32 la cabecera mide 52 bytes (0x34) y los campos de dirección son de 4 bytes; el resto del esquema es idéntico.

3.2 e_ident

Índice Nombre Tamaño Valor en un .so de Android
0–3 EI_MAG0EI_MAG3 4 7F 45 4C 46 (\x7fELF)
4 EI_CLASS 1 2 = ELFCLASS64, 1 = ELFCLASS32
5 EI_DATA 1 1 = ELFDATA2LSB
6 EI_VERSION 1 1
7 EI_OSABI 1 0
8 EI_ABIVERSION 1 0
9–15 EI_PAD 7 ceros

3.3 e_machine por ABI

ABI e_machine Constante
arm64-v8a 183 (0xB7) EM_AARCH64
armeabi-v7a 40 (0x28) EM_ARM
x86_64 62 (0x3E) EM_X86_64
x86 3 (0x03) EM_386

La ABI se deduce del binario, no de la ruta. Un .so colocado en el directorio equivocado es un error que solo se detecta leyendo e_machine.

3.4 program header

Campo Offset Tamaño Tipo Significado
p_type 0x00 4 Elf64_Word PT_LOAD = 1, PT_DYNAMIC = 2, PT_NOTE = 4, PT_PHDR = 6
p_flags 0x04 4 Elf64_Word Permisos: r, w, x
p_offset 0x08 8 Elf64_Off Offset dentro del fichero
p_vaddr 0x10 8 Elf64_Addr Dirección virtual
p_paddr 0x18 8 Elf64_Addr Dirección física. Irrelevante
p_filesz 0x20 8 Elf64_Xword Bytes en el fichero
p_memsz 0x28 8 Elf64_Xword Bytes en memoria. Mayor que p_filesz si hay .bss
p_align 0x30 8 Elf64_Xword El campo de la regla de 16 KB (sección 6)

«This member gives the value to which the segments are aligned in memory and in the file. Values 0 and 1 mean no alignment is required.»

y para los segmentos cargables, «p_vaddr should equal p_offset, modulo p_align».

3.5 Secciones que importan al analizar

Sección Contenido Para qué sirve al analista
.dynsym Tabla de símbolos dinámicos La única que sobrevive al stripping. De ahí salen los Java_* y JNI_OnLoad
.dynstr Nombres de .dynsym Los strings de los símbolos exportados e importados
.text Código máquina Lo que se desensambla
.rodata Datos de solo lectura Cadenas literales, tablas, claves embebidas
.init_array Punteros a constructores Se ejecutan antes que JNI_OnLoad: el sitio clásico del descifrado inicial y de las comprobaciones anti-análisis
.fini_array Punteros a destructores Simétrico del anterior
.dynamic NEEDED, SONAME, tablas de relocalización El grafo de dependencias entre .so
.symtab Tabla de símbolos completa Ausente en release. Ver sección 7
.note.android.ident Versión del NDK y minSdk de compilación Huella de la cadena de build

4. JNI: cómo se enlaza un método nativo

Un método declarado native en Java no tiene cuerpo en el DEX. En el momento de la primera llamada, ART tiene que encontrar la función en C que le corresponde. Hay dos vías, y la diferencia entre ellas decide si el análisis estático es trivial o es un problema.

4.1 Cargar la biblioteca

Desde un inicializador estático de la clase, con System.loadLibrary("fubar"): «the argument is the "undecorated" library name, so to load libfubar.so you would pass in "fubar"». El .so sale de lib/<abi>/ del APK instalado, o del directorio al que el instalador lo extrajo (sección 5).

4.2 Vía A: descubrimiento por nombre (dlsym)

Si el símbolo exportado sigue una convención de nombres, el runtime lo encuentra solo:

«A native method name is concatenated from the following components: the prefix Java_; a mangled fully-qualified class name; an underscore ("_") separator; a mangled method name; for overloaded native methods, two underscores ("__") followed by the mangled argument signature.»

La barra del nombre cualificado se sustituye por _, y como ni un nombre ni un descriptor empiezan por número, los dígitos quedan libres para escapes: _0XXXX es el carácter Unicode XXXX en minúsculas (_0abcd, no _0ABCD), _1 es _, _2 es ; y _3 es [. La signatura solo se añade cuando el método está sobrecargado: «The VM looks first for the short name… It then looks for the long name».

Esto es un regalo para el análisis: el nombre del símbolo contiene el paquete, la clase y el método Java a los que corresponde. En libtermux.so del corpus, .dynsym trae Java_com_termux_terminal_JNI_close, …_createSubprocess, …_setPtyUTF8Mode, …_setPtyWindowSize y …_waitFor: sin abrir el desensamblador ya se sabe que com.termux.terminal.JNI tiene cinco métodos nativos y cuál es cada uno.

4.3 Vía B: RegisterNatives desde JNI_OnLoad

La otra vía es registrar la correspondencia en tiempo de ejecución, con una tabla de punteros a función:

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    // … obtener JNIEnv* env y jclass c = env->FindClass("com/example/MyClass") …
    static const JNINativeMethod methods[] = {
        {"nativeFoo", "()V", reinterpret_cast<void*>(nativeFoo)},
        {"nativeBar", "(Ljava/lang/String;I)Z", reinterpret_cast<void*>(nativeBar)},
    };
    env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod));
    return JNI_VERSION_1_6;
}

Y es lo que Google recomienda, por motivos ajenos a la ofuscación: «Most apps should call RegisterNatives() from JNI_OnLoad(). […] Mistakes are caught early. An incorrectly named function will cause an error during library load. The only symbol that typically must be exported is JNI_OnLoad()», y «build with a version script (preferred) or use -fvisibility=hidden so that only your JNI_OnLoad is exported from your library».

4.4 Por qué RegisterNatives complica el análisis estático

Con la vía A la correspondencia entre método Java y función nativa está en la tabla de símbolos, que es un dato estático. Con la vía B está en el argumento de una llamada que solo ocurre en tiempo de ejecución, y recuperarla exige localizar JNI_OnLoad en .text, seguir el flujo hasta la llamada a RegisterNatives —que es indirecta, a través de la tabla de funciones de JNIEnv—, y reconstruir el array JNINativeMethod[], normalmente en .rodata, con sus tres campos: nombre, descriptor de firma y puntero a la implementación. Si el nombre y el descriptor se construyen o se descifran en ejecución, no queda nada estático que reconstruir.

El resultado se ve de un vistazo. libandroidx.graphics.path.so de com.x8bit.bitwarden_2026.5.0.apk exporta un solo símbolo propio:

00000000000016c8 g    DF .text  00000000000001c0  LIBANDROIDX.GRAPHICS.PATH JNI_OnLoad

Ni un Java_*. Todo lo demás en .dynsym son importaciones de libc.

El criterio de triaje: si .dynsym trae símbolos Java_*, el mapa está servido; si solo trae JNI_OnLoad, hay que entrar al desensamblador; y si no trae ninguno de los dos, o la biblioteca no es un puente JNI, o la carga se hace por otra vía y conviene sospechar de un packer (Packers y protectores).

5. extractNativeLibs y lo que implica para el ZIP

«This attribute indicates whether the package installer extracts native libraries from the APK to the file system. If set to "false", your native libraries are stored uncompressed in the APK. Although your APK might be larger, your application loads faster because the libraries load directly from the APK at runtime.»

Valor En el ZIP En el dispositivo
true .so en deflate, sin restricción de alineación El instalador los descomprime a disco: dos copias
false .so en stored y alineados Se mapean con mmap(2) desde el APK: una copia

Desde AGP 3.6 el valor por defecto es "false" —«the plugin now sets extractNativeLibs to "false" by default. That is, your native libraries are page aligned and packaged uncompressed»— y desde AGP 4.2 se configura por DSL con useLegacyPackaging. ⚠️ sin verificar: la documentación oficial dice que «the default value of extractNativeLibs depends on minSdkVersion and the version of AGP you're using», pero no se ha localizado el umbral concreto de minSdkVersion.

Contraste medido en el corpus con aapt2 dump xmltree y unzip -v:

APK targetSdk extractNativeLibs Método ZIP de los .so
com.termux_1002.apk 28 true Defl:N
com.x8bit.bitwarden_2026.5.0.apk 36 false Stored, alineado a 16 KiB

Consecuencia al modificar un APK: si extractNativeLibs es false, reempaquetar un .so comprimiéndolo rompe la aplicación en tiempo de carga, y hacerlo sin alinear la rompe igual. El mecanismo de alineación —relleno en el extra field del Local File Header— está en Contenedor ZIP, sección 8.1.

6. Páginas de 16 KB

6.1 Qué es el requisito

Android ha usado siempre páginas de memoria de 4 KB. A partir de Android 15 hay dispositivos con páginas de 16 KB, y un binario cuyos segmentos PT_LOAD estén alineados a 4 KB no se puede mapear en ellos. La condición se comprueba sobre p_align:

«Check the output lines to ensure that the load segments don't have values less than 2**14. If any load segments are 2**13, 2**12, or lower values, you'll need to update the packaging»

2**14 = 16.384 = 0x4000. Son dos alineaciones distintas y ambas obligatorias:

Dónde Qué se alinea Cómo se comprueba
Dentro del .so p_align de cada segmento PT_LOAD0x4000 objdump -p / readelf -l
Dentro del APK Offset de los datos de cada entrada .so stored zipalign -c -P 16 -v 4

Con NDK r27 o anterior hay que pedirlo al enlazador:

-Wl,-z,max-page-size=16384
-Wl,-z,common-page-size=16384

«NDK version r28 and higher compile 16 KB-aligned by default.»

6.2 Fecha y alcance en Google Play

«Starting February 1, 2027 […] all apps targeting Android 15 (API level 35) and higher must support 16 KB memory page sizes on 64-bit devices on Google Play»

y el efecto de incumplirlo:

«if your app updates don't support 16 KB memory page sizes, you won't be able to release these updates»

Es decir: 1 de febrero de 2027, targetSdk 35 o superior, dispositivos de 64 bits, y afecta también a las actualizaciones, no solo a las publicaciones nuevas.

6.3 Qué se ve en el corpus

Alineación de los segmentos PT_LOAD de los .so de arm64-v8a:

APK p_align observado Cumple
com.termux_1002.apk 0x1000 (4 KB)
com.x8bit.bitwarden_2026.5.0.apk 0x4000
org.fdroid.fdroid_1023052.apk 0x4000
de.westnordost.streetcomplete_6304.apk 0x4000
chat.simplex.app_354.apk 0x4000, y 0x10000 en libsimplex.so

libtermux.so está compilado con NDK r22b —dato que sale de su propia .note.android.ident— y es anterior a todo esto. libsimplex.so usa 64 KB, que también cumple: el requisito es un mínimo, no un valor exacto.

7. Stripping de símbolos

«By default, native code libraries are stripped in release builds of your app. This stripping consists of removing the symbol table and debugging information contained in any native libraries used by your app.»

7.1 Qué se pierde y qué queda

Se pierde Sobrevive
.symtab: nombres de todas las funciones, incluidas las internas .dynsym: solo los símbolos exportados e importados
.debug_*: ficheros fuente, números de línea, tipos .dynstr, .dynamic, NEEDED, SONAME
Nombres de variables locales .rodata: los strings literales siguen ahí

En un .so sin stripping el desensamblador etiqueta cada función con su nombre real; con stripping, todo lo no exportado aparece como sub_1A40 o FUN_00101a40 y reconstruirlo es trabajo manual. Combinado con RegisterNatives (sección 4.4) es el escenario habitual de una aplicación comercial. Comprobado en el corpus: libtermux.so y libandroidx.graphics.path.so responden no symbols a nm, que solo lee .symtab, y conservan .dynsym intacta.

7.2 Los símbolos de depuración de Play Console

El desarrollador conserva los símbolos y los sube por separado, para que Play pueda simbolicar los informes de fallo. Se configura en Gradle:

android.buildTypes.release.ndk.debugSymbolLevel = { SYMBOL_TABLE | FULL }
Valor Contenido
NONE Por defecto. Sin símbolos
SYMBOL_TABLE Nombres de función. Fichero más pequeño
FULL Nombres de función, ficheros y números de línea

El artefacto sale en app/build/outputs/native-debug-symbols/<variant-name>/native-debug-symbols.zip, con un límite de 1,6 GB.

Para el análisis, esto significa que los símbolos existen pero no están en el APK. No hay vía legítima de recuperarlos desde el binario distribuido: es el equivalente nativo del mapping.txt de R8 y ProGuard, con la diferencia de que Play nunca lo publica.

8. Recetas

objdump de esta máquina es Apple LLVM 21.0.0, que sabe leer ELF pese a ser el de macOS. Comprobado el 12 de agosto de 2026: en ~/Library/Android/sdk/build-tools/37.0.0/lld-bin/ solo hay lld, el enlazador, y no hay NDK instalado, así que no existen llvm-readelf ni llvm-objdump del SDK. readelf tampoco está en el PATH. Todo lo de abajo usa /usr/bin/objdump, /usr/bin/nm y xxd.

8.1 Inventariar las ABIs presentes

CORPUS=~/corpus-apk
unzip -l $CORPUS/com.x8bit.bitwarden_2026.5.0.apk \
  | awk '$4 ~ /^lib\// {split($4,a,"/"); c[a[2]]++; s[a[2]]+=$1}
         END {for (k in c) printf "%-14s %3d entradas %10d bytes\n", k, c[k], s[k]}' | sort
arm64-v8a        5 entradas   14544672 bytes
armeabi          1 entradas     126980 bytes
armeabi-v7a      5 entradas    9437328 bytes
mips             1 entradas     130556 bytes
mips64           1 entradas     150256 bytes
x86              5 entradas   15481932 bytes
x86_64           5 entradas   15964120 bytes

Sin agregación, unzip -l … | grep '^lib/' | cut -d/ -f2 | sort -u basta para la lista de directorios.

8.2 Extraer un .so y leer su cabecera con xxd

unzip -o -q $CORPUS/com.termux_1002.apk "lib/arm64-v8a/libtermux.so" -d /tmp/so/
xxd -l 64 /tmp/so/lib/arm64-v8a/libtermux.so
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............
00000010: 0300 b700 0100 0000 f40e 0000 0000 0000  ................
00000020: 4000 0000 0000 0000 b01d 0000 0000 0000  @...............
00000030: 0000 0000 4000 3800 0800 4000 1600 1500  ....@.8...@.....

Contra las tablas 3.1 y 3.2: magic \x7fELF; EI_CLASS = 02 = ELFCLASS64; EI_DATA = 01 = ELFDATA2LSB; e_type = 0x0003 = ET_DYN; e_machine = 0x00B7 = 183 = EM_AARCH64; e_phoff = 0x40; e_shoff = 0x1DB0, distinto de cero, así que la tabla de secciones sigue ahí; y e_ehsize = 64, e_phentsize = 56, e_phnum = 8, e_shentsize = 64, e_shnum = 22, e_shstrndx = 21.

La misma biblioteca en lib/armeabi-v7a/ empieza por 7f45 4c46 0101 0100 … 0300 2800 0100 0000 300a 0000 3400 0000: EI_CLASS = 01 = ELFCLASS32, e_machine = 0x0028 = 40 = EM_ARM, y e_phoff = 0x34 en vez de 0x40 porque la cabecera de 32 bits mide 52 bytes.

8.3 Comprobar la alineación de 16 KB con objdump -p

objdump -p /tmp/so/lib/arm64-v8a/libtermux.so | grep "LOAD"
unzip -o -q $CORPUS/com.x8bit.bitwarden_2026.5.0.apk \
  "lib/arm64-v8a/libandroidx.graphics.path.so" -d /tmp/bw/
objdump -p /tmp/bw/lib/arm64-v8a/libandroidx.graphics.path.so | grep "LOAD"
    LOAD off    0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**12
    LOAD off    0x00000000000018e0 vaddr 0x00000000000028e0 paddr 0x00000000000028e0 align 2**12
    LOAD off    0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**14
    LOAD off    0x0000000000001c40 vaddr 0x0000000000005c40 paddr 0x0000000000005c40 align 2**14
    LOAD off    0x0000000000001fd0 vaddr 0x0000000000009fd0 paddr 0x0000000000009fd0 align 2**14

2**12 = 4.096: libtermux.so no cumple. 2**14 = 16.384: el .so de 2026 sí.

8.4 Listar los símbolos JNI

objdump -T /tmp/so/lib/arm64-v8a/libtermux.so | grep -E "Java_|JNI_OnLoad"
objdump -T /tmp/bw/lib/arm64-v8a/libandroidx.graphics.path.so | grep -E "Java_|JNI_OnLoad"
00000000000016b8 g    DF .text	0000000000000008              Java_com_termux_terminal_JNI_close
0000000000000f3c g    DF .text	00000000000006a0              Java_com_termux_terminal_JNI_createSubprocess
000000000000161c g    DF .text	0000000000000054              Java_com_termux_terminal_JNI_setPtyUTF8Mode
00000000000015dc g    DF .text	0000000000000040              Java_com_termux_terminal_JNI_setPtyWindowSize
0000000000001670 g    DF .text	0000000000000048              Java_com_termux_terminal_JNI_waitFor
00000000000016c8 g    DF .text	00000000000001c0  LIBANDROIDX.GRAPHICS.PATH JNI_OnLoad

Las cinco primeras líneas son la vía A; la última, un binario entero cuya única exportación propia es JNI_OnLoad: vía B.

8.5 Confirmar el stripping y leer la huella del NDK

nm /tmp/so/lib/arm64-v8a/libtermux.so
objdump -s -j .note.android.ident /tmp/so/lib/arm64-v8a/libtermux.so | sed -n '3,5p'
/tmp/so/lib/arm64-v8a/libtermux.so: no symbols
Contents of section .note.android.ident:
 0200 08000000 84000000 01000000 416e6472  ............Andr
 0210 6f696400 18000000 72323262 00000000  oid.....r22b....

nm lee .symtab; «no symbols» confirma el stripping, mientras objdump -T, que lee .dynsym, sí devuelve símbolos. La nota dice Android, 0x18 = 24 —el nivel de API con el que se compiló— y r22b, la versión del NDK: una huella útil para fechar una biblioteca.

Fuentes

  1. Android ABIs — NDK — https://developer.android.com/ndk/guides/abis Consultado el 12 de agosto de 2026. De aquí sale la tabla de ABIs vigentes de la sección 2.1, sus notas literales, y la frase sobre la eliminación de armeabi, mips y mips64 en NDK r17 de la sección 2.2.
  2. System V ABI (gABI) — ELF Header — https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html Consultado el 12 de agosto de 2026. De aquí salen la estructura Elf64_Ehdr de la sección 3.1, los índices de e_ident de la 3.2 y los valores ELFCLASS64, ELFDATA2LSB, ET_DYN, EM_386, EM_ARM y EM_X86_64.
  3. System V ABI (gABI) — Program Header — https://refspecs.linuxfoundation.org/elf/gabi4+/ch5.pheader.html Consultado el 12 de agosto de 2026. De aquí salen la estructura Elf64_Phdr de la sección 3.4, la definición de p_align y los valores PT_LOAD, PT_DYNAMIC, PT_NOTE y PT_PHDR.
  4. ELF for the Arm 64-bit Architecture (AAELF64) — https://github.com/ARM-software/abi-aa/blob/main/aaelf64/aaelf64.rst Consultado el 12 de agosto de 2026. De aquí sale EM_AARCH64 (183, 0xB7) de la sección 3.3, que la especificación gABI no lista.
  5. JNI Tips — Android Developers — https://developer.android.com/training/articles/perf-jni Consultado el 12 de agosto de 2026. De aquí salen las citas sobre System.loadLibrary, el ejemplo de JNI_OnLoad con RegisterNatives, la recomendación de Google y la recomendación de -fvisibility=hidden de las secciones 4.1, 4.3 y 4.4.
  6. Java Native Interface Specification — Design Overview — https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html Consultado el 12 de agosto de 2026. De aquí salen la regla de composición del nombre mangled, la tabla de escapes _0_3 y la regla del nombre corto frente al largo de la sección 4.2.
  7. Support 16 KB page sizes — Android Developers — https://developer.android.com/guide/practices/page-sizes Consultado el 12 de agosto de 2026. De aquí salen la comprobación de 2**14 sobre los segmentos LOAD, los flags de enlazado, la afirmación de que NDK r28 alinea por defecto y la fecha y el alcance del requisito de Google Play de la sección 6.2.
  8. <application> — android:extractNativeLibs — https://developer.android.com/guide/topics/manifest/application-element Consultado el 12 de agosto de 2026. De aquí sale la descripción del atributo de la sección 5 y la advertencia sobre su valor por defecto.
  9. Android Gradle Plugin 3.6.0 release notes — https://developer.android.com/build/releases/past-releases/agp-3-6-0-release-notes Consultado el 12 de agosto de 2026. De aquí sale que AGP fija extractNativeLibs a "false" por defecto (sección 5).
  10. Include native symbols in your release build — https://developer.android.com/build/include-native-symbols Consultado el 12 de agosto de 2026. De aquí salen la cita sobre el stripping por defecto, los valores NONE, SYMBOL_TABLE y FULL, la ruta del artefacto y el límite de 1,6 GB de la sección 7.
  11. zipalign — Android Studio — https://developer.android.com/tools/zipalign Consultado el 12 de agosto de 2026. De aquí sale la orden zipalign -c -P 16 -v 4 que se cita en la sección 6.1.
  12. Corpus de verificación — 5,9 GB, 86 contenedores y 717 APK: 58 sueltos de F-Droid y 659 dentro de contenedores de splits. Medido el 12 de agosto de 2026. De aquí salen la tabla de ABIs de Bitwarden de la sección 2.2, la tabla de alineaciones de la 6.3, las observaciones sobre stripping de la 7.1 y todas las salidas de la sección 8.