第 7 章:虚拟机的创建

本文最后更新于:9 个月前

笔记侧重于了解模块/类的功能,尽量省略了代码的实现细节。

ART 虚拟机的启动流程:

Android 中所有 Java 进程都是由 zygote 进程 fork 而来,而 zygote 进程自己又是 Linux 系统上 init 进程通过解析配置脚本对于 32 位 CPU,配置脚本文件是 system/core/rootdir/init.zygote32.rc 来启动的:

  1. service zygote:告诉 init 进程——配置名为 zygote 的服务。
  2. /system/bin/app_process:指定 zygote 服务对应的二进制文件路径 init 进程会 fork 子进程来运行这个程序
  3. -Xzygote /system/bin --zygote --start-system-server:传递给 app_process 的启动参数。

从 /system/bin/app_process/app_main.cpp -> main 开始:

  • 调用 AndroidRuntime 的 start 函数。
    1. 调用 JniInvocation 的 Init 函数加载 ART 虚拟机的核心动态库。
      1. 调用 GetLibrary 函数获取 so 的文件名一般是 libart.so
      2. 调用 dlopen 动态加载 libart.so 到 zygote 进程。
      3. libart.so 取出并保存 JNI_GetDefaultJavaVMInitArgs 函数的地址。
      4. libart.so 取出并保存 JNI_CreateJavaVM 函数创建虚拟机的入口,该函数实际位于 java_vm_ext.cc 文件的地址,保存到了 JNI_CreateJavaVM_。
      5. libart.so 取出并保存 JNI_GetCreatedJavaVMs 函数的地址。
    2. 调用 AndroidRuntime 的 startVm 函数启动 ART 虚拟机。
      调用 AndroidRuntime 的 JNI_CreateJavaVM 最终调用到 libart.so java_vm_ext.cc 的 JNI_CreateJavaVM。
  • java_vm_ext.cc -> JNI_CreateJavaVM:
    1. 调用 Runtime 的 Create 函数。
      1. 调用 ParseOptions 函数将外部调用者传入的参数信息保存到 RuntimeArgumentMap。
      2. 调用 Create 函数(参数携带了传入的参数信息)采用单例方式创建 Runtime 对象 ART 虚拟机的化身
        • 调用 Init 函数。
          1. 调用 MemMap 管理内存映射 的 Init 函数。
          2. 创建 OatFileManager 管理打开的 oat 文件 对象。
          3. 调用 Monitor 实现线程同步 的 Init 函数。
          4. 创建 MonitorList 维护了一组 Monitor 对象 对象。
          5. 调用 MonitorPool 创建 Monitor 对象 的 Create 函数。
          6. 创建 ThreadList 管理 ART 中的线程对象 对象。
          7. 创建 InternTable 字符串常量池 对象。
          8. 创建 Heap 和虚拟机的内存管理相关 对象。
          9. 创建 ArenaPool 管理多个内存单元 对象。
          10. 调用 CreateLinearAlloc 函数创建 LinearAlloc 可在 ArenaPool 上分配任意大小的内存
          11. 调用 FaultManager 处理 SIGSEGV 信号 的 Init 函数。
          12. 创建 JavaVMExt JNI 中代表 Java 虚拟机的对象 对象。
          13. 调用 Thread 的 Startup 函数、Attach 函数初始化虚拟机主线程。
          14. 创建 ClassLinker 处理和 Class 有关的工作 对象。
          15. 调用 MethodVerifier 用于校验 Java 方法 的 Init 函数。
    2. 调用 runtime 对象的 Start 函数启动 Java 虚拟机。

.oat 文件和 .art 文件:

一个包含 classes.dex 项的 jar 或 apk 文件经由 dex2oat 编译处理后会生成两个结果文件:

  • .oat 文件:
    • 本质是 ELF 格式的文件。
    • boot oat 文件 boot imagezygote 进程第一个 Java 进程会首先加载一些基础与核心的 Oat 文件,这些 Oat 文件里包含了 Android 系统中所有 Java 程序所依赖的基础功能类比如 Java 标准类,也叫预加载类
    • app image:其他 APP 进程通过 zygote fork 得到,会继承预加载类将加载 APK 包经过 dex2oat 得到的 Oat 文件。
    • Oat 文件格式:
      • OatHeader 该信息不存储在 Oat 文件的头部
      • 每一个 OatDexFile 对应一个 DexFile,OatDexFile 指明了 DexFile 在 Oat 文件的位置 offset、其他信息在 Oat 文件中的位置 offset
      • DexFile 包含一个 Dex 文件的全部内容。
  • .art 文件 ART 虚拟机代码里常提到的 Image 文件
    • boot image:来源于 /system/framework 下的核心 jar 包core-oj.jar、framework.jar、org.apache.http.legacy.jar、okhttp.jar 等的 Art 文件。
    • app image:来源于某个 apk 的 Art 文件。
    • Art 文件格式:
      数据类型名称长度描述
      数组magic_4魔数 art + \n
      数组version_4版本号 029 + \0
       image_begin_内存的起始位置该 art 文件期望自己被映射到内存的什么位置
       image_size_映射多大空间到内存
      ImageSection 数组sections_kSectionCount = 9ImageSection 用于描述一个 section 存储了不同的信息在内存中的位置基于 image_begin_ 的偏移量和大小。
      ImageHeader 的内存被包括在第 1 个元素内。
      image_size_ 只覆盖到 sections_[kSectionImageBitmap -1],最后一个元素 sections_[kSectionImageBitmap] 从 image_size_ 之后的某个按页大小对齐的位置开始
       storage_mode_文件内容是否为压缩存储
       data_size_

