前置知识
ARM是嵌入式设备的主流品牌,A系列针对的是高性能的设备比如手机
架构名称 | 支持指令集 | 位数 | 性能 | 适用设备 | 当前状态 |
arm64-v8a | ARMv8-A | 64位 | 高性能和高能效 | 现代设备(主流架构) | 主流,强烈推荐 |
armeabi-v7a | ARMv7-A | 32位 | 中等性能 | 较旧设备或低端设备 | 次主流,逐渐被替代 |
armeabi | ARMv5TE | 32位 | 性能较低 | 非常旧的设备 | 已废弃,不再使用 |
走进JNI
https://github.com/jp1017/HelloJni?tab=readme-ov-file
java层代码
// 定义包名,用于组织和管理代码
package github.jp1017.hellojni;
// 导入需要使用的Android类
import android.os.Bundle; // 用于传递数据
import android.support.design.widget.FloatingActionButton; // Material Design浮动按钮
import android.support.design.widget.Snackbar; // Material Design提示条
import android.support.v7.app.AppCompatActivity; // 应用程序活动基类
import android.support.v7.widget.Toolbar; // 工具栏组件
import android.util.Log; // 日志工具
import android.view.Menu; // 菜单
import android.view.MenuItem; // 菜单项
import android.view.View; // 视图基类
import android.widget.Toast; // 提示框组件
// MainActivity类,继承自AppCompatActivity
public class MainActivity extends AppCompatActivity {
/********************************** JNI 开始 *********************************/
/**
* 静态代码块加载库
* 在类加载时就会执行,加载名为"hello_jni"的native库
*/
static {
System.loadLibrary("hello_jni");
}
// 声明native方法,使用静态注册方式
public native String staticRegFromJni(); //静态方法注册,实现在C代码中
// 声明native方法,使用动态注册方式
public native String dynamicRegFromJni(); //动态方法注册,实现在C代码中
/********************************** JNI 结束 *********************************/
// Activity创建时调用的方法
@Override
protected void onCreate(Bundle savedInstanceState) {
// 调用父类的onCreate方法
super.onCreate(savedInstanceState);
// 设置界面布局
setContentView(R.layout.activity_main);
// 初始化工具栏
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// 初始化浮动按钮
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
// 设置按钮点击监听器
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 创建并显示一个Snackbar提示条
Snackbar.make(
view, // 指定Snackbar的显示位置(通常是当前视图)
"替换为你的操作", // 显示的提示文本
Snackbar.LENGTH_LONG // 显示时长:LENGTH_LONG约2.5秒
)
.setAction(
"操作", // 操作按钮的文本
null // 按钮点击监听器,null表示不执行任何操作
)
.show(); // 显示Snackbar
}
});
}
// 创建选项菜单
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// 加载菜单布局文件,将菜单项添加到操作栏
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
// 处理菜单项选择事件
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// 获取被点击的菜单项ID
int id = item.getItemId();
// 处理设置菜单项的点击
if (id == R.id.action_settings) {
return true;
}
// 如果不是设置菜单,交由父类处理
return super.onOptionsItemSelected(item);
}
/**
* 按钮点击事件处理方法
* @param view 被点击的视图对象
*/
public void onClick(View view) {
// 根据被点击的按钮ID执行相应操作
switch (view.getId()) {
case R.id.bt_static:
// 调用静态注册的native方法并记录日志
Log.d("日志", staticRegFromJni());
// 显示Toast提示
Toast.makeText(this, staticRegFromJni(), Toast.LENGTH_SHORT).show();
break;
case R.id.bt_dynamic:
// 调用动态注册的native方法并记录日志
Log.d("日志", dynamicRegFromJni());
// 显示Toast提示
Toast.makeText(this, dynamicRegFromJni(), Toast.LENGTH_SHORT).show();
}
}
}
- 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.
c层代码
#include <jni.h>
/********* 静态注册方法 staticRegFromJni ***********/
//方法名:Java_完整包名类名_方法名();
/**
* env : 代表Java环境, 通过这个环境可以调用Java中的方法。类似
* obj : 代表调用JNI方法的对象, 即MainActivity对象。类似this指针
*/
JNIEXPORT jstring JNICALL Java_github_jp1017_hellojni_MainActivity_staticRegFromJni(JNIEnv * env, jobject obj)
{
return (*env) -> NewStringUTF(env, "静态注册调用成功"); //C 的字符串转成 Java 的字符串返回。JNI本质上也是一种c和java的中间库,它可以调用C方法。JNI函数表(JNINativeInterface)是一个包含了所有 JNI 函数指针的结构体,它就像是一个"功能目录表"。将字符串关联到对应多线程env
}
/********* 动态注册方法 dynamicRegFromJni ***********/
static jstring nativeDynamicRegFromJni(JNIEnv *env, jobject obj)
{
return (*env) -> NewStringUTF(env, "动态注册调用成功");
}
//方法数组,正是这个,可以动态调用任意 native 方法 ,dynamicRegFromJni是java的方法,()Ljava/lang/String;是java的方法签名
JNINativeMethod nativeMethod[] = {{"dynamicRegFromJni", "()Ljava/lang/String;", (void*)nativeDynamicRegFromJni}};
// JNIEXPORT // JNI导出标记,让这个函数可以被Java层调用
// jint // 返回一个整数(Java的int)
// JNICALL // JNI调用约定,定义如何参数压栈
// JNI_OnLoad // 函数名,这是JNI库加载时的钩子函数
// (
// JavaVM *jvm, // Java虚拟机实例的指针
// void *reserved // 保留参数,通常不使用
// )
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved)
{
JNIEnv *env;
if ((*jvm) -> GetEnv(jvm, (void**) &env, JNI_VERSION_1_4) != JNI_OK)//获取jvm的env到env中并判断版本相等。 jvm Java虚拟机实例,输出参数,env 用于存储获取到的JNIEnv指针,JNI_VERSION_1_4 期望的JNI版本号
{
return -1;
}
jclass clz = (*env) -> FindClass(env, "github/jp1017/hellojni/MainActivity"); //获取MainActivity类,clz 用于存储找到的MainActivity类
(*env) -> RegisterNatives(env, clz, nativeMethod, sizeof(nativeMethod) / sizeof(nativeMethod[0])); //注册native方法,将nativeMethod数组中的方法注册到MainActivity类中
return JNI_VERSION_1_4;
}
- 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.
反编译c
在反编译的角度。通过比较两个反编译器,还是ghidra厉害,ida错误多且可读性差,但是ghidra有一个重大错误,返回值是空。还是建议使用ghidra的反编译功能。而且ghidra支持多个指令集的反编译,ida只支持x86
我们分析一下错误如何产生的。
通过分析指令集ret,发现没有正确读取w0的值,属于软件缺陷
之后的分析在不说明的情况下默认是ghidra
1二进制和伪代码比对。
arm64反汇编
高地址
┌────────────┐
sp+24├─local_8───┤ 栈保护值
sp+16├─local_10──┤ JNIEnv指针
sp+8 ├─saved_x30─┤ 保存的返回地址
sp+0 ├─saved_x19─┤ 保存的x19值
└────────────┘
低地址
void JNI_OnLoad(long *param_1)
001006ec ff 83 00 d1 sub sp, sp, #0x20 ; sp=sp-32 在栈上分配4个变量,64位系统中一个变量占8位
001006f0 82 00 80 52 mov w2, #0x4 ; w2=4
long *local_10;
001006f4 e1 43 00 91 add x1, sp, #0x10 ; 参数2 x1=sp+16
001006f8 22 00 a0 72 movk w2, #0x1, LSL #16 ; 参数3 w2=1左移动十六位+w2=0x10004
001006fc f3 7b 00 a9 stp x19, x30, [sp] ; 保存x19和x30(链接寄存器)到栈上,查看ARM64调用约定:x19-x28 是被调用者保存寄存器(callee-saved),如果函数要使用这些寄存器,必须保存。要确定是否需要保存x19的值,我们需要看两点: x19是否在调用前有重要数据。x19在当前函数中的使用
00100700 93 00 00 90 adrp x19, 0x110000 ; 加载栈保护值的地址页
00100704 03 00 40 f9 ldr x3, [x0] ; x3=JNIEnv, x0 寄存器中存放的就是 param_1(jvm)的值,[x0] 表示取 jvm 指向的 JNI 函数表
00100708 64 fe 47 f9 ldr x4, [x19, #offset] ; 加载栈保护值的地址
0010070c 63 18 40 f9 ldr x3, [x3, #0x30] ; x3=JNIEnv->GetEnv函数指针,这里的#0x30表示jni文件中定义
00100710 84 00 40 f9 ldr x4, [x4] ; 加载实际的栈保护值
local_8 = ___stack_chk_guard;
00100714 e4 0f 00 f9 str x4, [sp, #local_8] ; 将栈保护值存储到栈上
iVar1 = (**(code **)(*param_1 + 0x30))(param_1,&local_10,0x10004);
00100718 60 00 3f d6 blr x3 ; GetEnv(x0, x1, w2)
if (iVar1 == 0) {
0010071c 60 03 00 35 cbnz w0, LAB_00100788 ; 如果返回值不为0,跳转到错误处理
uVar2 = (**(code **)(*local_10 + 0x30))(local_10,"github/jp1017/hellojni/MainActivity");
00100720 e2 0b 40 f9 ldr x2, [sp, #local_10] ; x2=local_10,这里的local_10是从jvm中获取JNIEnv并保存在这里的
00100724 01 00 00 90 adrp x1, 0x100000 ; 参数2 x1 = 包含字符串的页面起始地址
00100728 21 60 1f 91 add x1, "github/jp1017/hellojni/MainActivity" ; x1=x1+"github/jp1017/hellojni/MainActivity"
0010072c e0 03 02 aa mov x0, x2 ; 参数1 x0=x2
00100730 42 00 40 f9 ldr x2, [x2] ; x2 = JNIEnv->functions (加载函数表指针)
00100734 42 18 40 f9 ldr x2, [x2, #0x30] ; x2 = functions->FindClass (加载FindClass函数指针)
00100738 40 00 3f d6 blr x2 ; 调用x2
(**(code **)(*local_10 + 0x6b8))(local_10,uVar2,nativeMethod,1);
0010073c e1 03 00 aa mov x1, x0 ; 参数2 x1=x0=上一个调用x2的返回值
00100740 e2 0b 40 f9 ldr x2, [sp, #local_10]; x2=local_10 ,重新加载JNIEnv指针
00100744 23 00 80 52 mov w3, #0x1 ; 参数4 w3=1,设置要注册的方法数量为1
00100748 e0 03 02 aa mov x0, x2 ; 参数1 x0=x2,
0010074c 44 00 40 f9 ldr x4, [x2] ; x4 = JNIEnv->functions (加载函数表指针)
00100750 82 00 00 90 adrp x2, 0x110000 ; 加载本地方法结构体的地址页
00100754 84 5c 43 f9 ldr x4, [x4, #0x6b8] ; x4 = functions->RegisterNatives
00100758 42 f8 47 f9 ldr x2, nativeMethod ; 参数3 x2 =nativeMethod ,加载本地方法结构体
0010075c 80 00 3f d6 blr x4 ; 调用RegisterNatives
uVar2 = 0x10004;
00100760 80 00 80 52 mov w0, #0x4 ; 设置返回值为JNI_VERSION_1_4 (0x10004)
00100764 20 00 a0 72 movk w0, #0x1, LSL #16 ; 设置版本号高16位
LAB_00100768:
if (local_8 == ___stack_chk_guard) {
00100768 73 fe 47 f9 ldr x19, [x19, #offset]; 重新加载栈保护值地址
0010076c e2 0f 40 f9 ldr x2, [sp, #local_8] ; 加载保存的栈保护值
00100770 61 02 40 f9 ldr x1, [x19] ; 加载当前栈保护值
00100774 5f 00 01 eb cmp x2, x1 ; 比较栈保护值
00100778 c1 00 00 54 b.ne LAB_00100790 ; 如果不相等,检测到栈损坏
# 函数清理和返回
0010077c f3 7b 40 a9 ldp x19, x30, [sp] ; 恢复保存的寄存器
00100780 ff 83 00 91 add sp, sp, #0x20 ; 释放栈空间
00100784 c0 03 5f d6 ret ; 返回w0
LAB_00100788:
对应反编译代码:
else {
uVar2 = 0xffffffff;
}
00100788 00 00 80 12 mov w0, #0xffffffff ; w0=-1
0010078c f7 ff ff 17 b LAB_00100768 ; 跳转到清理代码
LAB_00100790:
__stack_chk_fail(uVar2);
00100790 ac ff ff 97 bl __stack_chk_fail ; 调用栈检查失败处理程序
- 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.
armeabi和armeabi-v7a
; void JNI_OnLoad(JavaVM *vm, void *reserved)
; 函数参数:R0 = JavaVM指针,R1 = 保留参数(未使用)
PUSH {R0-R2,R4,R5,LR} ; 保存寄存器到栈上,包括参数和返回地址
; 栈保护机制初始化
LDR R4, =(__stack_chk_guard_ptr - 0xE4A) ; 加载栈保护值的地址
MOV R1, SP ; 保存栈指针
MOV R1, SP 的作用是:
提供一个栈上的内存位置,用于存储GetEnv函数返回的JNIEnv指针
这个位置正好是之前PUSH指令保存的原始R0值的位置
GetEnv调用完成后,JNIEnv指针会被写入到这个栈位置
这就是为什么后面可以通过 LDR R0, [SP,#0x18+var_18] 来获取JNIEnv指针
这是一个巧妙的设计:
复用了保存原始参数的栈空间
避免了额外分配栈空间来存储JNIEnv指针
保证了后续操作可以方便地访问到JNIEnv指针
ADD R4, PC ; 计算栈保护值的实际地址
LDR R4, [R4] ; 加载栈保护值
LDR R2, =0x10004 ; 加载JNI版本号(1.4)
LDR R3, [R4] ; 获取栈保护值
STR R3, [SP,#0x18+var_14] ; 将栈保护值存储到局部变量
; 获取JNIEnv指针
LDR R3, [R0] ; R3 = JavaVM->函数表
LDR R3, [R3,#0x18] ; R3 = GetEnv函数指针
BLX R3 ; 调用GetEnv
CMP R0, #0 ; 检查返回值
BNE loc_E80 ; 如果获取失败,跳转到错误处理
; 查找Java类
LDR R0, [SP,#0x18+var_18] ; 加载JNIEnv指针
LDR R1, =(aGithubJp1017He - 0xE64) ; 加载类名字符串地址
LDR R3, [R0] ; 获取JNIEnv函数表
ADD R1, PC ; 计算类名字符串的实际地址
LDR R3, [R3,#0x18] ; 获取FindClass函数指针
BLX R3 ; 调用FindClass
; 注册本地方法
MOVS R3, #0xD7 ; 设置偏移量
MOVS R1, R0 ; R1 = 类引用
LDR R0, [SP,#0x18+var_18] ; 重新加载JNIEnv指针
LDR R2, =(nativeMethod_ptr - 0xE76) ; 加载本地方法结构体地址
LSLS R3, R3, #2 ; R3 = 0xD7 << 2 (计算RegisterNatives函数表偏移)
LDR R5, [R0] ; 获取JNIEnv函数表
ADD R2, PC ; 计算本地方法结构体实际地址
LDR R2, [R2] ; 加载本地方法结构体
LDR R5, [R5,R3] ; 获取RegisterNatives函数指针
MOVS R3, #1 ; 设置要注册的方法数量为1
BLX R5 ; 调用RegisterNatives
; 返回成功
LDR R0, =0x10004 ; 返回JNI版本号(1.4)
B loc_E84 ; 跳转到函数结束处理
loc_E80: ; 错误处理
MOVS R0, #1 ;
NEGS R0, R0 ; 返回-1表示错误
loc_E84: ; 函数结束处理
; 检查栈保护值
LDR R2, [SP,#0x18+var_14] ; 加载保存的栈保护值
LDR R3, [R4] ; 加载当前栈保护值
CMP R2, R3 ; 比较栈保护值
BEQ locret_E90 ; 如果相等,正常返回
BL j___stack_chk_fail ; 否则调用栈保护失败处理
locret_E90: ; 正常返回
POP {R1-R5,PC} ; 恢复寄存器并返回
.text:00000E90 ; ---------------------------------------------------------------------------
.text:00000E92 ALIGN 4
.text:00000E94 off_E94 DCD __stack_chk_guard_ptr - 0xE4A
.text:00000E94 ; DATA XREF: JNI_OnLoad+2↑r
.text:00000E98 dword_E98 DCD 0x10004 ; DATA XREF: JNI_OnLoad+A↑r
.text:00000E98 ; JNI_OnLoad+3C↑r
.text:00000E9C off_E9C DCD aGithubJp1017He - 0xE64
.text:00000E9C ; DATA XREF: JNI_OnLoad+1C↑r
.text:00000E9C ; "github/jp1017/hellojni/MainActivity"
.text:00000EA0 off_EA0 DCD nativeMethod_ptr - 0xE76
.text:00000EA0 ; DATA XREF: JNI_OnLoad+2C↑r
.text:00000EA0 ; } // starts at E40
- 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.
让我详细比较armeabiv7a版本与之前版本的所有差异:
- 函数入口和栈操作差异:
- 条件分支指令差异:
- RegisterNatives函数偏移差异:
- 错误处理实现:
- 地址计算和偏移量:
- 指令编码和优化:
- 数据段布局:
mips
# void JNI_OnLoad(JavaVM *vm, void *reserved)
# 参数: $a0 = JavaVM指针, $a1 = reserved(未使用)
# 初始化全局指针和栈帧
li $gp, (off_11020+0x7FF0 - .) ; 设置全局指针基址
addu $gp, $t9 ; 调整全局指针位置
addiu $sp, -0x30 ; 分配48字节栈空间
# 保存寄存器到栈
sw $gp, 0x24+var_14($sp) ; 保存全局指针
sw $s1, 0x24+var_s4($sp) ; 保存s1寄存器
sw $s0, 0x24+var_s0($sp) ; 保存s0寄存器
sw $ra, 0x24+var_s8($sp) ; 保存返回地址
# 设置栈保护
la $s1, __stack_chk_guard ; 加载栈保护值地址
lw $v0, 0($a0) ; 加载JavaVM函数表
lw $v1, (__stack_chk_guard - 0x11078)($s1) ; 加载栈保护值
lw $t9, 0x18($v0) ; 获取GetEnv函数指针
# 准备调用GetEnv
lui $s0, 1 ; 设置JNI版本高16位
addiu $a1, $sp, 0x24+var_C ; 设置GetEnv第二个参数(JNIEnv**)
addiu $a2, $s0, 4 ; 设置版本号0x10004
sw $v1, 0x24+var_8($sp) ; 保存栈保护值
jalr $t9 ; 调用GetEnv
nop ; 延迟槽
# 检查GetEnv返回值
bnez $v0, loc_6A0 ; 如果返回非0,跳转到错误处理
lw $gp, 0x24+var_14($sp) ; 恢复全局指针
# 准备调用FindClass
lw $a0, 0x24+var_C($sp) ; 加载JNIEnv指针
li $a2, 0 ; 清零
lw $a1, 0($a0) ; 加载JNIEnv函数表
lw $t9, 0x18($a1) ; 获取FindClass函数指针
jalr $t9 ; 调用FindClass
addiu $a1, $a2, aGithubJp1017He ; 设置类名参数
# 准备调用RegisterNatives
lw $a0, 0x24+var_C($sp) ; 重新加载JNIEnv指针
lw $gp, 0x24+var_14($sp) ; 恢复全局指针
lw $a3, 0($a0) ; 加载JNIEnv函数表
la $a2, nativeMethod ; 加载本地方法结构体地址
lw $t9, 0x35C($a3) ; 获取RegisterNatives函数指针
move $a1, $v0 ; 移动FindClass返回值作为参数
jalr $t9 ; 调用RegisterNatives
li $a3, 1 ; 设置方法数量为1
# 设置返回值
lw $gp, 0x24+var_14($sp) ; 恢复全局指针
ori $v0, $s0, 4 ; 返回JNI版本号0x10004
# 检查栈保护值
loc_680:
lw $a0, 0x24+var_8($sp) ; 加载保存的栈保护值
lw $t0, (__stack_chk_guard - 0x11078)($s1) ; 加载当前栈保护值
bne $a0, $t0, loc_6A8 ; 如果不相等,跳转到失败处理
# 函数返回清理
lw $ra, 0x24+var_s8($sp) ; 恢复返回地址
lw $s1, 0x24+var_s4($sp) ; 恢复s1
lw $s0, 0x24+var_s0($sp) ; 恢复s0
jr $ra ; 返回
addiu $sp, 0x30 ; 恢复栈指针
# 错误处理路径
loc_6A0:
b loc_680 ; 跳转到结束处理
li $v0, 0xFFFFFFFF ; 返回-1表示错误
# 栈检查失败处理
loc_6A8:
la $t9, __stack_chk_fail ; 加载栈检查失败函数
jalr $t9 ; 调用栈检查失败处理
nop ; 延迟槽
- 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.
这是MIPS64架构的JNI_OnLoad实现,让我们比较与之前MIPS32和ARM版本的主要区别:
- 64位指令和寄存器操作:
- 栈帧布局差异:
- 函数调用约定:
- 立即数加载:
- JNI函数表偏移:
- 栈保护机制:
x86
; int __cdecl JNI_OnLoad(JavaVM *vm)
; 局部变量定义
var_2C = dword ptr -2Ch ; JavaVM指针
var_28 = dword ptr -28h ; 参数2指针
var_24 = dword ptr -24h ; JNI版本号
var_20 = dword ptr -20h ; 方法数量
var_14 = dword ptr -14h ; JNIEnv指针
var_10 = dword ptr -10h ; 栈保护值
arg_0 = dword ptr 4 ; 第一个参数(JavaVM*)
; 函数序言
push esi ; 保存esi寄存器
push ebx ; 保存ebx寄存器
call sub_4A0 ; 调用子函数(可能是获取模块基址)
add ebx, (offset off_1FE8 - $) ; 计算GOT表偏移
; 栈帧设置
lea esp, [esp-24h] ; 分配36字节栈空间
mov eax, [esp+2Ch+arg_0] ; 获取JavaVM参数
lea ecx, [esp+2Ch+var_14] ; 获取JNIEnv指针存储位置
; 设置栈保护
mov esi, large gs:14h ; 获取栈保护值
mov [esp+2Ch+var_10], esi ; 保存栈保护值
xor esi, esi ; 清零esi
; 准备调用GetEnv
mov edx, [eax] ; 获取JavaVM函数表
mov [esp+2Ch+var_24], 10004h ; 设置JNI版本号(1.4)
mov [esp+2Ch+var_28], ecx ; 设置GetEnv第二个参数
mov [esp+2Ch+var_2C], eax ; 设置GetEnv第一个参数
call dword ptr [edx+18h] ; 调用GetEnv
; 检查GetEnv返回值
test eax, eax ; 检查返回值
jnz short loc_5A8 ; 如果非零跳转到错误处理
; 准备调用FindClass
mov eax, [esp+2Ch+var_14] ; 获取JNIEnv指针
lea ecx, (aGithubJp1017He - 1FE8h)[ebx] ; 加载类名字符串
mov edx, [eax] ; 获取JNIEnv函数表
mov [esp+2Ch+var_28], ecx ; 设置类名参数
mov [esp+2Ch+var_2C], eax ; 设置JNIEnv指针
call dword ptr [edx+18h] ; 调用FindClass
; 准备调用RegisterNatives
mov edx, [esp+2Ch+var_14] ; 重新获取JNIEnv指针
lea esi, (nativeMethod - 1FE8h)[ebx] ; 加载本地方法结构体
mov ecx, [edx] ; 获取JNIEnv函数表
mov [esp+2Ch+var_20], 1 ; 设置方法数量为1
mov [esp+2Ch+var_24], esi ; 设置本地方法结构体
mov [esp+2Ch+var_28], eax ; 设置类引用
mov [esp+2Ch+var_2C], edx ; 设置JNIEnv指针
call dword ptr [ecx+35Ch] ; 调用RegisterNatives
; 设置返回值
mov eax, 10004h ; 返回JNI版本号
loc_58E: ; 函数返回处理
mov esi, [esp+2Ch+var_10] ; 获取保存的栈保护值
xor esi, large gs:14h ; 检查栈保护值
jnz short loc_5AF ; 如果不匹配跳转到失败处理
; 函数清理和返回
lea esp, [esp+24h] ; 恢复栈指针
pop ebx ; 恢复ebx
pop esi ; 恢复esi
retn ; 返回
; 错误处理路径
loc_5A8:
mov eax, 0FFFFFFFFh ; 返回-1
jmp short loc_58E ; 跳转到返回处理
; 栈保护失败处理
loc_5AF:
call sub_480 ; 调用栈检查失败处理
- 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.
x86 64
让我比较这段x86_64架构的JNI_OnLoad与之前x86(32位)版本的主要区别:
- 寄存器使用差异:
- 调用约定差异:
- 栈帧管理:
- 栈保护实现:
- 函数调用方式:
- 代码布局:
主要改进:
- 更大的寄存器空间(64位)
- 更高效的参数传递(寄存器传参)
- 更简洁的代码结构
- 更大的寻址空间
- 额外的寄存器(r8-r15)
- 更现代的调用约定
但基本功能流程相同:
- 设置栈帧和保护
- 调用GetEnv获取JNIEnv
- 查找Java类
- 注册本地方法
- 返回JNI版本
x86_64版本的代码更简洁高效,这得益于:
- 更多的寄存器
- 更简单的调用约定
- 64位架构的优势
- 更现代的ABI设计
对arm64的二进制分析
HelloJni_1.0.apk改名为HelloJni_1.0.apk.zip解压后有如下数据包
安卓支持很多不同cpu框架上运行,但是随着不断的发展淘汰了mips和arm32位,目前arm64位和x86成为了主流支持cpu