前置知识

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

逆向工程 --- Android JNI,从一个简单示例开始_android

 在反编译的角度。通过比较两个反编译器,还是ghidra厉害,ida错误多且可读性差,但是ghidra有一个重大错误,返回值是空。还是建议使用ghidra的反编译功能。而且ghidra支持多个指令集的反编译,ida只支持x86

我们分析一下错误如何产生的。

通过分析指令集ret,发现没有正确读取w0的值,属于软件缺陷

逆向工程 --- Android JNI,从一个简单示例开始_加载_02

之后的分析在不说明的情况下默认是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版本与之前版本的所有差异:

  1. 函数入口和栈操作差异:
; armeabi版本
PUSH    {R0-R2,R4,R5,LR}
MOV     R1, SP
ADD     SP, SP, #0xC        ; 函数退出时调整栈指针
POP     {R4,R5,PC}         ; 只恢复必要寄存器

; 之前版本
PUSH    {R0-R2,R4,R5,LR}
MOV     R1, SP
POP     {R1-R5,PC}         ; 直接恢复所有寄存器
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  1. 条件分支指令差异:
; armeabi版本
CBNZ    R0, loc_E40        ; 使用更高效的条件分支指令

; 之前版本
CMP     R0, #0
BNE     loc_E80            ; 使用比较+分支两条指令
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  1. RegisterNatives函数偏移差异:
; armeabi版本
LDR.W   R5, [R3,#0x35C]    ; 使用更大的偏移量0x35C

; 之前版本
MOVS    R3, #0xD7
LSLS    R3, R3, #2         ; 通过移位计算偏移量
LDR     R5, [R5,R3]
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  1. 错误处理实现:
; armeabi版本
MOV.W   R0, #0xFFFFFFFF    ; 直接加载-1,使用32位指令

; 之前版本
MOVS    R0, #1
NEGS    R0, R0            ; 通过取反得到-1
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  1. 地址计算和偏移量:
; armeabi版本
LDR     R4, =(__stack_chk_guard_ptr - 0xE10)
LDR     R1, =(aGithubJp1017He - 0xE26)
LDR     R2, =(nativeMethod_ptr - 0xE2E)

; 之前版本
LDR     R4, =(__stack_chk_guard_ptr - 0xE4A)
LDR     R1, =(aGithubJp1017He - 0xE64)
LDR     R2, =(nativeMethod_ptr - 0xE76)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  1. 指令编码和优化:
; armeabi版本
使用.W后缀表示32位指令
代码更紧凑,减少了对齐填充

; 之前版本
使用基本指令集
有更多的对齐填充
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  1. 数据段布局:
; armeabi版本
数据段更紧凑,常量池位置不同
偏移量计算更优化

; 之前版本
数据段较分散,常量池位置靠后
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

 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版本的主要区别:

  1. 64位指令和寄存器操作:
; MIPS64版本(使用64位操作)
daddiu  $sp, -0x30        ; 64位立即数加法
sd      $gp, 0x10+var_s10($sp)  ; 64位存储
ld      $v1, 0($a0)       ; 64位加载

; MIPS32版本
addiu   $sp, -0x30        ; 32位立即数加法
sw      $gp, 0x24+var_14($sp)  ; 32位存储
lw      $v0, 0($a0)       ; 32位加载
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  1. 栈帧布局差异:
; MIPS64版本(8字节对齐)
var_10  = -0x10
var_8   = -8
var_s0  = 0
var_s8  = 8
var_s10 = 0x10
var_s18 = 0x18

; MIPS32版本(4字节对齐)
var_18  = -0x18
var_14  = -0x14
var_C   = -0xC
var_8   = -8
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  1. 函数调用约定:
; MIPS64版本
ld      $t9, 0x30($v1)    ; 64位函数指针
jalr    $t9               ; 调用函数

; MIPS32版本
lw      $t9, 0x18($v0)    ; 32位函数指针
jalr    $t9               ; 调用函数
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  1. 立即数加载:
; MIPS64版本
dli     $v0, 0xFFFFFFFFFFFFFFFF  ; 64位立即数
dla     $t9, __stack_chk_fail    ; 64位地址加载

; MIPS32版本
li      $v0, 0xFFFFFFFF          ; 32位立即数
la      $t9, __stack_chk_fail    ; 32位地址加载
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  1. JNI函数表偏移:
; MIPS64版本
ld      $t9, 0x6B8($a4)    ; RegisterNatives偏移
ld      $t9, 0x30($v1)     ; GetEnv偏移

; MIPS32版本
lw      $t9, 0x35C($a3)    ; RegisterNatives偏移
lw      $t9, 0x18($v0)     ; GetEnv偏移
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  1. 栈保护机制:
; MIPS64版本
sdc2    $25, ___stack_chk_guard($zero)  ; 使用协处理器2存储

; MIPS32版本
sw      $v1, 0x24+var_8($sp)           ; 直接存储
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

 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位)版本的主要区别:

  1. 寄存器使用差异:
