剖析 Android ART Runtime (2) – dex2oat
在上一篇文章中我们介绍了 Android 4.4 新开发的运行时 ART 项目,其中的一个重要模快是 dex2oat,简单讲就是使用 LLVM 把 dex 文件编译成 oat 文件(Optimized ART?)。下面我们详细研究一下 dex2oat 的功能,以及他是如何被调用的。
一、dex2oat 简介
dex2oat 顾名思义 dex file to oat file,就是在新旧两种运行时文件的转换。他是一个可执行 ELF 文件,adb 连接手机以后也可以直接调用。dex2oat 的源码只有一个文件 /art/dex2oat/dex2oat.cc。dex2oat 有很多参数,并且实用方法打印到 logcat里面,看起来非常不便,我们直接看源码 Usage()。我挑了几个比较重要的参数看了一下,先介绍一下,调用过程中可能会用到。
dex2oat 用法
--dex-file=<dex-file>: specifies a .dex file to compile.
: 需要转换的 dex 文件,也可以是 apk, jar,dex2oat 会找到里面的 classes.dex 进行转换。--oat-file=<file.oat>: specifies the oat output destination via a filename.
: 指定输出 oat 的文件名。--boot-image=<file.art>: provide the image file for the boot class path.
: 系统运行时工具类在 ART 下编译后的文件,他的例子是指向/system/framework/boot.art
。但其实 boot.art 不在这个目录下,而是在/data/dalvik-cache/system@[email protected]
。后面还会详细介绍这个 boot 文件。--compiler-backend=(Quick|QuickGBC|Portable): select compiler backend"
: dex2oat 好像只处理了 Quick 和 Portable 两种编译 backend,暂时还不理解有什么区别,待以后继续研究。
其他参数大多都能从他的描述中知道用途,等以后用到的时候再详细看。
oat 文件在哪里?
我们知道选择 ART 作为运行时后,需要重启手机。然后系统会使用 dex2oat 把所有 App 编译成 oat 文件。那么 oat 都存在哪里?我怎么找不到?通过查看代码和开机的 log,发现 oat 文件原来在 /data/dalvik-cache/*@classes.dex
。竟然和 odex 文件名字一样!检验一下文件格式:
$ file system@[email protected]@classes.dex
system@[email protected]@classes.dex: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (GNU/Linux), dynamically linked, stripped
果然是 ELF 文件。
二、调用过程分析
PackageManagerService 是负责 apk 包管理的服务,包括安装,检查,删除,更新等等所有与 apk 相关的服务。我在源码中研究了一下 dex2oat 详细的调用过程,在下面介绍过程的时候我打算从 PackageManagerService 入手,经过一系列的调用过程才真正执行 dex2oat。
PackageManagerService (PMS)
我们知道 PMS 服务启动之后会监控某些目录(/data/data/ 等)。如果目录多了某个文件,PMS 会调用一些方法对文件进行处理。扫描监控目录的方法是:scanDirLI(),在 PackageManagerService 的构造方法中调用,详见代码 /frameworks/base/services/java/com/android/server/pm/PackageManagerService.java
:
// Find base frameworks (resource packages without code). mFrameworkInstallObserver = new AppDirObserver( frameworkDir.getPath(), OBSERVER_EVENTS, true, false); mFrameworkInstallObserver.startWatching(); scanDirLI(frameworkDir, PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR, scanMode | SCAN_NO_DEX, 0); // Collected privileged system packages. File privilegedAppDir = new File(Environment.getRootDirectory(), "priv-app"); mPrivilegedInstallObserver = new AppDirObserver( privilegedAppDir.getPath(), OBSERVER_EVENTS, true, true); mPrivilegedInstallObserver.startWatching(); scanDirLI(privilegedAppDir, PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR | PackageParser.PARSE_IS_PRIVILEGED, scanMode, 0); // Collect ordinary system packages. File systemAppDir = new File(Environment.getRootDirectory(), "app"); mSystemInstallObserver = new AppDirObserver( systemAppDir.getPath(), OBSERVER_EVENTS, true, false); mSystemInstallObserver.startWatching(); scanDirLI(systemAppDir, PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR, scanMode, 0); // Collect all vendor packages. File vendorAppDir = new File("/vendor/app"); mVendorInstallObserver = new AppDirObserver( vendorAppDir.getPath(), OBSERVER_EVENTS, true, false); mVendorInstallObserver.startWatching(); scanDirLI(vendorAppDir, PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR, scanMode, 0);
从源码中可以看到,有四个地方调用 scanDirLI()。再调用之前有一个 AppDirObserver 类引起了我的注意,看一下是怎么对目录进行监控的。依然是在 PackageManagerService 中找到了 AppDirObserver 继承于 FileObserver。FileObserver 有一个 native 方法 startWatching 找到了他的实现 /frameworks/base/core/java/android/os/FileObserver.java
:
const char* path = env->GetStringUTFChars(pathString, NULL); res = inotify_add_watch(fd, path, mask); env->ReleaseStringUTFChars(pathString, path);
man 一下 inotify_add_watch:
INOTIFY_ADD_WATCH(2) Linux Programmer's Manual INOTIFY_ADD_WATCH(2) NAME inotify_add_watch - add a watch to an initialized inotify instance SYNOPSIS #include <sys/inotify.h> int inotify_add_watch(int fd, const char *pathname, uint32_t mask); DESCRIPTION inotify_add_watch() adds a new watch, or modifies an existing watch, for the file whose location is specified in pathname; the caller must have read permission for this file. The fd argument is a file descrip‐ tor referring to the inotify instance whose watch list is to be modi‐ fied. The events to be monitored for pathname are specified in the mask bit-mask argument. See inotify(7) for a description of the bits that can be set in mask. ...
原来 inotify_add_watch() 是监控目录的。
PackageManagerService.scanDirLI()
回到正题,我们从构造方法中找到了 scanDirLI(),现在来看一下 scanDirLI() 都做了什么:
for (i=0; i<files.length; i++) { File file = new File(dir, files[i]); if (!isPackageFilename(files[i])) { // Ignore entries which are not apk's continue; } PackageParser.Package pkg = scanPackageLI(file, flags|PackageParser.PARSE_MUST_BE_APK, scanMode, currentTime, null);
scanDirLI() 便利所有目录下的 apk 文件并调用 scanPackageLI(),我们继续。
PackageManagerService.scanPackageLI()
scanPackageLI() 先对文件的设置进行读取,还包含其他处理。我们跳过直接看
// Note that we invoke the following method only if we are about to unpack an application PackageParser.Package scannedPkg = scanPackageLI(pkg, parseFlags, scanMode | SCAN_UPDATE_SIGNATURE, currentTime, user);
又调用了另外一个重载的 scanPackageLI()。在这个 scanPackageLI() 里面又进行了一系列的包的解析,都与我们的目标无关,直接跳到这里:
if ((scanMode&SCAN_NO_DEX) == 0) { if (performDexOptLI(pkg, forceDex, (scanMode&SCAN_DEFER_DEX) != 0, false) == DEX_OPT_FAILED) { mLastScanError = PackageManager.INSTALL_FAILED_DEXOPT; return null; } }
在这里又调用 performDexOptLI() 来处理进行 dex 优化,参数就是这个 package。这里已经感觉到有点快到 dex2oat 的调用点了。我们继续看 performDexOptLI()。
PackageManagerService.performDexOptLI()
performDexOptLI 先检查 dex 是否需要优化,然后再有一些判断,忽略他,看到这里:
/* /frameworks/base/services/java/com/android/server/pm/PackageManagerService.java */
if (forceDex || dalvik.system.DexFile.isDexOptNeeded(path)) {
if (!forceDex && defer) {
if (mDeferredDexOpt == null) {
mDeferredDexOpt = new HashSet<PackageParser.Package>();
}
mDeferredDexOpt.add(pkg);
return DEX_OPT_DEFERRED;
} else {
Log.i(TAG, "Running dexopt on: " + pkg.applicationInfo.packageName);
final int sharedGid = UserHandle.getSharedAppGid(pkg.applicationInfo. uid);
ret = mInstaller.dexopt(path, sharedGid, !isForwardLocked(pkg));
pkg.mDidDexOpt = true;
performed = true;
}
}
最后 mInstaller.dexopt 调用的优化 dex 的函数,Google 在这里把 dex2oat 和 dex2odex 统称为 dexopt。mInstaller 是 Installer 类。下面我们详细了解一下 Installer 的运行机制。
Installer
从 Installer 的代码中我们可以看到 connect(), disconnect(), readBytes(), writeCommand() 等方法。在 connect() 方法中可以看到,Installer 就是与 installd daemon 进行通信的辅助类。代码在这里:
mSocket = new LocalSocket(); LocalSocketAddress address = new LocalSocketAddress("installd", LocalSocketAddress.Namespace.RESERVED); mSocket.connect(address); mIn = mSocket.getInputStream(); mOut = mSocket.getOutputStream();
了解了这个之后,我们找一下 dexopt 看看 Installer 向 installd 发送了什么信息:
public int dexopt(String apkPath, int uid, boolean isPublic) { StringBuilder builder = new StringBuilder("dexopt"); builder.append(' '); builder.append(apkPath); builder.append(' '); builder.append(uid); builder.append(isPublic ? " 1" : " 0"); return execute(builder.toString()); }
其实发送了 dexopt apkpath uid 1|0
给 installd。好了,Installer 分析结束,我们转战 installd。
installd.c
installd 是 socket 通信的 server,会 accept 连接,并读取数据。从下面的两个死循环就能看出来:
for (;;) { alen = sizeof(addr); s = accept(lsocket, &addr, &alen); if (s < 0) { ALOGE("Accept failed: %s\n", strerror(errno)); continue; } fcntl(s, F_SETFD, FD_CLOEXEC); ALOGI("new connection\n"); for (;;) { unsigned short count; if (readx(s, &count, sizeof(count))) {
之后会调用 execute 函数:
if (execute(s, buf)) break;
execute() 函数先把 cmd 分割然后通过 cmds 表查询函数名称:
struct cmdinfo { const char *name; unsigned numargs; int (*func)(char **arg, char reply[REPLY_MAX]); }; struct cmdinfo cmds[] = { { "ping", 0, do_ping }, { "install", 4, do_install }, { "dexopt", 3, do_dexopt }, { "movedex", 2, do_move_dex }, { "rmdex", 1, do_rm_dex }, { "remove", 2, do_remove }, { "rename", 2, do_rename }, { "fixuid", 3, do_fixuid }, { "freecache", 1, do_free_cache }, { "rmcache", 2, do_rm_cache }, { "getsize", 6, do_get_size }, { "rmuserdata", 2, do_rm_user_data }, { "movefiles", 0, do_movefiles }, { "linklib", 3, do_linklib }, { "mkuserdata", 3, do_mk_user_data }, { "rmuser", 1, do_rm_user }, };
dexopt 对应的是 do_dexopt() 函数,do_dexopt() 又调用了 commands.c 里面的 dexopt() 函数。感觉离我们的目标不远了,继续看 commands.c。
commands.c
在 commands.c 中的 dexopt() 函数,我们看到此函数从 property 中获得 dalvik.vm.dexopt-flags 和 persist.sys.dalvik.vm.lib:
/* platform-specific flags affecting optimization and verification */ property_get("dalvik.vm.dexopt-flags", dexopt_flags, ""); ALOGV("dalvik.vm.dexopt_flags=%s\n", dexopt_flags); /* The command to run depend ones the value of persist.sys.dalvik.vm.lib */ property_get("persist.sys.dalvik.vm.lib", persist_sys_dalvik_vm_lib, "libdvm.so");
dalvik.vm.dexopt-flags 暂时不知道是什么意思,persist.sys.dalvik.vm.lib 就是当前系统使用的运行时默认是 Dalvik VM,如果选择了 ART,应该就是 libart.so 了。
之后函数通过检查是否有 odex 文件来判断是否已经进行过 dexopt。然后进行一系列检查 chmod 和 chown。最后函数 fork 了一个子进程处理 dexopt:
pid_t pid; pid = fork(); if (pid == 0) { /* child -- drop privileges before continuing */ if (setgid(uid) != 0) { ALOGE("setgid(%d) failed in installd during dexopt\n", uid); exit(64); } if (setuid(uid) != 0) { ALOGE("setuid(%d) failed in installd during dexopt\n", uid); exit(65); } // drop capabilities struct __user_cap_header_struct capheader; struct __user_cap_data_struct capdata[2]; memset(&capheader, 0, sizeof(capheader)); memset(&capdata, 0, sizeof(capdata)); capheader.version = _LINUX_CAPABILITY_VERSION_3; if (capset(&capheader, &capdata[0]) < 0) { ALOGE("capset failed: %s\n", strerror(errno)); exit(66); } if (flock(out_fd, LOCK_EX | LOCK_NB) != 0) { ALOGE("flock(%s) failed: %s\n", out_path, strerror(errno)); exit(67); } if (strncmp(persist_sys_dalvik_vm_lib, "libdvm", 6) == 0) { run_dexopt(zip_fd, out_fd, apk_path, out_path, dexopt_flags); } else if (strncmp(persist_sys_dalvik_vm_lib, "libart", 6) == 0) { run_dex2oat(zip_fd, out_fd, apk_path, out_path, dexopt_flags); } else { exit(69); /* Unexpected persist.sys.dalvik.vm.lib value */ } exit(68); /* only get here on exec failure */ } else { res = wait_dexopt(pid, apk_path); if (res != 0) { ALOGE("dexopt in='%s' out='%s' res=%d\n", apk_path, out_path, res); goto fail; } }
开始先 drop capabilities(关于 Linux capabilities: man capabilities)。最后比较 persist_sys_dalvik_vm_lib,如果是 libart 则调用 run_dex2oat()。Good,终于有头绪了,再看 run_dex2oat():
static void run_dex2oat(int zip_fd, int oat_fd, const char* input_file_name, const char* output_file_name, const char* dexopt_flags) { static const char* DEX2OAT_BIN = "/system/bin/dex2oat"; static const int MAX_INT_LEN = 12; // '-'+10dig+'\0' -OR- 0x+8dig char zip_fd_arg[strlen("--zip-fd=") + MAX_INT_LEN]; char zip_location_arg[strlen("--zip-location=") + PKG_PATH_MAX]; char oat_fd_arg[strlen("--oat-fd=") + MAX_INT_LEN]; char oat_location_arg[strlen("--oat-name=") + PKG_PATH_MAX]; sprintf(zip_fd_arg, "--zip-fd=%d", zip_fd); sprintf(zip_location_arg, "--zip-location=%s", input_file_name); sprintf(oat_fd_arg, "--oat-fd=%d", oat_fd); sprintf(oat_location_arg, "--oat-location=%s", output_file_name); ALOGV("Running %s in=%s out=%s\n", DEX2OAT_BIN, input_file_name, output_file_name); execl(DEX2OAT_BIN, DEX2OAT_BIN, zip_fd_arg, zip_location_arg, oat_fd_arg, oat_location_arg, (char*) NULL); ALOGE("execl(%s) failed: %s\n", DEX2OAT_BIN, strerror(errno)); }
终于找到了,这就是调用 dex2oat 的地方,写的非常清楚。–zip-fd 是 apk 的 fd,–oat-fd 是输出 oat 的 fd。最终使用 execl() 执行 /system/bin/dex2oat 程序对 apk 进行编译。
三、总结
dex2oat 对所有 apk 进行编译并保存在 dalvik-cache 目录里。PMS 会持续扫描安装目录,如果有新的 App 安装则马上调用 dex2oat 进行编译。
最后,之前我们提到的 boot.art 我在 logcat 中找到了他的来源:
/system/bin/dex2oat --image=/data/dalvik-cache/system@[email protected] \
--runtime-arg -Xms64m --runtime-arg -Xmx64m \
--dex-file=/system/framework/core-libart.jar \
--dex-file=/system/framework/conscrypt.jar \
--dex-file=/system/framework/okhttp.jar \
--dex-file=/system/framework/core-junit.jar \
--dex-file=/system/framework/bouncycastle.jar \
--dex-file=/system/framework/ext.jar --dex-file=/system/framework/framework.jar \
--dex-file=/system/framework/framework2.jar \
--dex-file=/system/framework/telephony-common.jar \
--dex-file=/system/framework/voip-common.jar \
--dex-file=/system/framework/mms-common.jar \
--dex-file=/system/framework/android.policy.jar \
--dex-file=/system/framework/services.jar \
--dex-file=/system/framework/apache-xml.jar \
--dex-file=/system/framework/webviewchromium.jar \
--oat-file=/data/dalvik-cache/system@[email protected] --base=0x60000000 \
--image-classes-zip=/system/framework/framework.jar \
--image-classes=preloaded-classes
似乎是把所有 framework 的 jar 编译成 boot.art,在 dex2oat 的时候使用。还是有很多不理解的地方,相信在以后的分析中应该有更深入的理解。
附:相关代码链接
- PackageManagerService constructor: http://androidxref.com/4.4_r1/xref/frameworks/base/services/java/com/android/server/pm/PackageManagerService.java#1062
- scanDirLI(): http://androidxref.com/4.4_r1/xref/frameworks/base/services/java/com/android/server/pm/PackageManagerService.java#3445
- scanPackageLI(): http://androidxref.com/4.4_r1/xref/frameworks/base/services/java/com/android/server/pm/PackageManagerService.java#4103
- performDexOptLI(): http://androidxref.com/4.4_r1/xref/frameworks/base/services/java/com/android/server/pm/PackageManagerService.java#3860
- Installer.dexopt(): http://androidxref.com/4.4_r1/xref/frameworks/base/services/java/com/android/server/pm/Installer.java#204
- do_dexopt(): http://androidxref.com/4.4_r1/xref/frameworks/native/cmds/installd/installd.c#37
- dexopt(): http://androidxref.com/4.4_r1/xref/frameworks/native/cmds/installd/commands.c#653
- run_dex2oat(): http://androidxref.com/4.4_r1/xref/frameworks/native/cmds/installd/commands.c#run_dex2oat