🌒MoonLab

> 在MoonLab中搜索

  1. 1. 又是前言
  2. 2. start.sh
  3. 3. 小总结

Android Shizuku源码分析 第二篇

Category: Programming

🏷️  Android   中文

又是前言

上一篇的 Shizuku 源码分析,我大概从我们开发的应用到 ShizukuService 再到 SystemService 都通了一遍。在文章的结尾我只提到了 moe.shizuku.service 包下的 Starter 这个 Java 类的 main 方法启动了整个 ShizukuService ,所以我们才能愉快地调用 Shizuku 的 API。

我们接下来要更深入分析,去 Navtive 层看看。我想要知道 Starter 的 main 方法又是谁调用的?怎么调用的?

Shizuku 是什么?我就不再多说了。

Github: https://github.com/RikkaApps/Shizuku

(之后会简单涉及到 Android 中的 NDK 开发,和一些 C++ 的内容)

start.sh

当用户使用 ShizukuManager ,通过 ShizukuManager 激活应用时,需要先启动 ShizukuService 。

这里有 adb 和 root 两种启动方式,我们只分析 adb 方式。

adb shell sh /sdcard/Android/data/moe.shizuku.privileged.api/files/start.sh

这条 adb 命令想必用过 ShizukuManager 的人都很熟悉吧?只要在 adb 终端输入这条命令,就能激活 ShizukuManager。

这条命令很简单,就是调用 sh 脚本 start.sh 。这个 start.sh 在源码中 manager 模块的 raw 文件夹中:

这个 shell 脚本的内容是什么我们等会再看,我们先要知道 ShizukuManager 这个 APP 在启动时干了什么 ,看看 Manager 的 MainActivity 的 onCreate 方法:

  private static boolean sWriteFilesCalled;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
if (!sWriteFilesCalled) {
          ServerLauncher.writeFiles(this, true);
          sWriteFilesCalled = true;
  	}
  }

这里调用了 ServiceLauncher 的 writeFiles 方法,让我们来康康:

public static void writeFiles(Context context, boolean external) {
       // 从 MainActivity 传入的 external 是 true
       try {
           File out;
           if (external)
               // 执行这里,得到 /sdcard/Android/data/com.example.app/files/
               out = context.getExternalFilesDir(null);
           else
               out = getParent(context);

           if (out == null)
               return;
           int apiVersion = Math.min(ShizukuLegacy.MAX_SDK, Build.VERSION.SDK_INT);
           String source = String.format(Locale.ENGLISH, "server-v2-%d.dex", apiVersion);
           // 此时 i 为 1
           int i = external ? 1 : 0;

           // copyDex
           DEX_LEGACY_PATH[i] = copyDex(context, source, new File(out, V2_DEX_NAME));
           DEX_PATH[i] = copyDex(context, "server.dex", new File(out, V3_DEX_NAME));
           // 注意注意这个 writeShellFile 方法
           String command = writeShellFile(context, new File(out, "start.sh"), DEX_LEGACY_PATH[i], DEX_PATH[i]);
           // external 为 true ,不执行
           if (!external) {
               COMMAND_ROOT = command;
           }
       } catch (IOException e) {
           e.printStackTrace();
       }
   }

看到 writeShellFile 这个方法了吗?里面的参数是一个 File 类和一个 “start.sh”,还有两个 dex 文件路径。

继续看 ServiceLauncher#writeShellFile:

private static String writeShellFile(Context context, File out, String dexLegacy, String dex) throws IOException {
    if (!out.exists()) {
        //noinspection ResultOfMethodCallIgnored
        out.createNewFile();
    }

    BufferedReader is = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(R.raw.start)));
    PrintWriter os = new PrintWriter(new FileWriter(out));
    String line;
    while ((line = is.readLine()) != null) {
        // 这里将 start.sh 写出,并把 start.sh 的那些文字替换成路径值。
        os.println(line
                .replace("%%%STARTER_PATH%%%", getLibPath(context, "libshizuku.so"))
                .replace("%%%STARTER_PARAM%%%", getStarterParam(dexLegacy, dex))
                .replace("%%%LIBRARY_PATH%%%", getLibPath(context, "libhelper.so"))
        );
    }
    os.flush();
    os.close();

    return "sh " + out.getAbsolutePath();
}

还记得我们之前看的那个 start.sh 的内容吗?这里实际上就是把 raw 内的 start.sh 写出放到 /sdcard/Android/data/moe.shizuku.privileged.api/files。

这就是为什么那条激活用的 adb 命令能够执行:

adb shell sh /sdcard/Android/data/moe.shizuku.privileged.api/files/start.sh

看到了吗?如果你安装了 ShizukuManager 并打开了,你可以在 /sdcard/Android/data/moe.shizuku.privileged.api/files 目录里找到这个 start.sh 和两个 dex 文件。

这俩 dex 文件就是通过 ServiceLauncher#writeFiles 用 copyDex 方法把 assets 的 dex 写出到SD目录。

同时 start.sh 中的那开头的三个变量也在写出的时候被 ShizukuManager 替换了:

#!/system/bin/sh

// 这三个变量在 ShizukuManager 写出时被替换了
STARTER_PATH="%%%STARTER_PATH%%%" // 对应 libshizuku.so 文件路径
STARTER_PARAM="%%%STARTER_PARAM%%%" // 对应调用 main 函数的参数,里面包含了那两个 dex 文件的路径和 token 
LIBRARY_PATH="%%%LIBRARY_PATH%%%" // 对应 libhelper.so 文件路径

echo "info: start.sh begin"

if [[ -f "$STARTER_PATH" ]]; then
    rm -f /data/local/tmp/shizuku_starter
    // 将 libshizuku.so 文件移动到
    // 实际上 /data/local/tmp/ 下的 shizuku_starter 文件就是 libshizuku.so 文件 
    cp "$STARTER_PATH" /data/local/tmp/shizuku_starter
    // 修改权限
    chmod 700 /data/local/tmp/shizuku_starter
    chown 2000 /data/local/tmp/shizuku_starter
    chgrp 2000 /data/local/tmp/shizuku_starter
    // 这似乎是在修改 Selinux 啥的什么东西的啥子命令
    chcon u:object_r:shell_data_file:s0 /data/local/tmp/shizuku_starter

	// 设置环境变量
    export PATH=/data/local/tmp:/system/bin:$PATH
    // 调用 shizku_starter so文件(原来是 libshizuku.so)的 main 方法,传入参数
    shizuku_starter ${STARTER_PARAM} $1
    // 返回上 shizuku_starer 内 main 函数的返回值
    result=$?
    if [[ ${result} -ne 0 ]]; then
        echo "info: shizuku_starter exit with non-zero value $result"
    else
        echo "info: shizuku_starter exit with 0"
    fi
else
    echo "Starter file not exist, please open Shizuku Manager and try again."
fi

当我们输入那条用来激活 ShizukuManager 的 adb 命令时候,输出框会噼里啪啦滚出一大堆输出。那些输出是哪里来的?很明显在 start.sh 中没有几条 echo 命令,只能是在 shizuku_starter 的 main 方法里有什么大动作。

这个 libshizuku.so 文件的原身你可以在 ShizukuManager 源码中的 jni 文件夹中找到,文件名是 starter.cpp。

没错,它是一个 C++ 文件,我们看看它的 main 方法(我会适当地跳过一些与主题无关的代码):

#define SERVER_CLASS_PATH_LEGACY "moe.shizuku.server.ShizukuServer"
#define SERVER_CLASS_PATH "moe.shizuku.server.Starter"

int main(int argc, char **argv) {
    // argc 是传进来的参数的数量,argv是传入的参数的数组。里面包含了 token, 两个 dec 文件的路径
    ...
    char *token = nullptr;
    char *_path = nullptr;
    char *_path_legacy = nullptr;
    int v2 = 1;
    int i;
    int use_shell_context = 0;

    // for 循环,可以看到这是在取出参数
    for (i = 0; i < argc; ++i) {
        if (strncmp(argv[i], "--token=", 8) == 0) {
            token = strdup(argv[i] + 8);
        } else if (strncmp(argv[i], "--path=", 7) == 0) {
            _path = strdup(argv[i] + 7);
        } else if (strncmp(argv[i], "--path-legacy=", 14) == 0) {
            _path_legacy = strdup(argv[i] + 14);
        } else if (strncmp(argv[i], "--no-v2", 7) == 0) {
            v2 = 0;
        } else if (strncmp(argv[i], "--use-shell-context", 19) == 0) {
            use_shell_context = 1;
        }
    }
    ...
        
    // check_acess 会调用 acess 函数判断路径是否有读写权限
    check_access(_path, "source dex path");
    if (v2) check_access(_path_legacy, "source legacy dex path");
    
    // 这里我是没看懂的,不知道为什么又建了一个 /data/local/tmp/shizuku 文件夹,将之前 _path 内的文件全部移动到了 path 这个路径里。
    mkdir("/data/local/tmp/shizuku", 0707);
    chmod("/data/local/tmp/shizuku", 0707);
    
    ...
    
    // 一个 char 数组,也就相当于一个 string
    char path[PATH_MAX], path_legacy[PATH_MAX];
    // 格式化字符,也就是为 path 赋值了
    sprintf(path, "/data/local/tmp/shizuku/%s", basename(_path));
    sprintf(path_legacy, "/data/local/tmp/shizuku/%s", basename(_path_legacy));

    // 开始复制了,将 _path 路径下的两个 dex 移动到 path 路径
    copy_if_not_exist(_path, path);
    if (v2) copy_if_not_exist(_path_legacy, path_legacy);

    check_access(path, "dex path");
    if (v2) check_access(path_legacy, "legacy dex path");

    printf("info: starter begin\n");
    // 强制输出缓冲区的信息,就是为了快速输出上面的 printf 内容啦
    fflush(stdout); 
    ...
    
    printf("info: starting server v3...\n");
    fflush(stdout);
    // 我们重点分析这个 start_service 函数,传入 SERVICE_CLASS_PATH 这个常量,值为 "moe.shizuku.server.Starter",熟悉吗?这就是在应用层的主角啊,用于启动 ShizukuService 的 Java 类。
    start_server(path, SERVER_CLASS_PATH, token, SERVER_NAME, use_shell_context);

    if (v2) {
        printf("info: starting server v2 (legacy)...\n");
        fflush(stdout);
        start_server(path_legacy, SERVER_CLASS_PATH_LEGACY, token, SERVER_NAME_LEGACY, false);
    }
    
    exit_with_logcat(EXIT_SUCCESS);
    // starter.cpp 结束
}

注释上已经大概地说了一遍,我们接下来看 start_service 函数,同样 start_service 也是 starter.cpp 中的一个函数:

static int start_server(const char *path, const char *main_class, const char *token,
                        const char *nice_name, int change_context) {
    ...
    /* 省略了很多内容,详细可自己去看看 Shizuku 的源码 */
	char buf[128], class_path[PATH_MAX];
    sprintf(buf, "--nice-name=%s", nice_name);
    setClasspathEnv(path);
    snprintf(class_path, PATH_MAX, "-Djava.class.path=%s", path);

    char *appProcessArgs[] = {
        // app_process 是 Android 里专门用来启动 Java 程序的
        const_cast<char *>("/system/bin/app_process"),
        class_path,
        const_cast<char *>("/system/bin"),
        const_cast<char *>(buf),
        // main_class 值为 "moe.shizuku.server.Starter"
        const_cast<char *>(main_class),
        const_cast<char *>(token),
        nullptr
    };
    // 调用 app_process 去运行 main_class
    if (execvp(appProcessArgs[0], appProcessArgs)) {
        exit_with_logcat(EXIT_FATAL_APP_PROCESS);
    }
    ...
}

到这里我们的分析已经差不多了。

我们再仔细说说 app_process 的内容:

调用 app_process 的话,你需要在参数中提供 dex 文件,同时必须要使 dex 文件有执行权限

app_process -Djava.class.path=dex文件名 dex所处的目录路径 你要启动的 Java 类(有类名和包名)

我找来了一个参数列表:

vm-options – VM 选项
cmd-dir –父目录 (/system/bin)
options –运行的参数 :
    –zygote
    –start-system-server
    –application (api>=14)
    –nice-name=nice_proc_name (api>=14)
start-class-name –包含main方法的主类  (com.android.commands.am.Am)
main-options –启动时候传递到main方法中的参数

小总结

用户通过输入一条 adb 命令去执行 start.sh 文件,start.sh 文件会执行 ShizukuManager 早已准备好的 so 文件。在 so 文件中经过一大堆操作,会通过 app_process 运行 Starter 类的 main 方法,而 Starter 这个 Java 类的 main 方法会直接 new 一个 ShizukuService 使它跑起来。

看了一上午的代码,翘了一上午的在铛铛的网络课。

尼玛,都延长假期了和上个屎的网络课。

2018-2023 MoonLab