Boot image extension.

Allow the boot image to be split into the primary boot image
and any number of optional boot image extensions.

Test: New extension test in dex2oat_image_test.
Test: aosp_taimen-userdebug boots.
Test: m test-art-host-gtest
Test: testrunner.py --host --optimizing --jit
Bug: 119800099
Change-Id: If5a2fa1c0facccbfaf0a1f0cb015ea9233f1f4c6
diff --git a/build/Android.oat.mk b/build/Android.oat.mk
index c29cd29..7c3e791 100644
--- a/build/Android.oat.mk
+++ b/build/Android.oat.mk
@@ -63,6 +63,7 @@
     $$(error found $(1) expected interpreter, interp-ac, or optimizing)
   endif
 
+  core_image_location := $(HOST_OUT_JAVA_LIBRARIES)/core$$(core_infix)$(CORE_IMG_SUFFIX)
   core_image_name := $($(2)HOST_CORE_IMG_OUT_BASE)$$(core_infix)$(CORE_IMG_SUFFIX)
   core_oat_name := $($(2)HOST_CORE_OAT_OUT_BASE)$$(core_infix)$(CORE_OAT_SUFFIX)
 
@@ -76,18 +77,46 @@
   HOST_CORE_OAT_OUTS += $$(core_oat_name)
 
 $$(core_image_name): PRIVATE_CORE_COMPILE_OPTIONS := $$(core_compile_options)
+$$(core_image_name): PRIVATE_CORE_IMAGE_LOCATION := $$(core_image_location)
 $$(core_image_name): PRIVATE_CORE_IMG_NAME := $$(core_image_name)
 $$(core_image_name): PRIVATE_CORE_OAT_NAME := $$(core_oat_name)
-$$(core_image_name): $$(HOST_CORE_IMG_DEX_LOCATIONS) $$(core_dex2oat_dependency)
+# In addition to the primary core image containing HOST_CORE_IMG_DEX_FILES,
+# also build a boot image extension for the remaining HOST_CORE_DEX_FILES.
+$$(core_image_name): $$(HOST_CORE_DEX_LOCATIONS) $$(core_dex2oat_dependency)
 	@echo "host dex2oat: $$@"
 	@mkdir -p $$(dir $$@)
-	$$(hide) ANDROID_LOG_TAGS="*:e" $$(DEX2OAT) --runtime-arg -Xms$(DEX2OAT_IMAGE_XMS) \
+	$$(hide) ANDROID_LOG_TAGS="*:e" $$(DEX2OAT) \
+	  --runtime-arg -Xms$(DEX2OAT_IMAGE_XMS) \
 	  --runtime-arg -Xmx$(DEX2OAT_IMAGE_XMX) \
 	  $$(addprefix --dex-file=,$$(HOST_CORE_IMG_DEX_FILES)) \
 	  $$(addprefix --dex-location=,$$(HOST_CORE_IMG_DEX_LOCATIONS)) \
 	  --oat-file=$$(PRIVATE_CORE_OAT_NAME) \
-	  --oat-location=$$(PRIVATE_CORE_OAT_NAME) --image=$$(PRIVATE_CORE_IMG_NAME) \
-	  --base=$$(LIBART_IMG_HOST_BASE_ADDRESS) --instruction-set=$$($(2)ART_HOST_ARCH) \
+	  --oat-location=$$(PRIVATE_CORE_OAT_NAME) \
+          --image=$$(PRIVATE_CORE_IMG_NAME) \
+	  --base=$$(LIBART_IMG_HOST_BASE_ADDRESS) \
+	  --instruction-set=$$($(2)ART_HOST_ARCH) \
+	  $$(LOCAL_$(2)DEX2OAT_HOST_INSTRUCTION_SET_FEATURES_OPTION) \
+	  --host --android-root=$$(HOST_OUT) \
+	  --generate-debug-info --generate-build-id \
+	  --runtime-arg -XX:SlowDebug=true \
+	  --no-inline-from=core-oj-hostdex.jar \
+	  $$(PRIVATE_CORE_COMPILE_OPTIONS) && \
+	ANDROID_LOG_TAGS="*:e" $$(DEX2OAT) \
+	  --runtime-arg -Xms$(DEX2OAT_IMAGE_XMS) \
+	  --runtime-arg -Xmx$(DEX2OAT_IMAGE_XMX) \
+	  --runtime-arg -Xbootclasspath:$$(subst $$(space),:,$$(strip \
+	        $$(HOST_CORE_DEX_FILES))) \
+	  --runtime-arg -Xbootclasspath-locations:$$(subst $$(space),:,$$(strip \
+	        $$(HOST_CORE_DEX_LOCATIONS))) \
+	  $$(addprefix --dex-file=, \
+	      $$(filter-out $$(HOST_CORE_IMG_DEX_FILES),$$(HOST_CORE_DEX_FILES))) \
+	  $$(addprefix --dex-location=, \
+	      $$(filter-out $$(HOST_CORE_IMG_DEX_LOCATIONS),$$(HOST_CORE_DEX_LOCATIONS))) \
+	  --oat-file=$$(PRIVATE_CORE_OAT_NAME) \
+	  --oat-location=$$(PRIVATE_CORE_OAT_NAME) \
+	  --boot-image=$$(PRIVATE_CORE_IMAGE_LOCATION) \
+	  --image=$$(PRIVATE_CORE_IMG_NAME) \
+	  --instruction-set=$$($(2)ART_HOST_ARCH) \
 	  $$(LOCAL_$(2)DEX2OAT_HOST_INSTRUCTION_SET_FEATURES_OPTION) \
 	  --host --android-root=$$(HOST_OUT) \
 	  --generate-debug-info --generate-build-id \
@@ -149,6 +178,7 @@
     $$(error found $(1) expected interpreter, interp-ac, or optimizing)
   endif
 
+  core_image_location := $(ART_TARGET_TEST_OUT)/core$$(core_infix)$(CORE_IMG_SUFFIX)
   core_image_name := $($(2)TARGET_CORE_IMG_OUT_BASE)$$(core_infix)$(CORE_IMG_SUFFIX)
   core_oat_name := $($(2)TARGET_CORE_OAT_OUT_BASE)$$(core_infix)$(CORE_OAT_SUFFIX)
 
@@ -166,24 +196,53 @@
   TARGET_CORE_OAT_OUTS += $$(core_oat_name)
 
 $$(core_image_name): PRIVATE_CORE_COMPILE_OPTIONS := $$(core_compile_options)
+$$(core_image_name): PRIVATE_CORE_IMAGE_LOCATION := $$(core_image_location)
 $$(core_image_name): PRIVATE_CORE_IMG_NAME := $$(core_image_name)
 $$(core_image_name): PRIVATE_CORE_OAT_NAME := $$(core_oat_name)
-$$(core_image_name): $$(TARGET_CORE_IMG_DEX_FILES) $$(core_dex2oat_dependency)
+# In addition to the primary core image containing TARGET_CORE_IMG_DEX_FILES,
+# also build a boot image extension for the remaining TARGET_CORE_DEX_FILES.
+$$(core_image_name): $$(TARGET_CORE_DEX_FILES) $$(core_dex2oat_dependency)
 	@echo "target dex2oat: $$@"
 	@mkdir -p $$(dir $$@)
-	$$(hide) $$(DEX2OAT) --runtime-arg -Xms$(DEX2OAT_IMAGE_XMS) \
+	$$(hide) $$(DEX2OAT) \
+	  --runtime-arg -Xms$(DEX2OAT_IMAGE_XMS) \
 	  --runtime-arg -Xmx$(DEX2OAT_IMAGE_XMX) \
 	  $$(addprefix --dex-file=,$$(TARGET_CORE_IMG_DEX_FILES)) \
 	  $$(addprefix --dex-location=,$$(TARGET_CORE_IMG_DEX_LOCATIONS)) \
 	  --oat-file=$$(PRIVATE_CORE_OAT_NAME) \
-	  --oat-location=$$(PRIVATE_CORE_OAT_NAME) --image=$$(PRIVATE_CORE_IMG_NAME) \
-	  --base=$$(LIBART_IMG_TARGET_BASE_ADDRESS) --instruction-set=$$($(2)TARGET_ARCH) \
+	  --oat-location=$$(PRIVATE_CORE_OAT_NAME) \
+	  --image=$$(PRIVATE_CORE_IMG_NAME) \
+	  --base=$$(LIBART_IMG_TARGET_BASE_ADDRESS) \
+	  --instruction-set=$$($(2)TARGET_ARCH) \
 	  --instruction-set-variant=$$($(2)DEX2OAT_TARGET_CPU_VARIANT) \
 	  --instruction-set-features=$$($(2)DEX2OAT_TARGET_INSTRUCTION_SET_FEATURES) \
 	  --android-root=$$(PRODUCT_OUT)/system \
 	  --generate-debug-info --generate-build-id \
 	  --runtime-arg -XX:SlowDebug=true \
-	  $$(PRIVATE_CORE_COMPILE_OPTIONS) || (rm $$(PRIVATE_CORE_OAT_NAME); exit 1)
+	  $$(PRIVATE_CORE_COMPILE_OPTIONS) && \
+	$$(DEX2OAT) \
+	  --runtime-arg -Xms$(DEX2OAT_IMAGE_XMS) \
+	  --runtime-arg -Xmx$(DEX2OAT_IMAGE_XMX) \
+	  --runtime-arg -Xbootclasspath:$$(subst $$(space),:,$$(strip \
+	        $$(TARGET_CORE_DEX_FILES))) \
+	  --runtime-arg -Xbootclasspath-locations:$$(subst $$(space),:,$$(strip \
+	        $$(TARGET_CORE_DEX_LOCATIONS))) \
+	  $$(addprefix --dex-file=, \
+	       $$(filter-out $$(TARGET_CORE_IMG_DEX_FILES),$$(TARGET_CORE_DEX_FILES))) \
+	  $$(addprefix --dex-location=, \
+	       $$(filter-out $$(TARGET_CORE_IMG_DEX_LOCATIONS),$$(TARGET_CORE_DEX_LOCATIONS))) \
+	  --oat-file=$$(PRIVATE_CORE_OAT_NAME) \
+	  --oat-location=$$(PRIVATE_CORE_OAT_NAME) \
+	  --boot-image=$$(PRIVATE_CORE_IMAGE_LOCATION) \
+	  --image=$$(PRIVATE_CORE_IMG_NAME) \
+	  --instruction-set=$$($(2)TARGET_ARCH) \
+	  --instruction-set-variant=$$($(2)DEX2OAT_TARGET_CPU_VARIANT) \
+	  --instruction-set-features=$$($(2)DEX2OAT_TARGET_INSTRUCTION_SET_FEATURES) \
+	  --android-root=$$(PRODUCT_OUT)/system \
+	  --generate-debug-info --generate-build-id \
+	  --runtime-arg -XX:SlowDebug=true \
+	  $$(PRIVATE_CORE_COMPILE_OPTIONS) || \
+	(rm $$(PRIVATE_CORE_OAT_NAME); exit 1)
 
 $$(core_oat_name): $$(core_image_name)
 
diff --git a/dex2oat/dex2oat.cc b/dex2oat/dex2oat.cc
index 8e642e5..feda8ba 100644
--- a/dex2oat/dex2oat.cc
+++ b/dex2oat/dex2oat.cc
@@ -63,6 +63,7 @@
 #include "debug/method_debug_info.h"
 #include "dex/descriptors_names.h"
 #include "dex/dex_file-inl.h"
+#include "dex/dex_file_loader.h"
 #include "dex/quick_compiler_callbacks.h"
 #include "dex/verification_results.h"
 #include "dex2oat_options.h"
@@ -280,6 +281,14 @@
   UsageError("      Do not include the arch as part of the name, it is added automatically.");
   UsageError("      Example: --boot-image=/system/framework/boot.art");
   UsageError("               (specifies /system/framework/<arch>/boot.art as the image file)");
+  UsageError("      Example: --boot-image=boot.art:boot-framework.art");
+  UsageError("               (specifies <bcp-path1>/<arch>/boot.art as the image file and");
+  UsageError("               <bcp-path2>/<arch>/boot-framework.art as the image extension file");
+  UsageError("               with paths taken from corresponding boot class path components)");
+  UsageError("      Example: --boot-image=/apex/com.android.art/boot.art:/system/framework/*:*");
+  UsageError("               (specifies /apex/com.android.art/<arch>/boot.art as the image");
+  UsageError("               file and search for extensions in /framework/system and boot");
+  UsageError("               class path components' paths)");
   UsageError("      Default: $ANDROID_ROOT/system/framework/boot.art");
   UsageError("");
   UsageError("  --android-root=<path>: used to locate libraries for portable linking.");
