微信号:code_gg_home

介绍:分享技术干货,分享最新新闻动态,分享开心段子,让大家轻松愉快的成长.

深入Android源码系列(一)

2017-04-01 19:47 陆晓明

   首先,每天看到不断有新人关注着这个公众号,心情很是愉悦。一种认可,一种信任,也是我前进的动力。感谢大家的支持与鼓励。

   

    本文讲解内容有

    loadLibrary流程

    linker

    ELF

    ndk开发以及配置调试版本

    ndk-gdb --start调试so

    gdb 调试bin文件

 gdb调试android apk方案


    关于本文讲解使用的代码,都在网盘提供了,可以自行下载。

    (链接: https://pan.baidu.com/s/1hrIxJdq 密码: jfwz)

00

    开发android 应用,主要围绕着java语言,可是,如果我们需要追求性能,或者需要调用之前我们已经写好的c c++ so库的时候,或者和硬件打交道的时候,那么我们就会接触到JNI(java native interface)。


    我们知道,java是在其虚拟机里面运行。我们简单举个例子吧。我让你使用任何一个语言,写一段代码:打开一个文件,读取每一行,如果这一行内容是1,我们就在窗口显示生活真美好。 想一下,是否都能写出来?


    这里举的例子,简单的解释了虚拟机的动作,打开一个文件(输入),读取每一行(内容),如果这一行内容是1(解析),我们就在窗口显示生活真美好(输出),比起这个来说,java虚拟机比这个复杂,但是基础核心的原理就是这个了。

    我们本节去讲一个内容,System.loadLibrary(XXXX) 的执行过程。此过程完成将so库加载进来,打通java和c c++本地库的桥梁,实现相互调用。(此文牵扯概念 javaVM  JNI  ELF 动态库静态库)


   我们要做JNI,少不了使用

嗯,我们这节,就是展看loadLibrary,来看这个方法都做了哪些事情。

我们这里看下参数:libname 将加载的库名字,比如我们库为libtest_jni.so 这里则写为test_jni,其余的系统会帮我们拼接。

继续向下看,发现调用了Runtime类里面的loadLibrary0方法,我们看下:

我们看到有两个参数:第一个为Classloader,这个为我们的类加载器,我们这里的参数为VMStack.getCallingClassLoader(),于是我们看下这段代码。

看到这里为native,于是它本身是使用c或者c++本地语言编写的了,我们找下位置。通过搜索getCallingClassLoader,我们找到了本地实现的地方在dalvik_system_VMStack.cc里面,于是我们截图,来看下。

这里NATIVE_METHOD是个宏定义

于是

会转化为

这个就是jni编写中,需要配置的对应表,主要完成java和c语言函数对应,参数和返回值对应的关系,给了这些,虚拟机才会在java和c之间建立起来关系,知道哪个java函数调用的真正正确的c语言函数,同时c也是可以反向调用java的,更多可以百度jni的编写。

关于getCallingClassLoader这个是如何加入到系统的,就是上面的register_dalvik_system_VMStack方法了。

我们不对这里展开了,此方法是在runtime.cc的InitNativeMethods方法里面的RegisterRuntimeNativeMethods完成。有兴趣的可以去看看。我们继续跟踪system.loadLibrary,这里继续看VMStack.getCallingClassLoader()。

01

通过上面的展看,我们知道了这个对应的c方法为:VMStack_getCallingClassLoader,于是我们看到:

这里因为不熟悉,就不讲了。

loadLibrary0 里面主要调用的方法为:

loader.findLibrary(libraryName); 去查找是否存在此动态库,没有就报找不到异常。

然后我们调用doLoad去加载。

doLoad主要完成,传入设置的librarySearchPath,然后调用本地代码nativeLoad方法。搜索nativeLoad,我们找到了它对应的实现,在Runtime.c

里面

根据之前展开的方式,此函数为:Runtime_nativeLoad,于是我们看到:

在OpenjdkJvm.cc里面:

关键方法,通过拿到当前的虚拟机vm,调用对应的LoadNativeLibrary(java_vm_ext.cc)方法,去真正加载对应的so。


我们来到java_vm_ext.cc里面,去看下LoadNativeLibrary真正的执行过程:

这里我们关系的是高亮的几个函数:OpenNativeLibrary,完成加载so的过程。

FindSymbol("JNI_OnLoad")完成找出so里面的JNI_OnLoad方法,如果有,使用(*jni_on_load)(this, nullptr)调用,返回so使用的java版本。这个JNI_OnLoad就是我们加载so的时候,会主动触发的一个初始化方法了。在这里主要完成java和c的对应关系方法,然后使用RegisterNativeMethods将此关系注册进入vm,以便后续调用能够找到。

扩展:

用于Android ART虚拟机JNI调用的NativeBridge介绍,地址为:

http://www.aichengxu.com/android/1473706.htm


02


我们停一下,完成一个简单的测试demo代码,以便我们调试使用。

参考http://blog.csdn.net/a332324956/article/details/8703286 来写一个JNI
搭配着eclipse去编译出来一个libtest_jni.so。(后续此工程会直接提供下载)

工程目录为:这里jni就是需要编出来so的地方。我们右键jnidemo选择properties,然后选择下Builders,点击new,创建一个编译规则。


编写一个调试:

这里Location指的是ndk-build脚本位置
Working Directory 指的是当前项目的src/jni,我们要使用ndk-build将jni目录下的android.mk执行,完成生成so的动作。

最后生成出来libtest_jni.so
我们在java工程使用下。(我们要在此基础上进行调试,所以我们使用的是自己load,不是写在static语句里面)

完整代码,文章最后提供,可以看着代码然后阅读。

我们在loadLibrary0上面打断点,然后看下流程:

我们可以看到看到,这里的loader为PathClassLoader.java,所以此处的findLibrary就是PathClassLoader.java文件里面的了。然后发现PathClassLoader继承自 BaseDexClassLoader,于是我们关心BaseDexClassLoader代码。

此段代码,完成在此app的本地so库的搜索路径下,查找我们的test_jni动态库,找到后path返回此so的绝对路径,以使后面的dlopen去动态打开此库。在此处,libname就是/data/app/com.example.jnidemo-2/lib/arm/libtest_jni.so,这个就是我们的jni动态库真正的位置了。

关于动态库dlopen dlsym 的用法,参照http://blog.csdn.net/edonlii/article/details/8445239 主要就是打开so,然后找到对应函数,然后执行。

按照这个文档,去调试so(需要下载android的ndk)

http://blog.csdn.net/kaiqiangzhang001/article/details/21108857

打上断点的截图为:

我们这里提供一个Android 的加载/链接器linker 的讲解

http://blog.csdn.net/dinuliang/article/details/5509009

关于android linker的代码位置 bionic/linker,可以去阅读。


03


我们回到之前的讲解,来找下LoadNativeLibrary调用的OpenNativeLibrary方法。在native_loader.cpp文件内找到此文件。

这里android调用了android_dlopen_ext方法,来实现动态库的加载,返回dlextinfo,而非android的,则是调用dlopen加载的。

我们搜索android_dlopen_ext,发现在 /bionic/libdl/里面的/bionic/libdl/libdl.c 里面有

看,是个空方法,没有实际动作,看到这里的注释,意思是我们的dynamic linker 实现了这个方法,我们找到linker(手机里面的/system/bin/linker),我们在linker的源码里面dlfcn.cpp找到android_dlopen_ext

但是在最终编译出来的linker里面是被修改成了__dl_android_dlopen_ext

找到linker文件里面的方法,具体的操作是:

将linker提取出来

然后运行,导出来内容

然后看到了__dl_android_dlopen_ext方法的实现体:


关于linker的启动,可以参考http://www.myexception.cn/android/1930690.html 阅读。同时adnroid源码也是提供了一个简单解释:

/development/ndk/platforms/下面的README.CRT.TXT文件,有如下内容:

完整的我提交网盘了,可以去下载阅读。


04


bionic/linker里面的Android.mk文件,发现了一段注释,可以解释__dl_android_dlopen_ext和android_dlopen_ext 怎么变化的。

这里的--prefix-symbols=__dl_ 就是给名字的符号上面加入一个前缀,于是我们的android_dlopen_ext 就变成了__dl_android_dlopen_ext。想找到编译linker的所有编译规则,参数,去mmm bionic/linker,就会在out下面生成一个-mmm-._bionic_linker_Android.mk.ninja文件,这个就是我们生成linker的所有规则,从里面去找--prefix-symbols,能看到

生成linker的时候,使用了objcopy修改了方法名。

我们调试linker的代码,我们因为加载的是__dl_android_dlopen_ext ,于是我们gdb下断点 b __dl_android_dlopen_ext ,这样子我们打断点,运行时候会在加载动态库时候,停下来:

可以看到,断点成功。

info sharedlibrary 查看当前需要的so。

info breakpoints   查看断点信息

bt 查看堆栈

b 方法 下断点

delete num 删除对应断点。

file XXX.so (有调试信息的库,然后我们调试,就会变成有效信息)


05


关于gdb的使用,可以参考

http://blog.csdn.net/ghostyu/article/details/8083228

关于solib-absolute-prefix 和solib-search-path的区别 ,可以参考:

http://blog.csdn.net/caspiansea/article/details/16798735         

我们这里看到了一个地址信息,又没有显示出来,这里为0xaafceefa,我们想找到这个地址,对应的代码,该如何找呢?

adb shell

ps | grep demo (这里demo是我们包名)

我们关心的是10171(进程id),然后我们查找/proc/10171/maps 

cat /proc/10171/maps  ,找到aafc是在这个位置:

aada1000-ab1f4000 r-xp 00000000 103:08 1377      /system/lib/libart.so

于是我们file加载下这个libart.so

然后重新调试,看效果:

看#2,是不是出来了。

我们打断点,发现b android_dlopen_ext 和 b __dl_android_dlopen_ext 是一个位置(bionic/linker/dlfcn.cpp line 82).所以我们实际的android_dlopen_ext就是__dl_android_dlopen_ext,也就是dlfcn.cpp文件内容了。

我们将bionic放置到我们调试的ndk-gdb --start目录,再次调试,代码就检索出来了。

漫漫长路,我们又可以启程了,我们当前需要阅读的代码,就围绕着android_dlopen_ext(dlfcn.cpp)函数开展了。先开心看一个内容,这里我将编译出来的所有so加载进来了,我们看到调试栈就会变成:

看到没,调用信息一目了然。

我们看下追踪这条代码线,可以找到我们的调用关系:

android::OpenNativeLibrary -->dlopen_ext-->do_dlopen-->find_library-->find_libraries.再追下去就没完没了了,这些方法,都是有源码的,于是我们从源码去看看吧。

从这里开始。我们主要关注find_libraries函数,这里此方法完成扫描此so需要依赖的其他so,加入到tasklist里面,然后依据每个task,完成load的动作。

我们去看下task,这个类型为LoadTask:看下load方法:

再看个ElfReader的load即可(更深层次的自行学习了),参考链接器与加载器那本书

主要就是找空间(mmap)解析出来的ELF的格式,加载load段到内存空间。关于FindPhdr的方法含义,看下它本身的注释


嗯,我们就讲到这里,主要就是学习如何开发ndk,跟踪loadlibrary的流程,调试so,linker的具体含义。


06


我们延伸一个内容:

我们加载nativehelper库,这个是在手机/system/lib下的一个核心库。我们测试下:

运行报错,错误为:

意思就是这个动态库是系统核心的,不能单独加载起来,系统不允许。这段代码位置在:linker.cc里面的 load_library函数:

于是我们看下is_greylisted,便是判断灰名单的方法:

这里更详细的不看,只需要关注我们>23之后,直接返回出错,禁止调用系统这些库。

07

如何使用gdb调试android c可执行文件方案呢?

其中hello-jni是测试代码,操作如下:

可以看到调试结果如下:

调试成功。


08


如何使用gdb调试android apk方案呢?

手机端adb shell切入

ps | grep demo   找到我们的进程号

gdbserver :1234 --attach 8481 这里8481为进程号

电脑端输入:

shell adb forward tcp:1234 tcp:1234

target remote localhost:1234

然后我们需要加载下app_process32程序,这个是从你手机/system/bin  下面导出来的。

adb pull /system/bin/app_process32 ~/

file ~/app_process32

set solib-search-path  /home/user/workspace/jnidemo/obj/local/armeabi

将符号表导入。可以多次操作set solib-search-path

然后我们看下当前符号信息

info sharedlib

缺少某个库的符号,使用set solib-search-path继续导入

当没有打上断点的时候,使用set solib-search-path将对应的so加载上来,然后就可以了。


更多精彩,敬请期待。


更多内容,关注微信公众号:code_gg_home

加微信 code_gg_boy  进入代码GG交流群





 
代码GG之家 更多文章 google 进入分屏后在横屏模式按home键界面错乱( 四) google 分屏 横屏模式 按home键界面错乱故障分析(三) google 分屏 横屏模式 按home键界面错乱故障分析(二) google 分屏 横屏模式 按home键界面错乱故障分析(一) Android基础进阶之学习使用AIDL进行跨进程通信
猜您喜欢 IOS框架和服务介绍(一)Cocoa Touch Layer(触摸UI层) 我是运维,我就这样挽救了800万用户 R语言词云终极解决方案—wordcloud2包 PyTips 0x12-Python 线程与协程(1) 女程序员做了个梦,众网友的神回复