android动态加载dex
A. Android类加载机制
Android手写热修复(一)--ClassLoader
我们平时编写的 .java 文件不是可执行文件,需要先编译成 .class 文件才可以被虚拟机执行。所谓类加载是指通过 类加载器 把class文件加载到虚拟机的内存空间,具体来说是方法区。类通常是按需加载,即第一次使用该类时才加载。
首先,Java与Android都是把类加载到虚拟机内存中,然后由虚拟机转换成设备识别的机器码。但是由于二者使用的虚拟机不同,所以在类加载方面也是有所区别的。Java的虚拟机是JVM,Android的虚拟机是dalvik/art(5.0以后虚拟机是art,是对dalvik的一种升级)。 Java虚拟机运行的是class文件,而Android 虚拟机运行的是dex文件。 dex其实是class文件的集合,是对class文件优化的产物,是为了避免出现重复的class。
从上面的讲解中,我们已经知道我们平时写的类是被 类加载器 加载尽虚拟机内存才能运行。下面就通过Framework源码来为大家讲解Android中最主要的5个类加载器。
在Activity做个简单验证:
结果:
可以看出系统类由BootClassLoader加载,apk中的类由PathClassLoader加载,PathClassLoader的父类加载器是BootClassLoader。如果暂时不能理解父类加载器是什么,没关系,后面讲双亲委托机制的时候会理解的。
下面的源码解析基于 Android SDK API28 ,这几个类加载器(除了ClassLoader)没办法直接在AS上查看源码,AS搜索到的是反编译的class的内容,是不可信的,为大家推荐一个在线工具查看, 在线查看Android Framework源码 。
用来加载本地文件系统上的文件或目录,通常是用来加载apk中我们自己写的类,而像 Activity.class 这种系统的类不是由它加载。注意:这里,并不像很多网上文章说的那样只能加载apk,本地的其他目录的文件也是可以的,这一点我会在后面验证说明。
也是被用来加载 jar 、apk、dex,通常用来加载未安装到应用中的文件。注意,它需要一个应用私有的可写的目录来存放优化后的dex文件。千万不要选择外部存储路径,因为这样可能会导致你的应用遭到注入攻击。
关于dex文件优化,可能很多人还是不理解,水平有限,我简单解释一下,
构造器参数解释:
关于optimizedDirectory:
1、这是dex优化后的路径,它必须是一个应用私有的可写的目录否则会存在注入攻击的风险;
2、这个参数在API 26(8.0)之前是有值的,之后的话,这个参数已经没有影响了,因为在调用父构造器的时候这个参数始终为null,也就是说Android 8.0 以后DexClassLoader和PathClassLoader基本一样的来;
3、在加载app的时候,apk内部的dex已经执行过优化了,优化之后放在系统目录/data/dalvik-cache下。
这个构造器的关键是初始化了一个DexPathList对象,这个是后面加载class的关键类。
这个构造方法等关键是通过 makeDexElements() 方法来获取Element数组,这个Element数组非常关键,后面查找class就会用到它,也是热修复的关键点之一。
splitDexPath(dexPath) 方法是把dexPath目录下的所有文件转换成一个File集合,如果是多个文件的话,会用 : 作为分隔符。
makeDexElements()
小结一下,这个方法就是把指定目录下的文件apk/jar/zip/dex按不同的方式封装成Element对象,然后按顺序添加到Element[]数组中。
DexPathList#loadDexFile()
可以看到 DexFile 最终是调用了openDexFile、native方法openDexFileNative去打开Dex文件的,如果outputName为空,则自动生成一个缓存目录,具体来说是 /data/dalvik-cache/[email protected] 。openDexFileNative这个native方法就不具体分析了,主要是对dex文件进行了优化操作,将优化后得odex文件通过mmap映射到内存中。感兴趣的同学可以参考:
《DexClassLoader和PathClassLoader加载Dex流程》
现在在回头看看DexClassLoader与PathClassLoader的区别。DexClassLoader可以指定odex的路径,而PathClassLoader则采用系统默认的缓存路径,在8.0以后没有区别。
ClassLoader是一个抽象类,有3个构造方法,最终调用的还是第一个构造方法,主要功能是保存实现类传入的parent参数,也就是父类加载器。ClassLoader的实现类主要有2个,一个是前面讲过的BaseDexClassLoader,另一个是BootClassLoader。
BootClassLoader是ClassLoader的内部类,而且继承了ClassLoader。
这是加载一个类的入口,流程如下:
1、 先检查这个类是否已经被加载,有的话直接返回Class对象;
2、如果没有加载过,通过父类加载器去加载,可以看出parent是通过递归的方式去加载class的;
3、如果所有的父类加载器都没有加载过,就由当前的类加载器去加载。
通常我们自己写的类是通过当前类加载器调用 findClass 方法去加载的,但是在 ClassLoader 中这是个空方法,具体的实现在它的子类 BaseDexClassLoader 中。
BaseDexClassLoader # findClass
可以看到是通过pathList去查找class的,这个对象其实之前讲过,它是在BaseDexClassLoader 的构造方法中初始化的,它实际上是一个 DexPathList 对象。
DexPathList # findClass()
对Element数组遍历,再通过Element对象的 findClass 方法去查找class,有的话就直接返回这个class,找不到则返回null。 这里可以看出获取Class是通过DexFile来实现的,而各种类加载器操作的是Dex。Android虚拟机加载的dex文件,而不是class文件。
1、加载一个类是通过双亲委托机制来实现的。
2、如果是第一次加载class,那是通过 BaseDexClassLoader 中的findClass方法实现的;接着进入 DexPathList 中的findClass方法,内部通过遍历Element数组,从Element对象中去查找类;Element实际上是对Dex文件的包装,最终还是从dexfile去查找的class。
3、一般app运行主要用到2个类加载器,一个是PathClassLoader:主要用于加载自己写的类;另一个是BootClassLoader:用于加载Framework中的类;
4、热修复和插件化一般是利用DexClassLoader来实现。
5、PathClassLoader和DexClassLoader其实都可以加载apk/jar/dex,区别是 DexClassLoader 可以指定 optimizedDirectory ,也就是 dex2oat 的产物 .odex 存放的位置,而 PathClassLoader 只能使用系统默认位置。但是在8.0 以后二者是没有区别的,只能使用系统默认的位置了。
这张图来源于:
Android虚拟机框架:类加载机制
在类加载流程分析中,我们已经知道,查找class是通过DexPathList来完成的,实际上DexPathList最终还是遍历其Element数组,获取DexFile对象来加载Class文件。 由于数组是有序的,如果2个dex文件中存在相同类名的class,那么类加载器就只会加载数组前面的dex中的class。如果apk中出现了有bug的class,那只要把修复的class打包成dex文件并且放在 DexPathList 中Element数组`的前面,就可以实现bug修复了 。下一篇为大家带来的手写热修复。
Android类加载机制的细枝末节
从JVM到Dalivk再到ART(class,dex,odex,vdex,ELF)
类加载机制系列2——深入理解Android中的类加载器
Android 热修复核心原理,ClassLoader类加载
B. android dex何时加载
1 问题
在Android系统中,一个App的所有代码都在一个Dex文件里面。Dex是一个类似Jar的存
储了多有Java编译字节码的归档文件。因为Android系统使用Dalvik虚拟机,所以需要把
使用Java Compiler编译之后的class文件转换成Dalvik能够执行的class文件。这里需要强
调的是,Dex和Jar一样是一个归档文件,里面仍然是Java代码对应的字节码文件。
当Android系统启动一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的
工具来处理,叫DexOpt。DexOpt的执行过程是在第一次加载Dex文件的时候执行的。这
个过程会生成一个ODEX文件,即Optimised Dex。执行ODex的效率会比直接执行Dex文
件的效率要高很多。但是在早期的Android系统中,DexOpt有一个问题,也就是这篇文
章想要说明并解决的问题。DexOpt会把每一个类的方法id检索起来,存在一个链表结构
里面。但是这个链表的长度是用一个short类型来保存的,导致了方法id的数目不能够超过
65536个。当一个项目足够大的时候,显然这个方法数的上限是不够的。尽管在新版本的
Android系统中,DexOpt修复了这个问题,但是我们仍然需要对老系统做兼容。
2 思路
一种有效的解决思路是把Dex文件分割成多个较小的Dex。这就如同很多项目会把自己分
割成多个Jar文件一样,不同的功能在不同的Jar文件里面,通过一些配置和额外的操作,
可以让虚拟机有选择性的加载Jar文件。但是在Android系统中,一个应用是只允许有一
个Dex文件的。也就是说在编译期的时候,所有的Jar文件最终会被合并成一个Dex文件。
我们没有办法在Apk文件里面打包两个Dex,让DexOpt分别对两个Dex文件做处理,而
Android系统也不会同时为一个Apk加载两个Dex。
1
2.1 动态加载
如果我们把Dex分成多个文件,然后在程序运行的时候,再把多的那几个动态的加载进来
是否可行呢?也就是说我们能否在运行时阶段把代码加入虚拟机中。对于虚拟机来说,其
实所有的代码都是在运行时被加载进来的。而不同于C语言还存在着静态链接。虚拟机在
所有Java代码执行之前被启动,然后开始把字节码加载到环境中执行,我们可以理解成所
有的代码都是动态加载到虚拟机里的。
而说到加载,不得不说的是ClassLoader。它的工作就是加载.class文件。在Android的
Dalvik环境中,对应的是DexClassLoader,它们的功能是完全一样的。ClassLoader的
一大特点就是它是一个树状结构。每个ClassLoader都有一个父亲ClassLoader。也就是
说,ClassLoader不是把所有的Class放到一个巨大的数组或别的什么数据结构中来处理。
ClassLoader在加载一个Jar中的类的时候,需要制定另一个ClassLoader作为父亲节点,
当我们需要通过ClassLoader得到一个类类型的时候,ClassLoader会把请求优先交给父
亲ClassLoader来处理,而父亲ClassLoader又会交给它的父亲,一直到根ClassLoader。
如果根ClassLoader有这个类,而返回这个类的类类型,否则把这个请求交给这个请求的
来源子ClassLoader。这是一种向上传递,向下分发的机制。这种情况下,对于调用着来
说,子ClassLoader永远都是包含最多Class的ClassLoader。有一点我们需要注意,父亲
ClassLoader只会向请求来源分发自己的处理结果。所以如果来源是自己,那么如果没有
请求类它就会返回空,而不是遍历所有子ClassLoader去请求是否有被请求的类。
在Android系统中,对于一个应用来说,其实有两个ClassLoader,一个是SystemClassLoader,这个ClassLoader里面除了Java标准的类库之外,还有一个android.jar,所有
Android Framework层的类都在这里。而另外一个重要的ClassLoader就是基于Android
Context的ClassLoader。所有属于当前应用的类都是用这个ClassLoader来加载的,我们
可以在Android源码中看到,所有的Activity,Service,View都是使用这个ClassLoader
来反射并创建的。我们暂时把它叫做ContextClassLoader。
3 加载外部Dex
3.1 构建一个Dex文件
这一步并不复杂,首先我们把所需要的.class文件或者是Jar文件和一些源码一起编译生
成一个Jar文件。然后使用Android SDK提供的dx工具把Jar文件转成Dex文件。我们可以
提前对它进行ODex操作,让它在被DexClassLoader加载的时候,跳过DexOpt的部分工
作,从而加快加载的过程。
2
3.2 DexClassLoader
现在的工作就是在运行时加载这个Dex文件了。我们可以在Application启动的onCreate
方法里面加载Dex,但是如果你的Dex太大,那么它会让你的App启动变慢。我们也可以
使用线程去加载,但我们必须保证加载完成之后再进行某个外部类的请求。当然也可以真
正等到需要某个外部类的时候再进行Dex加载。这根本上取决于Dex文件本身的大小,太
大了可以预加载,而比较小可以等到实际需要的时候再加载。我们暂且把这个加载了外部
Dex的ClassLoader成为ExternalClassLoader
上面我们提到了树形结构和系统中的多个ClassLoader,当我们加载外部Dex的时候,我
们是否需要指定一个父ClassLoader呢?我们当然需要一个父ClassLoader,否则我们ExternalClassLoader连一些基本的Java类都没有,它根本不可能成功的加载一个Dex。进一
步的,我们要选择哪一个ClassLoader来作为我们的父亲呢?是SystemClassLoader还是
ExternalClassLoader?这是根据情况来定的,如果外部的Dex文件里没有任何和Android
相关的代码,那么SystemClassLoader是我们的首选,否则我们就应该用ContextClassLoader。如果是后者的情况,我们的树可以被看成一个链表。
3.3 外部的View, Acitivity等
我们知道,我们编写的四大组建都不是由我们自己来创建的,是由系统来给我们构造并
管理其生命周期的。那么这个过程是什么样的呢?拿Activity来举例,我们需要通过调用
当前Activity/Context的startActivity,传入一个Intent来调用启动一个新的Activity。系
统有一个ActivityManager来处理这里的逻辑。这里的逻辑相当的复杂,但简单来说,
ActivityManager会收到并处理这个Intent,从而决定是是启动一个新的,还是把旧的放
到前台。它会先查找这个Activity在哪个应用里面,这是通过扫描每个应用的AndroidManifest来确定。这些信息是在PackageManager里面被检索的。总之如果这个Activity
不再任何的manifest里面,它就不可能被启动。所以仅有一个Activity类是不够的,我们
需要在manifest里面声明它。
上面是Activity的情况,Service之类的也是同理。那么View怎么办?尽管我们可以直接创
建View,但是大部分的View都不是我们创建的,而是通过XML布局文件Inflate出来的。
也就是说,我们在XML定义了一些外部Dex里面的View,那么显然这个XML是不能被成
功的Inflate的。因为除非系统会使用我们的ExternalClassLoader,否则它肯定是找不到
我们的类的:ContextClassLoader里面并没有外部Dex中的类。
也就是说问题的根本在于,对于那些Android系统为我们创建的对象,它是不能包含在外
部Dex里面的。而Android系统中大部分的组建类的生命周期都交给了系统来管理。我们
3
不可能自己来创建这些类对象。那么另一种思路:我们是不是可以通过使用我们的ExternalClassLoader来代替ContextClassLoader呢?尽管系统的ContextClassLoader是私有
的,但是我们可以通过反射强制的把它替换成我们的ExternalClassLoader。而对于那些
外部的组建(Activity等),尽管我们没有它们的类,但是并不影响我们在AndroidManifest里面声明这个Activity。因为Android系统只是把它作为一个检索,并不会真正检查它
里面的组建是不是真的在虚拟机环境中已经被加载了,只有真正使用Intent启动某个组建
的时候才会去检查。而只要我们保证这个时候我们已经加载了外部的ClassLoader,那么
这个组建就可以被正常的启动。
还有一点,除了我们要为外部可能有的组建在AndroidManifest里面做声明一外,那些外
部组建可能用到的权限我们也需要一一声明,例如如果外部Activity使用了相机功能,那
么如果我们的Manifest里面没有声明使用相机功能的权限的话,即便这个Activity能成功
为加载出来,仍然是不能使用的。
4 核心代码段
加载外部Dex
mClassLoader = new DexClassLoader ( f . getAbsolutePath ( ) ,
mContext . getCacheDir ( ) . getAbsolutePath ( ) ,
null , mContext . getClassLoader ( ) ) ;
让系统使用ExternalClassLoader
t r y {
F ie ld mMainThread = ge t F ie ld ( A c t i v i t y . class , ”mMainThread”) ;
Object mainThread = mMainThread . get ( a c t i v i t y ) ;
Class t hreadClass = mainThread . get Class ( ) ;
F ie ld mPackages = ge t F ie ld ( threadClass , ”mPackages”) ;
WeakReference<?> r e f ;
Map< St ring , ?> map =(Map< St ring , ? >) mPackages . get (mainThread ) ;
r e f = (WeakReference<? >) map . get (mContext . getPackageName ( ) ) ;
Object apk = r e f . get ( ) ;
Class apkClass = apk . get Class ( ) ;
F ie ld mClassLoader = ge t F ie ld ( apkClass , ”mClassLoader”) ;
mClassLoader . set (apk , classLoader ) ;
} catch ( I llegalArgument Except ion e) {
i f (DEBUG) {
e . print St ackTrace ( ) ;
}
} catch ( I llega lAc c essEx c ept ion e) {
i f (DEBUG) {
4
e . print St ackTrace ( ) ;
}
}
5
C. android 怎么动态更新apk中jar包
核心类
1.1 DexClassLoader类
可以加载jar/apk/dex,可以从SD卡中加载为安装的apk。
1.2 PathClassLoader类
只能加载已经安装到Android系统中的apk文件。
一、正文
1.1
类似于eclipse的插件化实现, 首先定义好接口, 用户实现接口功能后即可通过动态加载的方式载入jar文件, 以实现具体功能。 注意 , 这里的jar包需要经过android dx工具的处理 , 否则不能使用。
D. Android Native库的加载及动态链接
我们从一个简单的NDK Demo开始分析。
下面从 System.loadLibrary() 开始分析。
下面看 loadLibrary0()
参数 loader 为Android的应用类加载器,它是 PathClassLoader 类型的对象,继承自 BaseDexClassLoader 对象,下面看 BaseDexClassLoader 的 findLibrary() 方法。
下面看 DexPathList 的 findLibrary() 方法
回到 loadLibrary0() ,有了动态库的全路径名就可以装载库了,下面看 doLoad() 。
nativeLoad() 最终调用 LoadNativeLibrary() ,下面直接分析 LoadNativeLibrary() 。
对于JNI注册,这里暂不讨论,下面看 OpenNativeLibrary() 的实现。
下面看 android_dlopen_ext() 的实现
接下来就Android链接器linker的工作了。
下面从 do_dlopen() 开始分析。
find_library() 当参数translated_name不为空时,直接调用 find_libraries() ,这是装载链接的关键函数,下面看它的实现。
find_libraries() 中动态库的装载可以分为两部分
下面从 find_library_internal() 开始分析。
下面分析 load_library()
下面看另一个 load_library() 的实现
下面分析ELF文件头以及段信息的读取过程,也就是LoadTask的 read() ,它直接调用ElfReader的 Read() 方法。
动态库的装载在LoadTask的 load() 中实现。
下面看ElfReader的 Load() 方法
动态库的装载已经完成,下面看链接过程。
下面看 prelink_image()
链接主要完成符号重定位工作,下面从 link_image() 开始分析
下面以函数引用重定位为例分析 relocate() 方法
E. android 怎么动态的加载类
android 如何动态的加载类----app插件技术
转自:http://blog.csdn.net/mingli198611/article/details/8858076
?
前言:
?
? ? ? 在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优势。现如今很多项目要求需要采用类似于微信或Q游这样的插件化开发模式越来越多,本文就是阐述android的动态加载技术来满足插件化开发模式的文章。
?
1.基本概念
1.1??在Android中可以动态加载,但无法像Java中那样方便动态加载jar。
Android的虚拟机(DalvikVM)是不认识Java打出jar的byte code,需要通过dx工具来优化转换成Dalvikbyte code才行。这一点在咱们Android项目打包的apk中可以看出:引入其他Jar的内容都被打包进了classes.dex。即android要加载的java类必须dex格式的代码文件.
1.2??在Android中可以加载基于NDK的so库。
NDK的执行效率很高,加密性很好,但同时开发入门难度大,一般用于加解密、数学运算等场合。so的加载很简单,如果APK发布时已经携带了so文件,只需要在加载时调用System.loadLibrary(libName)方法即可。由于软件的安装目录中存放so的目录是没有写权限的,开发者不能更改该目录的内容,所以如果要动态加载存放在其他地方的so文件,用System.load(pathName)方法即可。
1.3??在Android中支持动态加载dex文件的两种方式:
DexClassLoader:这个可以加载jar/apk/dex,也可以从SD卡中加载,也是本文的重点
PathClassLoader:只能加载已经安装到Android系统中的apk文件。也就是 /data/app 目录下的 apk 文件。其它位置的文件加载的时候都会出现 ClassNotFoundException.因为 PathClassLoader 会去读取 /data/dalvik-cache 目录下的经过 Dalvik 优化过的 dex 文件,这个目录的 dex 文件是在安装 apk 包的时候由 Dalvik 生成的。
?
2.注意
2.1 采用不用安装的插件开发模式,只能够使用?DexClassLoader进行加载.不过动态加载是有一些限制的,比如插件(子apk)包中的Activity、Service类是不能动态加载的,因为缺少声明;即使你在Manifest文件中进行了声明,系统默认也是到安装apk所在的路径中去寻找类,所以你会遇到一个ClassNotFound的异常。插件里你可以用主apk中先前放入的layout、strings等资源。但是插件中自带的界面只能用纯代码进行编写,插件中是不能加载插件(子apk)中的xml作为layout等资源使用的。所以在开发上一些特效会比较困难些,建议预先植入主apk中。
2.2?大家可以看看DexClassLoader的API文档,里面不提倡从SD卡加载,不安全
3.如何制作插件
3.1 把工程导出为jar包
3.2 执行SDK安装目录android-sdk-windows\platform-tools下的dx命令,把jar包转换为dex格式
dx?--dex?--output=dex名 jar包名
4.如何做到启动未安装的apk中的activity?
采用反射机制,把主apk中的activity的context传递到插件的activity中,然后采用反射进行回调插件activity的方法。不足之出就是,插件中的activity并不是真正的activity,它只是运行在主activity中。比如:点击返回直接退出当前activity而不是回到主activity。实例如下:
?
这是调用的Activity:
?
[java]?view plain ? ?
package?com.beyondsoft.activity;??
??
import?java.lang.reflect.Constructor;??
import?java.lang.reflect.InvocationTargetException;??
import?java.lang.reflect.Method;??
??
import?dalvik.system.DexClassLoader;??
import?android.app.Activity;??
import?android.content.pm.PackageInfo;??
import?android.os.Bundle;??
import?android.util.Log;??
??
public?class?PlugActivity?extends?Activity?{??
??
????private?Class?mActivityClass;??
????private?Object?mActivityInstance;??
????Class?localClass;??
????private?Object?instance;??
??
????@Override??
????protected?void?onCreate(Bundle?savedInstanceState)?{??
????????super.onCreate(savedInstanceState);??
??
????????Bundle?paramBundle?=?new?Bundle();??
????????paramBundle.putBoolean("KEY_START_FROM_OTHER_ACTIVITY",?true);??
????????paramBundle.putString("str",?"PlugActivity");??
????????String?dexpath?=?"/sdcard/FragmentProject.apk";??
????????String?dexoutputpath?=?"/mnt/sdcard/";??
????????LoadAPK(paramBundle,?dexpath,?dexoutputpath);??
????}??
??
????@Override??
????protected?void?onStart()?{??
????????super.onStart();??
????????Method?start;??
????????try?{??
????????????start?=?localClass.getMethod("onStart");??
????????????????start.invoke(instance);??
????????}?catch?(Exception?e)?{??
????????????//?TODO?Auto-generated?catch?block??
????????????e.printStackTrace();??
????????}??
????}??
??
????@Override??
????protected?void?onResume()?{??
????????//?TODO?Auto-generated?method?stub??
????????super.onResume();??
????????Method?resume;??
????????try?{??
????????????resume?=?localClass.getMethod("onResume");??
????????????resume.invoke(instance);??
????????}?catch?(Exception?e)?{??
????????????//?TODO?Auto-generated?catch?block??
????????????e.printStackTrace();??
????????}??
????}??
??
????@Override??
????protected?void?onPause()?{??
????????super.onPause();??
????????Method?pause;??
????????try?{??
????????????pause?=?localClass.getMethod("onPause");??
????????????pause.invoke(instance);??
????????}?catch?(Exception?e)?{??
????????????e.printStackTrace();??
????????}??
????}??
??
????@Override??
????protected?void?onStop()?{??
????????super.onStop();??
????????try?{??
????????????Method?stop?=?localClass.getMethod("onStop");??
????????????stop.invoke(instance);??
????????}?catch?(Exception?e)?{??
????????????e.printStackTrace();??
????????}??
????}??
??
????@Override??
????protected?void?onDestroy()?{??
????????//?TODO?Auto-generated?method?stub??
????????super.onDestroy();??
????????try?{??
????????????Method?des?=?localClass.getMethod("onDestroy");??
????????????des.invoke(instance);??
????????}?catch?(Exception?e)?{??
????????????//?TODO?Auto-generated?catch?block??
????????????e.printStackTrace();??
????????}??
????}??
F. android怎么动态调试dex
1.1 安装JDK
JAVA环境的搭建请自行查找资料,这里不做详述。
1.2 安装Android SDK
下载地址:http://developer.android.com/sdk/index.html。
下载完安装包后解压到任意一目录,然后点击运行SDK Manager.exe,然后选择你需要的版本进行安装,如图:
1.3 安装Eclipse集成开发环境
下载地址:http://www.eclipse.org/downloads。选择Eclipse for Mobile Developers,解压到任意目录即可。
1.4 创建Android Virtual Device
动态调试可以用真实的手机来做调试环境,也可以用虚拟机来做调试环境,本文采用虚拟机环境。因此创建虚拟机步骤如下:
1打开Eclipse –>windows->Android Virtual Device
2点击Create,然后选择各个参数如图:
这里Target 就是前面步骤中安装的SDK 选择任意你觉得喜欢的版本就可以。点击OK 就创建完毕。
1.5 安装 APK改之理
这个是一个很好用的辅助调试的软件,请自行搜索下载。
1.6 安装 IDA6.6
IDA6.6开始支持安卓APP指令的调试,现该版本已经提供免费下载安装,请自行搜搜。
0x02 Dalvik指令动态调试
2.1 准备工作
安卓APP应用程序后缀为apk,实际上是一个压缩包,我们把它改后缀为rar打开如图:
其中classes.dex是应用的主要执行程序,包含着所有Dalvik指令。我们用APK改之理打开apk,软件会自动对其进行反编译。反编译后会有很多smail文件,这些文件保存的就是APP的Dalvik指令。
在APK改之理里双击打开AndroidManifest.xml,为了让APP可调试,需要在application 标签里添加一句android:debuggable="true" 如图:
然后点击保存按钮,然后编译生成新的apk文件。接着打开Eclipse –>windows->Android Virtual Device,选择刚才创建的虚拟机,然后点击start,虚拟机便开始运行。偶尔如果Eclipse启动失败,报错,可以同目录下修改配置文件:
把配置参数原本为512的改为256 原本为1024的改为512,然后再尝试启动。
在SDK安装目录有个命令行下的调试工具adb shell,本机所在目录为E:\adt-bundle-windows-x86-20140702\sdk\platform-tools,把adb.exe注册到系统环境变量中,打开dos命令行窗口执行adb shell 就可以进入APP命令行调试环境,或者切换到adb所在目录来执行adb shell。
这里先不进入adb shell,在DOS命令行下执行命令:adb install d:\1.apk 来安装我们刚才重新编译好的APK文件。安装完毕会有成功提示。
2.2 利用IDA动态调试
将APP包里的classes.dex解压到任意一目录,然后拖进IDA。等待IDA加载分析完毕,点击Debugger->Debugger Options如图
按图所示勾选在进程入口挂起,然后点击Set specific options 填入APP包名称和入口activity 如图:
其中包的名称和入口activity 都可以通过APK改之理里的AndroidManifest.xml 文件获取:
1
2
3
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.simpleencryption">
<application android:allowBackup="true" android:debuggable="true" android:icon="@drawable/creakme_bg2" android:label="@string/app_name" android:theme="@style/AppTheme">
<activity android:label="@string/app_name" android:name=".MainActivity">
然后在IDA点击Debugger->Process Options
其他默认不变,端口这里改为8700。这里默认端口是23946,我在这里困扰了很久,就是因为这个端口没有改为8700所致。然后我们看看这个8700端口是怎么来的。在Android SDK里提供了一款工具DDMS,用来监视APP的运行状态和结果。在SDK的TOOLS目录有个DDMS.BAT的脚步,运行后就会启动DDMS。由于我的本机安装了SDK的ADT插件,DDMS集成到了Eclips中,打开Eclips->Open perspective->ddms就启动了DDMS。
如图所示:
在DDMS选中某个进程后面就会注释出它的调试端口,本机这里是8700。
到此所有的工作就准备就绪,然后就可以下断点来调试该APP了。我们在APK改之理中在com目录下查看smali文件 发现MainActivity.smali里有一个感兴趣的函数getPwdFromPic(),那么我们就对它下断以跟踪APP的运行。
在IDA里搜索字符串getPwdFromPic,发现onClick有调用该函数
我们在onClick 函数开始位置按F2下断如图:
然后点击上图中绿色三角形按钮启动调试如图:
调试过程中有一个问题出现了很多次,浪费了我大量的时间,就在写文章的时候,操作时还是遇到了这样的问题。就是点击启动后IDA提示can’t bind socket,琢磨了很久终于找到原因了,当打开过一次DDMS后 每次启动Eclips都会启动DDMS 而8700端口正是被这个DDMS给占用了,然后每次都会启动失败,解决办法就是 虚拟机运行起来后关闭掉Eclips,这时一切就正常了!
事例中是一个APP crackme 提示输入密码才能进入正确界面。这个时候我们输入123,点击登陆,IDA中断在了我们设置断点的地方,这时选中ida->debugger->use source level debugger,然后点击ida->debugger->debugger windows->locals打开本地变量窗口,如图:
然后按F7或F8单步跟踪程序流程,同时可以观察到变量值的变化,也可以在IDA右键选择图形视图,可以看到整个APP执行的流程图:
如上图所示 变量窗口中我们输入了123 被转化成的密码是么广亡,pw变量也显示出了正确的密码,其实这个时候已经很容易判断出正确密码了。
0x03 Andoid原生动态链接库动态调试
通常为了加密保护等措施,有时dex执行过程中会调用动态链接库文件,该文件以so为后缀,存在于APP文件包里。
这里我们以动态附加的方式来调试原生库。
3.1 准备工作
1、将IDA->dbgsrv目录下的android_server拷贝到虚拟机里,并赋予可执行权限
DOS命令分别为:
adb shell pull d:\ android_server /data/data/sv
adb shell chmod 755 /data/data/sv
2、启动调试服务器android_server
命令:adb shell /data/data/sv
服务器默认监听23946端口。
3、重新打开DOS窗口进行端口转发,命令:
adb forward tcp:23946 tcp:23946 如图:
3.2 利用IDA进行动态调试
1、虚拟机里启动要调试的APP 2、启动IDA,打开debugger->attach->remote Armlinux/andoid debugger
端口改为23946 其他保持不变,点击OK
如上图,选中要调试的APP 的数据包名,然后点击OK。
正常情况下,IDA会把APP进程挂起。
3、由于当前程序不是在动态链接库领空,这时我们要重新打开一个IDA,用它打开需要调试的so文件,找到需要下断的位置的文件偏移,并做记录,然后关闭后面打开的这个IDA。
4、在原IDA界面按下ctrl+s键,找到并找到需要调试的so,同时记录该文件的加载基址。然后点击OK 或者cancel按钮关闭对话框。
5、按下快捷键G 输入基址+文件偏移所得地址,点击OK 就跳转到SO文件需要下断的地方,这时按下F2键设置断点。当APP执行到此处时便可以断下来。
3.3 在反调试函数运行前进行动态调试
程序加载so的时候,会执行JNI_OnLoad函数,做一系列的准备工作。通常反调试函数也会放到JNI_OnLoad函数里。进行4.2中第2步时也许会遇到如下情况:
这时APP检测到了调试器,会自动退出,那么这时调试策略需要有所改变。
接着4.1第3步后,在DOS命令行执行命令:
adb shell am start -D -n com.yaotong.crackme/com.yaotong.crackme.MainActivity
来以调试模式启动APP 如图:
com.yaotong.crackme是APP包名称,com.yaotong.crackme.MainActivity是执行入口 这些可以用APK改之理查看。
这时由于APP还未运行,那么反调试函数也起不了作用,按照4.2中第2步把APP挂起。这时IDA会中断在某个位置
然后点击debugger->debugger opions设置如下:
点击OK 后按F9运行APP,然后再DOS命令下执行命令:
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
这时APP会断下来,然后按照4.2中的3、4、5补找到JNI_OnLoad函数的地址并下断,然后按F9 会中断下来。然后便可以继续动态跟踪调试分析。
G. Android apk中包含另一个apk
根据你所说的,我怀疑这个apk包起始并不是安装来用的,而是在360运行的时候,使用动态加载技术,动态加载了这个apk中的dex中的class,这也就解释了为什么manifest没有注册activity,因为这些都是由安装的那个包来执行的,你可以网络下android 动态加载,相信对你有帮助。
H. 【Android】Android中的类加载
前文: 【Java】ClassLoader与双亲委派机制
Android中的类加载器有三种, DexClassLoader 、 PathClassLoader 、 BootClassLoader 。
其中 BootClassLoader 是系统启动时预加载常用类的,一般使用不到。 DexClassLoader 、 PathClassLoader 都是继承自 BaseDexClassLoader 。
但 DexClassLoader 和 PathClassLoader 并没有重写 BaseDexClassLoader 中的任何方法,所以源码只需要看 BaseDexClassLoader 即可。
由于Android SDK并没有包含 BaseDexClassLoader ,所以需要到源码查询网站查询源码,如下:
复制这个java文件到对应源码文件夹下就可以在Android Studio中查看了。
通过调试可以看到,Android中普通类的加载器其实是 PathClassLoader 。追踪 PathClassLoader.findClass 方法,即可获取Android的类加载过程:
PathClassLoader.findClass -- 继承自 --> BaseDexClassLoader.findClass()
-> BaseDexClassLoader.pathList.findClass()
-> DexPathList.dexElements.foreach { element.findClass() }
-> Element.findClass()
-> Element.dexFile.loadClassBinaryName()
-> DexFile.defineClass()
即类加载过程通过 BaseDexClassLoader.findClass 、 DexPathList.findClass 、 Element.findClass 、 DexFile.loadClassBinaryName ,最终会落到 DexFile.defineClass 方法中,然后就交给native层了。
其中需要注意的是,在 BaseDexClassLoader.findClass 的开头有这么一段:
这段是在Android 10新加入的,据称是为了实现 shared library 功能的,在之前的版本中没有这一段。
在上一节中知道了,类加载的流程如下:
BaseDexClassLoader.findClass() ->
BaseDexClassLoader.pathList.findClass() ->
DexPathList.dexElements.foreach { element.findClass() } ->
Element.findClass() -> ...
看 DexPathList.findClass 方法:
可以发现, DexPathList 加载类的方法是遍历 dexElements 数组依次加载,知道获取到值为止。所以可以通过修改这个数组,把新的dex文件放在数组的前面,使其加载修改后的类,从而实现热修复。
根据以上原理,写下这个工具类,有效性待验证: