Xposed 实现原理分析

Xposed 实现原理分析

前言

Xposed 是 Android 平台上著名的 Java 层 Hook 框架,通过在 Android 设备上安装 Xposed 框架,编写 Xposed 模块,可实现对任意 Android 应用的 Java 方法的 Hook,以及应用资源的替换。

(Hook 是一种函数钩子技术,能够对函数进行接管,从而修改函数的返回值,改变函数的原始意图)

本文将基于 Xposed 最新的开源代码对 Xposed 的实现原理进行分析。Xposed 有两种实现版本,一个是基于 Dalvik 虚拟机的实现,它是针对早期的 Android 4.4 之前的 Android 设备设计的;另一个是基于 ART 虚拟机的实现,自 Android 5.0 系统开始,Android 系统正式采用了 ART 虚拟机模式运行,Dalvik 就成了历史,目前市面上几乎所有的手机都是以 ART 模式运行的,下面将主要对于 ART 上的 Xposed 实现进行详细分析,对于 Dalvik 上的 Xposed 的实现,进行必要性的分析。

通过了解 Xposed 的实现原理可以学到在 Android 平台上对于 Java 层代码的一种 Hook 机制的实现,同时复习 Android 系统的启动原理以及增加对于 Android ART 虚拟机运行原理的了解。

Xposed 使用方法

在对 Xposed 进行分析之前,先回顾一下 Xposed 基本 API 的使用。

Xposed 的核心用法就是对一个 Java 方法进行 Hook,它的典型调用如下:

XposedHelpers.findAndHookMethod(Application.class, "onCreate", Context.class,
    new XC_MethodHook() {
   
      @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
   
        Application app = (Application) param.thisObject;
        Context context = (Context) param.args[0];

        Log.d(TAG, "Application#onCreate(Context); this: " + app + " arg: " + context);

        param.setResult(null);
      }

      @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable {
   
        super.afterHookedMethod(param);
      }
    });

以上代码的意思是对 Application 这个类的 onCreate 方法进行 Hook,并使用 XC_MethodHook 对象提供一个 Hook 处理方法来接管原来方法的逻辑,当应用的 Application 类型的 onCreate 方法被调用时,beforeHookedMethod 将在被调用之前执行,同时 onCreate 的参数将会传递给 beforeHookedMethod 方法进行处理,上面的处理只是将参数打印了出来(一个 Context),同时还可以拿到被调用的 this 目标对象,也就是 Application 的对象,还可以使用 setResult 方法更改原始方法的返回值,不过这里的 Application#onCreate 方法是 void 返回类型的,setResult 不起作用,如果是其他类型,那么原方法的返回值将被更改。

这样就达到了修改一个 Java 方法的目的,即改变了原始方法的逻辑和意图。

public class App extends Application {
   
  @Override void onCreate(Context context) {
   
    // ...
  }  
}

可以看到,如果要使用 Xposed 对一个 Java 方法进行 Hook,需要提供要 Hook 方法的名字、参数列表类型和方法所在类,以及处理 Hook 的回调方法。

下面正式开始分析。

Xposed 原理概述

首先概述 Xposed 原理,之后再对具体细节进行分析。

Xposed 是一个 Hook 框架,它提供了对任意 Android 应用的 Java 方法进行 Hook 的一种方法,通常它的使用方法如下:

  1. 首先按照 Xposed 官网提供的开发规范编写一个 Xposed 模块,它是一个普通的 Android 应用,包含一块开发者自己定义的代码,这块代码有能力通过 Xposed 框架提供的 Hook API 对任意应用的 Java 方法进行 Hook。
  2. 在要启用 Xposed 的 Android 设备上安装 Xposed 框架和这个 Xposed 模块,然后在 Xposed 框架应用中启用这个 Xposed 模块,重新启动设备后,Xposed 模块将被激活,当任意的应用运行起来后,Xposed 模块的 Hook 代码将会在这个应用进程中被加载,然后执行,从而对这个应用的 Java 方法进行指定 Hook 操作。

