JNI全称为Java Native Interface,是使Java方法与C\C++函数互通的一座桥梁。通俗的讲,它的作用就是使Java可以调用C\C++写的函数、使C\C++可以调用Java写的方法。
众所周知,Android开发一般采用Java语言,虽Google推出了Kotlin语言的开发方案,但其实Kotlin的本质亦是基于Java虚拟机,那么在Android上系统,亦是基于Dalvik虚拟机的,所以性能上,与跟采用Java开发是没有任何区别的。由于Java是虚拟机语言(指需要被编译成虚拟机代码,由虚拟机执行的语言),所以无论是JVM(Java虚拟机)还是Dalvik(Android定制版JVM),其程序性能在性能需求较高的情况下,就显得有些不足了。
那么这个时候就需要编译型语言出马了,编译型语言将源代码编译为机器码,直接由CPU执行代码,使性能大幅提升。
Java代码的安全性很弱! 如果你没有逆向Java或者Android程序的经验,那么可以请你写一个简单的Java程序或者Android程序,然后在Github或者其他地方下载一个jadx,打开jadx-gui或者使用命令行,反编译你编译出来的程序,你可能会发现这是一个新世界,噢天哪,代码逻辑清晰可见,简直就跟在看源码一样!当然,这些只是反编译器生成的伪代码,但也足以惊人。
这个时候,你就可以开始考虑将关键代码放到C\C++里面写了,因为其编译之后就只有机器码,机器码可以反编译成汇编,但汇编比高级语言更加的晦涩难懂,没有一定技术功底的人无法直观的理解汇编代码。虽可通过一些神器(如:IDA F5)来获取伪码,但这些伪码相比Java的伪码,简直不堪入目。
所以编写原生代码,不但可以拥有更高的性能,还可获得一定的代码安全性保障。
Google为Android的原生开发提供了开发者工具NDK(Native Development Kit),用来编译C/C++项目。起初的时候构建一个NDK项目还需一番配置,现在随着Android Studio的不断更新,已经可以在Android Studio的项目中直接编写、编译了。
需要先对Android Studio进行一番配置。首先打开Android Studio的设置页面,File-Settings,搜索Android SDK,勾选上CMake(编译C\C++源码的程序)、LLDB(调试器)、NDK,然后点击Apply进行更新。
此处我没有勾选NDK是因为我使用自行下载的NDK版本,每个项目自行选择NDK路径。
打开Android Studio新建一个Project,并第一步勾选Include C++ support:
其余选项可按需改动。新建完成后,就是一个完整的JNI的Hello World了。
在左侧的Andorid视图中,可以看到比正常的项目多了一个cpp目录,这就是我们存放C\C++源码的地方了:
生成的这个函数声明看起来有点反人类,其实他是这样子的
Ctrl单击JNIEXPORT可以看到其宏定义,是一个defalut属性,而JNICALL则是个空定义,所以其实这两个是可以忽略的。
重点关注的是返回类型jstring、函数名Java_cn_hluwa_demo01_MainActivity_stringFromJNI、参数列表JNIEnv和jobject。
大家伙知道,Java中的基本数据类型是int、long、short、float、double、char、byte、boolean这些,为了避免与C语言的基本数据类型冲突,在JNI中,将JAVA的基本数据类型重定义成了:jint、jlong、jshort、jfloat、jdouble、jchar、jbyte、jboolean。那jstring又是怎么回事呢?虽然String不是Java基本数据类型,但它实在是太常用了,所以便有了jstring;对于数组,则是再后面再加个Array,如:jintArray、jbyteArray,但是没有jstringArray,欸,那如何表示呢?还有其他的非基本类型呢? 除了上述以及jclass、jthrowable、jarray这些有专用重定义之外其他类型均使用jobject表示,所以String数组就是jobjectArray啦。Ctrl+单击jstring就可跳到jni.h头文件查看各个定义了。
可以看到这个函数名非常的长,这是因为JNI函数的绑定需要依赖于一个函数命名规则,让Java层一下子就可以找到对应的原生函数。可以先看到java层的代码:
stringFromJNI
加了一个native描述符,表示是一个原生函数,MainActivity
是类名,cn.hluwa.demo01
是包名,Java_cn_hluwa_demo01_MainActivity_stringFromJNI
是对应的C函数名,那么这个规则就很显而易见了,将包名的.替换成_(因为.不能用于函数命名),然后Java_PackName_CLassName_MethodName
。运行时,JNI就会依赖此规则来对函数进行绑定。
至于Native层调用Java层呢,JNI提供里一系列函数,比如:
同样在jni.h中可以看到,或可自行查阅文档。
在上述的Java代码中,可以看到static代码块中多了一个System.loadLibrary("native-lib");
,在Android开发中,原生代码一般使用C\C++编写,然后编译为一个动态链接库,即文件后缀为".so"的ELF文件。loadLibrary
的作用就是加载这个动态链接库,这样后面的代码调用才能成功的找到对应的原生函数。而静态代码块的执行时机非常早,比什么构造函数、onCreate都要早,在类加载的时候就被调用。库加载并非一定要在当前类、static块中!。加载库还有其他方法,比如使用System.load(String)
方法,其传入链接库的具体路径;甚至有的是在Native层中使用dlopen、mmap等方式来进行加载,就相当于自己实现了一个loadLibrary,但是最终的目的都是一样的:将代码加载入内存中。
Android编译后的Apk其实只是个zip压缩包,打开后在其lib目录中可以看到那些被loadLibrary
加载的库(lib中可能有多个文件夹,对应多种CPU架构)。
如今许多开发者都出于安全性考虑或其他需求,不愿使用函数名规则绑定,而是自己动态注册来绑定native函数。方法也很简单,只需调用RegisterNatives
函数即可。其申明如下:
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2019-2-1 18:20
被admin编辑
,原因: