Android基础机制与调用流程解析

​ 昨天已经学习了Android的基础结构,今天学习一下安卓的底层开发基础,主要是JNI的学习,顺便找了相应的题学习加深一下学习印象。

SDK、NDK、JNI 与SO/ELF 的关系解析

SDK是 Android 应用开发工具包,主要用于 Java/Kotlin 层开发,提供 UI、组件(Activity、Service 等)及系统 API 支持。

NDK是 Android 原生开发工具包,用于使用 C/C++ 编写底层代码,并编译生成 .so 本地库。

JNI是 Java 与 C/C++ 之间的调用桥梁,负责实现 Java 层与 Native 层之间的函数调用与数据交互

.so是 Android 中的本地动态链接库,由 NDK 编译生成,在运行时被加载执行。

**ELF**是 Linux 系统下的可执行文件格式,.so 文件本质上就是 ELF 文件的一种形式。

1
2
3
4
5
6
7
8
9
SDK 写 Java 层逻辑

JNI 负责调用

NDK 编译 C/C++

生成 .so

.so 本质是 ELF 文件并在系统中执行

SDK 负责应用层开发,JNI 作为桥梁连接 Java 与 Native,NDK 负责构建 Native 代码,而生成的 .so 文件本质上是 ELF 格式,最终在 Android 系统中被加载执行。


JNI基础学习

1 .JNI 开发的基本流程

一个标准的 JNI 开发流程通常包括以下步骤:

  1. 编写 Java 类并声明 native 方法,作为 Java 调用本地代码的入口;

  2. 编译 Java 源文件,并使用 javac -h 生成对应的 JNI 头文件(.h),其中定义了本地方法的函数原型;

  3. 在 C/C++ 源文件中实现头文件中声明的函数逻辑;

  4. 使用编译工具将本地代码编译为动态库文件(.so);

  5. 在 Java 程序中通过 System.loadLibrary(...) 加载该动态库;

  6. 运行程序,完成 Java 层与本地 C/C++ 层之间的调用与数据交互。

该流程用示意图表示如下:

image-20260413195038636

1
JNI 开发流程本质就是:声明 → 生成接口 → 实现 → 编译 → 加载 → 调用

2.注册 JNI 函数

2.1静态注册

静态注册采用的是按照固定的命名规则,通过 javah 可以自动生成 native 方法对应的函数声明。例如:

1
Java_com_swdd_summertrain_MainActivity_Check

不过,静态注册的命名规则分为无重载和有重载2 种情况:无重载时采用短名称规则,有重载时采用长名称规则。

可以通过这个文章进行学习:NDK 系列(6):说一下注册 JNI 函数的方式和时机

2.2动态注册

动态注册需要提前手动建立映射关系,不用遵守静态注册的 JNI 函数命名规则。

动态注册需要使用 RegisterNatives(...) 函数,其定义在 jni.h 文件中:

1
2
3
4
5
6
7
8
9
10
struct JNINativeInterface {
jint (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*, jint);
jint (*UnregisterNatives)(JNIEnv*, jclass);
};

typedef struct {
const char* name; // Java 方法名
const char* signature; // Java 方法描述符
void* fnPtr; // JNI 函数指针
} JNINativeMethod;

JNINativeInterface = JNI 函数表(函数指针表)

RegisterNatives 函数是最终的注册函数,需要传递 jclass、JNINativeMethod 结构体数组和数组长度。

RegisterNatives 方式的本质是直接通过结构体指定映射关系,而不是等到调用 native 方法时搜索 JNI 函数指针。

此外,还能减少生成 so 库文件中导出符号的数量,增加进一步逆向的难度。

3.加载so库

整个流程:

1
2
3
4
5
6
7
8
9
10
11
System.load / loadLibrary

Runtime.load0 / loadLibrary0

找到 so 的绝对路径

nativeLoad

dlopen 加载 so

执行 JNI_OnLoad

3.1 System.load()方法

1
System.load("/data/app/libxxx.so");

特点是必须是绝对路径,不做任何查找。

3.2 System.loadLibrary()方法

1
System.loadLibrary("native-lib");

特点是传的是库名,自动补全。

Android 加载 so 库的本质流程是:通过 System.load 或 loadLibrary 获取库路径,再由 nativeLoad 调用 dlopen 加载到内存,并执行 JNI_OnLoad 完成初始化。


上面的内容比较基础,还没涉及到真正的JNI开发,后续会持续更新。


下面的内容是找的相应的题目练习,题目来源于swdd~

1、静态注册

关键函数,程序加载native库,看主函数确定主要逻辑在native层

1
2
3
4
5
public native Boolean Check(String str);

static {
System.loadLibrary("summertrain");
}

