NDK简介:
NDK(英语:Native Development Kit,简称NDK)是一种基于原生程序接口的软件开发工具。通过此工具开发的程序直接以本地语言运行,而非虚拟机。因此只有Java等基于虚拟机运行的语言的程序才会有原生开发工具包。
NDK是一系列工具的集合:
NDK提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。这些工具对开发者的帮助是巨大的;
NDK集成了交叉编译器,并提供了相应的.mk文件以隔离CPU、平台、ABI等差异,开发人员只需要简单修改.mk文件,指出“哪些文件需要编译”、“编译特性要求”等,就可以创建出.so文件;
NDK可以自动地将.so文件和Java应用一起打包,极大地减轻了开发人员的打包工作。
我们为什么要使用NDK?
a. 代码的保护。由于apk的java层代码很容易被反编译,而C/C++库的反汇编难度较大;
b. 可以方便地使用现存的开源库。大部分现存的开源库都是用C/C++代码编写的;
c. 提高程序的执行效率。将要求高性能的应用逻辑使用C开发,从而提高应用程序的执行效率;
d. 便于移植。用C/C++写出的库可以方便地在其他嵌入式平台上再次使用。
总体思路:
在Eclipse中建立一个Android工程,在工程的根目录下新建一个名称为jni的文件夹,将编写好的 test.c文件复制到此文件夹中,或者直接在jni文件夹下新建一个文件,命名为test.c,接着编写ndk-build所需的脚本文件。
Android.mk文件是工程的编译脚本,告知编译系统关于源文件的一些信息,描述了编译原生程序所需的编译选项、头文件、源文件等等。
一个Android.mk文件由若干条定义语句组成,此文件中常用到的几个语句包括:
LOCAL_PATH := $(call my-dir)
Android.mk文件开头必须先定义好LOCAL_PATH变量。LOCAL_PATH定义了本地源码的路径,它用于在开发树中查找源文件。call my-dir指定了调用my-dir宏,这个宏函数是编译系统提供的,用于返回Android.mk文件本身所在的路径。
include $(CLEAR_VARS)
CLEAR_VARS由编译系统提供,指定让编译系统清除掉一些已经定义过的宏(如LOCAL_MODULE,LOCAL_SRC_FILES,LOCAL_STATIC_LIBRARIES等等),这些宏的定义都是全局的。当一个GNU MAKE在编译多个模块时,必须清除并重新设置它们。
LOCAL_ARM_MODE := arm
LOCAL_ARM_MODE指定生成的原生程序所使用的ARM指令模式,取值为arm或者thumb。arm表示使用32位的arm指令系统,thumb表示使用16位的arm指令系统。
LOCAL_MODULE :=test
LOCAL_MODULE指定模块的名称,即原生程序生成后的文件名。注意,如果生成共享库模块,编译系统会自动产生合适的前缀和后缀,对于本例中的源文件最终将生成名为libtest.so的共享库。
LOCAL_SRC_FILES := test.c
LOCAL_SRC_FILES指定将要编译打包进模块中的C\C++源代码文件列表,此处只有一个test.c文件。
include $(BUILD_SHARED_LIBRARY)
此语句用来指定生成的文件类型。BUILD_EXECUTABLE表示生成可执行程序,BUILD_SHARED_LIBRARY表示生成动态库,BUILD_STATIC_LIBRARY表示生成静态库。
实验步骤:
①安装Android NDK:
在电脑上安装NDK,我使用的版本为android-ndk-r8-windows-x86_64。安装完成后,新建一个系统变量,命名为NDK_ROOT,值为安装路径,最后将%NDK_ROOT%添加到环境变量Path中后,Android NDK安装完成。
在DOS中输入ndk-build进行测试,输出如下信息,说明Android NDK已经正常安装了。
②新建一个Android工程,命名为JNI,右键工程JNI,新建一个文件夹,将其命名为jni。
新建Android工程JNI,如图所示:
③在JNI工程的jni文件夹下新建两个文件,按下图所示命名:
④Android.mk与test.c两个文件编写完后,在DOS窗口下进入工程下的jni目录。输入ndk-build命令,在工程根目录下生成了一个libs文件夹,其中的armeabi文件夹中就存放着生成的可执行文件或者共享库文件(.so文件)。
Android.mk中最后一行为BUILD_EXECUTABLE时,可以生成名为test的可执行程序;
而Android.mk中最后一行为BUILD_SHARED_LIBRARY时,可以生成名为libtest.so的共享库文件。
在DOS中,进入工程文件夹下包含Android.mk与test.c两个文件的jni目录,如图所示:
当Android.mk中最后一行为上面的 BUILD_EXECUTABLE 时,输入 ndk-build 命令:
执行命令后,在工程根目录下生成了一个libs文件夹,其中的armeabi文件夹中生成了名为test的可执行程序,如图所示:
将Android.mk中最后一行修改为 BUILD_SHARED_LIBRARY,如图所示:
再次执行ndk-build命令,生成共享库文件libtest.so如图:
在工程根目录下的libs\armeabi文件夹中生成了名为libtest.so的共享库文件,如下图所示:
将生成的可执行程序test复制到模拟器中,在命令行窗口下输入命令让其运行,如图所示:
可见成功输出了字符串“JNI Test!”。
由于C/C++代码需要用ndk-build来进行编译,而java代码需要用Android sdk编译,所以为了开发快捷,需要在每次更改完C语言代码后可以自动编译C语言为.so库。使用Eclipse自动编译原生程序的原理依旧是使用ndk-build工具,但是自动化的操作会使原生程序的开发更加高效。
①在Eclipse中配置NDK:
要想使用Eclipse自动编译原生程序,首先需要在Eclipse中对NDK进行配置。打开Eclipse,进入Eclipse窗口中,在 Window—>Prefernces—>Android—>NDK 选择NDK Location的存放路径后,点击Apply and Close,如图所示:
②Eclipse自动编译原生程序:
首先,新建一个Build,此后编写代码时,保存修改后,Eclipse会自动编译生成原生程序。
第一步,在JNI工程上右键选择Properties,点击Builders选项,再点击Builders选项页右侧的New按钮,然后双击Program项打开Edit Configuration对话框,在对话框的Name栏输入Builder的名称,这里输入“JNI_Builder”,在 Location 栏输入“F:\android-ndk-r8\ndk-build.cmd” 设置要执行的命令,点击Working Directory右侧的Browse Workspace按钮选择JNI工程,最后点击Apply按钮应用更改,操作完成后,效果如图所示:
然后,单击Refresh标签,勾选“Refresh resources upon completion”复选框,如图所示:
下一步,单击Build Options标签,勾选“During auto builds”与“Specify working set of relevant resources”复选框,点击“Specify Resources”按钮,勾选JNI工程的jni目录,点击finish按钮,点击OK按钮关闭“Edit Configuration”对话框,如图所示:
最后,点击OK按钮关闭Properties对话框,这时JNI工程就会自动编译,可以在libs\armeabi目录下生成test.c文件的可执行文件test,如图所示:
而若将Android.mk中最后一行修改为BUILD_SHARED_LIBRARY,则Eclipse则会自动将其编译为libtest.so文件,如图所示:
以后若每次在Eclipse中对jni目录下的任何文件进行修改或保存,都会触发JNI_Builder重新编译原来的工程,直接简便。
但如果是使用ndk-build手动编译工程,那么每次修改完jni目录下的文件后都要在命令行窗口下对工程进行重新手动编译,相对来说就比较麻烦。
现在的许多App为了安全或者效率的问题,会把一些重要的功能放到native层。一般在Android中,native层使用的是so库文件。
使用NDK开发能够编译C/C++程序,最终生成so文件。而.so文件是一个二进制文件,我们是无法直接分析.so文件的,所以这里需要用到IDA Pro。IDA Pro能够对so文件进行反汇编,从而将二进制代码转化为汇编语言,利用IDA Pro的F5功能还能将汇编语言反编译成C/C++程序。
使用IDA Pro调试Android原生程序一般有远程运行与远程附加两种方式,远程运行调试用来调试原生可执行程序,远程附加调试用来调试Android原生动态链接库。
首先,需要用到前面实验编写的原生可执行程序实例test,以及IDA Pro软件目录下的android_server文件,找到后我把它们放到了一个新创建的名为tmp的文件夹中。
android_server所在路径为:
然后,进入tmp文件夹所在路径,将test与android_server两个文件复制到模拟器的data/local/tmp目录中,在DOS中执行以下命令可以对文件进行上传:
adb push test /data/local/tmp
adb push android_server /data/local/tmp
执行结果如下图所示:
然后,执行以下两行命令给两个文件加上可执行权限:
chmod 755 /data/local/tmp/test
chmod 755 /data/local/tmp/android_server
如图所示:
接着执行“./android_server”,以启动android_server,如下图所示:
打开另一个命令窗口,然后再看,这里监听了设备的23946端口,那么如果要想让IDA Pro和这个android_server进行通信,必须让PC端的IDA也连上这个端口,这时候就需要借助于adb命令:
adb forward tcp:远端设备端口号(进行调试程序端) tcp:本地设备端口(被调试程序端)
这里端口号均为23946,然后就可以把android_server端口转发出去了:
这时,我们只要在PC端使用IDA Pro连接23946这个端口就可以了,因为后面在使用IDA Pro进行连接的时候,IDA Pro把这个端口设置死了,就是23946,所以我们没办法自定义这个端口。
执行后,启动IDA Pro,这里要使用IDA Android 32-bit,所以在打开IDA的时候一定要是32位的IDA,不能是64位的,否则保存后就会有两个可执行程序,一个是32位,一个是64位,如果打开不正确则会报错。
点击菜单栏“Debugger->Run->Remote AmLinux/Android debugger”,打开调试程序对话框。这里看到,端口是给定的:23946,不能进行修改,所以上面的adb forward进行端口转发的时候必须是23946。这里PC本地机就是调试端,所以host就是本机的ip地址:127.0.0.1。
在Application栏输入“/data/local/tmp/test”,在Directory栏输入“/data/local/tmp”,在Hostname栏输入“localhost”或者“127.0.0.1”,如下图所示 :
设置完成后点击OK按钮,IDA Pro就会远程执行test,并自动切换到调试界面,如下图所示:
调试Android原生动态链接库需要先安装并运行包含该动态链接库的程序,然后使用IDA Pro远程附加程序进程的方式来进行调试。
编写实例程序JavaCTest,导出为APK文件,如图所示:
点击“Change Text”按钮后,程序会调用动态链接库libjavac.so库中的stringFromJNI()方法返回一个字符串“Java C Test!”,并且更改字符串“Hello Xidain!”为此字符串。
接下来,对libjavac.so中stringFromJNI()方法的执行过程进行动态调试。执行下面的命令启动IDA Pro的Android调试服务器:
adb shell
./android_server
命令执行成功后,Android调试服务器会监听23946端口,结果如下图所示:
adb forward tcp:23946 tcp:23946
启动IDA Pro,点击菜单项“Debugger→Attach→Remote ARMLinux/Android debugger”,打开调试程序的设置对话框。在Hostname栏中输入127.0.0.1,如下图所示:
然而,此时代码并没有运行在动态链接库部分,要想调试动态链接库,就得为动态链接库中的函数设置断点。将JavaCTest.apk解压,找到libjavac.so文件,开启另一个IDA实例并载入它,找到stringFromJNI() 方法的代码,如下图所示:
从上图反汇编的代码中可以看出,stringFromJNI()方法的代码起始处位于 0x00002050 处。回到IDA Pro的调试窗口,按下“Ctrl+S”快捷键打开段选择对话框,查找libjavac.so动态链接库的基地址,此处的基地址为 0x00001F3C,如下图所示:
这里的跳转地址是可以计算的,如果想要跳转到某函数,然后下断点,那么可以使用 CTRL+S 查找到.so文件的内存开始的基地址,然后再用IDA View中查看该函数对应的相对地址,相加就是绝对地址,然后跳转即可。
对于一般的基地址,只要程序没有退出,在运行中它的值就不会改变,因为在程序的数据已经加载到内存中后,基地址就不会改变,除非程序退出后又重新运行,把数据又重新加载到内存中。同时相对地址也是不会改变的,只有在修改.so文件的情况下,如果文件大小改变了,相对地址可能会改变,其他情况下不会改变,相对地址就是数据在整个.so文件中的位置。
内存地址等于基地址加上偏移地址,由此可以计算出stringFromJNI()方法的内存地址为 0x00003F8C。关闭段选择对话框,按下快捷键G,打开地址跳转对话框,在“Jump to address”栏中输入 0x00003F8C,如下图所示:
点击OK按钮,程序会跳转到 stringFromJNI() 方法所在的代码行,并自动分析方法的代码。在 0x00003F8C 行按下快捷键F2设置一个断点,被设置断点的代码行会被红色标记,如下图所示:
设置好断点后,回到程序中点击“Change Text”按钮,程序就会自动中断在 0x00003F8C 行上,接下来就可以调试动态链接库中的原生程序了。
1.问题:
一开始,我在运行IDA Pro的Android服务器时,DOS命令下总是提示:not executable:magic 7F45,如图所示:
最初我以为是执行权限的问题。然而在修改权限后,依旧不能运行。把文件清空后,重新安装,还是不能奏效。
在查找了相关资料、反复调试后,我终于找到了出错原因:
因为我在Android.mk中使用的是arm的ABI,而我的Android模拟器却使用的是x86的内核,当然不能正常运行。
所以需要安装对应ABI为arm的虚拟机才可以。
2.解决方法:
在AVDM中,安装一个ABI为 arm 的Android模拟器,如下图所示: