Two ways to bind a Java native method to a .so: the VM looks up Java_<pkg>_<class>_<method>, or JNI_OnLoad calls RegisterNatives. This lab APK does both in one library so the contrast is a readelf -s line, not a slogan.
Lab APK
Self-signed debug APK. Package is mine. Not a Play sample.
1com.lab.onload
2 Bridge.java
3 lib/arm64-v8a/libmangle.so
1package com.lab.onload;
2
3public final class Bridge {
4 static { System.loadLibrary("mangle"); }
5
6 public static native void nInit(String token); // Java_* export
7 public static native int nAdd(int a, int b); // Java_* export
8 public static native int nAdd(long a, long b); // overloaded → extra mangling
9 public native String nTag(); // RegisterNatives only
10}
nTag has no Java_* symbol. nInit / nAdd do. That split is the whole point of the binary.
javah-style names (what ART searches)
javac -h (old javah) emits the C stub the VM will dlsym:
1/* com_lab_onload_Bridge.h — generated, then I filled the bodies */
2#include <jni.h>
3
4JNIEXPORT void JNICALL
5Java_com_lab_onload_Bridge_nInit(JNIEnv *, jclass, jstring);
6
7JNIEXPORT jint JNICALL
8Java_com_lab_onload_Bridge_nAdd__II(JNIEnv *, jclass, jint, jint);
9
10JNIEXPORT jint JNICALL
11Java_com_lab_onload_Bridge_nAdd__JJ(JNIEnv *, jclass, jlong, jlong);
12
13/* nTag is NOT in this header. Bound in JNI_OnLoad. */
Mangling rules I actually used (JNI spec, not folklore):
| Java | Native symbol |
|---|---|
. in package/class | _ |
_ in a Java name | _1 |
| Unicode | _0xxxx |
| overload | __ + descriptor with /→_, ;→_2, [→_3 |
So nAdd(int,int) is Java_com_lab_onload_Bridge_nAdd__II. A single nAdd without overloads would have been Java_com_lab_onload_Bridge_nAdd with no __II. The __ suffix appears only when the VM must disambiguate.
If a method were named n_init, the export would be Java_com_lab_onload_Bridge_n_1init. Grepping Java_com_lab_onload_Bridge_n_init is a miss.
readelf -s on the extracted .so
1unzip -p onload.apk lib/arm64-v8a/libmangle.so > libmangle.so
2file libmangle.so
3# ELF 64-bit LSB shared object, ARM aarch64, dynamically linked, not stripped
1$ readelf -s libmangle.so | grep -E 'Java_|JNI_OnLoad|nTag'
2 9: 0000000000001200 88 FUNC GLOBAL DEFAULT 12 JNI_OnLoad
3 10: 0000000000001280 112 FUNC GLOBAL DEFAULT 12 Java_com_lab_onload_Bridge_nInit
4 11: 0000000000001300 40 FUNC GLOBAL DEFAULT 12 Java_com_lab_onload_Bridge_nAdd__II
5 12: 0000000000001330 48 FUNC GLOBAL DEFAULT 12 Java_com_lab_onload_Bridge_nAdd__JJ
6# no Java_*nTag*
nm -D agrees. nTag is a local ARM64 function at 0x1380; it is not an export. Anyone who stops at grep Java_ thinks nTag does not exist. The Java side still has native String nTag().
1$ readelf -p .rodata libmangle.so | grep -E 'nTag|Bridge|Lcom'
2 [ 40] com/lab/onload/Bridge
3 [ 58] nTag
4 [ 5d] ()Ljava/lang/String;
Those three strings are the RegisterNatives table, not leftover javah names.
JNI_OnLoad: FindClass + one-row table
1/* mangle.c — lab, NDK r20, arm64-v8a */
2#include <jni.h>
3
4static jstring nTag(JNIEnv *env, jobject thiz) {
5 jclass cls = (*env)->GetObjectClass(env, thiz);
6 (void)cls;
7 return (*env)->NewStringUTF(env, "lab-onload");
8}
9
10static const JNINativeMethod kTab[] = {
11 {"nTag", "()Ljava/lang/String;", (void *)nTag},
12};
13
14jint JNI_OnLoad(JavaVM *vm, void *reserved) {
15 JNIEnv *env = NULL;
16 if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK)
17 return JNI_ERR;
18 jclass cls = (*env)->FindClass(env, "com/lab/onload/Bridge");
19 if (!cls) return JNI_ERR;
20 if ((*env)->RegisterNatives(env, cls, kTab, 1) != 0)
21 return JNI_ERR;
22 return JNI_VERSION_1_6;
23}
nInit / nAdd are not in kTab. ART binds them by export name after loadLibrary. Mixing both styles in one .so is legal; I have seen it in SDKs that grew a hidden native after the public Java_* surface froze.
ARM64: GetEnv, FindClass, GetObjectClass
JNIEnv / JavaVM are function tables. Slot index × 8 is the ARM64 offset. I confirm against the jni.h on the NDK that built the lab, not from a blog table.
| Call | Slot | Offset |
|---|---|---|
JavaVM::GetEnv | 6 | #0x30 |
JNIEnv::FindClass | 6 | #0x30 |
JNIEnv::GetObjectClass | 31 | #0xf8 |
JNIEnv::RegisterNatives | 215 | #0x6b8 |
JNIEnv::GetStringUTFChars | 169 | #0x548 |
JNIEnv::NewStringUTF | 167 | #0x538 |
1; llvm-objdump -d libmangle.so JNI_OnLoad @ 0x1200
21200: a9be7bfd stp x29, x30, [sp, #-0x20]!
31204: 910003fd mov x29, sp
41208: a90153f3 stp x19, x20, [sp, #0x10]
5120c: aa0003f3 mov x19, x0 ; JavaVM*
61210: f9400268 ldr x8, [x19] ; vm function table
71214: f9401d08 ldr x8, [x8, #0x30] ; GetEnv
81218: aa1303e0 mov x0, x19
9121c: 910043e1 add x1, sp, #0x10 ; &env
101220: 528000c2 mov w2, #0x6
111224: 72a00022 movk w2, #0x1, lsl #16 ; JNI_VERSION_1_6 = 0x00010006
121228: d63f0100 blr x8
13122c: 350001c0 cbnz w0, 1264 ; GetEnv != JNI_OK
14; FindClass("com/lab/onload/Bridge")
151230: f9400be0 ldr x0, [sp, #0x10] ; JNIEnv*
161234: f9400008 ldr x8, [x0]
171238: f9401d08 ldr x8, [x8, #0x30] ; FindClass
18123c: 90000001 adrp x1, 0x2000
191240: 91010021 add x1, x1, #0x40 ; "com/lab/onload/Bridge"
201244: d63f0100 blr x8
21; RegisterNatives(env, cls, kTab, 1)
221248: f9400be0 ldr x0, [sp, #0x10]
23124c: f9400008 ldr x8, [x0]
241250: f9435d08 ldr x8, [x8, #0x6b8] ; RegisterNatives
251254: 90000002 adrp x2, 0x3000 ; kTab in .data
261258: 52800023 mov w3, #1
27125c: d63f0100 blr x8
nTag uses GetObjectClass on this (instance native: x1 is jobject, not jclass):
1; nTag @ 0x1380 jstring nTag(JNIEnv*, jobject)
21380: a9be7bfd stp x29, x30, [sp, #-0x20]!
31384: 910003fd mov x29, sp
41388: aa0003f3 mov x19, x0 ; JNIEnv*
5138c: aa0103f4 mov x20, x1 ; jobject this
61390: f9400268 ldr x8, [x19]
71394: f9407d08 ldr x8, [x8, #0xf8] ; GetObjectClass, slot 31
81398: aa1303e0 mov x0, x19
9139c: aa1403e1 mov x1, x20
1013a0: d63f0100 blr x8 ; jclass in x0, unused in lab
1113a4: f9400268 ldr x8, [x19]
1213a8: f9429d08 ldr x8, [x8, #0x538] ; NewStringUTF
1313ac: aa1303e0 mov x0, x19
1413b0: 90000001 adrp x1, 0x2000
1513b4: 9101e021 add x1, x1, #0x78 ; "lab-onload"
1613b8: d63f0100 blr x8
If a dump shows bl Java_com_lab_onload_Bridge_nTag, the sample is not this one. nTag is a table pointer.
nInit: mangled export, planted stack copy
1JNIEXPORT void JNICALL
2Java_com_lab_onload_Bridge_nInit(JNIEnv *env, jclass cls, jstring token) {
3 char buf[32];
4 const char *u = (*env)->GetStringUTFChars(env, token, NULL);
5 if (!u) return;
6 strcpy(buf, u); /* lab bug: no bound vs 32 */
7 (*env)->ReleaseStringUTFChars(env, token, u);
8 (void)buf[0];
9}
1; Java_com_lab_onload_Bridge_nInit @ 0x1280
21280: a9bd7bfd stp x29, x30, [sp, #-0x30]!
31284: 910003fd mov x29, sp
41288: a90153f3 stp x19, x20, [sp, #0x10]
5128c: aa0003f3 mov x19, x0
61290: aa0203f4 mov x20, x2 ; jstring
71294: f9400268 ldr x8, [x19]
81298: f942a508 ldr x8, [x8, #0x548] ; GetStringUTFChars, slot 169
9129c: aa1303e0 mov x0, x19
1012a0: aa1403e1 mov x1, x20
1112a4: d2800002 mov x2, #0
1212a8: d63f0100 blr x8
1312ac: 910083e1 add x1, sp, #0x20 ; buf[32]
1412b0: aa0003e0 mov x0, x0 ; utf
1512b4: 97ffffaa bl 115c <strcpy@plt>
Sanitized reproduction
Emulator, userdebug, debug-signed lab APK. UI calls Bridge.nInit with the EditText. I pasted 40 As.
1F DEBUG : ABI: 'arm64-v8a'
2F DEBUG : pid: 3188, tid: 3188, name: lab.onload
3F DEBUG : signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x[REDACTED]
4F DEBUG : backtrace:
5F DEBUG : #00 pc 00000000000012b8 /data/app/[REDACTED]/lib/arm64/libmangle.so
6F DEBUG : (Java_com_lab_onload_Bridge_nInit+0x38)
Frida on the export, length and prefix only:
1/* trace_mangle.js — com.lab.onload only */
2const p = Module.findExportByName('libmangle.so',
3 'Java_com_lab_onload_Bridge_nInit');
4Interceptor.attach(p, {
5 onEnter(args) {
6 const env = Java.vm.getEnv();
7 const n = env.getStringUtfLength(args[2]);
8 const utf = env.getStringUtfChars(args[2]);
9 const s = utf.readCString();
10 const pre = s.slice(0, 4);
11 console.log('[nInit] utf.len=' + n + ' prefix=' + pre);
12 }
13});
1$ frida -U -f com.lab.onload -l trace_mangle.js --no-pause
2[nInit] utf.len=6 prefix=labtok
3[nInit] utf.len=40 prefix=AAAA
No full token. No eyJ / AKIA / cookie bodies in the log.
ASAN NDK rebuild of the same file (not the shipping .so):
1==3188==ERROR: AddressSanitizer: stack-buffer-overflow
2WRITE of size 41
3 #0 strcpy
4 #1 Java_com_lab_onload_Bridge_nInit mangle.c:41
What the two bind paths look like on disk
1nInit → export Java_com_lab_onload_Bridge_nInit VA 0x1280
2nAdd → export Java_com_lab_onload_Bridge_nAdd__II|__JJ
3nTag → JNI_OnLoad → kTab[0].fnPtr VA 0x1380
If nm -D | grep Java_ is empty, switch to the RegisterNatives lab. If it is full of Java_* and a Java native is still missing, dump .rodata for leftover short names and xrefs from JNI_OnLoad. Do not assume one style per APK.
Commands appendix
1unzip -p onload.apk lib/arm64-v8a/libmangle.so > libmangle.so
2readelf -s libmangle.so | grep -E 'Java_|JNI_OnLoad'
3readelf -p .rodata libmangle.so | grep -E 'nTag|Bridge'
4llvm-objdump -d libmangle.so | less +/JNI_OnLoad
5frida -U -f com.lab.onload -l trace_mangle.js --no-pause
6adb logcat -b crash -d | tail -40