@@ -752,16 +761,31 @@
 
   void ProcessOptions(ParserOptions* parser_options) {
     compiler_options_->compile_pic_ = true;  // All AOT compilation is PIC.
+
+    if (android_root_.empty()) {
+      const char* android_root_env_var = getenv("ANDROID_ROOT");
+      if (android_root_env_var == nullptr) {
+        Usage("--android-root unspecified and ANDROID_ROOT not set");
+      }
+      android_root_ += android_root_env_var;
+    }
+
+    if (!parser_options->boot_image_filename.empty()) {
+      boot_image_filename_ = parser_options->boot_image_filename;
+    }
+
     DCHECK(compiler_options_->image_type_ == CompilerOptions::ImageType::kNone);
     if (!image_filenames_.empty()) {
-      if (android::base::EndsWith(image_filenames_[0], "apex.art")) {
+      if (!boot_image_filename_.empty()) {
+        compiler_options_->image_type_ = CompilerOptions::ImageType::kBootImageExtension;
+      } else if (android::base::EndsWith(image_filenames_[0], "apex.art")) {
         compiler_options_->image_type_ = CompilerOptions::ImageType::kApexBootImage;
       } else {
         compiler_options_->image_type_ = CompilerOptions::ImageType::kBootImage;
       }
     }
     if (app_image_fd_ != -1 || !app_image_file_name_.empty()) {
-      if (compiler_options_->IsBootImage()) {
+      if (compiler_options_->IsBootImage() || compiler_options_->IsBootImageExtension()) {
         Usage("Can't have both --image and (--app-image-fd or --app-image-file)");
       }
       compiler_options_->image_type_ = CompilerOptions::ImageType::kAppImage;
@@ -818,19 +842,9 @@
       Usage("--oat-file arguments do not match --image arguments");
     }
 
-    if (android_root_.empty()) {
-      const char* android_root_env_var = getenv("ANDROID_ROOT");
-      if (android_root_env_var == nullptr) {
-        Usage("--android-root unspecified and ANDROID_ROOT not set");
-      }
-      android_root_ += android_root_env_var;
-    }
-
-    if (!IsBootImage() && parser_options->boot_image_filename.empty()) {
-      parser_options->boot_image_filename = GetDefaultBootImageLocation(android_root_);
-    }
-    if (!parser_options->boot_image_filename.empty()) {
-      boot_image_filename_ = parser_options->boot_image_filename;
+    if (!IsBootImage() && boot_image_filename_.empty()) {
+      DCHECK(!IsBootImageExtension());
+      boot_image_filename_ = GetDefaultBootImageLocation(android_root_);
     }
 
     if (dex_filenames_.empty() && zip_fd_ == -1) {
@@ -937,8 +951,8 @@
     // Fill some values into the key-value store for the oat header.
     key_value_store_.reset(new SafeMap<std::string, std::string>());
 
-    // Automatically force determinism for the boot image in a host build.
-    if (!kIsTargetBuild && IsBootImage()) {
+    // Automatically force determinism for the boot image and boot image extensions in a host build.
+    if (!kIsTargetBuild && (IsBootImage() || IsBootImageExtension())) {
       force_determinism_ = true;
     }
     compiler_options_->force_determinism_ = force_determinism_;
@@ -961,18 +975,21 @@
     if (image_filenames_[0].rfind('/') == std::string::npos) {
       Usage("Unusable boot image filename %s", image_filenames_[0].c_str());
     }
-    image_filenames_ = ImageSpace::ExpandMultiImageLocations(dex_locations_, image_filenames_[0]);
+    image_filenames_ = ImageSpace::ExpandMultiImageLocations(
+        ArrayRef<const std::string>(dex_locations_), image_filenames_[0], IsBootImageExtension());
 
     if (oat_filenames_[0].rfind('/') == std::string::npos) {
       Usage("Unusable boot image oat filename %s", oat_filenames_[0].c_str());
     }
-    oat_filenames_ = ImageSpace::ExpandMultiImageLocations(dex_locations_, oat_filenames_[0]);
+    oat_filenames_ = ImageSpace::ExpandMultiImageLocations(
+        ArrayRef<const std::string>(dex_locations_), oat_filenames_[0], IsBootImageExtension());
 
     if (!oat_unstripped_.empty()) {
       if (oat_unstripped_[0].rfind('/') == std::string::npos) {
         Usage("Unusable boot image symbol filename %s", oat_unstripped_[0].c_str());
       }
-      oat_unstripped_ = ImageSpace::ExpandMultiImageLocations(dex_locations_, oat_unstripped_[0]);
+      oat_unstripped_ = ImageSpace::ExpandMultiImageLocations(
+           ArrayRef<const std::string>(dex_locations_), oat_unstripped_[0], IsBootImageExtension());
     }
   }
 
@@ -1204,9 +1221,13 @@
   bool OpenFile() {
     // Prune non-existent dex files now so that we don't create empty oat files for multi-image.
     PruneNonExistentDexFiles();
+    if (dex_locations_.empty()) {
+      LOG(ERROR) << "Nothing to compile after pruning non-existent dex files.";
+      return false;
+    }
 
     // Expand oat and image filenames for multi image.
-    if (IsBootImage() && image_filenames_.size() == 1) {
+    if ((IsBootImage() || IsBootImageExtension()) && image_filenames_.size() == 1) {
       ExpandOatAndImageFilenames();
     }
 
@@ -1431,12 +1452,13 @@
       return dex2oat::ReturnCode::kOther;
     }
 
-    // Verification results are null since we don't know if we will need them yet as the compler
+    // Verification results are null since we don't know if we will need them yet as the compiler
     // filter may change.
     callbacks_.reset(new QuickCompilerCallbacks(
-        IsBootImage() ?
-            CompilerCallbacks::CallbackMode::kCompileBootImage :
-            CompilerCallbacks::CallbackMode::kCompileApp));
+        // For class verification purposes, boot image extension is the same as boot image.
+        (IsBootImage() || IsBootImageExtension())
+            ? CompilerCallbacks::CallbackMode::kCompileBootImage
+            : CompilerCallbacks::CallbackMode::kCompileApp));
 
     RuntimeArgumentMap runtime_options;
     if (!PrepareRuntimeOptions(&runtime_options, callbacks_.get())) {
@@ -1489,7 +1511,7 @@
     //       store which is used for determining whether the oat file is up to date,
     //       together with the boot class path locations and checksums stored below.
     CompilerFilter::Filter original_compiler_filter = compiler_options_->GetCompilerFilter();
-    if (!IsBootImage() && IsVeryLarge(dex_files)) {
+    if (!IsBootImage() && !IsBootImageExtension() && IsVeryLarge(dex_files)) {
       // Disable app image to make sure dex2oat unloading is enabled.
       compiler_options_->image_type_ = CompilerOptions::ImageType::kNone;
 
@@ -1512,14 +1534,64 @@
       callbacks_->SetVerificationResults(verification_results_.get());
     }
 
-    if (IsBootImage()) {
-      // For boot image, pass opened dex files to the Runtime::Create().
+    if (IsBootImage() || IsBootImageExtension()) {
+      // For boot image or boot image extension, pass opened dex files to the Runtime::Create().
       // Note: Runtime acquires ownership of these dex files.
       runtime_options.Set(RuntimeArgumentMap::BootClassPathDexList, &opened_dex_files_);
     }
     if (!CreateRuntime(std::move(runtime_options))) {
       return dex2oat::ReturnCode::kCreateRuntime;
     }
+    ArrayRef<const DexFile* const> bcp_dex_files(runtime_->GetClassLinker()->GetBootClassPath());
+    if (IsBootImage() || IsBootImageExtension()) {
+      // Check boot class path dex files and, if compiling an extension, the images it depends on.
+      if ((IsBootImage() && bcp_dex_files.size() != dex_files.size()) ||
+          (IsBootImageExtension() && bcp_dex_files.size() <= dex_files.size())) {
+        LOG(ERROR) << "Unexpected number of boot class path dex files for boot image or extension, "
+            << bcp_dex_files.size() << (IsBootImage() ? " != " : " <= ") << dex_files.size();
+        return dex2oat::ReturnCode::kOther;
+      }
+      if (!std::equal(dex_files.begin(), dex_files.end(), bcp_dex_files.end() - dex_files.size())) {
+        LOG(ERROR) << "Boot class path dex files do not end with the compiled dex files.";
+        return dex2oat::ReturnCode::kOther;
+      }
+      size_t bcp_df_pos = 0u;
+      size_t bcp_df_end = bcp_dex_files.size();
+      for (const std::string& bcp_location : runtime_->GetBootClassPathLocations()) {
+        if (bcp_df_pos == bcp_df_end || bcp_dex_files[bcp_df_pos]->GetLocation() != bcp_location) {
+          LOG(ERROR) << "Missing dex file for boot class component " << bcp_location;
+          return dex2oat::ReturnCode::kOther;
+        }
+        CHECK(!DexFileLoader::IsMultiDexLocation(bcp_dex_files[bcp_df_pos]->GetLocation().c_str()));
+        ++bcp_df_pos;
+        while (bcp_df_pos != bcp_df_end &&
+            DexFileLoader::IsMultiDexLocation(bcp_dex_files[bcp_df_pos]->GetLocation().c_str())) {
+          ++bcp_df_pos;
+        }
+      }
+      if (bcp_df_pos != bcp_df_end) {
+        LOG(ERROR) << "Unexpected dex file in boot class path "
+            << bcp_dex_files[bcp_df_pos]->GetLocation();
+        return dex2oat::ReturnCode::kOther;
+      }
+      auto lacks_image = [](const DexFile* df) {
+        if (kIsDebugBuild && df->GetOatDexFile() != nullptr) {
+          const OatFile* oat_file = df->GetOatDexFile()->GetOatFile();
+          CHECK(oat_file != nullptr);
+          const auto& image_spaces = Runtime::Current()->GetHeap()->GetBootImageSpaces();
+          CHECK(std::any_of(image_spaces.begin(),
+                            image_spaces.end(),
+                            [=](const ImageSpace* space) {
+                              return oat_file == space->GetOatFile();
+                            }));
+        }
+        return df->GetOatDexFile() == nullptr;
+      };
+      if (std::any_of(bcp_dex_files.begin(), bcp_dex_files.end() - dex_files.size(), lacks_image)) {
+        LOG(ERROR) << "Missing required boot image(s) for boot image extension.";
+        return dex2oat::ReturnCode::kOther;
+      }
+    }
 
     if (!compilation_reason_.empty()) {
       key_value_store_->Put(OatHeader::kCompilationReasonKey, compilation_reason_);
@@ -1529,17 +1601,32 @@
       // If we're compiling the boot image, store the boot classpath into the Key-Value store.
       // We use this when loading the boot image.
       key_value_store_->Put(OatHeader::kBootClassPathKey, android::base::Join(dex_locations_, ':'));
-    }
-
-    if (!IsBootImage()) {
+    } else if (IsBootImageExtension()) {
+      // Validate the boot class path and record the dependency on the loaded boot images.
+      TimingLogger::ScopedTiming t3("Loading image checksum", timings_);
+      Runtime* runtime = Runtime::Current();
+      std::string full_bcp = android::base::Join(runtime->GetBootClassPathLocations(), ':');
+      std::string extension_part = ":" + android::base::Join(dex_locations_, ':');
+      if (!android::base::EndsWith(full_bcp, extension_part)) {
+        LOG(ERROR) << "Full boot class path does not end with extension parts, full: " << full_bcp
+            << ", extension: " << extension_part.substr(1u);
+        return dex2oat::ReturnCode::kOther;
+      }
+      std::string bcp_dependency = full_bcp.substr(0u, full_bcp.size() - extension_part.size());
+      key_value_store_->Put(OatHeader::kBootClassPathKey, bcp_dependency);
+      ArrayRef<const DexFile* const> bcp_dex_files_dependency =
+          bcp_dex_files.SubArray(/*pos=*/ 0u, bcp_dex_files.size() - dex_files.size());
+      ArrayRef<ImageSpace* const> image_spaces(runtime->GetHeap()->GetBootImageSpaces());
+      key_value_store_->Put(
+          OatHeader::kBootClassPathChecksumsKey,
+          gc::space::ImageSpace::GetBootClassPathChecksums(image_spaces, bcp_dex_files_dependency));
+    } else {
       if (CompilerFilter::DependsOnImageChecksum(original_compiler_filter)) {
         TimingLogger::ScopedTiming t3("Loading image checksum", timings_);
         Runtime* runtime = Runtime::Current();
         key_value_store_->Put(OatHeader::kBootClassPathKey,
                               android::base::Join(runtime->GetBootClassPathLocations(), ':'));
         ArrayRef<ImageSpace* const> image_spaces(runtime->GetHeap()->GetBootImageSpaces());
-        ArrayRef<const DexFile* const> bcp_dex_files(
-            runtime->GetClassLinker()->GetBootClassPath());
         key_value_store_->Put(
             OatHeader::kBootClassPathChecksumsKey,
             gc::space::ImageSpace::GetBootClassPathChecksums(image_spaces, bcp_dex_files));
@@ -1607,7 +1694,7 @@
     CHECK(driver_ == nullptr);
     // If we use a swap file, ensure we are above the threshold to make it necessary.
     if (swap_fd_ != -1) {
-      if (!UseSwap(IsBootImage(), dex_files)) {
+      if (!UseSwap(IsBootImage() || IsBootImageExtension(), dex_files)) {
         close(swap_fd_);
         swap_fd_ = -1;
         VLOG(compiler) << "Decided to run without swap.";
@@ -1624,7 +1711,7 @@
     Thread* self = Thread::Current();
     WellKnownClasses::Init(self->GetJniEnv());
 
-    if (!IsBootImage()) {
+    if (!IsBootImage() && !IsBootImageExtension()) {
       constexpr bool kSaveDexInput = false;
       if (kSaveDexInput) {
         SaveDexInput();
@@ -1713,7 +1800,7 @@
 
     if (!no_inline_filters.empty()) {
       std::vector<const DexFile*> class_path_files;
-      if (!IsBootImage()) {
+      if (!IsBootImage() && !IsBootImageExtension()) {
         // The class loader context is used only for apps.
         class_path_files = class_loader_context_->FlattenOpenedDexFiles();
       }
@@ -1758,7 +1845,7 @@
                                      compiler_kind_,
                                      thread_count_,
                                      swap_fd_));
-    if (!IsBootImage()) {
+    if (!IsBootImage() && !IsBootImageExtension()) {
       driver_->SetClasspathDexFiles(class_loader_context_->FlattenOpenedDexFiles());
     }
 
@@ -1806,7 +1893,7 @@
     ClassLinker* const class_linker = Runtime::Current()->GetClassLinker();
 
     jobject class_loader = nullptr;
-    if (!IsBootImage()) {
+    if (!IsBootImage() && !IsBootImageExtension()) {
       class_loader =
           class_loader_context_->CreateClassLoader(compiler_options_->dex_files_for_oat_file_);
       callbacks_->SetDexFiles(&dex_files);
@@ -1950,7 +2037,7 @@
 
     {
       TimingLogger::ScopedTiming t2("dex2oat Write VDEX", timings_);
-      DCHECK(IsBootImage() || oat_files_.size() == 1u);
+      DCHECK(IsBootImage() || IsBootImageExtension() || oat_files_.size() == 1u);
       verifier::VerifierDeps* verifier_deps = callbacks_->GetVerifierDeps();
       for (size_t i = 0, size = oat_files_.size(); i != size; ++i) {
         File* vdex_file = vdex_files_[i].get();
@@ -2174,7 +2261,7 @@
   }
 
   bool IsImage() const {
-    return IsAppImage() || IsBootImage();
+    return IsAppImage() || IsBootImage() || IsBootImageExtension();
   }
 
   bool IsAppImage() const {
@@ -2185,6 +2272,10 @@
     return compiler_options_->IsBootImage();
   }
 
+  bool IsBootImageExtension() const {
+    return compiler_options_->IsBootImageExtension();
+  }
+
   bool IsHost() const {
     return is_host_;
   }
@@ -2389,7 +2480,7 @@
   bool PrepareRuntimeOptions(RuntimeArgumentMap* runtime_options,
                              QuickCompilerCallbacks* callbacks) {
     RuntimeOptions raw_options;
-    if (boot_image_filename_.empty()) {
+    if (IsBootImage()) {
       std::string boot_class_path = "-Xbootclasspath:";
       boot_class_path += android::base::Join(dex_filenames_, ':');
       raw_options.push_back(std::make_pair(boot_class_path, nullptr));
@@ -2479,7 +2570,7 @@
   bool CreateImageFile()
       REQUIRES(!Locks::mutator_lock_) {
     CHECK(image_writer_ != nullptr);
-    if (!IsBootImage()) {
+    if (!IsBootImage() && !IsBootImageExtension()) {
       CHECK(image_filenames_.empty());
       image_filenames_.push_back(app_image_file_name_);
     }
@@ -2862,7 +2953,10 @@
   //   3) Compiling with --host
   //   4) Compiling on the host (not a target build)
   // Otherwise, print a stripped command line.
-  if (kIsDebugBuild || dex2oat->IsBootImage() || dex2oat->IsHost() || !kIsTargetBuild) {
+  if (kIsDebugBuild ||
+      dex2oat->IsBootImage() || dex2oat->IsBootImageExtension() ||
+      dex2oat->IsHost() ||
+      !kIsTargetBuild) {
     LOG(INFO) << CommandLine();
   } else {
     LOG(INFO) << StrippedCommandLine();
diff --git a/dex2oat/dex2oat_image_test.cc b/dex2oat/dex2oat_image_test.cc
index d0d5081..ea87c1a 100644
--- a/dex2oat/dex2oat_image_test.cc
+++ b/dex2oat/dex2oat_image_test.cc
@@ -14,31 +14,44 @@
  * limitations under the License.
  */
 
+#include <fstream>
 #include <regex>
 #include <sstream>
 #include <string>
 #include <vector>
 
+#include <sys/mman.h>
 #include <sys/wait.h>
 #include <unistd.h>
 
 #include <android-base/logging.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
 
 #include "common_runtime_test.h"
 
+#include "base/array_ref.h"
 #include "base/file_utils.h"
 #include "base/macros.h"
+#include "base/mem_map.h"
+#include "base/string_view_cpp20.h"
 #include "base/unix_file/fd_file.h"
 #include "base/utils.h"
 #include "dex/art_dex_file_loader.h"
 #include "dex/dex_file-inl.h"
 #include "dex/dex_file_loader.h"
 #include "dex/method_reference.h"
+#include "gc/space/image_space.h"
 #include "profile/profile_compilation_info.h"
 #include "runtime.h"
+#include "scoped_thread_state_change-inl.h"
+#include "thread-current-inl.h"
 
 namespace art {
 
+// A suitable address for loading the core images.
+constexpr uint32_t kBaseAddress = 0x60000000;
+
 struct ImageSizes {
   size_t art_size = 0;
   size_t oat_size = 0;
@@ -132,8 +145,12 @@
       scratch_dir.pop_back();
     }
     CHECK(!scratch_dir.empty()) << "No directory " << scratch.GetFilename();
+    std::vector<std::string> libcore_dex_files = GetLibCoreDexFileNames();
+    ArrayRef<const std::string> dex_files(libcore_dex_files);
+    std::vector<std::string> local_extra_args = extra_args;
+    local_extra_args.push_back(android::base::StringPrintf("--base=0x%08x", kBaseAddress));
     std::string error_msg;
-    if (!CompileBootImage(extra_args, scratch.GetFilename(), &error_msg)) {
+    if (!CompileBootImage(local_extra_args, scratch.GetFilename(), dex_files, &error_msg)) {
       LOG(ERROR) << "Failed to compile image " << scratch.GetFilename() << error_msg;
     }
     std::string art_file = scratch.GetFilename() + ".art";
@@ -151,19 +168,19 @@
     scratch.Close();
     // Clear image files since we compile the image multiple times and don't want to leave any
     // artifacts behind.
-    ClearDirectory(scratch_dir.c_str(), /*recursive*/ false);
+    ClearDirectory(scratch_dir.c_str(), /*recursive=*/ false);
     return ret;
   }
 
   bool CompileBootImage(const std::vector<std::string>& extra_args,
                         const std::string& image_file_name_prefix,
+                        ArrayRef<const std::string> dex_files,
                         std::string* error_msg) {
     Runtime* const runtime = Runtime::Current();
     std::vector<std::string> argv;
     argv.push_back(runtime->GetCompilerExecutable());
     AddRuntimeArg(argv, "-Xms64m");
     AddRuntimeArg(argv, "-Xmx64m");
-    std::vector<std::string> dex_files = GetLibCoreDexFileNames();
     for (const std::string& dex_file : dex_files) {
       argv.push_back("--dex-file=" + dex_file);
       argv.push_back("--dex-location=" + dex_file);
@@ -182,7 +199,6 @@
     argv.push_back("--image=" + image_file_name_prefix + ".art");
     argv.push_back("--oat-file=" + image_file_name_prefix + ".oat");
     argv.push_back("--oat-location=" + image_file_name_prefix + ".oat");
-    argv.push_back("--base=0x60000000");
 
     std::vector<std::string> compiler_options = runtime->GetCompilerOptions();
     argv.insert(argv.end(), compiler_options.begin(), compiler_options.end());
@@ -265,4 +281,240 @@
   }
 }
 
+TEST_F(Dex2oatImageTest, TestExtension) {
+  constexpr size_t kReservationSize = 256 * MB;  // This should be enough for the compiled images.
+  std::string error_msg;
+  MemMap reservation = MemMap::MapAnonymous("Reservation",
+                                            reinterpret_cast<uint8_t*>(kBaseAddress),
+                                            kReservationSize,
+                                            PROT_NONE,
+                                            /*low_4gb=*/ true,
+                                            /*reuse=*/ false,
+                                            /*reservation=*/ nullptr,
+                                            &error_msg);
+  ASSERT_TRUE(reservation.IsValid());
+
+  ScratchFile scratch;
+  std::string scratch_dir = scratch.GetFilename();
+  while (!scratch_dir.empty() && scratch_dir.back() != '/') {
+    scratch_dir.pop_back();
+  }
+  CHECK(!scratch_dir.empty()) << "No directory " << scratch.GetFilename();
+  std::string image_dir = scratch_dir + GetInstructionSetString(kRuntimeISA);
+  int mkdir_result = mkdir(image_dir.c_str(), 0700);
+  ASSERT_EQ(0, mkdir_result);
+  std::string filename_prefix = image_dir + "/core";
+
+  std::vector<std::string> libcore_dex_files = GetLibCoreDexFileNames();
+  ArrayRef<const std::string> full_bcp(libcore_dex_files);
+  size_t total_dex_files = full_bcp.size();
+  ASSERT_GE(total_dex_files, 4u);  // 2 for "head", 1 for "tail", at least one for "mid", see below.
+
+  // The primary image must contain at least core-oj and core-libart to initialize the runtime.
+  ASSERT_NE(std::string::npos, full_bcp[0].find("core-oj"));
+  ASSERT_NE(std::string::npos, full_bcp[1].find("core-libart"));
+  ArrayRef<const std::string> head_dex_files = full_bcp.SubArray(/*pos=*/ 0u, /*length=*/ 2u);
+  // Middle part is everything else except for conscrypt.
+  ASSERT_NE(std::string::npos, full_bcp[full_bcp.size() - 1u].find("conscrypt"));
+  ArrayRef<const std::string> mid_bcp =
+      full_bcp.SubArray(/*pos=*/ 0u, /*length=*/ total_dex_files - 1u);
+  ArrayRef<const std::string> mid_dex_files = mid_bcp.SubArray(/*pos=*/ 2u);
+  // Tail is just the conscrypt.
+  ArrayRef<const std::string> tail_dex_files =
+      full_bcp.SubArray(/*pos=*/ total_dex_files - 1u, /*length=*/ 1u);
+
+  // Prepare the "head", "mid" and "tail" names and locations.
+  std::string base_name = "core.art";
+  std::string base_location = scratch_dir + base_name;
+  std::vector<std::string> expanded_mid = gc::space::ImageSpace::ExpandMultiImageLocations(
+      mid_dex_files.SubArray(/*pos=*/ 0u, /*length=*/ 1u),
+      base_location,
+      /*boot_image_extension=*/ true);
+  CHECK_EQ(1u, expanded_mid.size());
+  std::string mid_location = expanded_mid[0];
+  size_t mid_slash_pos = mid_location.rfind('/');
+  ASSERT_NE(std::string::npos, mid_slash_pos);
+  std::string mid_name = mid_location.substr(mid_slash_pos + 1u);
+  CHECK_EQ(1u, tail_dex_files.size());
+  std::vector<std::string> expanded_tail = gc::space::ImageSpace::ExpandMultiImageLocations(
+      tail_dex_files, base_location, /*boot_image_extension=*/ true);
+  CHECK_EQ(1u, expanded_tail.size());
+  std::string tail_location = expanded_tail[0];
+  size_t tail_slash_pos = tail_location.rfind('/');
+  ASSERT_NE(std::string::npos, tail_slash_pos);
+  std::string tail_name = tail_location.substr(tail_slash_pos + 1u);
+
+  // Compile the "head", i.e. the primary boot image.
+  std::string base = android::base::StringPrintf("--base=0x%08x", kBaseAddress);
+  bool head_ok = CompileBootImage({base}, filename_prefix, head_dex_files, &error_msg);
+  ASSERT_TRUE(head_ok) << error_msg;
+
+  // Compile the "mid", i.e. the first extension.
+  std::string mid_bcp_string = android::base::Join(mid_bcp, ':');
+  std::vector<std::string> extra_args;
+  AddRuntimeArg(extra_args, "-Xbootclasspath:" + mid_bcp_string);
+  AddRuntimeArg(extra_args, "-Xbootclasspath-locations:" + mid_bcp_string);
+  extra_args.push_back("--boot-image=" + base_location);
+  bool mid_ok = CompileBootImage(extra_args, filename_prefix, mid_dex_files, &error_msg);
+  ASSERT_TRUE(mid_ok) << error_msg;
+
+  // Try to compile the "tail" without specifying the "mid" extension. This shall fail.
+  std::string full_bcp_string = android::base::Join(full_bcp, ':');
+  extra_args.clear();
+  AddRuntimeArg(extra_args, "-Xbootclasspath:" + full_bcp_string);
+  AddRuntimeArg(extra_args, "-Xbootclasspath-locations:" + full_bcp_string);
+  extra_args.push_back("--boot-image=" + base_location);
+  bool tail_ok = CompileBootImage(extra_args, filename_prefix, tail_dex_files, &error_msg);
+  ASSERT_FALSE(tail_ok) << error_msg;
+
+  // Now compile the tail against both "head" and "mid".
+  CHECK(StartsWith(extra_args.back(), "--boot-image="));
+  extra_args.back() = "--boot-image=" + base_location + ':' + mid_location;
+  tail_ok = CompileBootImage(extra_args, filename_prefix, tail_dex_files, &error_msg);
+  ASSERT_TRUE(tail_ok) << error_msg;
+
+  reservation = MemMap::Invalid();  // Free the reserved memory for loading images.
+
+  // Try to load the boot image with different image locations.
+  std::vector<std::string> boot_class_path = libcore_dex_files;
+  std::vector<std::unique_ptr<gc::space::ImageSpace>> boot_image_spaces;
+  MemMap extra_reservation;
+  auto load = [&](const std::string& image_location) {
+    boot_image_spaces.clear();
+    extra_reservation = MemMap::Invalid();
+    ScopedObjectAccess soa(Thread::Current());
+    return gc::space::ImageSpace::LoadBootImage(/*boot_class_path=*/ boot_class_path,
+                                                /*boot_class_path_locations=*/ libcore_dex_files,
+                                                image_location,
+                                                kRuntimeISA,
+                                                gc::space::ImageSpaceLoadingOrder::kSystemFirst,
+                                                /*relocate=*/ false,
+                                                /*executable=*/ true,
+                                                /*is_zygote=*/ false,
+                                                /*extra_reservation_size=*/ 0u,
+                                                &boot_image_spaces,
+                                                &extra_reservation);
+  };
+
+  // Load primary image with full path.
+  bool load_ok = load(base_location);
+  ASSERT_TRUE(load_ok) << error_msg;
+  ASSERT_FALSE(extra_reservation.IsValid());
+  ASSERT_EQ(head_dex_files.size(), boot_image_spaces.size());
+
+  // Fail to load primary image with just the name.
+  load_ok = load(base_name);
+  ASSERT_FALSE(load_ok);
+
+  // Fail to load primary image with a search path.
+  load_ok = load("*");
+  ASSERT_FALSE(load_ok);
+  load_ok = load(scratch_dir + "*");
+  ASSERT_FALSE(load_ok);
+
+  // Load the primary and first extension with full path.
+  load_ok = load(base_location + ':' + mid_location);
+  ASSERT_TRUE(load_ok) << error_msg;
+  ASSERT_EQ(mid_bcp.size(), boot_image_spaces.size());
+
+  // Load the primary with full path and fail to load first extension without full path.
+  load_ok = load(base_location + ':' + mid_name);
+  ASSERT_TRUE(load_ok) << error_msg;  // Primary image loaded successfully.
+  ASSERT_EQ(head_dex_files.size(), boot_image_spaces.size());  // But only the primary image.
+
+  // Load all the libcore images with full paths.
+  load_ok = load(base_location + ':' + mid_location + ':' + tail_location);
+  ASSERT_TRUE(load_ok) << error_msg;
+  ASSERT_EQ(full_bcp.size(), boot_image_spaces.size());
+
+  // Load the primary and first extension with full paths, fail to load second extension by name.
+  load_ok = load(base_location + ':' + mid_location + ':' + tail_name);
+  ASSERT_TRUE(load_ok) << error_msg;
+  ASSERT_EQ(mid_bcp.size(), boot_image_spaces.size());
+
+  // Load the primary with full path and fail to load first extension without full path,
+  // fail to load second extension because it depends on the first.
+  load_ok = load(base_location + ':' + mid_name + ':' + tail_location);
+  ASSERT_TRUE(load_ok) << error_msg;  // Primary image loaded successfully.
+  ASSERT_EQ(head_dex_files.size(), boot_image_spaces.size());  // But only the primary image.
+
+  // Load the primary with full path and extensions with a specified search path.
+  load_ok = load(base_location + ':' + scratch_dir + '*');
+  ASSERT_TRUE(load_ok) << error_msg;
+  ASSERT_EQ(full_bcp.size(), boot_image_spaces.size());
+
+  // Load the primary with full path and fail to find extensions in BCP path.
+  load_ok = load(base_location + ":*");
+  ASSERT_TRUE(load_ok) << error_msg;
+  ASSERT_EQ(head_dex_files.size(), boot_image_spaces.size());
+
+  // Now copy the libcore dex files to the `scratch_dir` and retry loading the boot image
+  // with BCP in the scratch_dir so that the images can be found based on BCP paths.
+  for (std::string& bcp_component : boot_class_path) {
+    size_t slash_pos = bcp_component.rfind('/');
+    ASSERT_NE(std::string::npos, slash_pos);
+    std::string new_location = scratch_dir + bcp_component.substr(slash_pos + 1u);
+    std::ifstream src_stream(bcp_component, std::ios::binary);
+    std::ofstream dst_stream(new_location, std::ios::binary);
+    dst_stream << src_stream.rdbuf();
+    bcp_component = new_location;
+  }
+
+  // Loading the primary image with just the name now succeeds.
+  load_ok = load(base_name);
+  ASSERT_TRUE(load_ok) << error_msg;
+
+  // Loading the primary image with a search path still fails.
+  load_ok = load("*");
+  ASSERT_FALSE(load_ok);
+  load_ok = load(scratch_dir + "*");
+  ASSERT_FALSE(load_ok);
+
+  // Load the primary and first extension without paths.
+  load_ok = load(base_name + ':' + mid_name);
+  ASSERT_TRUE(load_ok) << error_msg;
+  ASSERT_EQ(mid_bcp.size(), boot_image_spaces.size());
+
+  // Load the primary with full path and the first extension without full path.
+  load_ok = load(base_location + ':' + mid_name);
+  ASSERT_TRUE(load_ok) << error_msg;  // Loaded successfully.
+  ASSERT_EQ(mid_bcp.size(), boot_image_spaces.size());  // Including the extension.
+
+  // Load all the libcore images without paths.
+  load_ok = load(base_name + ':' + mid_name + ':' + tail_name);
+  ASSERT_TRUE(load_ok) << error_msg;
+  ASSERT_EQ(full_bcp.size(), boot_image_spaces.size());
+
+  // Load the primary and first extension with full paths and second extension by name.
+  load_ok = load(base_location + ':' + mid_location + ':' + tail_name);
+  ASSERT_TRUE(load_ok) << error_msg;
+  ASSERT_EQ(full_bcp.size(), boot_image_spaces.size());
+
+  // Load the primary with full path, first extension without path,
+  // and second extension with full path.
+  load_ok = load(base_location + ':' + mid_name + ':' + tail_location);
+  ASSERT_TRUE(load_ok) << error_msg;  // Loaded successfully.
+  ASSERT_EQ(full_bcp.size(), boot_image_spaces.size());  // Including both extensions.
+
+  // Load the primary with full path and find both extensions in BCP path.
+  load_ok = load(base_location + ":*");
+  ASSERT_TRUE(load_ok) << error_msg;
+  ASSERT_EQ(full_bcp.size(), boot_image_spaces.size());
+
+  // Fail to load any images with invalid image locations (named component after search paths).
+  load_ok = load(base_location + ":*:" + tail_location);
+  ASSERT_FALSE(load_ok);
+  load_ok = load(base_location + ':' + scratch_dir + "*:" + tail_location);
+  ASSERT_FALSE(load_ok);
+
+  for (std::string& bcp_component : boot_class_path) {
+    int unlink_result = unlink(bcp_component.c_str());
+    ASSERT_EQ(0, unlink_result) << strerror(errno);
+  }
+
+  ClearDirectory(image_dir.c_str());
+  int rmdir_result = rmdir(image_dir.c_str());
+  ASSERT_EQ(0, rmdir_result);
+}
+
 }  // namespace art
diff --git a/dex2oat/linker/image_test.h b/dex2oat/linker/image_test.h
index 9e5da27..fa08627 100644
--- a/dex2oat/linker/image_test.h
+++ b/dex2oat/linker/image_test.h
@@ -171,8 +171,9 @@
     // Create a generic tmp file, to be the base of the .art and .oat temporary files.
     ScratchFile location;
     std::vector<std::string> image_locations =
-        gc::space::ImageSpace::ExpandMultiImageLocations(out_helper.dex_file_locations,
-                                                         location.GetFilename() + ".art");
+        gc::space::ImageSpace::ExpandMultiImageLocations(
+            ArrayRef<const std::string>(out_helper.dex_file_locations),
+            location.GetFilename() + ".art");
     for (size_t i = 0u; i != class_path.size(); ++i) {
       out_helper.image_locations.push_back(ScratchFile(image_locations[i]));
     }
diff --git a/dex2oat/linker/image_writer.cc b/dex2oat/linker/image_writer.cc
index ede5ef7..2c0447b 100644
--- a/dex2oat/linker/image_writer.cc
+++ b/dex2oat/linker/image_writer.cc
@@ -2693,8 +2693,8 @@
       PointerToLowMemUInt32(image_info.oat_data_begin_),
       PointerToLowMemUInt32(oat_data_end),
       PointerToLowMemUInt32(oat_file_end),
-      boot_image_begin_,
-      boot_image_size_,
+      compiler_options_.IsAppImage() ? boot_image_begin_ : 0u,
+      compiler_options_.IsAppImage() ? boot_image_size_ : 0u,
       static_cast<uint32_t>(target_ptr_size_));
 }
 
diff --git a/runtime/gc/space/image_space.cc b/runtime/gc/space/image_space.cc
index 760acc7..e19f964 100644
--- a/runtime/gc/space/image_space.cc
+++ b/runtime/gc/space/image_space.cc
@@ -65,6 +65,10 @@
 using android::base::StringAppendF;
 using android::base::StringPrintf;
 
+// We do not allow the boot image and extensions to take more than 1GiB. They are
+// supposed to be much smaller and allocating more that this would likely fail anyway.
+static constexpr size_t kMaxTotalImageReservationSize = 1 * GB;
+
 Atomic<uint32_t> ImageSpace::bitmap_index_(0);
 
 ImageSpace::ImageSpace(const std::string& image_filename,
@@ -1371,6 +1375,531 @@
   }
 };
 
+static void AppendImageChecksum(uint32_t component_count,
+                                uint32_t checksum,
+                                /*inout*/std::string* checksums) {
+  static_assert(ImageSpace::kImageChecksumPrefix == 'i', "Format prefix check.");
+  StringAppendF(checksums, "i;%u/%08x", component_count, checksum);
+}
+
+static bool CheckAndRemoveImageChecksum(uint32_t component_count,
+                                        uint32_t checksum,
+                                        /*inout*/std::string_view* oat_checksums,
+                                        /*out*/std::string* error_msg) {
+  std::string image_checksum;
+  AppendImageChecksum(component_count, checksum, &image_checksum);
+  if (!StartsWith(*oat_checksums, image_checksum)) {
+    *error_msg = StringPrintf("Image checksum mismatch, expected %s to start with %s",
+                              std::string(*oat_checksums).c_str(),
+                              image_checksum.c_str());
+    return false;
+  }
+  oat_checksums->remove_prefix(image_checksum.size());
+  return true;
+}
+
+// Helper class to find the primary boot image and boot image extensions
+// and determine the boot image layout.
+class ImageSpace::BootImageLayout {
+ public:
+  // Description of a "chunk" of the boot image, i.e. either primary boot image
+  // or a boot image extension, used in conjunction with the boot class path to
+  // load boot image components.
+  struct ImageChunk {
+    std::string base_location;
+    std::string base_filename;
+    size_t start_index;
+    size_t component_count;
+    size_t reservation_size;
+    uint32_t checksum;
+  };
+
+  BootImageLayout(const std::string& image_location, ArrayRef<const std::string> boot_class_path)
+     : image_location_(image_location),
+       boot_class_path_(boot_class_path) {}
+
+  std::string GetPrimaryImageLocation();
+
+  bool LoadFromSystem(InstructionSet image_isa, /*out*/std::string* error_msg) {
+    return LoadOrValidateFromSystem(image_isa, /*oat_checksums=*/ nullptr, error_msg);
+  }
+
+  bool ValidateFromSystem(InstructionSet image_isa,
+                          /*inout*/std::string_view* oat_checksums,
+                          /*out*/std::string* error_msg) {
+    DCHECK(oat_checksums != nullptr);
+    return LoadOrValidateFromSystem(image_isa, oat_checksums, error_msg);
+  }
+
+  bool LoadFromDalvikCache(const std::string& dalvik_cache, /*out*/std::string* error_msg) {
+    return LoadOrValidateFromDalvikCache(dalvik_cache, /*oat_checksums=*/ nullptr, error_msg);
+  }
+
+  bool ValidateFromDalvikCache(const std::string& dalvik_cache,
+                               /*inout*/std::string_view* oat_checksums,
+                               /*out*/std::string* error_msg) {
+    DCHECK(oat_checksums != nullptr);
+    return LoadOrValidateFromDalvikCache(dalvik_cache, oat_checksums, error_msg);
+  }
+
+  ArrayRef<const ImageChunk> GetChunks() const {
+    return ArrayRef<const ImageChunk>(chunks_);
+  }
+
+  uint32_t GetBaseAddress() const {
+    return base_address_;
+  }
+
+  size_t GetNextBcpIndex() const {
+    return next_bcp_index_;
+  }
+
+  size_t GetTotalComponentCount() const {
+    return total_component_count_;
+  }
+
+  size_t GetTotalReservationSize() const {
+    return total_reservation_size_;
+  }
+
+ private:
+  std::string ExpandLocationImpl(const std::string& location,
+                                 size_t bcp_index,
+                                 bool boot_image_extension) {
+    std::vector<std::string> expanded = ExpandMultiImageLocations(
+        ArrayRef<const std::string>(boot_class_path_).SubArray(bcp_index, 1u),
+        location,
+        boot_image_extension);
+    DCHECK_EQ(expanded.size(), 1u);
+    return expanded[0];
+  }
+
+  std::string ExpandLocation(const std::string& location, size_t bcp_index) {
+    if (bcp_index == 0u) {
+      DCHECK_EQ(location, ExpandLocationImpl(location, bcp_index, /*boot_image_extension=*/ false));
+      return location;
+    } else {
+      return ExpandLocationImpl(location, bcp_index, /*boot_image_extension=*/ true);
+    }
+  }
+
+  bool VerifyImageLocation(const std::vector<std::string>& components,
+                           /*out*/size_t* named_components_count,
+                           /*out*/std::string* error_msg);
+
+  bool MatchNamedComponents(
+      ArrayRef<const std::string> named_components,
+      /*out*/std::vector<std::pair<std::string, size_t>>* base_locations_and_bcp_indexes,
+      /*out*/std::string* error_msg);
+
+  bool ReadHeader(const std::string& base_location,
+                  const std::string& base_filename,
+                  size_t bcp_index,
+                  size_t bcp_component_count,
+                  /*out*/std::string* error_msg);
+
+  bool CheckAndRemoveLastChunkChecksum(/*inout*/std::string_view* oat_checksums,
+                                       /*out*/std::string* error_msg);
+
+  template <typename FilenameFn>
+  bool LoadOrValidate(FilenameFn&& filename_fn,
+                      /*inout*/std::string_view* oat_checksums,
+                      /*out*/std::string* error_msg);
+
+  bool LoadOrValidateFromSystem(InstructionSet image_isa,
+                                /*inout*/std::string_view* oat_checksums,
+                                /*out*/std::string* error_msg);
+
+  bool LoadOrValidateFromDalvikCache(const std::string& dalvik_cache,
+                                     /*inout*/std::string_view* oat_checksums,
+                                     /*out*/std::string* error_msg);
+
+  const std::string& image_location_;
+  ArrayRef<const std::string> boot_class_path_;
+
+  std::vector<ImageChunk> chunks_;
+  uint32_t base_address_ = 0u;
+  size_t next_bcp_index_ = 0u;
+  size_t total_component_count_ = 0u;
+  size_t total_reservation_size_ = 0u;
+};
+
+std::string ImageSpace::BootImageLayout::GetPrimaryImageLocation() {
+  size_t location_start = 0u;
+  size_t location_end = image_location_.find(':');
+  while (location_end == location_start) {
+    ++location_start;
+    location_end = image_location_.find(location_start, ':');
+  }
+  std::string location = (location_end == std::string::npos)
+      ? image_location_.substr(location_start)
+      : image_location_.substr(location_start, location_end - location_start);
+  if (image_location_.find('/') == std::string::npos) {
+    // No path, so use the path from the first boot class path component.
+    size_t slash_pos = boot_class_path_.empty()
+        ? std::string::npos
+        : boot_class_path_[0].rfind('/');
+    if (slash_pos == std::string::npos) {
+      return std::string();
+    }
+    location.insert(0u, boot_class_path_[0].substr(0u, slash_pos + 1u));
+  }
+  return location;
+}
+
+bool ImageSpace::BootImageLayout::VerifyImageLocation(
+    const std::vector<std::string>& components,
+    /*out*/size_t* named_components_count,
+    /*out*/std::string* error_msg) {
+  DCHECK(named_components_count != nullptr);
+
+  // Validate boot class path. Require a path and non-empty name in each component.
+  for (const std::string& bcp_component : boot_class_path_) {
+    size_t bcp_slash_pos = bcp_component.rfind('/');
+    if (bcp_slash_pos == std::string::npos || bcp_slash_pos == bcp_component.size() - 1u) {
+      *error_msg = StringPrintf("Invalid boot class path component: %s", bcp_component.c_str());
+      return false;
+    }
+  }
+
+  // Validate the format of image location components.
+  size_t components_size = components.size();
+  if (components_size == 0u) {
+    *error_msg = "Empty image location.";
+    return false;
+  }
+  size_t wildcards_start = components_size;  // No wildcards.
+  for (size_t i = 0; i != components_size; ++i) {
+    const std::string& component = components[i];
+    DCHECK(!component.empty());  // Guaranteed by Split().
+    size_t wildcard_pos = component.find('*');
+    if (wildcard_pos == std::string::npos) {
+      if (wildcards_start != components.size()) {
+        *error_msg =
+            StringPrintf("Image component without wildcard after component with wildcard: %s",
+                         component.c_str());
+        return false;
+      }
+      if (component.back() == '/') {
+        *error_msg = StringPrintf("Image component ends with path separator: %s",
+                                  component.c_str());
+        return false;
+      }
+    } else {
+      if (wildcards_start == components_size) {
+        wildcards_start = i;
+      }
+      // Wildcard must be the last character.
+      if (wildcard_pos != component.size() - 1u) {
+        *error_msg = StringPrintf("Unsupported wildcard (*) position in %s", component.c_str());
+        return false;
+      }
+      // And it must be either plain wildcard or preceded by a path separator.
+      if (component.size() != 1u && component[wildcard_pos - 1u] != '/') {
+        *error_msg = StringPrintf("Non-plain wildcard (*) not preceded by path separator '/': %s",
+                                  component.c_str());
+        return false;
+      }
+      if (i == 0) {
+        *error_msg = StringPrintf("Primary component contains wildcard (*): %s", component.c_str());
+        return false;
+      }
+    }
+  }
+
+  *named_components_count = wildcards_start;
+  return true;
+}
+
+bool ImageSpace::BootImageLayout::MatchNamedComponents(
+    ArrayRef<const std::string> named_components,
+    /*out*/std::vector<std::pair<std::string, size_t>>* base_locations_and_bcp_indexes,
+    /*out*/std::string* error_msg) {
+  DCHECK(!named_components.empty());
+  DCHECK(base_locations_and_bcp_indexes->empty());
+  base_locations_and_bcp_indexes->reserve(named_components.size());
+  size_t bcp_component_count = boot_class_path_.size();
+  size_t bcp_pos = 0;
+  std::string base_name;
+  for (size_t i = 0, size = named_components.size(); i != size; ++i) {
+    const std::string& component = named_components[i];
+    size_t slash_pos = component.rfind('/');
+    std::string base_location;
+    if (i == 0u) {
+      // The primary boot image name is taken as provided. It forms the base
+      // for expanding the extension filenames.
+      if (slash_pos != std::string::npos) {
+        base_name = component.substr(slash_pos + 1u);
+        base_location = component;
+      } else {
+        base_name = component;
+        size_t bcp_slash_pos = boot_class_path_[0u].rfind('/');
+        DCHECK_NE(bcp_slash_pos, std::string::npos);
+        base_location = boot_class_path_[0u].substr(0u, bcp_slash_pos + 1u) + component;
+      }
+    } else {
+      std::string to_match;
+      if (slash_pos != std::string::npos) {
+        // If we have the full path, we just need to match the filename to the BCP component.
+        base_location = component.substr(0u, slash_pos + 1u) + base_name;
+        to_match = component;
+      }
+      while (true) {
+        if (slash_pos == std::string::npos) {
+          // If we do not have a full path, we need to update the path based on the BCP location.
+          size_t bcp_slash_pos = boot_class_path_[bcp_pos].rfind('/');
+          DCHECK_NE(bcp_slash_pos, std::string::npos);
+          std::string path = boot_class_path_[bcp_pos].substr(0u, bcp_slash_pos + 1u);
+          to_match = path + component;
+          base_location = path + base_name;
+        }
+        if (ExpandLocation(base_location, bcp_pos) == to_match) {
+          break;
+        }
+        ++bcp_pos;
+        if (bcp_pos == bcp_component_count) {
+          *error_msg = StringPrintf("Image component %s does not match a boot class path component",
+                                    component.c_str());
+          return false;
+        }
+      }
+    }
+    base_locations_and_bcp_indexes->emplace_back(base_location, bcp_pos);
+    ++bcp_pos;
+  }
+  return true;
+}
+
+bool ImageSpace::BootImageLayout::ReadHeader(const std::string& base_location,
+                                             const std::string& base_filename,
+                                             size_t bcp_index,
+                                             size_t bcp_component_count,
+                                             /*out*/std::string* error_msg) {
+  DCHECK_LE(next_bcp_index_, bcp_index);
+  DCHECK_LT(bcp_index, bcp_component_count);
+  size_t allowed_component_count = bcp_component_count - bcp_index;
+  DCHECK_LE(total_reservation_size_, kMaxTotalImageReservationSize);
+  size_t allowed_reservation_size = kMaxTotalImageReservationSize - total_reservation_size_;
+
+  std::string actual_filename = ExpandLocation(base_filename, bcp_index);
+  ImageHeader header;
+  if (!ReadSpecificImageHeader(actual_filename.c_str(), &header, error_msg)) {
+    return false;
+  }
+  if (header.GetComponentCount() == 0u ||
+      header.GetComponentCount() > allowed_component_count) {
+    *error_msg = StringPrintf("Unexpected component count in %s, received %u, "
+                                  "expected non-zero and <= %zu",
+                              actual_filename.c_str(),
+                              header.GetComponentCount(),
+                              allowed_component_count);
+    return false;
+  }
+  if (header.GetImageReservationSize() > allowed_reservation_size) {
+    *error_msg = StringPrintf("Reservation size too big in %s: %u > %zu",
+                              actual_filename.c_str(),
+                              header.GetImageReservationSize(),
+                              allowed_reservation_size);
+    return false;
+  }
+
+  if (chunks_.empty()) {
+    base_address_ = reinterpret_cast32<uint32_t>(header.GetImageBegin());
+  }
+  ImageChunk chunk;
+  chunk.base_location = base_location;
+  chunk.base_filename = base_filename;
+  chunk.start_index = bcp_index;
+  chunk.component_count = header.GetComponentCount();
+  chunk.reservation_size = header.GetImageReservationSize();
+  chunk.checksum = header.GetImageChecksum();
+  chunks_.push_back(std::move(chunk));
+  next_bcp_index_ = bcp_index + header.GetComponentCount();
+  total_component_count_ += header.GetComponentCount();
+  total_reservation_size_ += header.GetImageReservationSize();
+  return true;
+}
+
+bool ImageSpace::BootImageLayout::CheckAndRemoveLastChunkChecksum(
+    /*inout*/std::string_view* oat_checksums,
+    /*out*/std::string* error_msg) {
+  DCHECK(oat_checksums != nullptr);
+  DCHECK(!chunks_.empty());
+  const ImageChunk& chunk = chunks_.back();
+  size_t component_count = chunk.component_count;
+  size_t checksum = chunk.checksum;
+  if (!CheckAndRemoveImageChecksum(component_count, checksum, oat_checksums, error_msg)) {
+    DCHECK(!error_msg->empty());
+    return false;
+  }
+  if (oat_checksums->empty()) {
+    if (next_bcp_index_ != boot_class_path_.size()) {
+      *error_msg = StringPrintf("Checksum too short, missing %zu components.",
+                                boot_class_path_.size() - next_bcp_index_);
+      return false;
+    }
+    return true;
+  }
+  if (!StartsWith(*oat_checksums, ":")) {
+    *error_msg = StringPrintf("Missing ':' separator at start of %s",
+                              std::string(*oat_checksums).c_str());
+    return false;
+  }
+  oat_checksums->remove_prefix(1u);
+  if (oat_checksums->empty()) {
+    *error_msg = "Missing checksums after the ':' separator.";
+    return false;
+  }
+  return true;
+}
+
+template <typename FilenameFn>
+bool ImageSpace::BootImageLayout::LoadOrValidate(FilenameFn&& filename_fn,
+                                                 /*inout*/std::string_view* oat_checksums,
+                                                 /*out*/std::string* error_msg) {
+  DCHECK(GetChunks().empty());
+  DCHECK_EQ(GetBaseAddress(), 0u);
+  bool validate = (oat_checksums != nullptr);
+  static_assert(ImageSpace::kImageChecksumPrefix == 'i', "Format prefix check.");
+  DCHECK(!validate || StartsWith(*oat_checksums, "i"));
+
+  std::vector<std::string> components;
+  Split(image_location_, ':', &components);
+  size_t named_components_count = 0u;
+  if (!VerifyImageLocation(components, &named_components_count, error_msg)) {
+    return false;
+  }
+
+  ArrayRef<const std::string> named_components =
+      ArrayRef<const std::string>(components).SubArray(/*pos=*/ 0u, named_components_count);
+
+  std::vector<std::pair<std::string, size_t>> base_locations_and_bcp_indexes;
+  if (!MatchNamedComponents(named_components, &base_locations_and_bcp_indexes, error_msg)) {
+    return false;
+  }
+
+  // Load the image headers of named components.
+  DCHECK_EQ(base_locations_and_bcp_indexes.size(), named_components.size());
+  const size_t bcp_component_count = boot_class_path_.size();
+  size_t bcp_pos = 0u;
+  for (size_t i = 0, size = named_components.size(); i != size; ++i) {
+    const std::string& base_location = base_locations_and_bcp_indexes[i].first;
+    size_t bcp_index = base_locations_and_bcp_indexes[i].second;
+    if (bcp_index < bcp_pos) {
+      DCHECK_NE(i, 0u);
+      LOG(ERROR) << "Named image component already covered by previous image: " << base_location;
+      continue;
+    }
+    if (validate && bcp_index > bcp_pos) {
+      *error_msg = StringPrintf("End of contiguous boot class path images, remaining checksum: %s",
+                                std::string(*oat_checksums).c_str());
+      return false;
+    }
+    std::string local_error_msg;
+    std::string* err_msg = (i == 0 || validate) ? error_msg : &local_error_msg;
+    std::string base_filename;
+    if (!filename_fn(base_location, &base_filename, err_msg) ||
+        !ReadHeader(base_location, base_filename, bcp_index, bcp_component_count, err_msg)) {
+      if (i == 0u || validate) {
+        return false;
+      }
+      VLOG(image) << "Error reading named image component header for " << base_location
+                  << ", error: " << local_error_msg;
+      bcp_pos = bcp_index + 1u;  // Skip at least this component.
+      DCHECK_GT(bcp_pos, GetNextBcpIndex());
+      continue;
+    }
+    if (validate) {
+      if (!CheckAndRemoveLastChunkChecksum(oat_checksums, error_msg)) {
+        return false;
+      }
+      if (oat_checksums->empty() || !StartsWith(*oat_checksums, "i")) {
+        return true;  // Let the caller deal with the dex file checksums if any.
+      }
+    }
+    bcp_pos = GetNextBcpIndex();
+  }
+
+  // Look for remaining components if there are any wildcard specifications.
+  ArrayRef<const std::string> search_paths =
+      ArrayRef<const std::string>(components).SubArray(/*pos=*/ named_components_count);
+  if (!search_paths.empty()) {
+    const std::string& primary_base_location = base_locations_and_bcp_indexes[0].first;
+    size_t base_slash_pos = primary_base_location.rfind('/');
+    DCHECK_NE(base_slash_pos, std::string::npos);
+    std::string base_name = primary_base_location.substr(base_slash_pos + 1u);
+    DCHECK(!base_name.empty());
+    while (bcp_pos != bcp_component_count) {
+      const std::string& bcp_component =  boot_class_path_[bcp_pos];
+      bool found = false;
+      for (const std::string& path : search_paths) {
+        std::string base_location;
+        if (path.size() == 1u) {
+          DCHECK_EQ(path, "*");
+          size_t slash_pos = bcp_component.rfind('/');
+          DCHECK_NE(slash_pos, std::string::npos);
+          base_location = bcp_component.substr(0u, slash_pos + 1u) + base_name;
+        } else {
+          DCHECK(EndsWith(path, "/*"));
+          base_location = path.substr(0u, path.size() - 1u) + base_name;
+        }
+        std::string err_msg;  // Ignored.
+        std::string base_filename;
+        if (filename_fn(base_location, &base_filename, &err_msg) &&
+            ReadHeader(base_location, base_filename, bcp_pos, bcp_component_count, &err_msg)) {
+          VLOG(image) << "Found image extension for " << ExpandLocation(base_location, bcp_pos);
+          bcp_pos = GetNextBcpIndex();
+          found = true;
+          if (validate) {
+            if (!CheckAndRemoveLastChunkChecksum(oat_checksums, error_msg)) {
+              return false;
+            }
+            if (oat_checksums->empty() || !StartsWith(*oat_checksums, "i")) {
+              return true;  // Let the caller deal with the dex file checksums if any.
+            }
+          }
+          break;
+        }
+      }
+      if (!found) {
+        if (validate) {
+          *error_msg = StringPrintf("Missing extension for %s, remaining checksum: %s",
+                                    bcp_component.c_str(),
+                                    std::string(*oat_checksums).c_str());
+          return false;
+        }
+        ++bcp_pos;
+      }
+    }
+  }
+
+  return true;
+}
+
+bool ImageSpace::BootImageLayout::LoadOrValidateFromSystem(InstructionSet image_isa,
+                                                           /*inout*/std::string_view* oat_checksums,
+                                                           /*out*/std::string* error_msg) {
+  auto filename_fn = [image_isa](const std::string& location,
+                                 /*out*/std::string* filename,
+                                 /*out*/std::string* err_msg ATTRIBUTE_UNUSED) {
+    *filename = GetSystemImageFilename(location.c_str(), image_isa);
+    return true;
+  };
+  return LoadOrValidate(filename_fn, oat_checksums, error_msg);
+}
+
+bool ImageSpace::BootImageLayout::LoadOrValidateFromDalvikCache(
+    const std::string& dalvik_cache,
+    /*inout*/std::string_view* oat_checksums,
+    /*out*/std::string* error_msg) {
+  auto filename_fn = [&dalvik_cache](const std::string& location,
+                                     /*out*/std::string* filename,
+                                     /*out*/std::string* err_msg) {
+    return GetDalvikCacheFilename(location.c_str(), dalvik_cache.c_str(), filename, err_msg);
+  };
+  return LoadOrValidate(filename_fn, oat_checksums, error_msg);
+}
+
 class ImageSpace::BootImageLoader {
  public:
   BootImageLoader(const std::vector<std::string>& boot_class_path,
@@ -1398,8 +1927,10 @@
   bool IsZygote() const { return is_zygote_; }
 
   void FindImageFiles() {
+    BootImageLayout layout(image_location_, boot_class_path_);
+    std::string image_location = layout.GetPrimaryImageLocation();
     std::string system_filename;
-    bool found_image = FindImageFilenameImpl(image_location_.c_str(),
+    bool found_image = FindImageFilenameImpl(image_location.c_str(),
                                              image_isa_,
                                              &has_system_,
                                              &system_filename,
@@ -1428,124 +1959,115 @@
 
   bool LoadFromSystem(bool validate_oat_file,
                       size_t extra_reservation_size,
-                      /*out*/std::vector<std::unique_ptr<space::ImageSpace>>* boot_image_spaces,
+                      /*out*/std::vector<std::unique_ptr<ImageSpace>>* boot_image_spaces,
                       /*out*/MemMap* extra_reservation,
-                      /*out*/std::string* error_msg) REQUIRES_SHARED(Locks::mutator_lock_) {
-    TimingLogger logger(__PRETTY_FUNCTION__, /*precise=*/ true, VLOG_IS_ON(image));
-    std::string filename = GetSystemImageFilename(image_location_.c_str(), image_isa_);
-
-    if (!LoadFromFile(filename,
-                      validate_oat_file,
-                      extra_reservation_size,
-                      &logger,
-                      boot_image_spaces,
-                      extra_reservation,
-                      error_msg)) {
-      return false;
-    }
-
-    if (VLOG_IS_ON(image)) {
-      LOG(INFO) << "ImageSpace::BootImageLoader::LoadFromSystem exiting "
-          << boot_image_spaces->front();
-      logger.Dump(LOG_STREAM(INFO));
-    }
-    return true;
-  }
+                      /*out*/std::string* error_msg) REQUIRES_SHARED(Locks::mutator_lock_);
 
   bool LoadFromDalvikCache(
       bool validate_oat_file,
       size_t extra_reservation_size,
-      /*out*/std::vector<std::unique_ptr<space::ImageSpace>>* boot_image_spaces,
+      /*out*/std::vector<std::unique_ptr<ImageSpace>>* boot_image_spaces,
       /*out*/MemMap* extra_reservation,
-      /*out*/std::string* error_msg) REQUIRES_SHARED(Locks::mutator_lock_) {
-    TimingLogger logger(__PRETTY_FUNCTION__, /*precise=*/ true, VLOG_IS_ON(image));
-    DCHECK(DalvikCacheExists());
-
-    if (!LoadFromFile(cache_filename_,
-                      validate_oat_file,
-                      extra_reservation_size,
-                      &logger,
-                      boot_image_spaces,
-                      extra_reservation,
-                      error_msg)) {
-      return false;
-    }
-
-    if (VLOG_IS_ON(image)) {
-      LOG(INFO) << "ImageSpace::BootImageLoader::LoadFromDalvikCache exiting "
-          << boot_image_spaces->front();
-      logger.Dump(LOG_STREAM(INFO));
-    }
-    return true;
-  }
+      /*out*/std::string* error_msg) REQUIRES_SHARED(Locks::mutator_lock_);
 
  private:
-  bool LoadFromFile(
-      const std::string& filename,
+  bool LoadImage(
+      const BootImageLayout& layout,
       bool validate_oat_file,
       size_t extra_reservation_size,
       TimingLogger* logger,
-      /*out*/std::vector<std::unique_ptr<space::ImageSpace>>* boot_image_spaces,
+      /*out*/std::vector<std::unique_ptr<ImageSpace>>* boot_image_spaces,
       /*out*/MemMap* extra_reservation,
       /*out*/std::string* error_msg) REQUIRES_SHARED(Locks::mutator_lock_) {
-    ImageHeader system_hdr;
-    if (!ReadSpecificImageHeader(filename.c_str(), &system_hdr, error_msg)) {
-      return false;
-    }
-    if (system_hdr.GetComponentCount() == 0u ||
-        system_hdr.GetComponentCount() > boot_class_path_.size()) {
-      *error_msg = StringPrintf("Unexpected component count in %s, received %u, "
-                                    "expected non-zero and <= %zu",
-                                filename.c_str(),
-                                system_hdr.GetComponentCount(),
-                                boot_class_path_.size());
-      return false;
-    }
-    MemMap image_reservation;
-    MemMap local_extra_reservation;
-    if (!ReserveBootImageMemory(system_hdr.GetImageReservationSize(),
-                                reinterpret_cast32<uint32_t>(system_hdr.GetImageBegin()),
-                                extra_reservation_size,
-                                &image_reservation,
-                                &local_extra_reservation,
-                                error_msg)) {
+    ArrayRef<const BootImageLayout::ImageChunk> chunks = layout.GetChunks();
+    DCHECK(!chunks.empty());
+    const uint32_t base_address = layout.GetBaseAddress();
+    const size_t image_component_count = layout.GetTotalComponentCount();
+    const size_t image_reservation_size = layout.GetTotalReservationSize();
+
+    DCHECK_LE(image_reservation_size, kMaxTotalImageReservationSize);
+    static_assert(kMaxTotalImageReservationSize < std::numeric_limits<uint32_t>::max());
+    if (extra_reservation_size > std::numeric_limits<uint32_t>::max() - image_reservation_size) {
+      // Since the `image_reservation_size` is limited to kMaxTotalImageReservationSize,
+      // the `extra_reservation_size` would have to be really excessive to fail this check.
+      *error_msg = StringPrintf("Excessive extra reservation size: %zu", extra_reservation_size);
       return false;
     }
 
-    ArrayRef<const std::string> provided_locations(boot_class_path_locations_.data(),
-                                                   system_hdr.GetComponentCount());
-    std::vector<std::string> locations =
-        ExpandMultiImageLocations(provided_locations, image_location_);
-    std::vector<std::string> filenames =
-        ExpandMultiImageLocations(provided_locations, filename);
-    DCHECK_EQ(locations.size(), filenames.size());
+    // Reserve address space. If relocating, choose a random address for ALSR.
+    uint8_t* addr = reinterpret_cast<uint8_t*>(
+        relocate_ ? ART_BASE_ADDRESS + ChooseRelocationOffsetDelta() : base_address);
+    MemMap image_reservation =
+        ReserveBootImageMemory(addr, image_reservation_size + extra_reservation_size, error_msg);
+    if (!image_reservation.IsValid()) {
+      return false;
+    }
+
+    // Load components.
     std::vector<std::unique_ptr<ImageSpace>> spaces;
-    spaces.reserve(locations.size());
-    for (std::size_t i = 0u, size = locations.size(); i != size; ++i) {
-      spaces.push_back(Load(locations[i], filenames[i], logger, &image_reservation, error_msg));
-      const ImageSpace* space = spaces.back().get();
-      if (space == nullptr) {
-        return false;
+    spaces.reserve(image_component_count);
+    size_t max_image_space_dependencies = 0u;
+    for (size_t i = 0, num_chunks = chunks.size(); i != num_chunks; ++i) {
+      const BootImageLayout::ImageChunk& chunk = chunks[i];
+      std::string extension_error_msg;
+      uint8_t* old_reservation_begin = image_reservation.Begin();
+      size_t old_reservation_size = image_reservation.Size();
+      DCHECK_LE(chunk.reservation_size, old_reservation_size);
+      if (!LoadComponents(chunk,
+                          validate_oat_file,
+                          max_image_space_dependencies,
+                          logger,
+                          &spaces,
+                          &image_reservation,
+                          (i == 0) ? error_msg : &extension_error_msg)) {
+        // Failed to load the chunk. If this is the primary boot image, report the error.
+        if (i == 0) {
+          return false;
+        }
+        // For extension, shrink the reservation (and remap if needed, see below).
+        size_t new_reservation_size = old_reservation_size - chunk.reservation_size;
+        if (new_reservation_size == 0u) {
+          DCHECK_EQ(extra_reservation_size, 0u);
+          DCHECK_EQ(i + 1u, num_chunks);
+          image_reservation.Reset();
+        } else if (old_reservation_begin != image_reservation.Begin()) {
+          // Part of the image reservation has been used and then unmapped when
+          // rollling back the partial boot image extension load. Try to remap
+          // the image reservation. As this should be running single-threaded,
+          // the address range should still be available to mmap().
+          image_reservation.Reset();
+          std::string remap_error_msg;
+          image_reservation = ReserveBootImageMemory(old_reservation_begin,
+                                                     new_reservation_size,
+                                                     &remap_error_msg);
+          if (!image_reservation.IsValid()) {
+            *error_msg = StringPrintf("Failed to remap boot image reservation after failing "
+                                          "to load boot image extension (%s: %s): %s",
+                                      boot_class_path_locations_[chunk.start_index].c_str(),
+                                      extension_error_msg.c_str(),
+                                      remap_error_msg.c_str());
+            return false;
+          }
+        } else {
+          DCHECK_EQ(old_reservation_size, image_reservation.Size());
+          image_reservation.SetSize(new_reservation_size);
+        }
+        LOG(ERROR) << "Failed to load boot image extension "
+            << boot_class_path_locations_[chunk.start_index] << ": " << extension_error_msg;
       }
-      uint32_t expected_component_count = (i == 0u) ? system_hdr.GetComponentCount() : 0u;
-      uint32_t expected_reservation_size = (i == 0u) ? system_hdr.GetImageReservationSize() : 0u;
-      if (!Loader::CheckImageReservationSize(*space, expected_reservation_size, error_msg) ||
-          !Loader::CheckImageComponentCount(*space, expected_component_count, error_msg)) {
-        return false;
+      // Update `max_image_space_dependencies` if all previous BCP components
+      // were covered and loading the current chunk succeeded.
+      if (max_image_space_dependencies == chunk.start_index &&
+          spaces.size() == chunk.start_index + chunk.component_count) {
+        max_image_space_dependencies = chunk.start_index + chunk.component_count;
       }
     }
-    for (size_t i = 0u, size = spaces.size(); i != size; ++i) {
-      if (!OpenOatFile(spaces[i].get(),
-                       boot_class_path_[i],
-                       validate_oat_file,
-                       /*available_dependencies=*/ ArrayRef<const std::unique_ptr<ImageSpace>>(),
-                       logger,
-                       &image_reservation,
-                       error_msg)) {
-        return false;
-      }
-    }
-    if (!CheckReservationExhausted(image_reservation, error_msg)) {
+
+    MemMap local_extra_reservation;
+    if (!RemapExtraReservation(extra_reservation_size,
+                               &image_reservation,
+                               &local_extra_reservation,
+                               error_msg)) {
       return false;
     }
 
@@ -2033,37 +2555,117 @@
     return true;
   }
 
-  bool ReserveBootImageMemory(uint32_t reservation_size,
-                              uint32_t image_start,
-                              size_t extra_reservation_size,
-                              /*out*/MemMap* image_reservation,
-                              /*out*/MemMap* extra_reservation,
-                              /*out*/std::string* error_msg) {
+  bool LoadComponents(const BootImageLayout::ImageChunk& chunk,
+                      bool validate_oat_file,
+                      size_t max_image_space_dependencies,
+                      TimingLogger* logger,
+                      /*inout*/std::vector<std::unique_ptr<ImageSpace>>* spaces,
+                      /*inout*/MemMap* image_reservation,
+                      /*out*/std::string* error_msg)
+      REQUIRES_SHARED(Locks::mutator_lock_) {
+    // Make sure we destroy the spaces we created if we're returning an error.
+    // Note that this can unmap part of the original `image_reservation`.
+    class Guard {
+     public:
+      explicit Guard(std::vector<std::unique_ptr<ImageSpace>>* spaces_in)
+          : spaces_(spaces_in), committed_(spaces_->size()) {}
+      void Commit() {
+        DCHECK_LT(committed_, spaces_->size());
+        committed_ = spaces_->size();
+      }
+      ~Guard() {
+        DCHECK_LE(committed_, spaces_->size());
+        spaces_->resize(committed_);
+      }
+     private:
+      std::vector<std::unique_ptr<ImageSpace>>* const spaces_;
+      size_t committed_;
+    };
+    Guard guard(spaces);
+
+    bool is_extension = (chunk.start_index != 0u);
+    DCHECK_NE(spaces->empty(), is_extension);
+    ArrayRef<const std::string> requested_bcp_locations =
+        ArrayRef<const std::string>(boot_class_path_locations_).SubArray(
+            chunk.start_index, chunk.component_count);
+    std::vector<std::string> locations =
+        ExpandMultiImageLocations(requested_bcp_locations, chunk.base_location, is_extension);
+    std::vector<std::string> filenames =
+        ExpandMultiImageLocations(requested_bcp_locations, chunk.base_filename, is_extension);
+    DCHECK_EQ(locations.size(), filenames.size());
+    for (std::size_t i = 0u, size = locations.size(); i != size; ++i) {
+      spaces->push_back(Load(locations[i], filenames[i], logger, image_reservation, error_msg));
+      const ImageSpace* space = spaces->back().get();
+      if (space == nullptr) {
+        return false;
+      }
+      uint32_t expected_component_count = (i == 0u) ? chunk.component_count : 0u;
+      uint32_t expected_reservation_size = (i == 0u) ? chunk.reservation_size : 0u;
+      if (!Loader::CheckImageReservationSize(*space, expected_reservation_size, error_msg) ||
+          !Loader::CheckImageComponentCount(*space, expected_component_count, error_msg)) {
+        return false;
+      }
+      if (i == 0 && chunk.checksum != space->GetImageHeader().GetImageChecksum()) {
+        *error_msg = StringPrintf("Image checksum modified since previously read from %s, "
+                                      "received %u, expected %u",
+                                  space->GetImageFilename().c_str(),
+                                  space->GetImageHeader().GetImageChecksum(),
+                                  chunk.checksum);
+        return false;
+      }
+    }
+    ArrayRef<const std::unique_ptr<ImageSpace>> available_dependencies =
+        ArrayRef<const std::unique_ptr<ImageSpace>>(*spaces).SubArray(/*pos=*/ 0u,
+                                                                      max_image_space_dependencies);
+    for (std::size_t i = 0u, size = locations.size(); i != size; ++i) {
+      ImageSpace* space = (*spaces)[spaces->size() - chunk.component_count + i].get();
+      if (!OpenOatFile(space,
+                       boot_class_path_[chunk.start_index + i],
+                       validate_oat_file,
+                       available_dependencies,
+                       logger,
+                       image_reservation,
+                       error_msg)) {
+        return false;
+      }
+    }
+
+    guard.Commit();
+    return true;
+  }
+
+  MemMap ReserveBootImageMemory(uint8_t* addr,
+                                uint32_t reservation_size,
+                                /*out*/std::string* error_msg) {
     DCHECK_ALIGNED(reservation_size, kPageSize);
-    DCHECK_ALIGNED(image_start, kPageSize);
-    DCHECK(!image_reservation->IsValid());
-    DCHECK_LT(extra_reservation_size, std::numeric_limits<uint32_t>::max() - reservation_size);
-    size_t total_size = reservation_size + extra_reservation_size;
-    // If relocating, choose a random address for ALSR.
-    uint32_t addr = relocate_ ? ART_BASE_ADDRESS + ChooseRelocationOffsetDelta() : image_start;
-    *image_reservation =
-        MemMap::MapAnonymous("Boot image reservation",
-                             reinterpret_cast32<uint8_t*>(addr),
-                             total_size,
-                             PROT_NONE,
-                             /*low_4gb=*/ true,
-                             /*reuse=*/ false,
-                             /*reservation=*/ nullptr,
-                             error_msg);
-    if (!image_reservation->IsValid()) {
+    DCHECK_ALIGNED(addr, kPageSize);
+    return MemMap::MapAnonymous("Boot image reservation",
+                                addr,
+                                reservation_size,
+                                PROT_NONE,
+                                /*low_4gb=*/ true,
+                                /*reuse=*/ false,
+                                /*reservation=*/ nullptr,
+                                error_msg);
+  }
+
+  bool RemapExtraReservation(size_t extra_reservation_size,
+                             /*inout*/MemMap* image_reservation,
+                             /*out*/MemMap* extra_reservation,
+                             /*out*/std::string* error_msg) {
+    DCHECK_ALIGNED(extra_reservation_size, kPageSize);
+    DCHECK(!extra_reservation->IsValid());
+    size_t expected_size = image_reservation->IsValid() ? image_reservation->Size() : 0u;
+    if (extra_reservation_size != expected_size) {
+      *error_msg = StringPrintf("Image reservation mismatch after loading boot image: %zu != %zu",
+                                extra_reservation_size,
+                                expected_size);
       return false;
     }
-    DCHECK(!extra_reservation->IsValid());
     if (extra_reservation_size != 0u) {
-      DCHECK_ALIGNED(extra_reservation_size, kPageSize);
-      DCHECK_LT(extra_reservation_size, image_reservation->Size());
-      uint8_t* split = image_reservation->End() - extra_reservation_size;
-      *extra_reservation = image_reservation->RemapAtEnd(split,
+      DCHECK(image_reservation->IsValid());
+      DCHECK_EQ(extra_reservation_size, image_reservation->Size());
+      *extra_reservation = image_reservation->RemapAtEnd(image_reservation->Begin(),
                                                          "Boot image extra reservation",
                                                          PROT_NONE,
                                                          error_msg);
@@ -2071,27 +2673,17 @@
         return false;
       }
     }
-
+    DCHECK(!image_reservation->IsValid());
     return true;
   }
 
-  bool CheckReservationExhausted(const MemMap& image_reservation, /*out*/std::string* error_msg) {
-    if (image_reservation.IsValid()) {
-      *error_msg = StringPrintf("Excessive image reservation after loading boot image: %p-%p",
-                                image_reservation.Begin(),
-                                image_reservation.End());
-      return false;
-    }
-    return true;
-  }
-
-  const std::vector<std::string>& boot_class_path_;
-  const std::vector<std::string>& boot_class_path_locations_;
-  const std::string& image_location_;
-  InstructionSet image_isa_;
-  bool relocate_;
-  bool executable_;
-  bool is_zygote_;
+  const ArrayRef<const std::string> boot_class_path_;
+  const ArrayRef<const std::string> boot_class_path_locations_;
+  const std::string image_location_;
+  const InstructionSet image_isa_;
+  const bool relocate_;
+  const bool executable_;
+  const bool is_zygote_;
   bool has_system_;
   bool has_cache_;
   bool is_global_cache_;
@@ -2100,6 +2692,68 @@
   std::string cache_filename_;
 };
 
+bool ImageSpace::BootImageLoader::LoadFromSystem(
+    bool validate_oat_file,
+    size_t extra_reservation_size,
+    /*out*/std::vector<std::unique_ptr<ImageSpace>>* boot_image_spaces,
+    /*out*/MemMap* extra_reservation,
+    /*out*/std::string* error_msg) {
+  TimingLogger logger(__PRETTY_FUNCTION__, /*precise=*/ true, VLOG_IS_ON(image));
+
+  BootImageLayout layout(image_location_, boot_class_path_);
+  if (!layout.LoadFromSystem(image_isa_, error_msg)) {
+    return false;
+  }
+
+  if (!LoadImage(layout,
+                 validate_oat_file,
+                 extra_reservation_size,
+                 &logger,
+                 boot_image_spaces,
+                 extra_reservation,
+                 error_msg)) {
+    return false;
+  }
+
+  if (VLOG_IS_ON(image)) {
+    LOG(INFO) << "ImageSpace::BootImageLoader::LoadFromSystem exiting "
+        << boot_image_spaces->front();
+    logger.Dump(LOG_STREAM(INFO));
+  }
+  return true;
+}
+
+bool ImageSpace::BootImageLoader::LoadFromDalvikCache(
+    bool validate_oat_file,
+    size_t extra_reservation_size,
+    /*out*/std::vector<std::unique_ptr<ImageSpace>>* boot_image_spaces,
+    /*out*/MemMap* extra_reservation,
+    /*out*/std::string* error_msg) {
+  TimingLogger logger(__PRETTY_FUNCTION__, /*precise=*/ true, VLOG_IS_ON(image));
+  DCHECK(DalvikCacheExists());
+
+  BootImageLayout layout(image_location_, boot_class_path_);
+  if (!layout.LoadFromDalvikCache(dalvik_cache_, error_msg)) {
+    return false;
+  }
+  if (!LoadImage(layout,
+                 validate_oat_file,
+                 extra_reservation_size,
+                 &logger,
+                 boot_image_spaces,
+                 extra_reservation,
+                 error_msg)) {
+    return false;
+  }
+
+  if (VLOG_IS_ON(image)) {
+    LOG(INFO) << "ImageSpace::BootImageLoader::LoadFromDalvikCache exiting "
+        << boot_image_spaces->front();
+    logger.Dump(LOG_STREAM(INFO));
+  }
+  return true;
+}
+
 static constexpr uint64_t kLowSpaceValue = 50 * MB;
 static constexpr uint64_t kTmpFsSentinelValue = 384 * MB;
 
@@ -2145,7 +2799,7 @@
     bool executable,
     bool is_zygote,
     size_t extra_reservation_size,
-    /*out*/std::vector<std::unique_ptr<space::ImageSpace>>* boot_image_spaces,
+    /*out*/std::vector<std::unique_ptr<ImageSpace>>* boot_image_spaces,
     /*out*/MemMap* extra_reservation) {
   ScopedTrace trace(__FUNCTION__);
 
@@ -2388,11 +3042,6 @@
   return true;
 }
 
-static void AppendImageChecksum(const ImageHeader& header, /*inout*/std::string* checksums) {
-  static_assert(ImageSpace::kImageChecksumPrefix == 'i', "Format prefix check.");
-  StringAppendF(checksums, "i;%u/%08x", header.GetComponentCount(), header.GetImageChecksum());
-}
-
 std::string ImageSpace::GetBootClassPathChecksums(
     ArrayRef<ImageSpace* const> image_spaces,
     ArrayRef<const DexFile* const> boot_class_path) {
@@ -2413,7 +3062,7 @@
     if (image_pos != 0u) {
       boot_image_checksum += ':';
     }
-    AppendImageChecksum(current_header, &boot_image_checksum);
+    AppendImageChecksum(component_count, current_header.GetImageChecksum(), &boot_image_checksum);
     for (size_t component_index = 0; component_index != component_count; ++component_index) {
       const ImageSpace* space = image_spaces[image_pos + component_index];
       const OatFile* oat_file = space->oat_file_non_owned_;
@@ -2436,10 +3085,10 @@
   DCHECK(boot_class_path_tail.empty() ||
          !DexFileLoader::IsMultiDexLocation(boot_class_path_tail.front()->GetLocation().c_str()));
   for (const DexFile* dex_file : boot_class_path_tail) {
-    if (!boot_image_checksum.empty()) {
-      boot_image_checksum += ':';
-    }
     if (!DexFileLoader::IsMultiDexLocation(dex_file->GetLocation().c_str())) {
+      if (!boot_image_checksum.empty()) {
+        boot_image_checksum += ':';
+      }
       boot_image_checksum += kDexFileChecksumPrefix;
     }
     StringAppendF(&boot_image_checksum, "/%08x", dex_file->GetLocationChecksum());
@@ -2479,21 +3128,6 @@
   return component_count;
 }
 
-static bool CheckAndRemoveImageChecksum(const ImageHeader& header,
-                                        /*inout*/std::string_view* oat_checksums,
-                                        /*out*/std::string* error_msg) {
-  std::string image_checksum;
-  AppendImageChecksum(header, &image_checksum);
-  if (!StartsWith(*oat_checksums, image_checksum)) {
-    *error_msg = StringPrintf("Image checksum mismatch, expected %s to start with %s",
-                              std::string(*oat_checksums).c_str(),
-                              image_checksum.c_str());
-    return false;
-  }
-  oat_checksums->remove_prefix(image_checksum.size());
-  return true;
-}
-
 bool ImageSpace::VerifyBootClassPathChecksums(std::string_view oat_checksums,
                                               std::string_view oat_boot_class_path,
                                               const std::string& image_location,
@@ -2515,75 +3149,39 @@
     return false;
   }
 
-  bool load_extensions = false;  // TODO: Boot image extensions.
-  const std::string& actual_image_location = image_location;  // TODO: Boot image extensions.
-  std::string system_filename;
-  bool has_system = false;
-  std::string cache_filename;
-  bool has_cache = false;
-  bool dalvik_cache_exists = false;
-  bool is_global_cache = false;
-  if (!FindImageFilename(actual_image_location.c_str(),
-                         image_isa,
-                         &system_filename,
-                         &has_system,
-                         &cache_filename,
-                         &dalvik_cache_exists,
-                         &has_cache,
-                         &is_global_cache)) {
-    *error_msg = StringPrintf("Unable to find image file for %s and %s",
-                              image_location.c_str(),
-                              GetInstructionSetString(image_isa));
-    return false;
-  }
-
-  DCHECK(has_system || has_cache);
-  const std::string& filename = (order == ImageSpaceLoadingOrder::kSystemFirst)
-      ? (has_system ? system_filename : cache_filename)
-      : (has_cache ? cache_filename : system_filename);
-
   size_t bcp_pos = 0u;
-  while (StartsWith(oat_checksums, "i")) {
-    const std::string& current_filename = filename;
-    if (bcp_pos != 0u) {
-      if (!load_extensions) {
-        *error_msg = "Checksum specifies boot image extension but extensions are not used.";
-        return false;
-      }
-      UNREACHABLE();  // TODO: Boot image extensions.
-    }
-    ImageHeader header;
-    if (!ReadSpecificImageHeader(current_filename.c_str(), &header, error_msg)) {
+  if (StartsWith(oat_checksums, "i")) {
+    BootImageLayout layout(image_location, boot_class_path);
+    std::string primary_image_location = layout.GetPrimaryImageLocation();
+    std::string system_filename;
+    bool has_system = false;
+    std::string cache_filename;
+    bool has_cache = false;
+    bool dalvik_cache_exists = false;
+    bool is_global_cache = false;
+    if (!FindImageFilename(primary_image_location.c_str(),
+                           image_isa,
+                           &system_filename,
+                           &has_system,
+                           &cache_filename,
+                           &dalvik_cache_exists,
+                           &has_cache,
+                           &is_global_cache)) {
+      *error_msg = StringPrintf("Unable to find image file for %s and %s",
+                                image_location.c_str(),
+                                GetInstructionSetString(image_isa));
       return false;
     }
-    size_t component_count = header.GetComponentCount();
-    if (component_count == 0u || component_count > bcp_size - bcp_pos) {
-      *error_msg = StringPrintf("Unexpected component count in %s, received %u, "
-                                    "expected non-zero and <= %zu",
-                                current_filename.c_str(),
-                                header.GetComponentCount(),
-                                bcp_size - bcp_pos);
+
+    DCHECK(has_system || has_cache);
+    bool use_system = (order == ImageSpaceLoadingOrder::kSystemFirst) ? has_system : !has_cache;
+    bool image_checksums_ok = use_system
+        ? layout.ValidateFromSystem(image_isa, &oat_checksums, error_msg)
+        : layout.ValidateFromDalvikCache(cache_filename, &oat_checksums, error_msg);
+    if (!image_checksums_ok) {
       return false;
     }
-    if (!CheckAndRemoveImageChecksum(header, &oat_checksums, error_msg)) {
-      DCHECK(!error_msg->empty());
-      return false;
-    }
-    bcp_pos += component_count;
-    if (oat_checksums.empty()) {
-      if (bcp_pos != bcp_size) {
-        *error_msg = StringPrintf("Checksum too short, missing %zu components.",
-                                  bcp_size - bcp_pos);
-        return false;
-      }
-      return true;
-    }
-    if (!StartsWith(oat_checksums, ":")) {
-      *error_msg = StringPrintf("Missing ':' separator at start of %s",
-                                std::string(oat_checksums).c_str());
-      return false;
-    }
-    oat_checksums.remove_prefix(1u);
+    bcp_pos = layout.GetNextBcpIndex();
   }
 
   for ( ; bcp_pos != bcp_size; ++bcp_pos) {
@@ -2662,7 +3260,8 @@
     uint32_t component_count = current_header.GetComponentCount();
     DCHECK_NE(component_count, 0u);
     DCHECK_LE(component_count, image_spaces.size() - image_pos);
-    if (!CheckAndRemoveImageChecksum(current_header, &oat_checksums, error_msg)) {
+    uint32_t checksum = current_header.GetImageChecksum();
+    if (!CheckAndRemoveImageChecksum(component_count, checksum, &oat_checksums, error_msg)) {
       DCHECK(!error_msg->empty());
       return false;
     }
@@ -2673,7 +3272,9 @@
         size_t num_dex_files = oat_file->GetOatDexFiles().size();
         CHECK_NE(num_dex_files, 0u);
         const std::string main_location = oat_file->GetOatDexFiles()[0]->GetDexFileLocation();
-        CHECK_EQ(main_location, boot_class_path[image_pos + component_index]);
+        // TODO: Get rid of the weird ResolveRelativeEncodedDexLocation() stuff from oat_file.cc
+        // and enable this check:
+        // CHECK_EQ(main_location, boot_class_path_locations[image_pos + component_index]);
         CHECK(!DexFileLoader::IsMultiDexLocation(main_location.c_str()));
         for (size_t i = 1u; i != num_dex_files; ++i) {
           CHECK(DexFileLoader::IsMultiDexLocation(
@@ -2708,14 +3309,6 @@
 }
 
 std::vector<std::string> ImageSpace::ExpandMultiImageLocations(
-    const std::vector<std::string>& dex_locations,
-    const std::string& image_location,
-    bool boot_image_extension) {
-  return ExpandMultiImageLocations(
-      ArrayRef<const std::string>(dex_locations), image_location, boot_image_extension);
-}
-
-std::vector<std::string> ImageSpace::ExpandMultiImageLocations(
     ArrayRef<const std::string> dex_locations,
     const std::string& image_location,
     bool boot_image_extension) {
diff --git a/runtime/gc/space/image_space.h b/runtime/gc/space/image_space.h
index 837facc..f56b42b 100644
--- a/runtime/gc/space/image_space.h
+++ b/runtime/gc/space/image_space.h
@@ -39,11 +39,61 @@
     return kSpaceTypeImageSpace;
   }
 
-  // Load boot image spaces from a primary image file for a specified instruction set.
+  // Load boot image spaces for specified boot class path, image location, instruction set, etc.
   //
   // On successful return, the loaded spaces are added to boot_image_spaces (which must be
   // empty on entry) and `extra_reservation` is set to the requested reservation located
   // after the end of the last loaded oat file.
+  //
+  // IMAGE LOCATION
+  //
+  // The "image location" is a colon-separated list that specifies one or more
+  // components by name and may also specify search paths for extensions
+  // corresponding to the remaining boot class path (BCP) extensions.
+  //
+  // The primary boot image can be specified as one of
+  //     <path>/<base-name>
+  //     <base-name>
+  // and the path of the first BCP component is used for the second form.
+  //
+  // Named extension specifications must correspond to an expansion of the
+  // <base-name> with a BCP component (for example boot.art with the BCP
+  // component name <jar-path>/framework.jar expands to boot-framework.art).
+  // They can be similarly specified as one of
+  //     <ext-path>/<ext-name>
+  //     <ext-name>
+  // and must be listed in the order of their corresponding BCP components.
+  //
+  // Search paths for remaining extensions can be specified after named
+  // components as one of
+  //     <search-path>/*
+  //     *
+  // where the second form means that the path of a particular BCP component
+  // should be used to search for that component's boot image extension. These
+  // paths will be searched in the specifed order.
+  //
+  // The actual filename shall be derived from the specified locations using
+  // `GetSystemImageFilename()` or `GetDalvikCacheFilename()`.
+  //
+  // Example image locations:
+  //     /system/framework/boot.art
+  //         - only primary boot image with full path.
+  //     boot.art:boot-framework.art
+  //         - primary and one extension, use BCP component paths.
+  //     /apex/com.android.art/boot.art:*
+  //         - primary with exact location, search for the rest based on BCP
+  //           component paths.
+  //     boot.art:/system/framework/*
+  //         - primary based on BCP component path, search for extensions in
+  //           /system/framework.
+  //     /apex/com.android.art/boot.art:/system/framework/*:*
+  //         - primary with exact location, search for extensions first in
+  //           /system/framework, then in the corresponding BCP component path.
+  //     /apex/com.android.art/boot.art:*:/system/framework/*
+  //         - primary with exact location, search for extensions first in the
+  //           corresponding BCP component path and then in /system/framework.
+  //     /apex/com.android.art/boot.art:*:boot-framework.jar
+  //         - invalid, named components may not follow search paths.
   static bool LoadBootImage(
       const std::vector<std::string>& boot_class_path,
       const std::vector<std::string>& boot_class_path_locations,
@@ -54,7 +104,7 @@
       bool executable,
       bool is_zygote,
       size_t extra_reservation_size,
-      /*out*/std::vector<std::unique_ptr<space::ImageSpace>>* boot_image_spaces,
+      /*out*/std::vector<std::unique_ptr<ImageSpace>>* boot_image_spaces,
       /*out*/MemMap* extra_reservation) REQUIRES_SHARED(Locks::mutator_lock_);
 
   // Try to open an existing app image space.
@@ -167,7 +217,7 @@
 
   // Expand a single image location to multi-image locations based on the dex locations.
   static std::vector<std::string> ExpandMultiImageLocations(
-      const std::vector<std::string>& dex_locations,
+      ArrayRef<const std::string> dex_locations,
       const std::string& image_location,
       bool boot_image_extension = false);
 
@@ -233,12 +283,7 @@
   friend class Space;
 
  private:
-  // Internal overload that takes ArrayRef<> instead of vector<>.
-  static std::vector<std::string> ExpandMultiImageLocations(
-      ArrayRef<const std::string> dex_locations,
-      const std::string& image_location,
-      bool boot_image_extension = false);
-
+  class BootImageLayout;
   class BootImageLoader;
   template <typename ReferenceVisitor>
   class ClassTableVisitor;
diff --git a/runtime/oat.h b/runtime/oat.h
index 54d111c..7817bd3 100644
--- a/runtime/oat.h
+++ b/runtime/oat.h
@@ -32,8 +32,8 @@
 class PACKED(4) OatHeader {
  public:
   static constexpr std::array<uint8_t, 4> kOatMagic { { 'o', 'a', 't', '\n' } };
-  // Last oat version changed reason: Optimize stack map decoding - interleave varints.
-  static constexpr std::array<uint8_t, 4> kOatVersion { { '1', '7', '3', '\0' } };
+  // Last oat version changed reason: Boot image extension.
+  static constexpr std::array<uint8_t, 4> kOatVersion { { '1', '7', '4', '\0' } };
 
   static constexpr const char* kDex2OatCmdLineKey = "dex2oat-cmdline";
   static constexpr const char* kDebuggableKey = "debuggable";
diff --git a/runtime/oat_file.cc b/runtime/oat_file.cc
index 4b5d5c3..9ef5fbb 100644
--- a/runtime/oat_file.cc
+++ b/runtime/oat_file.cc
@@ -855,14 +855,18 @@
     CheckedCall(mprotect, "protect relocations", reloc_begin, DataBimgRelRoSize(), PROT_READ);
     // Make sure the file lists a boot image dependency, otherwise the .data.bimg.rel.ro
     // section is bogus. The full dependency is checked before the code is executed.
-    const char* boot_class_path_checksum =
-        GetOatHeader().GetStoreValueByKey(OatHeader::kBootClassPathChecksumsKey);
-    if (boot_class_path_checksum == nullptr ||
-        boot_class_path_checksum[0] != gc::space::ImageSpace::kImageChecksumPrefix) {
-      *error_msg = StringPrintf("Oat file '%s' contains .data.bimg.rel.ro section "
-                                    "without boot image dependency.",
-                                GetLocation().c_str());
-      return false;
+    // We cannot do this check if we do not have a key-value store, i.e. for secondary
+    // oat files for boot image extensions.
+    if (GetOatHeader().GetKeyValueStoreSize() != 0u) {
+      const char* boot_class_path_checksum =
+          GetOatHeader().GetStoreValueByKey(OatHeader::kBootClassPathChecksumsKey);
+      if (boot_class_path_checksum == nullptr ||
+          boot_class_path_checksum[0] != gc::space::ImageSpace::kImageChecksumPrefix) {
+        *error_msg = StringPrintf("Oat file '%s' contains .data.bimg.rel.ro section "
+                                      "without boot image dependency.",
+                                  GetLocation().c_str());
+        return false;
+      }
     }
   }
 
diff --git a/test/run-test b/test/run-test
index eeeefbb..72e7562 100755
--- a/test/run-test
+++ b/test/run-test
@@ -672,12 +672,12 @@
 elif [ "$runtime" = "art" ]; then
     if [ "$target_mode" = "no" ]; then
         guess_host_arch_name
-        run_args+=(--boot "${ANDROID_HOST_OUT}/framework/core${image_suffix}.art")
+        run_args+=(--boot "${ANDROID_HOST_OUT}/framework/core${image_suffix}.art:*")
         run_args+=(--runtime-option "-Djava.library.path=${host_lib_root}/lib${suffix64}:${host_lib_root}/nativetest${suffix64}")
     else
         guess_target_arch_name
         run_args+=(--runtime-option "-Djava.library.path=/data/nativetest${suffix64}/art/${target_arch_name}")
-        run_args+=(--boot "/data/art-test/core${image_suffix}.art")
+        run_args+=(--boot "/data/art-test/core${image_suffix}.art:/data/art-test/*")
     fi
     if [ "$relocate" = "yes" ]; then
       run_args+=(--relocate)