那么根据以上使用方法实现一个 Xposed 框架需要分成如下几个部分:

  1. 提供用于 Hook 操作的 API,为了让开发者进行模块开发。它通常是一个 jar 包;
  2. 提供一个具有界面的管理器应用,用于安装和管理 Xposed 本身和 Xposed 模块;
  3. 提供将代码加载到每一个应用进程中的能力,目的是支持 Xposed 模块的代码在进程中使用 Xposed API 进行 Hook 操作;
  4. 提供 Hook 任意 Java 方法的能力,为 Xposed 模块的调用提供支持,当 Xposed 模块在应用进程中执行时可对方法进行 Hook。

前两点对于我们开发者来说都很熟悉,没有什么难点,后面两点才是实现 Xposed 的核心。

首先是 Xposed 怎样实现的将代码加载到每一个应用进程中(Xposed 是基于 Root 权限实现的,所以有修改 Android 系统的能力)?

Xposed 是通过修改系统 zygote 进程的实现将代码注入应用进程中的。

为了知道 Xposed 是如何修改 Zygote 进程的,下面首先介绍 Android 系统 Zygote 相关内容。

Android zygote 进程

zygote 进程是 Android 系统中第一个拥有 Java 运行环境的进程,它是由用户空间 1 号进程 init 进程通过解析 init.rc 文件创建出来的,从 init 进程 fork 而来。

zygote 进程是一个孵化器。Android 系统中所有运行在 Java 虚拟机中的系统服务以及应用均由 zygote 进程孵化而来。

zygote 通过克隆(fork)的方式创建子进程,fork 出来的子进程将继承父进程的所有资源,基于这个特性,zygote 进程在启动过程将创建 Java ART 虚拟机,预加载一个 Java 进程需要的所有系统资源,之后子进程被创建后,就可以直接使用这些资源运行了。

自 Android 5.0 系统开始,zygote 不再是一个进程,而是两个进程,一个是 32 位 zygote,负责孵化 32 位进程(为了兼容使用了 armeabi 和 armeabi-v7a 等 32 位架构的本地动态库的应用),另一个是 64 位 zygote 进程,负责孵化 64 位应用进程(可加载 arm64-v8a 等 64 位架构本地库)。

init 进程是 Android 系统中的 pid 为 1 的进程,是用户空间的第一个进程,它会在 Android 系统启动时被内核创建出来,之后会对 init.rc 文件进行解析,init.rc 文件是一个按照特定规则编写的脚本文件,init 进程通过解析它的规则来创建对应的服务进程。下面看一下 zygote 相关的 rc 文件的内容。

注:自 Android 5.0 开始,32 位 zygote 启动内容在 init.zygote32.rc 文件中,64 位 zygote 启动内容在 init.zygote64.rc 中。

注:自 Android 9.0 开始,两个 zygote 启动配置放在一个文件中 init.zygote64_32.rc。

这里看一下 Android 8.1 系统的 32 位 zygote 的 rc 文件内容:

# init.zygote32.rc

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart media
    onrestart restart netd
    writepid /dev/cpuset/foreground/tasks

上面的含义是,创建一个名为 zygote 的服务进程,它的可执行文件在 /system/bin/app_process 中,后面的 -Xzygote/system.bin 等是可执行文件的 main 函数将要接收的参数。

具体的 init 进程和 zygote 进程的启动细节,可以参考之前的文章:

Android init 进程启动分析

Android zygote 进程启动分析

那么现在回到 Xposed,Xposed 对 zygote 进程的实现源码进行修改后,重新编译出 app_process 可执行文件,替换了系统的 app_process 文件(包括 64 位 zygote),并在其中加载了 XposedBridge.jar 这个 Dex 代码包,它包含 Xposed 的 Java 层实现代码和提供给 Xposed 模块的 API 代码,那么当 init 进程启动 zygote 服务进程时,将执行修改过的 app_process 文件,此时 zygote 进程就具有了 Xposed 的代码,Xposed 可以进行加载 Xposed 模块代码等任意操作了。

所有 Android 应用都是运行在 Java 虚拟机上的,所有的 Android 应用都是 zygote 的子进程,那么当 Android 应用进程启动后,将具备 zygote 进程加载的所有资源,从而将 Xposed 代码继承到了 Android 应用进程中,实现了将 Xposed 代码加载到每一个进程中的目的。

接下来是如何实现对应用中 Java 方法的 Hook。Hook 的基本原理如下,将 Java 方法的原始逻辑,转接到一个中间处理方法上,这个处理方法会对原始 Java 方法的参数进行转发,转发到一个用于处理 Hook 的方法上(即 XC_MethodHook 的实现),等处理 Hook 的方法执行自定义逻辑后(自定义逻辑可选择调用原始逻辑先获取原始返回值,再处理),再返回新的返回值。

在这里插入图片描述

下面分别是 Xposed 在 Dalvik 虚拟机和 ART 虚拟机下的 Hook 实现。

基于 Dalvik 的方法 Hook

基于 Dalvik 的 Hook 方案是通过将被 Hook 方法修改为一个 JNI 方法,然后绑定一个 Xposed 自定义处理方法逻辑的函数上来实现的。

当需要 Hook 一个指定方法时,需要提供要 Hook 方法的名字、参数列表类型和方法所在类型,还要提供一个用于处理 Hook 的回调,回调方法用于修改原始方法的逻辑,它可以接收 Hook 方法的参数,然后返回一个新的返回值。

首先 Xposed 会取得这个方法的反射表示对象(例如通过 Class.getDeclaredMethod),它是一个 java.lang.reflect.Method 对象,然后取得这个对象的一个私有成员变量 slot 的值,将它和处理 Hook 的回调传递给 Xposed 的 Native 层代码,这个 slot 变量实际上是一个 Java 方法在虚拟机中的索引,使用这个索引可以从 Dalvik 中用于表示 Java 类的 ClassObject 映射类型的 directMethodvirtualMethods 数组中取出一个 Method 对象,它在虚拟机中表示一个 Java 方法,Xposed 的 Native 层代码接收到 Xposed Java 层传递过来的 slot 变量后,取出虚拟机中的 Method 对象,然后将这个 Method 对象的类型设置为 JNI 方法,即前面带有 native 修饰符的方法,然后将它的 nativeFunc 赋值给一个处理 Hook 逻辑的函数上,这个函数中将对这个 Method 的参数进行处理,传递给一开始提供的 Java 层中用于处理 Hook 的回调方法,让它来决定方法的新逻辑,从而返回新的返回值。此时便完成了 Hook。

那么调用一个被 Hook 的方法的过程是:当一个 Android 应用内的代码调用一个被 Hook 的方法时,Dalvik 将会进行代码的解释执行,Java 方法进入 Dalvik 虚拟机中会被转化为一个 Method 对象,然后虚拟机判断这个方法如果是一个 JNI 方法,就会直接调用它绑定的的 nativeFunc 函数,那么就走到了 Xposed 处理 Hook 的函数中,这个函数将这个被 Hook 方法的参数进行转发,让 Xposed 模块提供的处理 Hook 的回调方法来接管原来的逻辑,获得新的返回值返回给被 Hook 方法,即可完成整个 Hook 操作。

基于 ART 的方法 Hook

基于 ART 的 Hook 方案相比 Dalvik 要复杂一些,需要重新修改编译 ART 虚拟机的源码,重新编译出 ART 虚拟机的可执行文件 libart.so,替换 Android 系统中的 ART 虚拟机实现。

它的核心原理就是直接修改一个方法对应的汇编代码的地址,让方法直接跳转到指定地址执行,然后就可以执行自定义的逻辑进行 Hook 处理了。

ART 虚拟机为了提高执行效率,采用了 AOT(Ahead Of Time,预编译) 模式运行,在应用运行之前先将整个 APK 包含的 Java 编译为二进制代码,然后应用运行时将执行每个方法对应的机器代码,比采用 JIT(Just In Time Compiler,即时编译) 的 Dalvik 虚拟机每次在运行时才编译代码执行的效率更高。

前面的过程和 Dalvik 一样,都需要在 Hook 一个指定方法时,提供要 Hook 方法的名字、参数列表类型和方法所在类型,和一个用于处理 Hook 的回调,这个回调用于修改原始方法的逻辑。

接下来 Xposed 取得这个方法的反射表示对象,它是一个 java.lang.reflect.Method 对象,然后和用于处理 Hook 的回调一起传递给 Xposed 的 Native 层代码,Native 层代码使用 ArtMethod 的一个静态转换方法,将 Java 层的反射对象 Method 转换为一个 ART 中用于表示一个 Java 方法的 ArtMethod 对象,获取这个表示被 Hook 的 Java 方法的 ArtMethod 对象后,会创建它的副本对象用于备份,备份目的是可以在可是的时候再调用原始方法,然后给这个 ArtMethod 对象重新设置汇编代码的地址,这个地址指向一段汇编代码,这个汇编代码是一段蹦床代码(Trampoline),会跳入原本用于处理 Java 动态代理的方法的函数,Xposed 对其进行了修改,在其中加入了处理 Hook 的逻辑,也就是转发被 Hook 方法的参数给处理 Hook 的回调方法,让 Hook 回调方法处理被 Hook 方法的逻辑,从而完成 Hook。至此就完成了 ART 中的 Hook 处理。

那么调用一个被 Hook 的方法的过程是:当一个 Android 应用内代码调用一个被 Hook 的方法时,ART 将会对方法代码进行执行,首先这个 Java 方法在 ART 虚拟机中将使用一个 ArtMethod 对象表示,然后进入 ART 的 Java 方法执行函数中,会跳入一段蹦床代码中进行执行,这段蹦床代码又会跳入这个 ArtMethod 对象设置的汇编代码地址处,从而执行到 Xposed 用于处理 Hook 的代码中,之后完成 Hook 逻辑。

上面使用书面语言分别概述了基于 Dalvik 和 ART 的方法 Hook 的实现,目的是对整个 Xposed 实现对方法的 Hook 原理进行概括,建立一个初步的印象。真正的细节还是在源代码中,为了分析最终源代码,下面进一步对 Xposed 进行分析。

Xposed 工作流程

为了进一步分析 Xposed 的实现原理,先对 Xposed 的整体工作流程进行了解。

要使 Xposed 在 Android 设备上工作,首先需要安装 Xposed 框架。

首先获取 XposedInstaller 应用(去官方下载,或者通过 clone XposedInstaller 项目后自行编译),安装到已经 root 的设备上,然后打开 XposedInstaller。

XposedInstaller 主页会有“INSTALL/UPDATE” 的按钮,点击将会出现 InstallInstall via recovery 两个选择,一个是直接进行安装;另一个是通过 recovery 进行刷入安装。不管选择哪个,都会首先从服务器下载相同的 xposed 补丁包。

XposedInstaller 会根据系统版本和 CPU 支持架构下载对应的系统补丁包。

在 ARM64 架构 CPU 的 Android 8.1 系统上,补丁包内容如下:

xposed-v90-sdk27-arm64-beta3.zip
 +-META-INF
 │ +- CERT.RSA
 │ +- CERT.SF
 │ +- MANIFEST.MF
 │     +- com/google/android
 │         +- flash-script.sh
 │         +- update-binary
 │         +- updater-script
 │
 +- system
     +- xposed.prop
     +- bin
     |   +- app_process32_xposed
     |   +- app_process64_xposed
     |   +- dex2oat
     |   +- dexdiag
     |   +- dexlist
     |   +- dexoptanalyzer
     |   +- oatdump
     |   +- patchoat
     |   +- profman
     |
     +- framework
     |   +- XposedBridge.jar
     |
     +- lib
     |   +- libart-compiler.so
     |   +- libart-dexlayout.so
     |   +- libart.so
     |   +- libopenjdkjvm.so
     |   +- libsigchain.so
     |   +- libxposed_art.so
     |
     +- lib64
         +- libart-compiler.so
         +- libart-disassembler.so
         +- libart.so
         +- libopenjdkjvm.so
         +- libsigchain.so
         +- libxposed_art.so

压缩包名为 xposed-v90-sdk27-arm64-beta3.zip,文件名包含系统版本、CPU 架构和 Xposed 版本信息。

META-INF 目录存放文件签名信息,和 Xposed 刷机脚本 flash-script.sh 文件,update-binary 为刷入文件时执行的文件,它的源代码在 Android 源代码 bootable/recovery/updater/ 目录中。

system 目录为 Xposed 所需的文件,刷入时将会复制到系统 system 目录下,同名文件将进行覆盖,其中 xposed.prop 为 Xposed 的属性文件,里面会存放 Xposed 版本相关信息,如下:

version=90-beta3
arch=arm64
minsdk=27
maxsdk=27
requires:fbe_aware=1

bin 目录存放系统可执行文件;framwrok 目录存放 Xposed 的 Java 层 Dex 代码包,用于在 Zygote 进程中进行加载;lib、lib64 是 32 位和 64 位系统库,包括 ART 虚拟机库 libart.so 和依赖的库,还有 Xposed Native 层代码的实现 libxposed_art.so。

回到 XposedInstaller 中,如果选择了 Install,那么首先将压缩包中的 system 目录下的的可执行文件以及依赖库、配置文件等复制入系统 system 中覆盖相应系统文件,然后请求重启 Android 系统,重启后开机过程中,系统将会执行 app_process 可执行文件,从而启动 Xposed 修改过的 zygote 进程,其中会把 XposedBridge.jar 代码包加载起来,加载后其中的 Java 代码会加载已经安装的 Xposed 模块,当手机中的应用进程启动后,Xposed 模块代码将会被包含在应用进程中,开始工作;

如果是 Install via recovery,将创建文件 /cache/recovery/command 并写入指定刷机包路径的刷机命令,然后重启手机进入 recovery 模式,recovery 模式会自动执行 command 文件中的命令将 Xposed 文件刷入,然后正常重启至系统,启动过程和上面一致。

了解了 Xposed 的整体工作流程,下面开始着手进行源码分析。

Xposed 项目结构

首先了解 Xposed 开源项目的结构,Xposed 包含如下几个开源项目:

Xposed

仓库地址:https://github.com/rovo89/Xposed

Xposed Native 层代码的实现,主要修改了系统 app_process 的实现(即 zygote 服务进程的实现),为将 Hook 代码注入每个应用进程提供了入口。

XposedBridge

仓库地址:https://github.com/rovo89/XposedBridge

Xposed Java 层的代码,它将单独作为一个 jar 包的形式通过 zygote 的分裂(fork)注入到每一个应用进程中,内部会 Xposed 模块,并为 Xposed 模块中的 Hook 操作提供 API 支持。

XposedInstaller

仓库地址:https://github.com/rovo89/XposedInstaller

统一管理 Xposed 框架的 Android 应用,也是一个 Xposed 框架安装器,用于安装更新 Xposed 框架核心以及作为统一管理 Xposed 模块安装的模块管理器。

android_art

仓库地址:https://github.com/rovo89/android_art

Xposed 修改后的 Android ART 虚拟机的实现,将编译出 libart.so 和其依赖库,替换系统的 ART 虚拟机实现。包含方法 Hook 的核心实现。

这个仓库最新分支是基于 Android Nougat MR2 源码修改的 ART 代码,目前 Xposed 最新版本支持到了 Android 8.1 系统,说明作者没有开源出最新代码,不过都是基于 ART 实现的 Hook,核心 Hook 实现是一致的,不影响分析。

XposedTools

仓库地址:https://github.com/rovo89/XposedTools

用于编译 Xposed 框架的脚本工具。

目前只分析 Xposed 的实现,不需要对 Xposed 进行定制,所以先不关注 XposedTools 这个项目。

Xposed 源码分析

可以对上面的项目进行 clone,然后用 Android Studio 和 VS Code 打开源代码,方便阅读。下面进入源码中分析具体实现。

Xposed 安装

下载

