我们已经知道,android的底层是linux。本质上,其实android就是一个经过特殊裁剪处理的linux系统,裁剪是为了让我们有限的移动端硬件能够更好地运行这个linux。
相对linux,很多人更加熟悉window。这里我们来做个对比。
windows linux
可执行文件 exe elf
静态链接库 .lib .a
动态链接库 .dll .so
这里说的不太准确,比如windows 下可执行程序不仅仅只有.exe,还有.com、.sys等(这里重点不是这个,杠精请忽略)。虽说在linux中,不像windows那样通过文件后缀来判断文件名,但是依旧是可以使用文件后缀的,而且使用后缀也能帮助我们快速分辨文件类型。
ps:这里插入一个小拓展。原生程序的类型,其实可以大致分为三类。
1、可重定位文件(relocatable file)。通常是.o结尾的目标文件
2、原生可执行文件(executable file)
3、共享的目标文件(shared object file)
这里我们注意到,在apk中,经常会出现有so文件,它之所以会执行,也就是因为linux的内核中,so是作为动态链接库或者可执行程序的。也就是说,so还是elf 可执行文件,但是这种情况下,so的后缀一般是不显示的。
这里我们脑洞打开
android 的原生开发,我的理解本质上就是在android 中跑 C/C++ 代码。
因为android 是linux 系统。所以我想的是,换句话说,就是在linux 下跑C/C++ 代码。
如此,我们是否可以把一个linux文件放到我们的android 系统上去运行呢??
瞎搞开始了,我打开我的ubuntu,然后用gedit写了一个hello world
然后,找到压箱底很久的 《C程序设计》,终于按照上面的方法编译得到了可执行文件。而且成功用运行了。
对,就是成功打印出来了 "hello world"
我们使用hex 文件查看器来看看我们生成的hello 文件。可以清楚发现,文件头是有.ELF标记的,也就是hello 确实就是linux下的可执行文件
验证我们想法的时候到了,我们将这个hello 文件上传到android 手机上,让android 来运行它。
然后好像运行不来额,报了一个错误说这个是64-bit 的ELF文件。
其实这里就很接近真相了,我们的elf 是二进制文件直接用来运行的。但是与运行直接相关的是cpu处理器。处理器架构的不同,对应的执行程序肯定是不通用的。
目前市面上的CPU分类主要有两大阵营。
1、intel、AMD为首的复杂指令集CPU。架构是X86
2、IBM、ARM为首的精简指令集CPU。IBM公司的CPU是PowerPC架构,ARM公司是ARM架构。
这里我的pc 是x86架构,android 手机是arm,所以肯定是不能运行的。
那么有没方法可以跨平台编译?
肯定是有的,在windows 下进行android开发不就是这么回事么?
我们知道,android开发需要下载两个组件。一个叫sdk,一个叫ndk。其中ndk就是做native 层开发的,现在就派上用场了。
我们接下来就是要用ndk来编译我们的hello.c 代码,然后来生成适用于android 的可执行程序elf文件。
1、先创建一个叫jni 的文件夹(名称一定是jni 不能改),然后将我们的.c 文件放进去,同时创建Android.mk、Application.mk。如下所示:
2、在Application.mk 中输入如下:
这里也就是说编译生成所有平台的可执行文件,当然也可以不生成所有的平台。
比如你这样:
APP_ABI:= x86 armeabi-v7a
那最后就只生成 x86 和 armeabi-v7a 下 使用的文件
3、在Android.mk 输入如下:
这里设置的是编译 hello.c 这个文件 并最后输出 libhello-jni 文件
这里输出的是可执行程序 BUILD_EXECUTABLE ,所以最终我们生成的elf 是不带so后缀的,可以直接运行
但是在一般的android开发中,我们生成的elf文件并不是传统原生可执行文件,而是可执行文件的一种拓展,叫共享目标库文件。因此这里的编译类型需要稍微修改一下。
nclude $(BUILD_SHARED_LIBRARY)
这个要区别开。
LOCAL_CFLAGS += -pie -fPIE
LOCAL_LDFLAGS += -pie -fPIE
是因为,在Android 4.4之后添加了新的保护机制,可执行文件必须是采用PIE编译的。
这三个文件准备好之后,我们就可以开始编译了。使用ndk-build。这里我将ndk 加到了环境变量中,所以可以直接调用,不然需要进到ndk目录下找到ndk-build.cmd 所在的目录下。
命令执行之后,会看到生成了两个目录 obj 和 libs
我们打开libs ,已经得到了各个平台的编译结果。这些平台就是由Application.mk 配置的
这里面的 libhello-jni 就是我们的生成可执行的elf 文件。是没有so后缀的额。
我们找到自己android设备支持的cpu架构,从里面找到对应的 libhello-jni ,最后将这个 elf 放到 android 中去执行,如下:
顺利执行,打印出 hello world。
上面手动让android运行了elf程序,其实在apk中,调用so原理上也是差不多的。
介绍了使用Android.mk 来生成我们的elf 文件之后这里再介绍另一种比较传统的做法,那就是使用makefile来生成我们的elf。相比android.mk,makefile 使用比较麻烦。需要制定生成的目标文件,依赖的文件等等。但是这样的好处就是我们能更加直观体会到elf的生成过程,同时也可以生成出c源码对应的汇编代码.s文件。可以方便我们进行对比理解和学习arm汇编指令。
其实,android.mk 就是android编译环境下的一种特殊的“makefile”文件, 它是经过了android编译系统处理的。所谓android编译系统,就是android顶层目录下的build目录里面的一系列编译控制文件,其实就是一系列makefile文件和 *.mk 文件,这些文件才是编译android系统完整的makefile文件.每个模块里的android.mk只不过是被包含进android编译系统的一小部分而已。经过android编译系统的一大堆处理,android.mk的格式就变得非常简单,且与普通的makefile文件书写格式不一样了。
所以我们可以理解为使用makefile其实就是绕过了android.mk 文件在android编译系统中的层层解析,直接就可以调用gcc进行编译了。
接下来简单介绍下这个过程。
需要准备两个文件:
一个是c源文件,然后写上最简单的C代码,比如:
接下来就是编写makefile 文件:
#ndk根目录
NDK_ROOT=E:/android/android-ndk-r14b-windows-x86_64/android-ndk-r14b
#编译器根目录
TOOLCHAINS_ROOT=$(NDK_ROOT)/toolchains/arm-linux-androideabi-4.9/prebuilt/windows-x86_64
#编译器目录
TOOLCHAINS_PREFIX=$(TOOLCHAINS_ROOT)/bin/arm-linux-androideabi
#头文件搜索路径
TOOLCHAINS_INCLUDE=$(TOOLCHAINS_ROOT)/lib/gcc/arm-linux-androideabi/4.9.x/include-fixed
#SDK根目录
PLATFROM_ROOT=$(NDK_ROOT)/platforms/android-21/arch-arm
#sdk头文件搜索路径
PLATFROM_INCLUDE=$(PLATFROM_ROOT)/usr/include
#sdk库文件搜索路径
PLATFROM_LIB=$(PLATFROM_ROOT)/usr/lib
#文件名称
MODALE_NAME=demo
MODALE_TYPE=c
PATH_ANDROID=/data/local/tmp
#删除
RM=rm
#编译选项
FLAGS=-I$(TOOLCHAINS_INCLUDE) \
-I$(PLATFROM_INCLUDE) \
-L$(PLATFROM_LIB) \
-nostdlib \
-lgcc \
-Bdynamic \
-lc \
-pie -fPIE
#所有obj文件
OBJS=$(MODALE_NAME).o \
$(PLATFROM_LIB)/crtbegin_dynamic.o \
$(PLATFROM_LIB)/crtend_android.o
#编译器链接
all:
$(TOOLCHAINS_PREFIX)-gcc $(FLAGS) -c $(MODALE_NAME).$(MODALE_TYPE) -o $(MODALE_NAME).o
$(TOOLCHAINS_PREFIX)-gcc $(FLAGS) -s $(MODALE_NAME).$(MODALE_TYPE) -o $(MODALE_NAME).s
$(TOOLCHAINS_PREFIX)-gcc $(FLAGS) $(OBJS) -o $(MODALE_NAME)
old:
$(TOOLCHAINS_PREFIX)-gcc $(FLAGS) -E $(MODALE_NAME).$(MODALE_TYPE) -o $(MODALE_NAME).i
$(TOOLCHAINS_PREFIX)-gcc $(FLAGS) -S $(MODALE_NAME).i -marm -o $(MODALE_NAME).s
$(TOOLCHAINS_PREFIX)-gcc $(FLAGS) -c $(MODALE_NAME).s -o $(MODALE_NAME).o
$(TOOLCHAINS_PREFIX)-gcc $(FLAGS) $(OBJS) -o $(MODALE_NAME)
#删除所有.o文件
clean:
$(RM) *.o
$(RM) *.i
$(RM) $(MODALE_NAME)
#安装程序到手机
install:
adb push $(MODALE_NAME) /data/local/tmp
adb shell chmod 755 /data/local/tmp/$(MODALE_NAME)
adb shell /data/local/tmp/$(MODALE_NAME)
#运行程序
run:
adb shell /data/local/tmp/$(MODALE_NAME)
这里要注意的是我上面设置的 ndk 路径和版本问题。在实际使用中要改成自己的路径,对应的版本也要改。
还有一个就是 makefile 文件中的MODALE_NAME ,MODALE_TYPE。这里我的源程序就是demo.c ,实际使用的时候要改成自己的。
然后还有一个就是找到ndk 中的make.exe .这个就是我们待会要使用的的make 工具。我的在
E:\android\android-ndk-r14b-windows-x86_64\android-ndk-r14b\prebuilt\windows-x86_64\bin
这个目录下,有兴趣的可以对应找找自己的,并将这个路径添加到 path 环境变量中,方便我们使用。
最后我们进入到我们的demo.c 和 makefile 文件目录下,使用make 工具进行编译 ,如:
过程很顺利,然后查看目录。发现已经生成了很多文件:
demo 就是我们的elf 文件,可以直接放到手机上去运行的。比如,我们使用刚刚写的makefile 中的install 命令
将demo文件上传到手机 ,并给与权限和运行。打印出结果。
再打开我们的demo.s 文件,这里有我们关注的 c 的arm汇编代码。接下来我们可以通过比较来学习c 语句和对应的arm 汇编的翻译过程。最后给出上面demo.c 对应的arm 汇编代码。有兴趣的话可以看看。
.arch armv5te
.fpu softvfp
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 2
.eabi_attribute 30, 6
.eabi_attribute 34, 0
.eabi_attribute 18, 4
.file "demo.c"
.text
.align 2
.global hunter_add
.type hunter_add, %function
hunter_add:
@ args = 0, pretend = 0, frame = 16
@ frame_needed = 1, uses_anonymous_args = 0
@ link register save eliminated.
str fp, [sp, #-4]!
add fp, sp, #0
sub sp, sp, #20
str r0, [fp, #-8]
str r1, [fp, #-12]
str r2, [fp, #-16]
str r3, [fp, #-20]
ldr r2, [fp, #-8]
ldr r3, [fp, #-12]
add r2, r2, r3
ldr r3, [fp, #-16]
add r2, r2, r3
ldr r3, [fp, #-20]
add r3, r2, r3
mov r0, r3
sub sp, fp, #0
@ sp needed
ldr fp, [sp], #4
bx lr
.size hunter_add, .-hunter_add
.section .rodata
.align 2
.LC0:
.ascii "add: %d\012\000"
.text
.align 2
.global main
.type main, %function
main:
@ args = 0, pretend = 0, frame = 8
@ frame_needed = 1, uses_anonymous_args = 0
stmfd sp!, {fp, lr}
add fp, sp, #4
sub sp, sp, #8
str r0, [fp, #-8]
str r1, [fp, #-12]
mov r0, #1
mov r1, #2
mov r2, #3
mov r3, #5
bl hunter_add(PLT)
mov r2, r0
ldr r3, .L5
.LPIC0:
add r3, pc, r3
mov r0, r3
mov r1, r2
bl printf(PLT)
mov r3, #0
mov r0, r3
sub sp, fp, #4
@ sp needed
ldmfd sp!, {fp, pc}
.L6:
.align 2
.L5:
.word .LC0-(.LPIC0+8)
.size main, .-main
.ident "GCC: (GNU) 4.9.x 20150123 (prerelease)"
.section .note.GNU-stack,"",%progbits
对于arm 反汇编的学习,首先是要有C/C++的功底。看懂是最基本的要求,最好是能写C/C++代码.然后,我们可以尝试自己编写一些小型的C/C++,再对应编译成elf和.s 文件。通过IDA 的调试来学习这个arm指令与C/C++ 的转化。我的话是先编译一份elf,然后通过ida查看arm指令,在还原为C代码,最后跟原来写的代码比较或者运行一遍逆向结果来判断是否成功逆向。如此反复,同时也能促进arm 汇编指令的学习。