; x86_64版本 (使用64位寄存器)
mov     rdi, [rsp+18h+var_18]    ; 使用rdi作为第一个参数
mov     rsi, rsp                 ; 使用rsi作为第二个参数

; x86版本 (使用32位寄存器)
mov     eax, [esp+2Ch+arg_0]     ; 使用eax
mov     ecx, [esp+2Ch+var_14]    ; 使用ecx
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  1. 调用约定差异:
; x86_64版本 (System V AMD64 ABI)
rdi - 第一个参数
rsi - 第二个参数
rdx - 第三个参数
rcx - 第四个参数

; x86版本 (cdecl)
通过栈传递参数
[esp+2Ch+arg_0] - 第一个参数
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  1. 栈帧管理:
; x86_64版本
lea     rsp, [rsp-18h]          ; 更简洁的栈空间分配
lea     rsp, [rsp+18h]          ; 释放栈空间

; x86版本
lea     esp, [esp-24h]          ; 32位栈操作
lea     esp, [esp+24h]          ; 释放栈空间
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  1. 栈保护实现:
; x86_64版本
mov     rax, fs:28h             ; 64位段寄存器偏移
mov     [rsp+18h+var_10], rax   ; 保存64位栈保护值

; x86版本
mov     esi, large gs:14h       ; 32位段寄存器偏移
mov     [esp+2Ch+var_10], esi   ; 保存32位栈保护值
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  1. 函数调用方式:
; x86_64版本
call    qword ptr [rax+30h]     ; 64位函数指针调用
call    qword ptr [r8+6B8h]     ; 使用r8扩展寄存器

; x86版本
call    dword ptr [edx+18h]     ; 32位函数指针调用
call    dword ptr [ecx+35Ch]    ; 使用基本寄存器
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  1. 代码布局:
; x86_64版本
更紧凑的代码布局
不需要显式的GOT表访问

; x86版本
需要通过ebx访问GOT表
需要更多的对齐和填充
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

主要改进:

  1. 更大的寄存器空间(64位)
  2. 更高效的参数传递(寄存器传参)
  3. 更简洁的代码结构
  4. 更大的寻址空间
  5. 额外的寄存器(r8-r15)
  6. 更现代的调用约定

但基本功能流程相同:

  1. 设置栈帧和保护
  2. 调用GetEnv获取JNIEnv
  3. 查找Java类
  4. 注册本地方法
  5. 返回JNI版本

x86_64版本的代码更简洁高效,这得益于:

  • 更多的寄存器
  • 更简单的调用约定
  • 64位架构的优势
  • 更现代的ABI设计

 对arm64的二进制分析

HelloJni_1.0.apk改名为HelloJni_1.0.apk.zip解压后有如下数据包

逆向工程 --- Android JNI,从一个简单示例开始_Java_03

逆向工程 --- Android JNI,从一个简单示例开始_android_04

 安卓支持很多不同cpu框架上运行,但是随着不断的发展淘汰了mips和arm32位,目前arm64位和x86成为了主流支持cpu