首先从 Xposed 的安装开始分析,这部分代码的实现在 XposedInstaller 中。

在一台 Root 过的设备上安装 XposedInstaller 后打开,点击主页的“INSTALL/UPDATE”,会弹出一个对话框,选择“Install”或“Install via recovery”安装 Xposed 框架,此时会首先进行框架核心文件的下载,进入 StatusInstallerFragment#download 方法中:

// StatusInstallerFragment.java

private void download(Context context, String title, FrameworkZips.Type type, final RunnableWithParam<File> callback) {
   
    OnlineFrameworkZip zip = FrameworkZips.getOnline(title, type);
    new DownloadsUtil.Builder(context)
            .setTitle(zip.title)
	        // 设置下载 url
            .setUrl(zip.url)
            .setDestinationFromUrl(DownloadsUtil.DOWNLOAD_FRAMEWORK)
            .setCallback(new DownloadFinishedCallback() {
   
                @Override
                public void onDownloadFinished(Context context, DownloadInfo info) {
   
                    // 下载完成,触发回调
                    LOCAL_ZIP_LOADER.triggerReload(true);
                    callback.run(new File(info.localFilename));
                }
            })
            .setMimeType(DownloadsUtil.MIME_TYPES.ZIP)
            .setDialog(true)
            .download();
}

其中 zip.url 为 Xposed 框架压缩包的下载地址,我们重点关注安装,所以这里简要描述 zip 对象,zipOnlineFrameworkZip 类的对象,表示一个 Xposed 框架包,它包含 title、type 和 url 三个成员,type 有两种,Installer 和 Uninstaller,即安装包和卸载包,都是包含刷机脚本的 Xposed 补丁包(就是上面工作流程中的压缩包),title 有三种,Xposed 测试版、Xposed 正式版、和 Uninstaller,用于界面显示。上面的 zip.url 在 Android 8.1 的 Pixel 手机上运行出来是 http://dl-xda.xposed.info/framework/sdk27/arm64/xposed-v90-sdk27-arm64-beta3.zip,这个 url 是根据设备支持的 CPU 架构、系统版本和 Xposed 当前最新版本组合出来的,组合规则由一个 framework.json 提供,它的本地路径是 /data/data/de.robv.android.xposed.installer/cache/framework.json,是从 http://dl-xda.xposed.info/framework.json 解析后得到的,内容如下:

{
   
  "zips": [
    {
   
      "title": "Version 90-beta$(version)",
      "url": "http://dl-xda.xposed.info/framework/sdk$(sdk)/$(arch)/xposed-v90-sdk$(sdk)-$(arch)-beta$(version).zip",
      "versions": [
        {
    "version": "3", "current": true },
        {
    "version": "2" },
        {
    "version": "1" }
      ],
      "archs": ["arm", "arm64", "x86"],
      "sdks" : [26, 27]
    },
    {
   
      "title": "Version $(version)",
      "url": "http://dl-xda.xposed.info/framework/sdk$(sdk)/$(arch)/xposed-v$(version)-sdk$(sdk)-$(arch).zip",
      "versions": [
        {
    "version": "89", "current": true },
        {
    "version": "88.2" },
        {
    "version": "88.1" },
        {
    "version": "88" },
        ...
      ],
      "archs": ["arm", "arm64", "x86"],
      "sdks" : [21, 22, 23, 24, 25],
      "exclude": [
        {
   
          "versions": ["88.1"],
          "sdks": [21, 22, 23]
        },
        {
   
          "versions": ["78", "79", "80", "81", "82", "83", "84", "85", "86", "87"],
          "sdks": [24, 25]
        },
        ...
      ]
    },
    {
   
      "title": "Uninstaller ($(version))",
      "url": "http://dl-xda.xposed.info/framework/uninstaller/xposed-uninstaller-$(version)-$(arch).zip",
      "type": "uninstaller",
      "versions": [
        {
    "version": "20180117", "current": true },
        {
    "version": "20180108" },
        ...
      ],
      "archs": ["arm", "arm64", "x86"],
      "sdks" : [21, 22, 23, 24, 25, 26, 27]
    }
  ]
}

