前言(其实是吐槽)
这是我看(android应用安全防护和逆向分析)遇到的第一个坑了,在章节2.1和2.2里,虽然作者很贴心的给了步骤教你如何搭建ndk的开发环境,但是,我要说的是,如果按照作者在2.1.2的五个步骤按部就班的来,你绝对!不可能!完成!
主要的原因我就不再分析了,大约就是少了一堆乱七八糟的说明和步骤,这里我重新写一遍ndk开发相关。(如果你不信,可以尝试只按照2.1.2章节的五步来尝试)
搭建NDK开发环境
NDK相关概念
首先,普及一下ndk的概念,何谓ndk开发呢?
简而言之,就是让安卓(java)可以调用前人用c语言完成的库,这么做的好处主要有两个,
第一,节约代码量,提高工作(运行)效率,可以用之前c写好的很多很棒的库
第二,防止应用被逆向,因为java层的代码很容易被反编译逆向突破。
再介绍几个ndk里相关概念名词,
.c,.cpp,这几个不用多说了吧,就是c或者c++等文件的后缀名;
.so,这个是编译c文件得到的库文件的后缀名,.so文件大概就相当于windows上的.dll文件,他可以方便让别人调用;
.h,也就是通过javah命令编译类(class)文件编译出来的头文件,这玩意开始在c里用作声明管理,现在在jni里已经意义不大了,你可以当做没啥用;
jni,全名 java native interface,由他提供java和c之间的通信;
一个NDK开发的流程
编写Java代码(.java) —————> 编译生成字节码文件(.class) —————> 产生C头文件(.h) —————> 编写jni实现代码(.c) —————> 编译成链接库(.so)
基于Android Studio3 & CMAKE的NDK环境搭建
- 打开你的as(Android Studio),新建一个支持c++的项目(如图),然后一路next到结束;
- 此时一个基本工程环境已经新建好了,不需要任何改动,我们直接可以点击build生成一个系统自带的示例so库也就是cpp目录里的native-lib,但在此之前,我们先看一下工程目录:
可以看到,相对于普通项目,我们的ndk的项目其实就是多了cpp目录,多了CMakelists.txt文件,此外build.gradle里多了一些配置;
- 因为编译出so库,主要看的是CMakelists文件里的配置,下面我们重点关注一下CMakelists文件的内容,如图:
默认的CMakelists文件里,主要有三个配置需要关注,分别是
add_library:
主要用作给生成的so库做配置,在三个注释的下面,每一个都代表了一个配置项(其实注释的英文已经说明一切了),
Sets the name of the library下,填写你希望生成的so库的名称,这里注意的是,你填写的名称,最终会被自动加上lib的前缀和.so的后缀,因此你只需要基础的名称就够了,最终生成的so文件会在工程的app-build-intermediates-cmake-debug-obj目录下,每个abi环境都有个文件夹,里面放着对应的so库;
Sets the library as a shared library下,填写你要生成的so库的类型,这里的SHARED是指动态链接库类型;
Provides a relative path to your source file(s)下,填写你要编译成so文件的对应的c源文件的路径;
find_library:
主要是用作添加我们本地库的依赖库,
Sets the name of the path variable下,填写你引用的库的别名,便于引用;
you want CMake to locate下,填写你要引用的依赖库;
target_link_libraries:
是为了关联我们自己的库和一些第三方库或者系统库,
Specifies the target library下,填写本地库的名称;
included in the NDK下,填写你要关联的库的名称。
- ok,现在让我们点击一下工具栏的锤子图标,就可以生成so库了。
补充一些tips
关于环境
android ndk开发需要设置环境变量以及安装sdk,ndk,cmake,lldb,如果没有,可以在as的tools选项里,点击sdk manager,在sdk tools栏里,把这些都勾上,点击确定即可;
关于默认生成的例子
按照常规的ndk开发流程,应该是先写java类,这个类里可能会引用到native的方法也就是拿c实现的方法;然后javah命令编译这个java类,生成.h的头文件(这一步可以不用);再写一个.c或者.cpp实现在你java类里引用的这个native方法;最后编译你的c文件,生成最终的so库。
在系统默认的例子里,你点一下编译按钮就能得到so库,可能对这个流程不太明白,下面我们具体说一下:
先看一下MainActivity类的代码:
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = (TextView) findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
你可以当做这是系统给你先写好的一个引用了native方法的java类,也就是说这就是ndk第一步编写java类的产物;
在static里,System.loadLibrary("native-lib")这句会先于onCreate函数触发,这句是载入我们生成的动态链接库.so文件,这里只需要填写CMakeLists里面填写的名称即可;然后你会看到下面引用了这个库里的native方法,也就是这句public native String stringFromJNI();这就是系统帮你做好的第一步编写java类;
然后我们需要编写实现这个native方法的.c文件,这里系统也帮你写好了就是在cpp文件夹里的那个native-lib.cpp的文件,这个文件里实现了stringFromJNI()这个方法;
最后,我们点击编译,成功生成了so文件,这就是系统的例子里,ndk开发的完整流程。
Hello World 工程引发的被坑事件
前面是介绍了如何在as3的环境下进行ndk开发以及解析了系统自带的例子,接下来我们肯定要仿照系统的例子,自己走一遍ndk开发的流程,当然第一个工程肯定就是通用的HelloWorld了。
在小黄书(android应用安全防护和逆向分析)里,也有对helloworld工程的举例,流程是:新建一个java类,在java类里申明一个native方法,再在main方法里载入动态链接库,新建一个实例调用native方法;然后使用vc6.0编译器编写好native方法,生成.dll动态链接库文件;最后把该dll文件写入环境变量内,运行java类;具体代码和流程参见小黄书p14-p15页。
不过,其实不用如此麻烦,之所以用vc写c的内容,是因为当时as编译器对c的编写支持的还不够好,但as3以后,这块已经开始逐步完善了,因此,我们可以不用按照书里的流程,直接使用as3来写c的内容,不过,这里有一个坑,开始我是想仿造小黄书的示例代码,单独写一个具有main方法的java类来调用本地方法,大概是这样:
package com.example.test.testndk;
public class HelloWorld {
public native String stringFromJNITest();
static {
System.loadLibrary("JNITest");
}
public static void main(String[] args){
HelloWorld hello = new HelloWorld();
String hi = hello.stringFromJNITest();
System.out.println(hi);
}
}
JNITest是我事先写好的cpp文件编译成的.so库,里面实现了stringFromJNITest()方法,.cpp内容是这样的:
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring
JNICALL
Java_com_example_test_testndk_MainActivity_stringFromJNITest(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello world";
return env->NewStringUTF(hello.c_str());
}
以上其实就是我们自己实现一个ndk开发的流程,即先写一个java类,在类中声明一个native方法,然后写.cpp实现这个native方法,编译cpp成so库,在java类里用loadlibrary加载这个so库。
本例中,这是一个简单的native方法就是返回了一串字符串“Hello World”而已,原本我以为到此结束,点击运行这个java类我们的Hello World之旅就结束了,但是没想到报了个错:java.lang.UnsatisfiedLinkError: no JNITest in java.library.path。
开始我怀疑是不是CMakelists.txt的配置问题,但是我把这个so库在android工程的mainactivity里引用,居然没有问题,也就是说,应该不是配置的路径问题。经过在群里讨论以及百度,又开始怀疑是不是java.library.path的问题,因为据了解,其实你用as3运行android工程的mainactivity,编译器是会自动替你生成so库用的(只要你CMakelists配置好了),所以不会有路径问题,但是当你新建一个java类,写上main方法,独立运行这个java类的时候,其实是按java工程的方式找你的动态链接库文件的,而这个方式,在win下,是在环境变量path里找;在linux下,是在系统变量LD_LIBRARY_PATH里找,为了证明这个结论,我在系统的环境变量path里,添加了so库所在的目录路径。
但是,依旧报错。。no JNITest in java.library.path;这时群友又提醒我,我是win的系统,而win的动态链接库的格式是.dll,因此光配置了路径,但因为我的格式是 .so,因此也找不到;现搭一个linux系统来做实验成本太高,我采用了一个取巧的方式,直接把生成的.so文件后缀改为了dll,因为如果是后缀的原因,那么会报错,但肯定不会再报找不到的错,算是侧面论证。
我把后缀改为dll之后,再次运行!奇迹发生了,果然,还是报on JNITest in java.library.path,为什么呢,我猜可能是win下和android下,寻找规则不同,在android工程里,只需要写库名,不需要写前缀lib和后缀 .so,在win下,则需要写上前缀lib;最后,我把 System.loadLibrary("JNITest");改为System.loadLibrary("libJNITest");再次点击运行!
这次报错不同了,不再是找不到,而是Can't load this .dll (machine code=0x34) on a AMD 64-bit platform;终于,本次Hello World被坑事件的坑算是完美填上。