so文件主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
__int64 __fastcall Java_com_swdd_summertrain_MainActivity_Check(__int64 a1, __int64 a2, __int64 a3)
{
const char *s; // x0
const char *s_1; // x21
size_t n; // x23
int8x16_t *s1; // x0
int8x16_t *s1_1; // x22
bool v10; // cf
unsigned __int64 n_1; // x8
__int64 v12; // x23
size_t v13; // x9
_BYTE *v14; // x10
const char *v15; // x8
char v16; // t1
_BOOL4 v17; // w23
__int64 v18; // x24
__int64 v19; // x0
int8x16_t *v21; // x9
int8x16_t v22; // q0
int8x16_t *v23; // x10
unsigned __int64 v24; // x11
int8x16_t v25; // q1
int8x16_t v26; // q2
unsigned __int64 n_2; // x11
int8x8_t *v28; // x9
int8x8_t *v29; // x10
unsigned __int64 v30; // x11
int8x8_t v31; // t1

s = (*(*a1 + 1352LL))(a1, a3, 0LL);
if ( s )
{
s_1 = s;
n = strlen(s);
s1 = operator new[](n + 1);
s1_1 = s1;
if ( !n )
{
LABEL_15:
s1->n128_u8[n] = 0;
v17 = memcmp(s1, "U_RTH}\\Dlj\\Flx]\\Dl}RGZEVlWJ]R^ZPlAVTZ@GARGZ\\]Ncovariant return thunk to ", n) == 0;
v18 = (*(*a1 + 48LL))(a1, "java/lang/Boolean");
v19 = (*(*a1 + 264LL))(a1, v18, "<init>", "(Z)V");
v12 = _JNIEnv::NewObject(a1, v18, v19, v17);
(*(*a1 + 1360LL))(a1, a3, s_1);
operator delete[](s1_1);
return v12;
}
if ( n < 8 || (s1 < &s_1[n] ? (v10 = s_1 >= s1 + n) : (v10 = 1), !v10) )
{
n_1 = 0LL;
LABEL_13:
v13 = n - n_1;
v14 = s1 + n_1;
v15 = &s_1[n_1];
do
{
v16 = *v15++;
--v13;
*v14++ = v16 ^ 0x33;
}
while ( v13 );
goto LABEL_15;
}
if ( n >= 0x20 )
{
n_1 = n & 0xFFFFFFFFFFFFFFE0LL;
v21 = (s_1 + 16);
v22.n128_u64[0] = 0x3333333333333333LL;
v22.n128_u64[1] = 0x3333333333333333LL;
v23 = s1 + 1;
v24 = n & 0xFFFFFFFFFFFFFFE0LL;
do
{
v25 = v21[-1];
v26 = *v21;
v21 += 2;
v24 -= 32LL;
v23[-1] = veorq_s8(v25, v22);
*v23 = veorq_s8(v26, v22);
v23 += 2;
}
while ( v24 );
if ( n == n_1 )
goto LABEL_15;
if ( (n & 0x18) == 0 )
goto LABEL_13;
}
else
{
n_1 = 0LL;
}
n_2 = n_1;
n_1 = n & 0xFFFFFFFFFFFFFFF8LL;
v28 = &s_1[n_2];
v29 = (s1 + n_2);
v30 = n_2 - (n & 0xFFFFFFFFFFFFFFF8LL);
do
{
v31.n64_u64[0] = v28->n64_u64[0];
++v28;
v30 += 8LL;
v29->n64_u64[0] = veor_s8(v31, 0x3333333333333333LL).n64_u64[0];
++v29;
}
while ( v30 );
if ( n == n_1 )
goto LABEL_15;
goto LABEL_13;
}
return 0LL;
}

就是一个xor0x33的简单逻辑

1
2
3
4
5
6
target = r"U_RTH}\Dlj\Flx]\Dl}RGZEVlWJ]R^ZPlAVTZ@GARGZ\]N"

flag = "".join(chr(ord(c) ^ 0x33) for c in target)

print("flag:", flag)
//flag{Now_You_Know_Native_dynamic_registration}

2、动态注册

和上述流程一样,不过在查找主函数时需要注意这个题目是动态注册,包名已经不按规定了,看JNI_OnLoad函数里调用check最终调用主函数image-20260316202813082

image-20260316203307320

主函数就是简单的移位

1
2
3
4
5
6
7
8
9
10
s2 = [
0x99,0x93,0x9E,0x98,0x84,0xB1,0x90,0x88,0xA0,0xA6,0x90,0x8A,0xA0,0xB4,0x91,0x90,
0x88,0xA0,0xB1,0x9E,0x8B,0x96,0x89,0x9A,0xA0,0x8D,0x9A,0x9E,0x93,0xA0,0x9B,0x86,
0x91,0x9E,0x92,0x96,0x9C,0xA0,0x8D,0x9A,0x98,0x96,0x8C,0x8B,0x8D,0x9E,0x8B,0x96,
0x90,0x91,0x82
]

flag = ''.join(chr((~b) & 0xff) for b in s2)

print(flag)//flag{Now_You_Know_Native_real_dynamic_registration}

静态注册vs动态注册

静态注册:

Java 直接声明 native 函数:

1
public native Boolean Check(String str);

JNI 会自动按照 固定命名规则去寻找 C 函数,函数名必须是:

1
Java_包名_类名_方法名

例如静态注册中的函数名:Java_com_swdd_summertrain_MainActivity_Check,很容易定位函数

动态注册:

在java中还是和静态注册时一样声明,但是 native函数名可以随便写,例如:

1
jboolean sub_1544C(JNIEnv *env, jobject obj, jstring input)

然后在 JNI_OnLoad里手动绑定:

1
2
3
static JNINativeMethod methods[] = {
{"Check", "(Ljava/lang/String;)Z", (void*)sub_1544C}
};

注册:

1
env->RegisterNatives(clazz, methods, 1);

动态注册依赖一个结构体:

1
2
3
4
5
typedef struct {
const char* name; // Java方法名
const char* signature; // 方法签名
void* fnPtr; // C函数地址
} JNINativeMethod;

动态注册可以隐藏函数名,减少字符信息,可以添加混淆


Android基础机制与调用流程解析
https://j1nxem-o.github.io/2026/04/13/Android-基础机制与调用流程解析/
作者
J1NXEM
发布于
2026年4月13日
更新于
2026年4月13日
许可协议