能看到,其中包含了 Xposed 测试版、Xposed 正式版、和 Xposed 的 Uninstaller 三种 title 的下载信息,每个下载信息中的 url 为下载地址的模板,versions 为可用的版本,根据系统信息和 Xposed 版本对 url 模板进行填充后组成下载地址。

回到上面的下载,下载成功后,将进入回调根据用户选择的安装类型进行安装,看一下回调的实现:

// StatusInstallerFragment.java

if (action == ACTION_FLASH) {
   
    runAfterDownload = new RunnableWithParam<File>() {
   
        @Override
        public void run(File file) {
   
            // 直接刷入
            flash(context, new FlashDirectly(file, type, title, false));
        }
    };
} else if (action == ACTION_FLASH_RECOVERY) {
   
    runAfterDownload = new RunnableWithParam<File>() {
   
        @Override
        public void run(File file) {
   
            // 依赖 recovery 模式进行刷入
            flash(context, new FlashRecoveryAuto(file, type, title));
        }
    };
} else if (action == ACTION_SAVE) {
   
    runAfterDownload = new RunnableWithParam<File>() {
   
        @Override
        public void run(File file) {
   
            // 仅保存
            saveTo(context, file);
        }
    };
}

上面两个分支分别对应 InstallInstall via recovery 两种安装方式的实现,flash 方法将会启动一个新的负责展示安装执行的界面,然后执行传入的 Flashable 对象的 flash 方法,执行成功后展示一个对话框,询问用户是否重启,重启后将激活 Xposed。分别看一下两种 Flashable 的实现。

直接刷入

// FlashDirectly.java

public void flash(Context context, FlashCallback callback) {
   
    ZipCheckResult zipCheck = openAndCheckZip(callback);
    if (zipCheck == null) {
   
        return;
    }

    // 获取压缩包文件
    ZipFile zip = zipCheck.getZip();
    if (!zipCheck.isFlashableInApp()) {
   
        triggerError(callback, FlashCallback.ERROR_NOT_FLASHABLE_IN_APP);
        closeSilently(zip);
        return;
    }

    // 释放 update-binary 文件至 cache 目录中
    ZipEntry entry = zip.getEntry("META-INF/com/google/android/update-binary");
    File updateBinaryFile = new File(XposedApp.getInstance().getCacheDir(), "update-binary");
    try {
   
        AssetUtil.writeStreamToFile(zip.getInputStream(entry), updateBinaryFile, 0700);
    } catch (IOException e) {
   
        Log.e(XposedApp.TAG, "Could not extract update-binary", e);
        triggerError(callback, FlashCallback.ERROR_INVALID_ZIP);
        return;
    } finally {
   
        closeSilently(zip);
    }

    // 使用 Root 身份执行刷入命令
    RootUtil rootUtil = new RootUtil();
    if (!rootUtil.startShell(callback)) {
   
        return;
    }

    callback.onStarted();

    rootUtil.execute("export NO_UIPRINT=1", callback);
    if (mSystemless) {
   
        rootUtil.execute("export SYSTEMLESS=1", callback);
    }

    // 执行 update-binary 文件
    int result = rootUtil.execute(getShellPath(updateBinaryFile) + " 2 1 " + getShellPath(mZipPath), callback);
    if (result != FlashCallback.OK) {
   
        triggerError(callback, result);
        return;
    }

    callback.onDone();
}

直接刷入会直接使用 Root 身份执行 update-binary 可执行文件,其中会调用 flash-script.sh 文件,它将压缩包中的目录复制到对应的系统目录中,同名文件进行覆盖,在覆盖前会对原始系统文件进行备份,例如 libart.so.orig.gz,为了在卸载时恢复。

刷入后正常重启系统,系统在启动时将会加载自定义的 app_process 可执行文件,启动了带有 Xposed 框架代码的定制版 zygote 服务进程,为 Xposed 提供支持。

使用 recovery 刷入

// FlashRecoveryAuto.java

