Código nativo y ELF
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
ELFla declara en su propia cabecera, en el byteEI_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_MAG0–EI_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 are2**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_LOAD ≥ 0x4000 |
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
- 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,mipsymips64en NDK r17 de la sección 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_Ehdrde la sección 3.1, los índices dee_identde la 3.2 y los valoresELFCLASS64,ELFDATA2LSB,ET_DYN,EM_386,EM_ARMyEM_X86_64. - 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_Phdrde la sección 3.4, la definición dep_aligny los valoresPT_LOAD,PT_DYNAMIC,PT_NOTEyPT_PHDR. - 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. - 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 deJNI_OnLoadconRegisterNatives, la recomendación de Google y la recomendación de-fvisibility=hiddende las secciones 4.1, 4.3 y 4.4. - 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–_3y la regla del nombre corto frente al largo de la sección 4.2. - 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**14sobre los segmentosLOAD, 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. <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.- 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
extractNativeLibsa"false"por defecto (sección 5). - 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_TABLEyFULL, la ruta del artefacto y el límite de 1,6 GB de la sección 7. - 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 4que se cita en la sección 6.1. - 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.