关键模块:

MemMap:

  • 封装了和内存映射有关的操作 mmap、msync,可以设置内存读写权限 mprotect,可创建基于文件的内存映射 MapFile、MapFileAtAddress 以及匿名内存映射 MapAnonymous
  • mmap 系统调用的返回值只是一个代表地址的指针,而 MemMap 提供了更多的成员变量以便更好地使用 mmap 得到的这块映射内存。

OatFileManager:

  • 用于管理虚拟机加载的 Oat 文件。

FaultManager:

  • 信号处理管控类,管理了一系列的 FaultHandler 对象。
  • 当虚拟机收到来自操作系统的信号时,FaultManager 将调用相关 FaultHandler 虚基类,实际由派生类处理 进行处理。
  • fault_handler.cc -> FaultManager::Init() :
    1. 调用 SetUpArtAction 函数为 action 对象设置 ART 虚拟机指定的信号处理函数。
    2. 调用 sigaction 函数将 action 对象设置为 SIGSEGV 信号的信号处理结构体。
    3. 调用 ClaimSignalChain 函数声明信号被使用。
  • 信号处理:
    • Linux 系统中,一个进程可以接受来自操作系统或其它进程发送的信号分为标准和实时两大类信号,ART 只处理标准信号
    • 信号由信号 ID 唯一标识,每一个信号都对应一个信号处理方法,如果进程不为信号设置特定的处理方法,操作系统将使用预定的方法来处理信号。
    • fault_handler.cc -> FaultManager::HandleFault 函数处理 SIGSEGV 信号的流程:先判断该信号是否发生在编译时所生成的机器码 generated code 里。如果是,则交给 generated_code_handlers_ 中的成员处理;否则,则交给 other_handlers_ 中的成员处理。

Thread:

  • Android 平台上线程的栈空间的创建过程:
    1. mmap 得到一块内存,其返回值为该内存的低地址 malloc 或 mmap 返回的都是某块内存的低地址
    2. 设置该内存从低地址开始的某段区域为不可访问。
    3. 得到该内存段的高地址,将其作为线程栈的栈底位置 Linux 系统上线程栈由高地址往低地址拓展传递给 clone 系统调用。
  • thread.cc -> Thread::Startup():
    • 调用 pthread_key_create 函数创建 TLS Thread Local Storage,一块调用线程特有的数据区域
  • thread.cc -> Thread::Attach():
    1. 创建 Thread 对象。
      • 初始化 tlsPtr_ 里的一些成员变量。
    2. 调用 Thread 的 Init 函数。
      1. 调用 InitStackHwm 函数设置线程栈。
      2. 调用 InitCpu 函数【X86】让每个线程将自己的地址设置到 GDT 中,同时 GDT 关联 FS 寄存器,以便实现跳转前提是操作系统实现了 FS 的内容会随着线程切换而切换,因为 FS 只有一个;【ARM】无实际操作。
      3. 调用 InitTlsEntryPoints 函数初始化 tlsPtr_ 里的 jni_entrypoints 和 quick_entrypoints generated code 和 ART 虚拟机交互的通道
      4. 调用 Interpreter 的 InitInterpreterTls 函数初始化解释执行模块的入口地址。
    3. 调用 Thread 的 InitStringEntryPoints 函数设置 tlsPtr_.quick_entrypoints 中的某些成员。

Heap:

  • 位图:减少指针本身所占据的内存空间将对象的指针转换成位图里的一个索引
    • 位图:0 或 1 表示对象是否存在单位是字节
      • 如果指针未存储,就计算偏移量 -> 计算字节 -> 计算字节中的比特位,保存指针,同时将比特位设置为 1。
    • 内存块 1:存储对象的指针根据 CPU 架构,单位长度是 32 位或 64 位
    • 内存块 2:存储更大块的空间单位长度是 4k

JavaVmExt 和 JNIEnvExt:

  • JavaVmExt 是 JavaVM 在 ART 虚拟机中的实际代表类。
    • JavaVM 在 JNI 层中表示 Java 虚拟机,是 JNI 规范里指定的数据结构,不同虚拟机有不同的实现比如 ART 虚拟机中是 Runtime
    • 一个 Java 进程只有一个 JavaVM 实例。
  • JNIEnvExt 是 JNIEnv 在 ART 虚拟机中的实际代表类。
    • JNIEnv 表示 JNI 环境,是 JNI 规范里指定的数据结构,不同虚拟机有不同的实现。
    • 每一个需要和 Java 交互的线程都有一个独立的 JNIEnv 对象。

ClassLinker:

  • 类的连接器:关联和管理类。
  • 入口函数:runtime.cc -> Runtime::Init:
    1. 创建 ClassLinker 对象。
    2. 调用 InitFromBootImage 函数 根据 boot image 初始化 ClassLinker 对象。