@Override
public void flash(Context context, FlashCallback callback) {
   
    ZipCheckResult zipCheck = openAndCheckZip(callback);
    if (zipCheck == null) {
   
        return;
    } else {
   
        closeSilently(zipCheck.getZip());
    }

    final String zipName = mZipPath.getName();
    String cmd;

    // 执行刷入命令
    RootUtil rootUtil = new RootUtil();
    if (!rootUtil.startShell(callback)) {
   
        return;
    }

    callback.onStarted();

    // 确认 /cache/recovery/ 目录存在
    if (rootUtil.execute("ls /cache/recovery", null) != 0) {
   
        callback.onLine(context.getString(R.string.file_creating_directory, "/cache/recovery"));
        if (rootUtil.executeWithBusybox("mkdir /cache/recovery", callback) != 0) {
   
            callback.onError(FlashCallback.ERROR_GENERIC,
                    context.getString(R.string.file_create_directory_failed, "/cache/recovery"));
            return;
        }
    }

    // 复制 zip 到 /cache/recovery/ 目录
    callback.onLine(context.getString(R.string.file_copying, zipName));
    cmd = "cp -a " + RootUtil.getShellPath(mZipPath) + " /cache/recovery/" + zipName;
    if (rootUtil.executeWithBusybox(cmd, callback) != 0) {
   
        callback.onError(FlashCallback.ERROR_GENERIC,
                context.getString(R.string.file_copy_failed, zipName, "/cache/recovery"));
        return;
    }

    // 将刷机命令写入 /cache/recovery/command 文件中
    callback.onLine(context.getString(R.string.file_writing_recovery_command));
    cmd = "echo --update_package=/cache/recovery/" + zipName + " > /cache/recovery/command";
    if (rootUtil.execute(cmd, callback) != 0) {
   
        callback.onError(FlashCallback.ERROR_GENERIC,
                context.getString(R.string.file_writing_recovery_command_failed));
        return;
    }

    callback.onLine(context.getString(R.string.auto_flash_note, zipName));
    callback.onDone();
}

通过 recovery 模式进行刷入就是首先复制压缩包到 /cache/recovery/ 中,然后向 /cache/recovery/command 文件中写入一条刷入压缩包的命令,然后询问用户是否重启至 recovery 模式,当系统处于 recovery 模式后将会自动检测 command 文件是否存在,如果存在将执行其中的指令,然后执行刷机包提供的脚本,过程和上面直接刷入一致,首先执行 update-binary 可执行文件,然后其中会调用 flash-script.sh 文件,将刷机包中的文件进行复制。此时,系统退出 reocvery 正常重启后将会加载成功 Xposed。

这里就分析完了安装,主要是通过刷入文件将系统关键组件替换为 Xposed 修改过的实现。

下面开始分析 Xposed 的启动,当系统启动后,init 进程将会通过解析 init.rc 文件后执行 app_process 创建 zygote 进程,此时就进入了 Xposed 重新编译修改过的 app_process 文件中。

Xposed 启动

这部分的实现代码在项目 Xposed 中,是使用 C++ 代码编写的,如果这些代码出现崩溃,则会卡在开机界面,即 boot loop 情况。

Xposed 的 app_process 分为 Dalvik 和 ART 两种实现,这里只关注 ART 的实现,在 app_main2.cpp 中。

Native 层

入口为 main 函数:

// app_main2.cpp

int main(int argc, char* const argv[])
{
   
    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) {
   
        if (errno != EINVAL) {
   
            LOG_ALWAYS_FATAL("PR_SET_NO_NEW_PRIVS failed: %s", strerror(errno));
            return 12;
        }
    }

    // 1. 处理 xposed 测试选项
    if (xposed::handleOptions(argc, argv)) {
   
        return 0;
    }

    AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
    argc--;
    argv++;

    int i;
    for (i = 0; i < argc; i++) {
   
        if (argv[i][0] != '-') {
   
            break;
        }
        if (argv[i][1] == '-' && argv[i][2] == 0) {
   
            ++i; // Skip --.
            break;
        }
        runtime.addOption(strdup(argv[i]))
  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值