Loading...
cache_builder/NewSharedCacheBuilder.cpp dyld-1066.8 dyld-1235.2
--- dyld/dyld-1066.8/cache_builder/NewSharedCacheBuilder.cpp
+++ dyld/dyld-1235.2/cache_builder/NewSharedCacheBuilder.cpp
@@ -24,6 +24,7 @@
 
 #include "Defines.h"
 #include "NewSharedCacheBuilder.h"
+#include "MachOFile.h"
 #include "NewAdjustDylibSegments.h"
 #include "CacheDylib.h"
 #include "ClosureFileSystem.h"
@@ -36,12 +37,17 @@
 #include "ObjCVisitor.h"
 #include "Trie.hpp"
 #include "JustInTimeLoader.h"
+#include "OptimizerObjC.h"
 #include "OptimizerSwift.h"
 #include "PrebuiltLoader.h"
 #include "DyldProcessConfig.h"
 #include "DyldRuntimeState.h"
 #include "SwiftVisitor.h"
 #include "ParallelUtils.h"
+#include "CString.h"
+#include "Version32.h"
+#include "ExternalGenericMetadataBuilderImport.h"
+#include <SharedCacheLinker/SharedCacheLinker.h>
 
 // FIXME: Remove this once we don't write to the old objc header struct.  See emitObjCOptsHeader()
 #include "objc-shared-cache.h"
@@ -50,6 +56,8 @@
 #include <list>
 #include <mach-o/nlist.h>
 #include <sstream>
+#include <sys/fcntl.h>
+#include <sys/stat.h>
 #include <unordered_set>
 
 using dyld3::GradedArchs;
@@ -61,8 +69,9 @@
 using dyld4::ProcessConfig;
 using dyld4::RuntimeState;
 using dyld4::SyscallDelegate;
-
-using lsl::EphemeralAllocator;
+using dyld4::RuntimeLocks;
+
+using lsl::Allocator;
 
 using metadata_visitor::SwiftConformance;
 using metadata_visitor::SwiftVisitor;
@@ -93,9 +102,21 @@
     }
 }
 
+void SharedCacheBuilder::forEachError(void (^callback)(const std::string_view& str)) const
+{
+    for ( const std::string& str : this->errors ) {
+        callback(str);
+    }
+}
+
 void SharedCacheBuilder::forEachCacheDylib(void (^callback)(const std::string_view& path)) const
 {
     for ( const CacheDylib& cacheDylib : this->cacheDylibs ) {
+        // skip Swift prespecialized dylib if it's been built
+        // it's synthesized by the builder, so mrm doesn't need to remove it
+        if ( swiftPrespecializedDylib && &cacheDylib == swiftPrespecializedDylib )
+            continue;
+
         // Note this has to return the path, not the install name, as MRM uses this to delete
         // the path from disk
         callback(cacheDylib.inputFile->path);
@@ -110,18 +131,21 @@
 }
 
 void SharedCacheBuilder::addFile(const void* buffer, size_t bufferSize, std::string_view path,
-                                 uint64_t inode, uint64_t modTime)
+                                 uint64_t inode, uint64_t modTime, bool forceNotCacheEligible)
 {
     Diagnostics diag;
     const bool  isOSBinary = false;
-    if ( const MachOFile* mf = MachOFile::compatibleSlice(diag, buffer, bufferSize, path.data(),
+    uint64_t    sliceLen = 0;
+    if ( const MachOFile* mf = MachOFile::compatibleSlice(diag, sliceLen, buffer, bufferSize, path.data(),
                                                           this->options.platform, isOSBinary,
                                                           this->options.archs) ) {
         InputFile inputFile;
-        inputFile.mf        = mf;
-        inputFile.inode     = inode;
-        inputFile.mtime     = modTime;
-        inputFile.path      = path;
+        inputFile.mf                    = mf;
+        inputFile.inode                 = inode;
+        inputFile.mtime                 = modTime;
+        inputFile.size                  = sliceLen;
+        inputFile.path                  = path;
+        inputFile.forceNotCacheEligible = forceNotCacheEligible;
         allInputFiles.push_back(std::move(inputFile));
         return;
     }
@@ -129,14 +153,16 @@
     // On macOS, also allow iOSMac dylibs
     if ( this->options.platform == dyld3::Platform::macOS ) {
         diag.clearError();
-        if ( const MachOFile* mf = MachOFile::compatibleSlice(diag, buffer, bufferSize, path.data(),
+        if ( const MachOFile* mf = MachOFile::compatibleSlice(diag, sliceLen, buffer, bufferSize, path.data(),
                                                               dyld3::Platform::iOSMac, isOSBinary,
                                                               this->options.archs) ) {
             InputFile inputFile;
-            inputFile.mf        = mf;
-            inputFile.inode     = inode;
-            inputFile.mtime     = modTime;
-            inputFile.path      = path;
+            inputFile.mf                    = mf;
+            inputFile.inode                 = inode;
+            inputFile.mtime                 = modTime;
+            inputFile.size                  = sliceLen;
+            inputFile.path                  = path;
+            inputFile.forceNotCacheEligible = forceNotCacheEligible;
             allInputFiles.push_back(std::move(inputFile));
             return;
         }
@@ -163,6 +189,9 @@
     if ( this->allInputFiles.empty() )
         return Error("Cannot build cache with no inputs");
 
+    // Reserve a slot for the Swift prespecialized dylib early, so that it can be ordered
+    this->reserveSwiftPrespecializedInputFile();
+
     this->categorizeInputs();
     this->verifySelfContained();
 
@@ -171,10 +200,22 @@
 
     this->sortDylibs();
 
+    // Note this needs to be after sorting, so the order of objc dylibs is consistent with all dylibs list
+    this->findObjCDylibs();
+
+    // ObjC dylibs order is now set, so we can create the Swift prespecialized dylib
+    // Note this needs to happen after order is known because the Swift dylib needs to
+    // known indices of other shared cache dylibs. To create the dylib earlier we would need
+    // to add split seg support for dylib indices.
+    if ( Error error = this->createSwiftPrespecializedDylib() ) {
+        swiftPrespecializedDylibBuildError = error.message();
+        return error;
+    }
+
     // Note this needs to be after sorting, as aliases point to the cache dylibs
     this->calculateDylibAliases();
 
-    if ( Error error = this->calculateDylibDependents(); error.hasError() )
+    if ( Error error = this->calculateDylibDependents() )
         return error;
 
     this->categorizeDylibSegments();
@@ -191,14 +232,15 @@
 Error SharedCacheBuilder::estimateGlobalOptimizations()
 {
     this->estimateIMPCaches();
-    this->findObjCDylibs();
     this->findCanonicalObjCSelectors();
     this->findCanonicalObjCClassNames();
     this->findCanonicalObjCProtocolNames();
     this->findObjCClasses();
     this->findObjCProtocols();
+    this->findObjCCategories();
     this->estimateObjCHashTableSizes();
     this->calculateObjCCanonicalProtocolsSize();
+    this->calculateObjCCategoriesSize();
 
     // Note, swift hash tables depends on findObjCClasses()
     this->estimateSwiftHashTableSizes();
@@ -238,8 +280,218 @@
     return Error();
 }
 
-// This is phase 4 of the build() process.  It takes the inputs and Optimizers
-// from the previous phases, and creates the SubCache objects
+// This name is used only to create a placeholder input file and determine the library order.
+const std::string_view swiftPrespecializedDylibInstallName = "/usr/lib/libswiftPrespecialized.dylib";
+
+bool SharedCacheBuilder::shouldBuildSwiftPrespecializedDylib()
+{
+    if ( options.platform == dyld3::Platform::driverKit )
+        return false;
+
+    // build the dylib, only if the order file is defined
+    if ( options.swiftGenericMetadataFile.empty() )
+        return false;
+
+    // check if the metadata builder is available
+#if !BUILDING_CACHE_BUILDER_UNIT_TESTS && !BUILDING_SIM_CACHE_BUILDER
+    if ( swift_externalMetadataBuilder_create == nullptr )
+        return false;
+#endif // !BUILDING_CACHE_BUILDER_UNIT_TESTS
+
+    return true;
+}
+
+Error SharedCacheBuilder::buildSwiftPrespecializedDylibJSON()
+{
+#if !BUILDING_CACHE_BUILDER_UNIT_TESTS && !BUILDING_SIM_CACHE_BUILDER
+    Timer::Scope timedScope(this->config, "buildSwiftPrespecializedDylibJSON time");
+
+    SwiftExternalMetadataBuilder* builder = swift_externalMetadataBuilder_create((int)options.platform, options.archs.name());
+    if ( !builder )
+        return Error("swift_externalMetadataBuilder_create failed");
+
+    for ( const CacheDylib& dylib : this->cacheDylibs ) {
+        if ( dylib.inputMF == nullptr ) continue;
+
+        // TODO: rdar://132262275 (dyld shared cache builder should tell Swift Metadata builder also about dyld)
+        if ( dylib.inputMF->isDyld() ) continue;
+
+        if ( const char* err = swift_externalMetadataBuilder_addDylib(builder, dylib.inputMF->installName(),
+                (const struct mach_header*)dylib.inputMF, dylib.inputFile->size) )
+            return Error("swift_externalMetadataBuilder_addDylib failed: %s", err);
+    }
+
+    if ( const char* err = swift_externalMetadataBuilder_readNamesJSON(builder, options.swiftGenericMetadataFile.c_str()) )
+        return Error("swift_externalMetadataBuilder_readNamesJSON failed: %s", err);
+
+    if ( const char* err = swift_externalMetadataBuilder_buildMetadata(builder) )
+        return Error("swift_externalMetadataBuilder_buildMetadata failed: %s", err);
+
+    if ( const char* json = swift_externalMetadataBuilder_getMetadataJSON(builder) )
+        swiftPrespecializedDylibJSON = json;
+    else
+        return Error("swift_externalMetadataBuilder_getMetadataJSON returned an empty JSON");
+
+    const std::string_view placeholderVersion = R"("platformVersion": "1.0")";
+    // Patch platformVersion if it's 1.0 until rdar://122585868 is fixed
+    if ( auto pos = swiftPrespecializedDylibJSON.find(placeholderVersion);
+            pos != swiftPrespecializedDylibJSON.npos ) {
+
+        __block mach_o::Version32 newMinOS;
+        // determine new deployment target based on dyld's version
+        for ( const InputFile& inputFile : allInputFiles ) {
+            if ( !inputFile.mf )
+                continue;
+
+            if ( !endsWith(inputFile.path, "dyld") )
+                continue;
+
+            inputFile.mf->forEachSupportedPlatform(^(dyld3::Platform platform, uint32_t currentMinOS, uint32_t sdk) {
+                if ( platform == options.platform )
+                    newMinOS = mach_o::Version32(currentMinOS);
+            });
+            break;
+        }
+
+        if ( newMinOS > mach_o::Version32(1, 0) ) {
+            char verStr[32];
+            newMinOS.toString(verStr);
+            std::string newVersion = "\"platformVersion\": \"";
+            newVersion += verStr;
+            newVersion += "\"";
+
+            swiftPrespecializedDylibJSON.replace(pos, placeholderVersion.size(), newVersion);
+        }
+    }
+
+    swift_externalMetadataBuilder_destroy(builder);
+
+    if ( options.debug ) {
+        std::string path;
+        if ( const char* dir = getenv("TMPDIR") )
+            path = dir;
+        if ( path.empty() )
+            path = "/tmp";
+        path += "/swift-prespecialized.json-XXXXXX";
+
+        int outFileFd = mkstemp(path.data());
+        if ( outFileFd != -1 ) {
+            write(outFileFd, swiftPrespecializedDylibJSON.data(), swiftPrespecializedDylibJSON.size());
+        }
+    }
+#endif // !BUILDING_CACHE_BUILDER_UNIT_TESTS
+
+    return Error::none();
+}
+
+bool SharedCacheBuilder::reserveSwiftPrespecializedInputFile()
+{
+    if ( !shouldBuildSwiftPrespecializedDylib() )
+        return false;
+
+    InputFile inputFile;
+    inputFile.mf = nullptr;
+    inputFile.inode = 0;
+    inputFile.mtime = 0;
+    inputFile.path = swiftPrespecializedDylibInstallName;
+    allInputFiles.push_back(std::move(inputFile));
+    cacheDylibs.push_back(CacheDylib(swiftPrespecializedDylibInstallName));
+    return true;
+}
+
+Error SharedCacheBuilder::createSwiftPrespecializedDylib()
+{
+    if ( !shouldBuildSwiftPrespecializedDylib() )
+        return Error::none();
+
+    if ( Error err = buildSwiftPrespecializedDylibJSON() )
+        return err;
+
+    InputFile* inputFile = nullptr;
+    if ( allInputFiles.empty() || allInputFiles.back().path != swiftPrespecializedDylibInstallName )
+        return Error("missing input file placeholder for Swift prespecialized dylib");
+    inputFile = &allInputFiles.back();
+
+    std::vector<const char*> dylibsList;
+    // the dylib list needs to be in order of objc dylibs
+    for ( const CacheDylib* dylib : this->objcOptimizer.objcDylibs )
+        dylibsList.push_back(CString::dup(dylib->installName).c_str());
+
+    // TODO: support in-memory file buffer
+    std::string path;
+    if ( const char* dir = getenv("TMPDIR") )
+        path = dir;
+    if ( path.empty() )
+        path = "/tmp";
+    path += "/libswiftPrespecialized.dylib-XXXXXX";
+
+    int outFileFd = mkstemp(path.data());
+    if ( outFileFd == -1 )
+        return Error("couldn't create a temporary file for Swift prespecialized dylib: %s", (const char*)strerror(errno));
+
+    close(outFileFd);
+    if ( const char* err = ldMakeDylibFromJSON(swiftPrespecializedDylibJSON, dylibsList, path.c_str()) )
+        return Error("%s", err);
+
+    // cleanup dylibs list
+    for ( const char* str : dylibsList )
+        free((void*)str);
+
+    // re-open output file
+    outFileFd = open(path.c_str(), O_RDONLY);
+    if ( outFileFd < 0 )
+        return Error("could not open swift dylib file because: %s", (const char*)strerror(errno));
+
+    struct stat stat_buf;
+    if (  fstat(outFileFd, &stat_buf) == -1 )
+        return Error("could not stat swift dylib file because: %s", (const char*)strerror(errno));
+
+    vm_size_t bufferSize = stat_buf.st_size;
+    void* buffer = mmap(nullptr, bufferSize, PROT_READ, MAP_FILE | MAP_SHARED, outFileFd, 0);
+    if ( buffer == MAP_FAILED ) {
+        // Failed to mmap the file
+        return Error("could not mmap swift dylib file because: %s", (const char*)strerror(errno));
+    }
+
+    Diagnostics diag;
+    inputFile->mf = MachOFile::compatibleSlice(diag, inputFile->size, buffer, bufferSize, path.data(),
+                                    this->options.platform, /* isOSBinary */ false,
+                                    this->options.archs);
+    if ( diag.hasError() )
+        return Error("%s", diag.errorMessageCStr());
+
+    // recreate cache dylib at the reserved slot
+    auto cacheDylibIt = std::find_if(cacheDylibs.begin(), cacheDylibs.end(), [](CacheDylib& dylib) {
+            return dylib.inputMF == nullptr && dylib.installName == swiftPrespecializedDylibInstallName;
+    });
+    if ( cacheDylibIt == cacheDylibs.end() )
+        return Error("missing cache dylib slot for Swift prespecialized dylib");
+
+    // save previously computed cache index
+    uint32_t index = cacheDylibIt->cacheIndex;
+    // recreate cache dylib with the updated input file
+    *cacheDylibIt = CacheDylib(*inputFile);
+    cacheDylibIt->cacheIndex = index;
+    // rdar://122906481 (Shared cache builder - explicitly model dylibs without a need for a patch table)
+    cacheDylibIt->needsPatchTable = false;
+    this->swiftPrespecializedDylib = &*cacheDylibIt;
+
+    // sanity check Swift dylib compatibility
+    __block Error err = Error::none();
+    inputFile->mf->withFileLayout(diag, ^(const mach_o::Layout& layout) {
+        mach_o::SplitSeg splitSeg(layout);
+
+        if ( !splitSeg.isV2() )
+            err = Error("Swift prespecialized dylib must use split seg V2");
+    });
+    if ( !inputFile->mf->hasChainedFixups() )
+        err = Error("Swift prespecialized dylib must use chained fixups");
+
+    return std::move(err);
+}
+
+// This is phase 4 of the build() process. It takes the inputs and Optimizers
+// from the previous phases, and emits them to the cache buffers
 // Inputs:  subCaches, various Optimizers
 // Outputs: emitted objc strings in the subCache buffers
 Error SharedCacheBuilder::preDylibEmitChunks()
@@ -250,6 +502,7 @@
     this->setupSplitSegAdjustors();
     this->adjustObjCClasses();
     this->adjustObjCProtocols();
+    this->adjustObjCCategories();
 
     // Note this could be after dylib passes, but having the strings emitted now makes
     // it easier to debug the ObjC dylib passes
@@ -261,8 +514,8 @@
     return Error();
 }
 
-// This is phase 4 of the build() process.
-// It runs the passes on each of the cacheDylib's
+// This is phase 5 of the build() process.
+// It runs the passes on each of the cache Dylibs
 // Inputs:  subCaches, various Optimizers
 // Outputs: emitted objc strings in the subCache buffers
 Error SharedCacheBuilder::runDylibPasses()
@@ -282,6 +535,11 @@
 
         cacheDylib.copyRawSegments(this->config, aggregateTimer);
 
+        // patch linked dylibs (load commands) as soon as the raw segments were coppied
+        // so next steps have accurate view of the dylib
+        if ( Error patchErr = this->patchLinkedDylibs(cacheDylib) )
+            return patchErr;
+
         PatchInfo& dylibPatchInfo = this->patchTableOptimizer.patchInfos[cacheDylib.cacheIndex];
         cacheDylib.applySplitSegInfo(diag, this->options, this->config,
                                      aggregateTimer, this->unmappedSymbolsOptimizer);
@@ -292,8 +550,12 @@
         if ( diag.hasError() )
             return Error("%s", diag.errorMessageCStr());
 
-        cacheDylib.calculateBindTargets(diag, this->config, aggregateTimer, builderCacheDylibs,
-                                        dylibPatchInfo);
+        std::vector<Error> symbolErrors = cacheDylib.calculateBindTargets(diag, this->config, aggregateTimer, builderCacheDylibs,
+                                                                          dylibPatchInfo);
+        if ( !symbolErrors.empty() ) {
+            for ( const Error& symbolErr : symbolErrors )
+                this->errors.push_back(symbolErr.message());
+        }
         if ( diag.hasError() )
             return Error("%s", diag.errorMessageCStr());
 
@@ -334,12 +596,13 @@
     return err;
 }
 
-// This is phase 5 of the build() process.  It takes the Optimizers
+// This is phase 6 of the build() process. It takes the Optimizers
 // from the previous phases, and emits them to the cache buffers
 // Inputs:  subCaches, various Optimizers
-// Outputs: emitted optimiations in the subCache buffers
+// Outputs: emitted optimizations in the subCache buffers
 Error SharedCacheBuilder::postDylibEmitChunks()
 {
+
     this->optimizeTLVs();
 
     if ( Error error = this->emitUniquedGOTs(); error.hasError() )
@@ -349,7 +612,25 @@
     if ( Error error = this->emitCanonicalObjCProtocols(); error.hasError() )
         return error;
 
+    this->emitCacheDylibsTrie();
+    if ( Error error = this->emitPatchTable(); error.hasError() )
+        return error;
+
+    // Note, this must be after we emit the patch table
+    if ( Error error = this->emitCacheDylibsPrebuiltLoaders(); error.hasError() )
+        return error;
+
     this->emitObjCHashTables();
+
+    bool preAttachedCategories = true;
+    if ( preAttachedCategories ) {
+        // Note this has to be after anyone walking the objc metadata format
+        if ( Error error = this->emitPreAttachedObjCCategories(); error.hasError() )
+            return error;
+    }
+
+    // Note, this must be after emitCacheDylibsPrebuiltLoaders() as it needs the offset to the SectionLocations*
+    // in the PrebuiltLoader*
     this->emitObjCHeaderInfo();
     if ( Error error = this->computeObjCClassLayout(); error.hasError() )
         return error;
@@ -363,14 +644,6 @@
     if ( Error error = this->emitSwiftHashTables(); error.hasError() )
         return error;
 
-    this->emitCacheDylibsTrie();
-    if ( Error error = this->emitPatchTable(); error.hasError() )
-        return error;
-
-    // Note, this must be after we emit the patch table
-    if ( Error error = this->emitCacheDylibsPrebuiltLoaders(); error.hasError() )
-        return error;
-
     // Note, this has to be after we've emitted the objc hash tables and the objc header infos
     if ( Error error = this->emitExecutablePrebuiltLoaders(); error.hasError() )
         return error;
@@ -385,8 +658,8 @@
     return Error();
 }
 
-// This is phase 6 of the build() process.  it does any final work to emit
-// the sub caches
+// This is phase 7 of the build() process. It does any final work
+// to emit the sub caches
 // Inputs: everything else
 // Outputs: final emitted data in the sub caches
 Error SharedCacheBuilder::finalize()
@@ -474,9 +747,17 @@
     Timer::Scope timedScope(this->config, "categorizeInputs time");
 
     for ( InputFile& inputFile : this->allInputFiles ) {
+        if ( inputFile.mf == nullptr ) continue;
+
         if ( inputFile.mf->isDylib() || inputFile.mf->isDyld() ) {
-            auto failureHandler = ^(const char* reason) {
-                inputFile.setError(Error("%s", reason));
+            auto failureHandler = ^(const char* format, ...) __attribute__((format(printf, 1, 2))) {
+                char*   output_string;
+                va_list list;
+                va_start(list, format);
+                vasprintf(&output_string, format, list);
+                va_end(list);
+                inputFile.setError(Error("%s", (const char*)output_string));
+                free(output_string);
             };
 
             std::string_view installName = inputFile.mf->installName();
@@ -501,7 +782,7 @@
                 }
             }
 
-            if ( inputFile.mf->canBePlacedInDyldCache(dylibPath.data(), failureHandler) ) {
+            if ( !inputFile.forceNotCacheEligible && inputFile.mf->canBePlacedInDyldCache(dylibPath.data(), failureHandler) ) {
                 CacheDylib cacheDylib(inputFile);
                 this->cacheDylibs.push_back(std::move(cacheDylib));
             }
@@ -537,6 +818,8 @@
     __block std::unordered_set<std::string_view> allDylibs;
     allDylibs.reserve(this->allInputFiles.size());
     for ( const InputFile& inputFile : this->allInputFiles ) {
+        if ( inputFile.mf == nullptr ) continue;
+
         if ( inputFile.mf->isDylib() )
             allDylibs.insert(inputFile.mf->installName());
     }
@@ -555,6 +838,8 @@
         doAgain = false;
         // scan dylib list making sure all dependents are in dylib list
         for ( const CacheDylib& cacheDylib : this->cacheDylibs ) {
+            if ( cacheDylib.inputFile == nullptr ) continue;
+
             //Timer::Scope timedScope(this->config, cacheDylib.installName);
             // Skip dylibs we marked bad from a previous iteration
             if ( cacheDylib.inputFile->hasError() )
@@ -603,6 +888,8 @@
 
     // Add bad dylibs to the "other" dylibs for use in prebuilt loaders
     for ( const CacheDylib& cacheDylib : this->cacheDylibs ) {
+        if ( cacheDylib.inputFile == nullptr ) continue;
+
         if ( cacheDylib.inputFile->hasError() ) {
             this->nonCacheDylibInputFiles.push_back(cacheDylib.inputFile);
             this->dylibHasMissingDependency = true;
@@ -611,9 +898,18 @@
 
     this->cacheDylibs.erase(std::remove_if(this->cacheDylibs.begin(), this->cacheDylibs.end(), [&](const CacheDylib& dylib) {
                                 // Dylibs with errors must be removed from the cache
-                                return dylib.inputFile->hasError();
+                                return dylib.inputFile != nullptr && dylib.inputFile->hasError();
                             }),
                             this->cacheDylibs.end());
+
+    // verify that there's at least one dylib that has an input file
+    if ( !std::any_of(cacheDylibs.begin(), cacheDylibs.end(), [](const CacheDylib& dylib) {
+                    return dylib.inputFile != nullptr;
+                    }) ) {
+        // the only remaining dylib is the synthesized Swift prespecialized dylib
+        // so remove it too
+        cacheDylibs.clear();
+    }
 }
 
 void SharedCacheBuilder::calculateDylibAliases()
@@ -728,6 +1024,30 @@
                 stop = true;
             }
         });
+
+        // copy the original list of dependents
+        cacheDylib.inputDependents = cacheDylib.dependents;
+
+        // note: below changes to dependents need to be kept in sync with load command patching in `patchLinkedDylibs`
+        // we might want to generalize that if more libraries require patching
+
+        // force swiftCore link the prespecialized dylib
+        if ( swiftPrespecializedDylib && cacheDylib.installName.find("libswiftCore.dylib") != std::string_view::npos ) {
+            CacheDylib::DependentDylib depDylib;
+            depDylib.kind  = CacheDylib::DependentDylib::Kind::normal;
+            depDylib.dylib = swiftPrespecializedDylib;
+            cacheDylib.dependents.push_back(std::move(depDylib));
+        }
+
+        // clear all dependents of the prespecialized dylib except libSystem
+        // otherwise loading the library would pull in lots of other dependencies
+        if ( swiftPrespecializedDylib && &cacheDylib == swiftPrespecializedDylib ) {
+            if ( cacheDylib.dependents.empty() || cacheDylib.dependents.front().dylib->installName.find("libSystem") == std::string_view::npos ) {
+                diag.error("expected libSystem as the first linked dylib of %s", cacheDylib.inputMF->installName());
+            } else {
+                cacheDylib.dependents.erase(cacheDylib.dependents.begin()+1, cacheDylib.dependents.end());
+            }
+        }
 
         if ( diag.hasError() )
             return Error("%s", diag.errorMessageCStr());
@@ -933,14 +1253,18 @@
 
 void SharedCacheBuilder::estimateIMPCaches()
 {
+    // Only LP64 is supported by the runtime
     if ( !this->config.layout.is64 )
         return;
 
-    if ( this->config.layout.cacheSize.rawValue() > 0x100000000 )
+    // Limited by ImpCacheEntry_v2::impOffset which is 38-bits.  For now limit to 16GB
+    // as that is the maximum we know slide info v5 can get to
+    if ( this->config.layout.cacheSize.rawValue() > 16_GB )
         return;
 
-    // Only iOS for now
-    if ( this->options.platform != dyld3::Platform::iOS )
+    // Only arm64* are is supported by the runtime
+    std::string_view archName = this->options.archs.name();
+    if ( archName != "arm64e" && archName != "arm64")
         return;
 
     // Skip everything if the JSON file is empty
@@ -962,6 +1286,27 @@
             // diag.warning("libobjc's magical IMP caches shared cache offsets list section missing (metadata not optimized)");
             return;
         }
+
+        // Also find the _objc_opt_preopt_caches_version symbol, which has the IMP caches version
+        __block Diagnostics diag;
+        cacheDylib.inputMF->withFileLayout(diag, ^(const mach_o::Layout &layout) {
+            mach_o::Layout::FoundSymbol foundInfo;
+            if ( !layout.findExportedSymbol(diag, "_objc_opt_preopt_caches_version", false, foundInfo) )
+                return;
+
+            // We only support header offsets in this dylib, as we are looking for self binds
+            // which are likely only to classes
+            if ( foundInfo.kind != mach_o::Layout::FoundSymbol::Kind::headerOffset )
+                return;
+
+            uint64_t vmAddr = layout.textUnslidVMAddr() + foundInfo.value;
+
+            __block objc_visitor::Visitor objcVisitor = makeInputDylibObjCVisitor(cacheDylib);
+            metadata_visitor::ResolvedValue value = objcVisitor.getValueFor(VMAddress(vmAddr));
+            this->objcIMPCachesOptimizer.libobjcImpCachesVersion = *(int*)value.value();
+        });
+        if ( diag.hasError() )
+            return;
     }
 
     // Find all the objc dylibs, classes, categories
@@ -1007,6 +1352,9 @@
         });
 
         objcVisitor.forEachCategory(^(const objc_visitor::Category& objcCategory, bool& stopCategory) {
+            if ( objcCategory.isForSwiftStubClass() )
+                return;
+
             imp_caches::Category impCacheCategory(objcCategory.getName(objcVisitor));
 
             // instance methods
@@ -1185,6 +1533,9 @@
         // Walk each category and set the class pointer
         __block uint32_t categoryIndex = 0;
         objcVisitor.forEachCategory(^(const objc_visitor::Category& objcCategory, bool& stopCategory) {
+            if ( objcCategory.isForSwiftStubClass() )
+                return;
+
             imp_caches::Category& impCacheCategory = dylib.categories[categoryIndex];
             const DylibClasses& classMap = dylibClassMaps[cacheDylib.cacheIndex];
 
@@ -1351,6 +1702,8 @@
 
     assert(this->objcOptimizer.objcDylibs.empty());
     for ( CacheDylib& cacheDylib : this->cacheDylibs ) {
+        if ( cacheDylib.inputMF == nullptr ) continue;
+
         if ( cacheDylib.inputMF->hasObjC() )
             this->objcOptimizer.objcDylibs.push_back(&cacheDylib);
     }
@@ -1371,6 +1724,8 @@
         this->objcOptimizer.headerInfoReadWriteByteSize = sizeof(ObjCOptimizer::header_info_rw_list_t);
         this->objcOptimizer.headerInfoReadWriteByteSize += (uint32_t)this->objcOptimizer.objcDylibs.size() * sizeof(ObjCOptimizer::header_info_rw_32_t);
     }
+
+    this->objcOptimizer.imageInfoSize = this->objcOptimizer.objcDylibs.size() * sizeof(objc::objc_image_info);
 
     if ( this->config.log.printStats ) {
         stats.add("  objc: found %lld objc dylibs\n", (uint64_t)this->objcOptimizer.objcDylibs.size());
@@ -1751,6 +2106,166 @@
     }
 }
 
+// Walk all the dylibs and build a map of ObjC categories
+void SharedCacheBuilder::findObjCCategories()
+{
+    if ( this->objcOptimizer.objcDylibs.empty() )
+        return;
+
+    Stats        stats(this->config);
+    Timer::Scope timedScope(this->config, "findObjCCategories time");
+
+    // Reserve space for 15k categories, as we have 10k as of writing
+    const uint32_t numCategoriesToReserve = 1 << 14;
+    this->objcCategoryOptimizer.categories.reserve(numCategoriesToReserve);
+    size_t objcIndex = 0;
+    for (size_t cacheIndex = 0; cacheIndex < this->cacheDylibs.size(); cacheIndex++) {
+        CacheDylib& cacheDylib = this->cacheDylibs[cacheIndex];
+        if ( !cacheDylib.inputMF->hasObjC() )
+            continue;
+
+        // Skip dylibs with opcode fixups, as the Category visitor operates on chained fixups to find classes
+        if ( cacheDylib.inputMF->hasOpcodeFixups() ) {
+            this->objcCategoryOptimizer.excludedDylibs.insert(objcIndex);
+            objcIndex++;
+            continue;
+        }
+        struct BindTarget {
+            std::string_view        symbolName;
+            std::optional<uint32_t> targetDylibIndex;
+            bool                    isWeakImport     = false;
+        };
+        __block std::vector<BindTarget> bindTargets;
+        __block Diagnostics diag;
+        cacheDylib.inputMF->withFileLayout(diag, ^(const mach_o::Layout& layout) {
+            mach_o::Fixups fixups(layout);
+
+            fixups.forEachBindTarget(diag, false, 0, ^(const mach_o::Fixups::BindTargetInfo& info, bool& stop) {
+                if ( info.libOrdinal == BIND_SPECIAL_DYLIB_SELF ) {
+                    bindTargets.push_back({ info.symbolName, cacheDylib.cacheIndex, info.weakImport });
+                } else if ( info.libOrdinal < 0 ) {
+                    // A special ordinal such as weak.  Just put in a placeholder for now
+                    bindTargets.push_back({ info.symbolName, std::nullopt, info.weakImport });
+                } else {
+                    assert(info.libOrdinal <= (int)cacheDylib.dependents.size());
+                    const CacheDylib *targetDylib = cacheDylib.dependents[info.libOrdinal-1].dylib;
+                    assert(info.weakImport || (targetDylib != nullptr));
+                    std::optional<uint32_t> targetDylibIndex;
+                    if ( targetDylib != nullptr )
+                        targetDylibIndex = targetDylib->cacheIndex;
+                    bindTargets.push_back({ info.symbolName, targetDylibIndex, info.weakImport });
+                }
+
+                if ( diag.hasError() )
+                    stop = true;
+            }, ^(const mach_o::Fixups::BindTargetInfo& info, bool& stop) {
+                // This shouldn't happen with chained fixups
+                assert(0);
+            });
+        });
+        diag.assertNoError();
+
+        __block objc_visitor::Visitor objCVisitor = makeInputDylibObjCVisitor(cacheDylib);
+
+        __block bool categoriesHaveClassProperties = false;
+        objCVisitor.withImageInfo(^(const uint32_t version, const uint32_t flags) {
+            const uint64_t hasCategoryClassPropertiesFlag = (1 << 6);
+            categoriesHaveClassProperties = (flags & hasCategoryClassPropertiesFlag);
+        });
+
+        objCVisitor.forEachCategory(^(const objc_visitor::Category &objcCategory, bool &stopCategory) {
+
+            // Skip catlist2 entries.  These are only for Swift stub classes
+            if ( objcCategory.isForSwiftStubClass() )
+                return;
+
+            __block ObjCCategoryOptimizer::Category objCCategoryInfo(objcCategory.getName(objCVisitor));
+            objCCategoryInfo.dylibObjcIndex = objcIndex;
+            objCCategoryInfo.vmAddress = objcCategory.getVMAddress();
+
+            objcCategory.withClass(objCVisitor, ^(const dyld3::MachOFile::ChainedFixupPointerOnDisk *fixup, uint16_t pointerFormat) {
+                assert(fixup->raw64 != 0);
+
+                uint64_t runtimeOffset = 0;
+                if ( fixup->isRebase(pointerFormat, objCVisitor.getOnDiskDylibChainedPointerBaseAddress().rawValue(), runtimeOffset) ) {
+                    // Rebase to a class in this image.
+                    objCCategoryInfo.classDylibIndex = cacheDylib.cacheIndex;
+                    objCCategoryInfo.classVMAddress = VMAddress(runtimeOffset);
+                } else {
+                    uint32_t bindOrdinal = 0;
+                    int64_t bindAddend = 0;
+                    if ( fixup->isBind(pointerFormat, bindOrdinal, bindAddend) ) {
+                        BindTarget& bindTarget = bindTargets[bindOrdinal];
+                        FoundSymbol foundSymbol = findTargetClass(diag, this->cacheDylibs,
+                                                                  bindTarget.symbolName,
+                                                                  bindTarget.targetDylibIndex);
+                        if ( foundSymbol.foundInDylib == nullptr ) {
+                            // Ignore category if class is missing. Usually due to a weak-link
+                            if ( !bindTarget.isWeakImport ) {
+                                this->warning("Class %s could not be found for category %s in %s.", bindTarget.symbolName.data(), objCCategoryInfo.name.data(), cacheDylib.installName.data());
+                            }
+                            return;
+                        }
+                        objCCategoryInfo.classVMAddress = VMAddress(foundSymbol.offsetInDylib.rawValue());
+                        objCCategoryInfo.classDylibIndex = foundSymbol.foundInDylib->cacheIndex;
+                    } else {
+                        assert(0);
+                    }
+                }
+            });
+
+            if ( !objCCategoryInfo.classVMAddress.has_value() )
+                return;
+
+            // instance methods
+            {
+                objc_visitor::MethodList objcMethodList = objcCategory.getInstanceMethods(objCVisitor);
+                uint32_t numMethods = objcMethodList.numMethods();
+                if ( numMethods > 0 )
+                    objCCategoryInfo.iMethodListVMAddress = objcMethodList.getVMAddress().value();
+            }
+            
+            // class methods
+            {
+                objc_visitor::MethodList objcMethodList = objcCategory.getClassMethods(objCVisitor);
+                uint32_t numMethods = objcMethodList.numMethods();
+                if ( numMethods > 0 )
+                    objCCategoryInfo.cMethodListVMAddress = objcMethodList.getVMAddress().value();
+            }
+
+            // protocols
+            {
+                objc_visitor::ProtocolList protocols = objcCategory.getProtocols(objCVisitor);
+                uint64_t numProtocols = protocols.numProtocols(objCVisitor);
+                if ( numProtocols > 0 )
+                    objCCategoryInfo.protocolListVMAddress = protocols.getVMAddress().value();
+            }
+
+            // instance properties
+            {
+                objc_visitor::PropertyList objcPropertyList = objcCategory.getInstanceProperties(objCVisitor);
+                uint64_t numProperties = objcPropertyList.numProperties();
+                if ( numProperties > 0 )
+                    objCCategoryInfo.iPropertyListVMAddress = objcPropertyList.getVMAddress().value();
+            }
+
+            // class properties
+            if ( categoriesHaveClassProperties ) {
+                objc_visitor::PropertyList objcPropertyList = objcCategory.getClassProperties(objCVisitor);
+                uint64_t numProperties = objcPropertyList.numProperties();
+                if ( numProperties > 0 )
+                    objCCategoryInfo.cPropertyListVMAddress = objcPropertyList.getVMAddress().value();
+            }
+            this->objcCategoryOptimizer.categories.push_back(std::move(objCCategoryInfo));
+        });
+        objcIndex++;
+    }
+
+    if ( this->config.log.printStats ) {
+        stats.add("  objc: found %lld categories\n", (uint64_t)this->objcCategoryOptimizer.categories.size());
+    }
+}
+
 static uint32_t hashTableSize(uint32_t maxElements, uint32_t perElementData)
 {
     uint32_t elementsWithPadding = maxElements * 11 / 10; // if close to power of 2, perfect hash may fail, so don't get within 10% of that
@@ -1810,6 +2325,71 @@
     }
 }
 
+void SharedCacheBuilder::calculateObjCCategoriesSize()
+{
+    if ( this->objcOptimizer.objcDylibs.empty() )
+        return;
+
+    Stats        stats(this->config);
+    Timer::Scope timedScope(this->config, "calculateObjCCanonicalCategoriesSize time");
+
+    uint64_t sizeAndCountSize = sizeof(struct ListOfListsEntry);
+    uint64_t listEntrySize = sizeof(struct ListOfListsEntry);
+
+    // Add an empty method list that all lists of lists can use if the need it
+    // This is used for classes when they don't have method lists, but we want them to
+    // FIXME: Get the size from somewhere.  Its really the { uint32_t entsize; uint32_t count }
+    this->objcCategoryOptimizer.categoriesTotalByteSize += 8;
+
+    // This will allocate one listEntrySize for each category list.
+    // We might end up using less if the category does not extend a specific list.
+    // This will also allocate one listEntrySize and one sizeAndCountSize
+    // for every list in the original class.
+    // We might end up using less due to multiple categories extending the same class.
+    for ( auto& categoryInfo : this->objcCategoryOptimizer.categories ) {
+
+        if ( categoryInfo.iMethodListVMAddress.has_value()) {
+            // instance methods
+            this->objcCategoryOptimizer.categoriesTotalByteSize += sizeAndCountSize;
+            this->objcCategoryOptimizer.categoriesTotalByteSize += listEntrySize;
+            // original instance methods
+            this->objcCategoryOptimizer.categoriesTotalByteSize += listEntrySize;
+        }
+        if ( categoryInfo.cMethodListVMAddress.has_value()) {
+            // class methods
+            this->objcCategoryOptimizer.categoriesTotalByteSize += sizeAndCountSize;
+            this->objcCategoryOptimizer.categoriesTotalByteSize += listEntrySize;
+            // original class methods
+            this->objcCategoryOptimizer.categoriesTotalByteSize += listEntrySize;
+        }
+        if ( categoryInfo.protocolListVMAddress.has_value()) {
+            // protocols
+            this->objcCategoryOptimizer.categoriesTotalByteSize += sizeAndCountSize;
+            this->objcCategoryOptimizer.categoriesTotalByteSize += listEntrySize;
+            // original protocols
+            this->objcCategoryOptimizer.categoriesTotalByteSize += listEntrySize;
+        }
+        if ( categoryInfo.iPropertyListVMAddress.has_value()) {
+            // instance properties
+            this->objcCategoryOptimizer.categoriesTotalByteSize += sizeAndCountSize;
+            this->objcCategoryOptimizer.categoriesTotalByteSize += listEntrySize;
+            // original instance properties
+            this->objcCategoryOptimizer.categoriesTotalByteSize += listEntrySize;
+        }
+        if ( categoryInfo.cPropertyListVMAddress.has_value()) {
+            // instance properties
+            this->objcCategoryOptimizer.categoriesTotalByteSize += sizeAndCountSize;
+            this->objcCategoryOptimizer.categoriesTotalByteSize += listEntrySize;
+            // original class properties
+            this->objcCategoryOptimizer.categoriesTotalByteSize += listEntrySize;
+        }
+    }
+
+    if ( this->config.log.printStats ) {
+        stats.add("  objc: categories size: %lld\n", (uint64_t)this->objcCategoryOptimizer.categoriesTotalByteSize);
+    }
+}
+
 // Each conformance entry is 3 uint64_t's internally, plus the space for the hash table
 static uint32_t swiftHashTableSize(uint32_t maxElements)
 {
@@ -1826,6 +2406,11 @@
 
     // Add in the 3 uint64_t's for the payload
     return hashTableSize + (3 * sizeof(uint64_t) * maxElements);
+}
+
+static uint32_t ptrHashTableSize(uint32_t maxElement, uint32_t numPointerKeys)
+{
+    return swiftHashTableSize(maxElement) + numPointerKeys * sizeof(uint64_t);
 }
 
 void SharedCacheBuilder::estimateSwiftHashTableSizes()
@@ -1883,6 +2468,28 @@
     }
 
     auto& optimizer = this->swiftProtocolConformanceOptimizer;
+
+    if ( swiftPrespecializedDylib ) {
+        Diagnostics diagVal;
+        Diagnostics& diag = diagVal;
+        SwiftVisitor swiftVisitorVal = makeInputDylibSwiftVisitor(*swiftPrespecializedDylib);
+        SwiftVisitor& swiftVisitor = swiftVisitorVal;
+        swiftVisitor.forEachPointerHashTable(diag, ^(metadata_visitor::ResolvedValue sectionBase, size_t tableIndex, uint8_t *tableStart, size_t numEntries) {
+            assert(optimizer.prespecializedMetadataHashTables.size() == tableIndex);
+
+            PointerHashTableOptimizerInfo& tableInfo = optimizer.prespecializedMetadataHashTables.emplace_back();
+            swiftVisitor.forEachPointerHashTableRelativeEntry(diag, tableStart, VMAddress(0ull), ^(size_t index, std::span<uint64_t> keys, uint64_t value) {
+                assert(!keys.empty() && "pointer keys can't be empty");
+
+                ++tableInfo.numEntries;
+                tableInfo.numPointerKeys += (uint32_t)keys.size();
+            });
+
+            tableInfo.size = ptrHashTableSize(tableInfo.numEntries, tableInfo.numPointerKeys);
+            assert(tableInfo.numEntries == numEntries);
+        });
+    }
+
     optimizer.typeConformancesHashTableSize = swiftHashTableSize(numTypeConformances);
     optimizer.metadataConformancesHashTableSize = swiftHashTableSize(numMetadataConformances);
     optimizer.foreignTypeConformancesHashTableSize = swiftHashTableSize(numForeignConformances);
@@ -1892,6 +2499,12 @@
                   (uint64_t)optimizer.typeConformancesHashTableSize, numTypeConformances);
         stats.add("  swift: metadata hash table estimated size: %lld (from %d entries)\n", (uint64_t)optimizer.metadataConformancesHashTableSize, numMetadataConformances);
         stats.add("  swift: foreign metadata hash table estimated size: %lld (from %d entries)\n", (uint64_t)optimizer.foreignTypeConformancesHashTableSize, numForeignConformances);
+
+        stats.add("  swift: prespecialized metadata hash tables %lu\n", optimizer.prespecializedMetadataHashTables.size());
+        for ( int i = 0; i < optimizer.prespecializedMetadataHashTables.size(); ++i ) {
+            const PointerHashTableOptimizerInfo& tableInfo = optimizer.prespecializedMetadataHashTables[i];
+            stats.add("  swift: prespecialized metadata hash table #%d. estimated size: %lld (from %u entries)\n", i, (uint64_t)tableInfo.size, tableInfo.numEntries);
+        }
     }
 }
 
@@ -1964,6 +2577,8 @@
     __block uint32_t numBinds = 0;
     uint32_t numClients = 0;
     for ( const CacheDylib& cacheDylib : this->cacheDylibs ) {
+        if ( !cacheDylib.needsPatchTable )
+            continue;
         __block Diagnostics diag;
         cacheDylib.inputMF->withFileLayout(diag, ^(const mach_o::Layout& layout) {
             mach_o::Fixups fixups(layout);
@@ -2048,7 +2663,7 @@
             size += cacheDylib.inputFile->path.size() + 1;
             size = alignTo(size, alignof(dyld4::Loader::LoaderRef));
             size += sizeof(dyld4::Loader::LoaderRef) * cacheDylib.dependents.size();
-            size += sizeof(Loader::DependentKind) * cacheDylib.dependents.size();
+            size += sizeof(Loader::LinkedDylibAttributes) * cacheDylib.dependents.size();
             size += sizeof(Loader::FileValidationInfo);
             size += sizeof(Loader::Region) * cacheDylib.segments.size();
 
@@ -2163,16 +2778,7 @@
 void SharedCacheBuilder::computeSubCaches()
 {
     Timer::Scope timedScope(this->config, "computeSubCaches time");
-
-    // We have 3 different kinds of caches.
-    // - regular: put everything in a single file
-    // - large: A file is (TEXT, DATA, LINKEDIT), and we might have > 1 file
-    // - split: A file is TEXT/DATA/LINKEDIT, and we've have 1 or more TEXT, and exactly 1 DATA and LINKEDIT
-    if ( config.layout.large.has_value() ) {
-        computeLargeSubCache();
-    } else {
-        computeRegularSubCache();
-    }
+    computeLargeSubCache();
 }
 
 // ObjC/Swift optimizations produce arrays, hash tables, string sections, etc.
@@ -2185,27 +2791,33 @@
     // Add canonical objc protocols
     subCache.addObjCCanonicalProtocolsChunk(this->config, this->objcProtocolOptimizer);
 
+    // Add objc categories
+    subCache.addObjCCategoriesChunk(this->config, this->objcCategoryOptimizer);
+
     // Add objc opts header
     subCache.addObjCOptsHeaderChunk(this->objcOptimizer);
 
     // Add objc header info RO
-    subCache.addObjCHeaderInfoReadOnlyChunk(this->objcOptimizer);
+    subCache.addObjCHeaderInfoReadOnlyChunk(this->config, this->objcOptimizer);
+
+    // Add objc image info
+    subCache.addObjCImageInfoChunk(this->config, this->objcOptimizer);
 
     // Add selector strings and hash table. These need to be adjacent as the table has offsets in
     // to the string section
-    subCache.addObjCSelectorStringsChunk(this->objcSelectorOptimizer);
-    subCache.addObjCSelectorHashTableChunk(this->objcSelectorOptimizer);
+    subCache.addObjCSelectorStringsChunk(this->config, this->objcSelectorOptimizer);
+    subCache.addObjCSelectorHashTableChunk(this->config, this->objcSelectorOptimizer);
 
     // Add class name strings and hash table
-    subCache.addObjCClassNameStringsChunk(this->objcClassOptimizer);
-    subCache.addObjCClassHashTableChunk(this->objcClassOptimizer);
+    subCache.addObjCClassNameStringsChunk(this->config, this->objcClassOptimizer);
+    subCache.addObjCClassHashTableChunk(this->config, this->objcClassOptimizer);
 
     // Add protocol name strings and hash table
-    subCache.addObjCProtocolNameStringsChunk(this->objcProtocolOptimizer);
-    subCache.addObjCProtocolHashTableChunk(this->objcProtocolOptimizer);
+    subCache.addObjCProtocolNameStringsChunk(this->config, this->objcProtocolOptimizer);
+    subCache.addObjCProtocolHashTableChunk(this->config, this->objcProtocolOptimizer);
 
     // Add Swift demangled name strings found in ObjC protocol metadata
-    subCache.addObjCProtocolSwiftDemangledNamesChunk(this->objcProtocolOptimizer);
+    subCache.addObjCProtocolSwiftDemangledNamesChunk(this->config, this->objcProtocolOptimizer);
 
     // Add ObjC IMP Caches
     subCache.addObjCIMPCachesChunk(this->objcIMPCachesOptimizer);
@@ -2217,6 +2829,7 @@
     subCache.addSwiftTypeHashTableChunk(this->swiftProtocolConformanceOptimizer);
     subCache.addSwiftMetadataHashTableChunk(this->swiftProtocolConformanceOptimizer);
     subCache.addSwiftForeignHashTableChunk(this->swiftProtocolConformanceOptimizer);
+    subCache.addSwiftPrespecializedMetadataPointerTableChunks(this->swiftProtocolConformanceOptimizer);
 }
 
 // The shared cache contains many global optimizations such as dyld4 loaders, trie's, etc.
@@ -2244,7 +2857,7 @@
 // anything we need, based on whatever else is already in the SubCache.
 void SharedCacheBuilder::addFinalChunksToSubCache(SubCache& subCache)
 {
-    subCache.addCacheHeaderChunk(this->cacheDylibs);
+    subCache.addCacheHeaderChunk(this->config, this->cacheDylibs);
 
     // Add slide info for each DATA/AUTH segment.  Do this after we've added any other DATA*
     // segments
@@ -2256,30 +2869,6 @@
 
     // Finalize the SubCache, by removing any unused regions
     subCache.removeEmptyRegions();
-}
-
-void SharedCacheBuilder::computeRegularSubCache()
-{
-    // Put everything into a single file.
-    SubCache subCache = SubCache::makeMainCache(this->options, true);
-
-    // Add all the objc tables.  This must be done before we add libobjc's __TEXT
-    this->addObjCOptimizationsToSubCache(subCache);
-
-    for ( CacheDylib& cacheDylib : this->cacheDylibs ) {
-        bool addLinkedit = true;
-        subCache.addDylib(cacheDylib, addLinkedit);
-    }
-
-    // Add all the global optimizations
-    this->addGlobalOptimizationsToSubCache(subCache);
-
-    // Reserve space in the last sub cache for dynamic config data
-    subCache.addDynamicConfigChunk();
-
-    this->addFinalChunksToSubCache(subCache);
-
-    this->subCaches.push_back(std::move(subCache));
 }
 
 // Add stubs Chunk's for every stubs section in the given text subCache
@@ -2453,13 +3042,16 @@
                         // Nothing to do here
                         break;
                     case cache_builder::Region::Kind::dataConst:
+                    case cache_builder::Region::Kind::tproConst:
                     case cache_builder::Region::Kind::data:
                     case cache_builder::Region::Kind::auth:
-                    case cache_builder::Region::Kind::authConst: {
+                    case cache_builder::Region::Kind::authConst:
+                    case cache_builder::Region::Kind::tproAuthConst:{
                         Region& newRegion = newSubCache.regions[(uint32_t)oldRegion.kind];
                         newRegion.chunks = std::move(oldRegion.chunks);
                         break;
                     }
+                    case cache_builder::Region::Kind::readOnly:
                     case cache_builder::Region::Kind::linkedit:
                     case cache_builder::Region::Kind::unmapped:
                     case cache_builder::Region::Kind::dynamicConfig:
@@ -2481,10 +3073,6 @@
                                                   std::list<SubCache>& otherCaches)
 {
     SubCache* currentSubCache = firstSubCache;
-
-    // We'll add LINKEDIT at the end.  As the shared region is <= 4GB in size, we can fit
-    // all the LINKEDIT in the last subCache and still keep it in range of 32-bit offsets
-    bool allLinkeditInLastSubCache = this->config.layout.allLinkeditInLastSubCache;
 
     // Walk all the dylibs, and create a new subCache every time we are about to cross
     // the subCacheTextLimit
@@ -2499,7 +3087,7 @@
 
         // If we exceed the current limit, then the current subCache is complete and we need
         // to start a new one
-        if ( (subCacheTextSize + textSize) > this->config.layout.large->subCacheTextLimit ) {
+        if ( (subCacheTextSize + textSize) > this->config.layout.subCacheTextLimit ) {
             // Create a new subCache
             otherCaches.push_back(SubCache::makeSubCache(this->options));
             currentSubCache = &otherCaches.back();
@@ -2512,23 +3100,16 @@
 
         // The subCache with libobjc gets the header info sections
         // Add all the objc tables.  This must be done before we add libobjc's __TEXT
-        if ( cacheDylib.installName == "/usr/lib/libobjc.A.dylib" )
+        std::string_view libObjcInstallName = "/usr/lib/libobjc.A.dylib";
+        if ( dyld3::MachOFile::isExclaveKitPlatform(this->options.platform) )
+            libObjcInstallName = "/System/ExclaveKit/usr/lib/libobjc.A.dylib";
+        if ( cacheDylib.installName == libObjcInstallName )
             this->addObjCOptimizationsToSubCache(*currentSubCache);
 
-        // We'll add LINKEDIT at the end.  As the shared region is <= 4GB in size, we can fit
-        // all the LINKEDIT in the last subCache and still keep it in range of 32-bit offsets
-        bool addLinkedit = !allLinkeditInLastSubCache;
-        currentSubCache->addDylib(cacheDylib, addLinkedit);
+        currentSubCache->addDylib(this->config, cacheDylib);
     }
 
     // Add all the remaining content in to the final (current) subCache
-
-    // Add linkedit chunks from dylibs, if needed
-    if ( allLinkeditInLastSubCache ) {
-        for ( CacheDylib& cacheDylib : this->cacheDylibs )
-            currentSubCache->addLinkeditFromDylib(cacheDylib);
-    }
-
 
     // Add all the global optimizations
     this->addGlobalOptimizationsToSubCache(*currentSubCache);
@@ -2907,6 +3488,13 @@
                 sourceStringSize += symbolString.size() + 1;
                 ++sourceStringCount;
 
+                // rdar://129398821 (dyld cache builder add support for binds relative to dylib segments)
+                // skip synthetic dyld symbols
+                if ( symbolString.find("$dyld$") != std::string_view::npos ) {
+                    ++oldSymbolIndex;
+                    return;
+                }
+
                 auto itAndInserted = stringMap.insert({ symbolString, stringBufferSize });
                 // If we inserted the string, then account for the space
                 if ( itAndInserted.second )
@@ -3363,7 +3951,7 @@
 Error SharedCacheBuilder::calculateUniqueGOTs()
 {
     // Skip this optimiation on simulator until we've qualified it there
-    if ( this->options.isSimultor() )
+    if ( this->options.isSimulator() )
         return Error();
 
     Stats        stats(this->config);
@@ -3395,20 +3983,51 @@
         if ( (dataConstRegion == nullptr) && (authConstRegion == nullptr) )
             continue;
 
-        for ( bool auth : { false, true } ) {
-            if ( auth && (authConstRegion == nullptr) )
-                continue;
-            if ( !auth && (dataConstRegion == nullptr) )
-                continue;
-
-            Region& region = auth ? *authConstRegion : *dataConstRegion;
-            std::string_view segmentName = auth ? "__AUTH_CONST" : "__DATA_CONST";
-            std::string_view sectionName = auth ? "__auth_got" : "__got";
-            CoalescedGOTSection& subCacheUniquedGOTs = auth ? subCache.uniquedGOTsOptimizer.authGOTs : subCache.uniquedGOTsOptimizer.regularGOTs;
+        for ( UniquedGOTKind sectionKind : { UniquedGOTKind::regular, UniquedGOTKind::authGot, UniquedGOTKind::authPtr } ) {
+
+            Region* region = nullptr;
+            std::string_view segmentName;
+            std::string_view sectionName;
+            const char* kindName = nullptr;
+            CoalescedGOTSection* subCacheUniquedGOTs = nullptr;
+
+            // Skip sections if their segment doesn't exist
+            switch ( sectionKind ) {
+                case UniquedGOTKind::regular:
+                    if ( dataConstRegion == nullptr )
+                        continue;
+
+                    region = dataConstRegion;
+                    segmentName = "__DATA_CONST";
+                    sectionName = "__got";
+                    kindName = "regular";
+                    subCacheUniquedGOTs = &subCache.uniquedGOTsOptimizer.regularGOTs;
+                    break;
+                case UniquedGOTKind::authGot:
+                    if ( authConstRegion == nullptr )
+                        continue;
+
+                    region = authConstRegion;
+                    segmentName = "__AUTH_CONST";
+                    sectionName = "__auth_got";
+                    kindName = "auth-gots";
+                    subCacheUniquedGOTs = &subCache.uniquedGOTsOptimizer.authGOTs;
+                    break;
+                case UniquedGOTKind::authPtr:
+                    if ( authConstRegion == nullptr )
+                        continue;
+
+                    region = authConstRegion;
+                    segmentName = "__AUTH_CONST";
+                    sectionName = "__auth_ptr";
+                    kindName = "auth-ptrs";
+                    subCacheUniquedGOTs = &subCache.uniquedGOTsOptimizer.authPtrs;
+                    break;
+            }
 
             std::vector<DylibSectionCoalescer::OptimizedSection*> dylibOptimizedSections;
-            dylibOptimizedSections.reserve(region.chunks.size());
-            for ( const Chunk* chunk : region.chunks ) {
+            dylibOptimizedSections.reserve(region->chunks.size());
+            for ( const Chunk* chunk : region->chunks ) {
                 const DylibSegmentChunk* segmentChunk = chunk->isDylibSegmentChunk();
                 if ( !segmentChunk )
                     continue;
@@ -3416,17 +4035,28 @@
                 if ( chunk->name() != segmentName )
                     continue;
 
-                CacheDylib*                 dylib = fileToDylibMap.at(segmentChunk->inputFile);
-                auto&                       dylibUniquedGOTs = auth ? dylib->optimizedSections.auth_gots : dylib->optimizedSections.gots;
+                CacheDylib* dylib = fileToDylibMap.at(segmentChunk->inputFile);
+                DylibSectionCoalescer::OptimizedSection* dylibUniquedGOTs = nullptr;
+                switch ( sectionKind ) {
+                    case UniquedGOTKind::regular:
+                        dylibUniquedGOTs = &dylib->optimizedSections.gots;
+                        break;
+                    case UniquedGOTKind::authGot:
+                        dylibUniquedGOTs = &dylib->optimizedSections.auth_gots;
+                        break;
+                    case UniquedGOTKind::authPtr:
+                        dylibUniquedGOTs = &dylib->optimizedSections.auth_ptrs;
+                        break;
+                }
 
                 // Set the dylib GOTs to point to the subCache they'll be uniqued to
-                dylibUniquedGOTs.subCacheSection = &subCacheUniquedGOTs;
-                dylibOptimizedSections.push_back(&dylibUniquedGOTs);
-
-                parseGOTs(dylib, segmentChunk, segmentName, sectionName, dylibUniquedGOTs);
-            }
-
-            if ( subCacheUniquedGOTs.gotTargetsToOffsets.empty() )
+                dylibUniquedGOTs->subCacheSection = subCacheUniquedGOTs;
+                dylibOptimizedSections.push_back(dylibUniquedGOTs);
+
+                parseGOTs(dylib, segmentChunk, segmentName, sectionName, *dylibUniquedGOTs);
+            }
+
+            if ( subCacheUniquedGOTs->gotTargetsToOffsets.empty() )
                 continue;
 
             // Sort the coalesced GOTs based on the target install name.  We find GOTs in the order we parse
@@ -3434,8 +4064,8 @@
             // each other
             typedef CoalescedGOTSection::GOTKey Key;
             std::vector<Key> sortedKeys;
-            sortedKeys.reserve(subCacheUniquedGOTs.gotTargetsToOffsets.size());
-            for ( const auto& keyAndValue : subCacheUniquedGOTs.gotTargetsToOffsets )
+            sortedKeys.reserve(subCacheUniquedGOTs->gotTargetsToOffsets.size());
+            for ( const auto& keyAndValue : subCacheUniquedGOTs->gotTargetsToOffsets )
                 sortedKeys.push_back(keyAndValue.first);
 
             std::sort(sortedKeys.begin(), sortedKeys.end(),
@@ -3463,8 +4093,8 @@
             std::unordered_map<uint32_t, uint32_t> oldToNewOffsetMap;
             for ( uint32_t i = 0; i != sortedKeys.size(); ++i ) {
                 const Key& key = sortedKeys[i];
-                auto it = subCacheUniquedGOTs.gotTargetsToOffsets.find(key);
-                assert(it != subCacheUniquedGOTs.gotTargetsToOffsets.end());
+                auto it = subCacheUniquedGOTs->gotTargetsToOffsets.find(key);
+                assert(it != subCacheUniquedGOTs->gotTargetsToOffsets.end());
 
                 uint32_t newCacheSectionOffset = i * pointerSize;
 
@@ -3489,26 +4119,34 @@
             }
 
             // Add the new chunks to the subCache
-            if ( auth ) {
-                subCache.uniquedAuthGOTs                    = std::make_unique<UniquedGOTsChunk>();
-                subCache.uniquedAuthGOTs->cacheVMSize       = CacheVMSize((uint64_t)subCacheUniquedGOTs.gotTargetsToOffsets.size() * pointerSize);
-                subCache.uniquedAuthGOTs->subCacheFileSize  = CacheFileSize((uint64_t)subCacheUniquedGOTs.gotTargetsToOffsets.size() * pointerSize);
-
-                region.chunks.push_back(subCache.uniquedAuthGOTs.get());
-
-                // FIXME: Do we need this. No-one seems to read it from here, or could get it from the subCache instead
-                subCache.uniquedGOTsOptimizer.authGOTsChunk = subCache.uniquedAuthGOTs.get();
-                subCache.uniquedGOTsOptimizer.authGOTs.cacheChunk = subCache.uniquedGOTsOptimizer.authGOTsChunk;
-            } else {
-                subCache.uniquedGOTs                    = std::make_unique<UniquedGOTsChunk>();
-                subCache.uniquedGOTs->cacheVMSize       = CacheVMSize((uint64_t)subCacheUniquedGOTs.gotTargetsToOffsets.size() * pointerSize);
-                subCache.uniquedGOTs->subCacheFileSize  = CacheFileSize((uint64_t)subCacheUniquedGOTs.gotTargetsToOffsets.size() * pointerSize);
-
-                region.chunks.push_back(subCache.uniquedGOTs.get());
-
-                // FIXME: Do we need this. No-one seems to read it from here, or could get it from the subCache instead
-                subCache.uniquedGOTsOptimizer.regularGOTsChunk = subCache.uniquedGOTs.get();
-                subCache.uniquedGOTsOptimizer.regularGOTs.cacheChunk = subCache.uniquedGOTsOptimizer.regularGOTsChunk;
+            switch ( sectionKind ) {
+                case UniquedGOTKind::regular:
+                    subCache.uniquedGOTs                    = std::make_unique<UniquedGOTsChunk>();
+                    subCache.uniquedGOTs->cacheVMSize       = CacheVMSize((uint64_t)subCacheUniquedGOTs->gotTargetsToOffsets.size() * pointerSize);
+                    subCache.uniquedGOTs->subCacheFileSize  = CacheFileSize((uint64_t)subCacheUniquedGOTs->gotTargetsToOffsets.size() * pointerSize);
+
+                    region->chunks.push_back(subCache.uniquedGOTs.get());
+
+                    subCache.uniquedGOTsOptimizer.regularGOTs.cacheChunk = subCache.uniquedGOTs.get();
+                    break;
+                case UniquedGOTKind::authGot:
+                    subCache.uniquedAuthGOTs                    = std::make_unique<UniquedGOTsChunk>();
+                    subCache.uniquedAuthGOTs->cacheVMSize       = CacheVMSize((uint64_t)subCacheUniquedGOTs->gotTargetsToOffsets.size() * pointerSize);
+                    subCache.uniquedAuthGOTs->subCacheFileSize  = CacheFileSize((uint64_t)subCacheUniquedGOTs->gotTargetsToOffsets.size() * pointerSize);
+
+                    region->chunks.push_back(subCache.uniquedAuthGOTs.get());
+
+                    subCache.uniquedGOTsOptimizer.authGOTs.cacheChunk = subCache.uniquedAuthGOTs.get();
+                    break;
+                case UniquedGOTKind::authPtr:
+                    subCache.uniquedAuthPtrs                    = std::make_unique<UniquedGOTsChunk>();
+                    subCache.uniquedAuthPtrs->cacheVMSize       = CacheVMSize((uint64_t)subCacheUniquedGOTs->gotTargetsToOffsets.size() * pointerSize);
+                    subCache.uniquedAuthPtrs->subCacheFileSize  = CacheFileSize((uint64_t)subCacheUniquedGOTs->gotTargetsToOffsets.size() * pointerSize);
+
+                    region->chunks.push_back(subCache.uniquedAuthPtrs.get());
+
+                    subCache.uniquedGOTsOptimizer.authPtrs.cacheChunk = subCache.uniquedAuthPtrs.get();
+                    break;
             }
 
             if ( this->config.log.printStats ) {
@@ -3516,9 +4154,8 @@
                 for ( DylibSectionCoalescer::OptimizedSection* dylibOptimizedSection : dylibOptimizedSections ) {
                     totalSourceGOTs += dylibOptimizedSection->offsetMap.size();
                 }
-                const char* kind = auth ? "auth" : "regular";
                 stats.add("  got uniquing: uniqued %lld %s GOTs to %lld GOTs\n",
-                          totalSourceGOTs, kind, (uint64_t)subCacheUniquedGOTs.gotTargetsToOffsets.size());
+                          totalSourceGOTs, kindName, (uint64_t)subCacheUniquedGOTs->gotTargetsToOffsets.size());
             }
         }
     }
@@ -3551,7 +4188,8 @@
         const DylibSegmentChunk* segmentA = a->isDylibSegmentChunk();
         const DylibSegmentChunk* segmentB = b->isDylibSegmentChunk();
 
-        if ( segmentA->kind == DylibSegmentChunk::Kind::dylibDataDirty ) {
+        // There can be data chunks that aren't dylib segments, e.g. ObjCHeaderInfoReadWriteChunk.
+        if ( segmentA && segmentB && segmentA->kind == DylibSegmentChunk::Kind::dylibDataDirty ) {
             const auto& orderA = dirtyDataSegmentOrdering.find(segmentA->inputFile->path);
             const auto& orderB = dirtyDataSegmentOrdering.find(segmentB->inputFile->path);
             bool        foundA = (orderA != dirtyDataSegmentOrdering.end());
@@ -3567,11 +4205,51 @@
                 return false;
         }
 
+        const DylibSegmentChunk* dylibA = a->isTPROChunk();
+        const DylibSegmentChunk* dylibB = b->isTPROChunk();
+        // Note this shouldn't be possible, but best to be safe and avoid asserting
+        if ( dylibA && dylibB ) {
+            // Sort dyld last so that its allocator gets packed with TPRO from other dylibs
+            bool isDyldA = dylibA->inputFile->path == "/usr/lib/dyld";
+            bool isDyldB = dylibB->inputFile->path == "/usr/lib/dyld";
+            if ( isDyldA != isDyldB )
+                return !isDyldA;
+        }
+
         // Note we are using a stable sort, so if the kind's aren't different, return false
         // and we'll keep Section's in the order they were added to the vector
         return false;
     };
 
+    auto dataConstSortOrder = [](const Chunk* a, const Chunk* b) -> bool {
+        // Sort TPRO_CONST before DATA_CONST. This only happens on x86_64
+        // where we put TPRO_CONST and DATA_CONST in the same Region
+        if ( a->sortOrder() != b->sortOrder() )
+            return a->sortOrder() < b->sortOrder();
+
+        // Note we are using a stable sort, so if the kind's aren't different, return false
+        // and we'll keep Section's in the order they were added to the vector
+        return false;
+    };
+
+    auto tproConstSortOrder = [](const Chunk* a, const Chunk* b) -> bool {
+        const DylibSegmentChunk* dylibA = a->isTPROChunk();
+        const DylibSegmentChunk* dylibB = b->isTPROChunk();
+        // Note this shouldn't be possible, but best to be safe and avoid asserting
+        if ( !dylibA || !dylibB )
+            return false;
+
+        // Sort dyld last so that its allocator gets packed with TPRO from other dylibs
+        bool isDyldA = dylibA->inputFile->path == "/usr/lib/dyld";
+        bool isDyldB = dylibB->inputFile->path == "/usr/lib/dyld";
+        if ( isDyldA != isDyldB )
+            return !isDyldA;
+
+        // Note we are using a stable sort, so if the kind's aren't different, return false
+        // and we'll keep Section's in the order they were added to the vector
+        return false;
+    };
+
     auto linkeditSortOrder = [](const Chunk* a, const Chunk* b) -> bool {
         // Sort read-only segments before LINKEDIT
         if ( a->sortOrder() != b->sortOrder() )
@@ -3582,7 +4260,6 @@
         return false;
     };
 
-    // Only sort data/auth.  Everything else is already in order
     for ( SubCache& subCache : this->subCaches ) {
         for ( Region& region : subCache.regions ) {
             if ( region.kind == Region::Kind::text ) {
@@ -3591,10 +4268,41 @@
             else if ( (region.kind == Region::Kind::data) || (region.kind == Region::Kind::auth) ) {
                 std::stable_sort(region.chunks.begin(), region.chunks.end(), dataSortOrder);
             }
+            else if ( region.kind == Region::Kind::dataConst ) {
+                std::stable_sort(region.chunks.begin(), region.chunks.end(), dataConstSortOrder);
+            }
+            else if ( (region.kind == Region::Kind::tproConst) || (region.kind == Region::Kind::tproAuthConst) ) {
+                std::stable_sort(region.chunks.begin(), region.chunks.end(), tproConstSortOrder);
+            }
             else if ( region.kind == Region::Kind::linkedit ) {
                 std::stable_sort(region.chunks.begin(), region.chunks.end(), linkeditSortOrder);
             }
         }
+    }
+
+    // After sorting, we have to add alignment chunks before/after x86_64 TPRO
+    if ( this->config.layout.tproIsInData )
+        addAlignmentChunks();
+}
+
+void SharedCacheBuilder::addAlignmentChunks()
+{
+    for ( SubCache& subCache : this->subCaches ) {
+        SubCache::forEachTPRORegionInData(&subCache, {}, ^(Region& region, const Chunk *firstChunk, const Chunk *lastChunk) {
+            // Add alignment before the first chunk
+            {
+                auto firstPos = std::find(region.chunks.begin(), region.chunks.end(), firstChunk);
+                assert(firstPos != region.chunks.end());
+                region.chunks.insert(firstPos, &region.alignmentChunks.emplace_back());
+            }
+
+            // Add alignment after the last chunk
+            {
+                auto lastPos = std::find(region.chunks.begin(), region.chunks.end(), lastChunk);
+                assert(lastPos != region.chunks.end());
+                region.chunks.insert(lastPos + 1, &region.alignmentChunks.emplace_back());
+            }
+        });
     }
 }
 
@@ -3643,6 +4351,9 @@
             case cache_builder::SlideInfo::SlideInfoFormat::v3:
                 slideInfoSize += sizeof(dyld_cache_slide_info3);
                 break;
+            case cache_builder::SlideInfo::SlideInfoFormat::v5:
+                slideInfoSize += sizeof(dyld_cache_slide_info5);
+                break;
         }
         slideInfoSize += pagesToSlide * builderConfig.slideInfo.slideInfoBytesPerDataPage;
 
@@ -3651,8 +4362,10 @@
     };
 
     for ( const SubCache& subCache : this->subCaches ) {
+        calculateRegionSlideInfoSize(this->config, Region::Kind::tproConst, subCache.regions, subCache.tproConstSlideInfo);
         calculateRegionSlideInfoSize(this->config, Region::Kind::data, subCache.regions, subCache.dataSlideInfo);
         calculateRegionSlideInfoSize(this->config, Region::Kind::dataConst, subCache.regions, subCache.dataConstSlideInfo);
+        calculateRegionSlideInfoSize(this->config, Region::Kind::tproAuthConst, subCache.regions, subCache.tproAuthConstSlideInfo);
         calculateRegionSlideInfoSize(this->config, Region::Kind::auth, subCache.regions, subCache.authSlideInfo);
         calculateRegionSlideInfoSize(this->config, Region::Kind::authConst, subCache.regions, subCache.authConstSlideInfo);
     }
@@ -3684,7 +4397,7 @@
 
 void SharedCacheBuilder::printSubCaches() const
 {
-    const bool printSegments = false;
+    const bool printSegments = this->config.log.printDebug;
 
     if ( !this->config.log.printStats )
         return;
@@ -3703,11 +4416,20 @@
                 case Region::Kind::dataConst:
                     regionName = "dataConst";
                     break;
+                case Region::Kind::tproConst:
+                    regionName = "tproConst";
+                    break;
                 case Region::Kind::auth:
                     regionName = "auth";
                     break;
                 case Region::Kind::authConst:
                     regionName = "authConst";
+                    break;
+                case Region::Kind::tproAuthConst:
+                    regionName = "tproAuthConst";
+                    break;
+                case Region::Kind::readOnly:
+                    regionName = "readOnly";
                     break;
                 case Region::Kind::linkedit:
                     regionName = "linkedit";
@@ -3862,106 +4584,6 @@
         assert(this->totalVMSize == totalCustomerCacheSize);
     }
 
-    if ( this->totalVMSize > this->config.layout.cacheSize ) {
-        return Error("Cache overflow (0x%llx > 0x%llx)",
-                     this->totalVMSize.rawValue(),
-                     this->config.layout.cacheSize.rawValue());
-    }
-
-    return Error();
-}
-
-// This is the x86_64 sim layout, where each of TEXT/DATA/LINKEDIT has its own fixed address
-Error SharedCacheBuilder::computeSubCacheDiscontiguousSimVMLayout()
-{
-    // Add padding between each region, and set the Region VMAddr's
-    CacheVMAddress maxVMAddress = this->config.layout.cacheBaseAddress;
-    assert(this->subCaches.size() == 1);
-    SubCache& subCache = this->subCaches.front();
-    subCache.subCacheVMAddress = this->config.layout.cacheBaseAddress;
-
-    bool seenText = false;
-    bool seenData = false;
-    bool seenLinkedit = false;
-    bool seenDynamicConfig = false;
-    CacheVMAddress lastDataEnd;
-    CacheVMAddress linkEditEnd;
-    for ( Region& region : subCache.regions ) {
-        switch ( region.kind ) {
-            case Region::Kind::text:
-                assert(!seenText);
-                seenText = true;
-                region.subCacheVMAddress = this->config.layout.discontiguous->simTextBaseAddress;
-
-                // Check for overflow
-                if ( region.subCacheVMSize > this->config.layout.discontiguous->simTextSize ) {
-                    return Error("Overflow in text (0x%llx > 0x%llx)",
-                                 region.subCacheVMSize.rawValue(),
-                                 this->config.layout.discontiguous->simTextSize.rawValue());
-                }
-                break;
-            case Region::Kind::dataConst:
-            case Region::Kind::data:
-            case Region::Kind::auth:
-            case Region::Kind::authConst:
-                if ( seenData ) {
-                    // This data follows from the previous one
-                    region.subCacheVMAddress = lastDataEnd;
-                } else {
-                    seenData = true;
-                    region.subCacheVMAddress = this->config.layout.discontiguous->simDataBaseAddress;
-                }
-                lastDataEnd = region.subCacheVMAddress + region.subCacheVMSize;
-                break;
-            case Region::Kind::linkedit:
-                assert(!seenLinkedit);
-                seenLinkedit = true;
-                region.subCacheVMAddress = this->config.layout.discontiguous->simLinkeditBaseAddress;
-
-                // Check for overflow
-                if ( region.subCacheVMSize > this->config.layout.discontiguous->simLinkeditSize ) {
-                    return Error("Overflow in linkedit (0x%llx > 0x%llx)",
-                                 region.subCacheVMSize.rawValue(),
-                                 this->config.layout.discontiguous->simLinkeditSize.rawValue());
-                }
-                linkEditEnd = region.subCacheVMAddress + region.subCacheVMSize;
-                break;
-            case Region::Kind::dynamicConfig:
-                assert(!seenDynamicConfig);
-                seenDynamicConfig = true;
-                // Grab space right after the linkedit
-                region.subCacheVMAddress = linkEditEnd;
-                // Check for overflow
-                if ( region.subCacheVMSize > this->config.layout.discontiguous->simLinkeditSize ) {
-                    return Error("Overflow in dynamicConfig (0x%llx > 0x%llx)",
-                                 region.subCacheVMSize.rawValue(),
-                                 this->config.layout.discontiguous->simLinkeditSize.rawValue());
-                }
-                break;
-            case Region::Kind::unmapped:
-            case Region::Kind::codeSignature:
-                break;
-            case Region::Kind::numKinds:
-                assert(0);
-                break;
-        }
-
-        if ( seenData ) {
-            // Check for overflow
-            CacheVMSize dataSize(lastDataEnd.rawValue() - this->config.layout.discontiguous->simDataBaseAddress.rawValue());
-            if ( dataSize > this->config.layout.discontiguous->simDataSize ) {
-                return Error("Overflow in data (0x%llx > 0x%llx)",
-                             dataSize.rawValue(),
-                             this->config.layout.discontiguous->simDataSize.rawValue());
-            }
-        }
-
-        if ( region.needsSharedCacheReserveAddressSpace() )
-            maxVMAddress = region.subCacheVMAddress + region.subCacheVMSize;
-    }
-
-    this->totalVMSize = CacheVMSize((maxVMAddress - this->config.layout.cacheBaseAddress).rawValue());
-
     return Error();
 }
 
@@ -4009,12 +4631,15 @@
                     case Region::Kind::codeSignature:
                     case Region::Kind::numKinds:
                         break;
+                    case Region::Kind::tproConst:
                     case Region::Kind::data:
                     case Region::Kind::dataConst:
+                    case Region::Kind::tproAuthConst:
                     case Region::Kind::auth:
                     case Region::Kind::authConst:
                         lastReadWriteRegion = &region;
                         break;
+                    case Region::Kind::readOnly:
                     case Region::Kind::dynamicConfig:
                     case Region::Kind::linkedit:
                         lastReadOnlyRegion = &region;
@@ -4051,14 +4676,105 @@
     }
 
     this->totalVMSize = CacheVMSize((vmAddress - this->config.layout.cacheBaseAddress).rawValue());
+
+    return Error();
+}
+
+void SharedCacheBuilder::evictLeafDylibs(CacheVMSize reductionTarget)
+{
+    // build a reverse map of all dylib dependencies
+    std::unordered_map<std::string_view, std::unordered_set<std::string_view>> references;
+    // Ensure we have an entry (even if it is empty)
+    for ( const CacheDylib& cacheDylib : cacheDylibs )
+        references[cacheDylib.installName] = { };
     
-    if ( this->totalVMSize > this->config.layout.cacheSize ) {
-        return Error("Cache overflow (0x%llx > 0x%llx)",
-                     this->totalVMSize.rawValue(),
-                     this->config.layout.cacheSize.rawValue());
-    }
-
-    return Error();
+    for ( const CacheDylib& cacheDylib : cacheDylibs ) {
+        for ( const CacheDylib::DependentDylib& depDylib : cacheDylib.dependents ) {
+            // Skip missing weak links
+            if ( depDylib.dylib == nullptr )
+                continue;
+            references[depDylib.dylib->installName].insert(cacheDylib.installName);
+        }
+    }
+
+    struct DylibAndSize
+    {
+        CacheDylib* dylib;
+        CacheVMSize size;
+    };
+
+    // Find the sizes of all the dylibs
+    std::vector<DylibAndSize> dylibsToSort;
+    for ( CacheDylib& cacheDylib : cacheDylibs ) {
+        CacheVMSize segsSize = CacheVMSize(0ULL);
+        for ( const DylibSegmentChunk& segment : cacheDylib.segments ) {
+            if ( segment.segmentName == "__LINKEDIT" )
+                continue;
+
+            segsSize += segment.cacheVMSize;
+        }
+        dylibsToSort.push_back({ &cacheDylib, segsSize });
+    }
+
+    // Build an ordered list of what to remove. At each step we do following
+    // 1) Find all dylibs that nothing else depends on
+    // 2a) If any of those dylibs are not in the order select the largest one of them
+    // 2b) If all the leaf dylibs are in the order file select the last dylib that appears last in the order file
+    // 3) Remove all entries to the removed file from the reverse dependency map
+    // 4) Go back to one and repeat until there are no more evictable dylibs
+    // This results in us always choosing the locally optimal selection, and then taking into account how that impacts
+    // the dependency graph for subsequent selections
+
+    std::vector<DylibAndSize> sortedDylibs;
+    bool candidateFound = true;
+    while ( candidateFound ) {
+        candidateFound = false;
+        DylibAndSize candidate;
+        uint64_t candidateOrder = 0;
+        for( const auto& dylib : dylibsToSort ) {
+            const auto& dylibRefs = references.at(dylib.dylib->installName);
+            if ( !dylibRefs.empty())
+                continue;
+
+            const auto& j = options.dylibOrdering.find(std::string(dylib.dylib->installName));
+            uint64_t order = 0;
+            if ( j != options.dylibOrdering.end() ) {
+                order = j->second;
+            } else {
+                // Not in the order file, set order sot it goes to the front of the list
+                order = UINT64_MAX;
+            }
+            if ( order > candidateOrder || (order == UINT64_MAX && candidate.size < dylib.size) ) {
+                // The new file is either a lower priority in the order file
+                // or the same priority as the candidate but larger
+                candidate = dylib;
+                candidateOrder = order;
+                candidateFound = true;
+            }
+        }
+        if (candidateFound) {
+            sortedDylibs.push_back(candidate);
+            references.erase(candidate.dylib->installName);
+            for (auto& dependent : references) {
+                (void)dependent.second.erase(candidate.dylib->installName);
+            }
+            auto j = std::find_if(dylibsToSort.begin(), dylibsToSort.end(),
+                                  [&candidate](const DylibAndSize& dylib) {
+                return candidate.dylib->installName == dylib.dylib->installName;
+            });
+            if ( j != dylibsToSort.end() ) {
+                dylibsToSort.erase(j);
+            }
+        }
+    }
+
+     // build set of dylibs that if removed will allow cache to build
+    for ( DylibAndSize& dylib : sortedDylibs ) {
+        this->evictedDylibs.push_back(dylib.dylib->inputFile->path);
+        if ( dylib.size > reductionTarget )
+            break;
+        reductionTarget -= dylib.size;
+    }
 }
 
 // In file layout, we need each Region to start page-aligned.  Within a Region, we can pack pages
@@ -4137,13 +4853,15 @@
         if ( Error error = computeSubCacheContiguousVMLayout(); error.hasError() )
             return error;
     } else {
-        if ( this->options.isSimultor() ) {
-            if ( Error error = computeSubCacheDiscontiguousSimVMLayout(); error.hasError() )
-                return error;
-        } else {
-            if ( Error error = computeSubCacheDiscontiguousVMLayout(); error.hasError() )
-                return error;
-        }
+        if ( Error error = computeSubCacheDiscontiguousVMLayout(); error.hasError() )
+            return error;
+    }
+    
+    if ( this->totalVMSize > this->config.layout.cacheSize ) {
+        evictLeafDylibs(this->totalVMSize - this->config.layout.cacheSize);
+        return Error("Cache overflow (0x%llx > 0x%llx)",
+                     this->totalVMSize.rawValue(),
+                     this->config.layout.cacheSize.rawValue());
     }
 
     // Update Section VMAddr's now that we know where all the Region's are in memory
@@ -4160,18 +4878,12 @@
         }
     }
 
-    if ( this->totalVMSize > this->config.layout.cacheSize ) {
-        return Error("Cache overflow (0x%llx > 0x%llx)",
-                     this->totalVMSize.rawValue(),
-                     this->config.layout.cacheSize.rawValue());
-    }
-
     return Error();
 }
 
 Error SharedCacheBuilder::allocateSubCacheBuffers()
 {
-    const bool log = false;
+    const bool log = this->options.debug;
 
     Timer::Scope timedScope(this->config, "allocateSubCacheBuffers time");
 
@@ -4487,6 +5199,76 @@
     }
 }
 
+void SharedCacheBuilder::adjustObjCCategories()
+{
+    if ( this->objcOptimizer.objcDylibs.empty() )
+        return;
+
+    Timer::Scope timedScope(this->config, "adjustObjCCategories time");
+
+    // Categories were stored as input dylib VMAddr's.  Convert to cache dylib VMAddr's
+    for ( auto& categoryInfo : this->objcCategoryOptimizer.categories ) {
+        CacheDylib* cacheDylib =  this->objcOptimizer.objcDylibs[categoryInfo.dylibObjcIndex.value()];
+
+        // category address
+        if ( categoryInfo.vmAddress.has_value() ) {
+            uint64_t inputAddr = categoryInfo.vmAddress.value().rawValue();
+            InputDylibVMAddress inputVMAddr(inputAddr);
+            CacheVMAddress cacheVMAddr = cacheDylib->adjustor->adjustVMAddr(inputVMAddr);
+            categoryInfo.vmAddress = VMAddress(cacheVMAddr.rawValue());
+        }
+
+        // class address
+        if ( categoryInfo.classVMAddress.has_value() ) {
+            CacheDylib& classDylib = this->cacheDylibs[categoryInfo.classDylibIndex.value()];
+            uint64_t inputAddr = categoryInfo.classVMAddress.value().rawValue();
+            InputDylibVMAddress inputVMAddr(inputAddr);
+            CacheVMAddress cacheVMAddr = classDylib.adjustor->adjustVMAddr(inputVMAddr);
+            categoryInfo.classVMAddress = VMAddress(cacheVMAddr.rawValue());
+        }
+
+        // instance methods
+        if ( categoryInfo.iMethodListVMAddress.has_value() ) {
+            uint64_t inputAddr = categoryInfo.iMethodListVMAddress.value().rawValue();
+            InputDylibVMAddress inputVMAddr(inputAddr);
+            CacheVMAddress cacheVMAddr = cacheDylib->adjustor->adjustVMAddr(inputVMAddr);
+            categoryInfo.iMethodListVMAddress = VMAddress(cacheVMAddr.rawValue());
+        }
+
+        // class methods
+        if ( categoryInfo.cMethodListVMAddress.has_value() ) {
+            uint64_t inputAddr = categoryInfo.cMethodListVMAddress.value().rawValue();
+            InputDylibVMAddress inputVMAddr(inputAddr);
+            CacheVMAddress cacheVMAddr = cacheDylib->adjustor->adjustVMAddr(inputVMAddr);
+            categoryInfo.cMethodListVMAddress = VMAddress(cacheVMAddr.rawValue());
+        }
+
+        // protocols
+        if ( categoryInfo.protocolListVMAddress.has_value() ) {
+            uint64_t inputAddr = categoryInfo.protocolListVMAddress.value().rawValue();
+            InputDylibVMAddress inputVMAddr(inputAddr);
+            CacheVMAddress cacheVMAddr = cacheDylib->adjustor->adjustVMAddr(inputVMAddr);
+            categoryInfo.protocolListVMAddress = VMAddress(cacheVMAddr.rawValue());
+        }
+
+        // instance properties
+        if ( categoryInfo.iPropertyListVMAddress.has_value() ) {
+            uint64_t inputAddr = categoryInfo.iPropertyListVMAddress.value().rawValue();
+            InputDylibVMAddress inputVMAddr(inputAddr);
+            CacheVMAddress cacheVMAddr = cacheDylib->adjustor->adjustVMAddr(inputVMAddr);
+            categoryInfo.iPropertyListVMAddress = VMAddress(cacheVMAddr.rawValue());
+        }
+
+        // class properties
+        if ( categoryInfo.cPropertyListVMAddress.has_value() ) {
+            uint64_t inputAddr = categoryInfo.cPropertyListVMAddress.value().rawValue();
+            InputDylibVMAddress inputVMAddr(inputAddr);
+            CacheVMAddress cacheVMAddr = cacheDylib->adjustor->adjustVMAddr(inputVMAddr);
+            categoryInfo.cPropertyListVMAddress = VMAddress(cacheVMAddr.rawValue());
+        }
+    }
+}
+
 Error SharedCacheBuilder::emitPatchTable()
 {
     Stats        stats(this->config);
@@ -4495,7 +5277,7 @@
     // Skip this optimization on simulator until we've qualified it there
     __block PatchTableBuilder::PatchableClassesSet      patchableObjCClasses;
     __block PatchTableBuilder::PatchableSingletonsSet   patchableCFObj2;
-    if ( !this->options.isSimultor() ) {
+    if ( !this->options.isSimulator() ) {
         for ( const CacheDylib& cacheDylib : this->cacheDylibs ) {
             __block objc_visitor::Visitor objcVisitor = makeInputDylibObjCVisitor(cacheDylib);
             objcVisitor.forEachClassAndMetaClass(^(const objc_visitor::Class& objcClass, bool& stopClass) {
@@ -4541,13 +5323,21 @@
 // dyld4 needs a fake "main.exe" to set up the state.
 // On macOS this *has* to come from an actual executable, as choosing a zippered
 // dylib may incorrectly lead to setting up the ProcessConfig as iOSMac.
-// Simulators don't have executables yet so choose a dylib there
+// Simulators and ExclaveKit don't have executables yet so choose a dylib there
 static const MachOFile* getFakeMainExecutable(const BuilderOptions& options,
                                               std::span<CacheDylib> cacheDylibs,
                                               std::span<InputFile*> executableFiles)
 {
-    if ( options.isSimultor() ) {
+    if ( options.isSimulator() ) {
         std::string_view installName = "/usr/lib/libSystem.B.dylib";
+        for ( const CacheDylib& cacheDylib : cacheDylibs ) {
+            if ( cacheDylib.installName == installName ) {
+                assert(cacheDylib.cacheMF != nullptr);
+                return cacheDylib.cacheMF;
+            }
+        }
+    } else if (options.isExclaveKit() ) {
+        std::string_view installName = "/System/ExclaveKit/usr/lib/libSystem.dylib";
         for ( const CacheDylib& cacheDylib : cacheDylibs ) {
             if ( cacheDylib.installName == installName ) {
                 assert(cacheDylib.cacheMF != nullptr);
@@ -4802,7 +5592,7 @@
         Diagnostics loadDiag;
         ((Loader*)ldr)->loadDependents(loadDiag, state, options);
         if ( loadDiag.hasError() ) {
-            return Error("%s, loading dependents of %s", loadDiag.errorMessageCStr(), ldr->path());
+            return Error("%s, loading dependents of %s", loadDiag.errorMessageCStr(), ldr->path(state));
         }
     }
 
@@ -4834,7 +5624,7 @@
         return Error("Could not find a main executable for building cache loaders");
 
     const LayoutBuilder  layoutBuilder(cacheDylibs, { });
-    EphemeralAllocator   processConfigAlloc;
+    STACK_ALLOCATOR(processConfigAlloc, 0);
     __block dyld4::Vector<ProcessConfig::DyldCache::CacheDylib> processConfigDylibs(processConfigAlloc);
 
     for ( uint32_t dylibIndex = 0; dylibIndex != this->cacheDylibs.size(); ++dylibIndex ) {
@@ -4857,11 +5647,12 @@
     }
 
     // build PrebuiltLoaderSet of all dylibs in cache
+    STACK_ALLOCATOR(alloc, 0);
     KernelArgs         kernArgs(mainExecutable, { "test.exe" }, {}, {});
     SyscallDelegate    osDelegate;
-    EphemeralAllocator alloc;
     ProcessConfig      processConfig(&kernArgs, osDelegate, alloc);
-    RuntimeState       state(processConfig, alloc);
+    RuntimeLocks       locks;
+    RuntimeState       state(processConfig, locks, alloc);
 
     // FIXME: This is terrible and needs to be a real reset method
     processConfig.dyldCache.cacheBuilderDylibs = &processConfigDylibs;
@@ -4917,7 +5708,7 @@
                                VMAddress& protocolClassVMAddr, MachOFile::PointerMetaData& protocolClassPMD)
 {
     for ( CacheDylib* cacheDylib : objcDylibs ) {
-        if ( cacheDylib->installName == "/usr/lib/libobjc.A.dylib" ) {
+        if ( cacheDylib->installName.ends_with("/usr/lib/libobjc.A.dylib" )) {
             __block InputDylibVMAddress inputOptPtrsVMAddress;
             __block uint64_t            sectionSize = 0;
             __block bool                found       = false;
@@ -4951,7 +5742,7 @@
 
             CacheVMAddress cacheOptPtrsVMAddr = cacheDylib->adjustor->adjustVMAddr(inputOptPtrsVMAddress);
 
-            objc_visitor::Visitor objcVisitor = cacheDylib->makeCacheObjCVisitor(config, nullptr, nullptr);
+            objc_visitor::Visitor objcVisitor = cacheDylib->makeCacheObjCVisitor(config, nullptr, nullptr, nullptr);
 
             metadata_visitor::ResolvedValue protocolClassValue = objcVisitor.getValueFor(VMAddress(cacheOptPtrsVMAddr.rawValue()));
             protocolClassVMAddr                           = objcVisitor.resolveRebase(protocolClassValue).vmAddress();
@@ -5007,7 +5798,7 @@
 
     const LayoutBuilder  layoutBuilder(cacheDylibs, this->exeInputFiles);
     const LayoutBuilder* layoutBuilderPtr = &layoutBuilder;
-    EphemeralAllocator   processConfigAlloc;
+    STACK_ALLOCATOR(processConfigAlloc, 0);
     dyld4::Vector<ProcessConfig::DyldCache::CacheDylib> processConfigDylibsOwner(processConfigAlloc);
     auto& processConfigDylibs = processConfigDylibsOwner;
 
@@ -5077,9 +5868,10 @@
         osDelegate._mappedOtherDylibs = otherMapping;
         osDelegate._gradedArchs       = &this->options.archs;
         //osDelegate._dyldCache           = dyldCache;
-        EphemeralAllocator alloc;
+        STACK_ALLOCATOR(alloc, 0);
         ProcessConfig      processConfig(&kernArgs, osDelegate, alloc);
-        RuntimeState       state(processConfig, alloc);
+        RuntimeLocks       locks;
+        RuntimeState       state(processConfig, locks, alloc);
         RuntimeState*      statePtr = &state;
         Diagnostics        launchDiag;
 
@@ -5108,12 +5900,12 @@
                 char altPath[PATH_MAX];
                 strlcpy(altPath, "/System/iOSSupport", PATH_MAX);
                 strlcat(altPath, loadPath, PATH_MAX);
-                if ( const dyld4::PrebuiltLoader* ldr = cachedDylibsLoaderSet->findLoader(altPath) )
+                if ( const dyld4::PrebuiltLoader* ldr = cachedDylibsLoaderSet->findLoader(*statePtr, altPath) )
                     return (const Loader*)ldr;
             }
 
             // check if path is a dylib in the dyld cache, then use its PrebuiltLoader
-            if ( const dyld4::PrebuiltLoader* ldr = cachedDylibsLoaderSet->findLoader(loadPath) )
+            if ( const dyld4::PrebuiltLoader* ldr = cachedDylibsLoaderSet->findLoader(*statePtr, loadPath) )
                 return (const Loader*)ldr;
 
             // call through to getLoader() which will expand @paths
@@ -5142,6 +5934,23 @@
                 // FIXME: Propagate errors
                 return Error();
             }
+
+            // Set dylibs to be fixedUp before we partition delay init, as it uses this state
+            for ( const Loader* ldr : state.loaded ) {
+                if ( const PrebuiltLoader* prebuiltLdr = ldr->isPrebuiltLoader() )
+                    prebuiltLdr->setFixedUp(state);
+            }
+
+            // split off delay loaded dylibs into delayLoaded vector
+            // We have to do this before making the PrebuiltLoaderSet as objc in the closure needs
+            // to know which shared cache dylibs are delay or not.
+            STACK_ALLOC_ARRAY(const Loader*, loadersTemp, state.loaded.size());
+            for (const Loader* ldr : state.loaded)
+                loadersTemp.push_back(ldr);
+            std::span<const Loader*> allLoaders(&loadersTemp[0], (size_t)loadersTemp.count());
+            std::span<const Loader*> topLoaders = allLoaders.subspan(0, 1);
+            state.partitionDelayLoads(allLoaders, topLoaders);
+
             state.setMainLoader(mainLoader);
             const dyld4::PrebuiltLoaderSet* prebuiltAppSet = dyld4::PrebuiltLoaderSet::makeLaunchSet(launchDiag, state, missingPaths);
             if ( launchDiag.hasError() ) {
@@ -5423,9 +6232,9 @@
     Diagnostics diag;
 
     // Find the subCache with the hash tables
-    cache_builder::ObjCSelectorHashTableChunk* selectorsHashTable = nullptr;
-    cache_builder::ObjCClassHashTableChunk*    classesHashTable   = nullptr;
-    cache_builder::ObjCProtocolHashTableChunk* protocolsHashTable = nullptr;
+    cache_builder::ObjCSelectorHashTableChunk* selectorsHashTable  = nullptr;
+    cache_builder::ObjCClassHashTableChunk*    classesHashTable    = nullptr;
+    cache_builder::ObjCProtocolHashTableChunk* protocolsHashTable  = nullptr;
     for ( SubCache& subCache : this->subCaches ) {
         if ( subCache.objcSelectorsHashTable ) {
             assert(selectorsHashTable == nullptr);
@@ -5502,15 +6311,32 @@
 
     Timer::Scope timedScope(this->config, "emitObjCHeaderInfo time");
 
+    // We need the prebuilt loaders from the cache dylibs as they contain SectionLocations.
+    auto* cachedDylibsLoaderSet = this->prebuiltLoaderBuilder.cachedDylibsLoaderSet;
+    assert(cachedDylibsLoaderSet != nullptr);
+
     // Emit header info RO
     auto* readOnlyList    = (ObjCOptimizer::header_info_ro_list_t*)this->objcOptimizer.headerInfoReadOnlyChunk->subCacheBuffer;
     readOnlyList->count   = (uint32_t)this->objcOptimizer.objcDylibs.size();
     readOnlyList->entsize = this->config.layout.is64 ? sizeof(ObjCOptimizer::header_info_ro_64_t) : sizeof(ObjCOptimizer::header_info_ro_32_t);
 
+    // We're also going to populate the objc image info array with the image infos we have here
+    auto* imageInfoArray   = (objc::objc_image_info*)this->objcOptimizer.imageInfoChunk->subCacheBuffer;
+    CacheVMAddress cacheImageInfoBaseAddress = this->objcOptimizer.imageInfoChunk->cacheVMAddress;
+    assert(this->objcOptimizer.imageInfoChunk->subCacheFileSize.rawValue() == (readOnlyList->count * sizeof(objc::objc_image_info)));
+
     for ( uint32_t i = 0; i != readOnlyList->count; ++i ) {
         CacheDylib& cacheDylib = *this->objcOptimizer.objcDylibs[i];
 
-        __block CacheVMAddress  cacheImageInfoAddress;
+        const uint64_t dyldCategoriesOptimizedFlag = (1 << 0);
+        const uint64_t optimizedByDyldFlag         = (1 << 3);
+
+        // We want the headerinfo_ro_t to point to the imageInfo in the contiguous buffer
+        // not the imageinfo in the original dylib
+        // Note: We only have standard (8-byte) image infos in the array right now.  We'll ignore
+        // the array for any elements which use a different sized image info.
+        __block CacheVMAddress cacheImageInfoAddress = cacheImageInfoBaseAddress + VMOffset((uint64_t)i * sizeof(objc::objc_image_info));
+
         __block uint8_t*        cacheImageInfoBuffer = nullptr;
         cacheDylib.forEachCacheSection(^(std::string_view segmentName, std::string_view sectionName,
                                          uint8_t* sectionBuffer, CacheVMAddress sectionVMAddr,
@@ -5520,7 +6346,11 @@
             if ( sectionName != "__objc_imageinfo" )
                 return;
 
-            cacheImageInfoAddress = sectionVMAddr;
+            if ( sectionVMSize.rawValue() != sizeof(objc::objc_image_info) ) {
+                // Skip the optimized array and use this element directly
+                cacheImageInfoAddress = sectionVMAddr;
+            }
+
             cacheImageInfoBuffer = sectionBuffer;
             stop = true;
         });
@@ -5529,6 +6359,12 @@
 
         void*          arrayElement     = &readOnlyList->arrayBase[0] + (i * readOnlyList->entsize);
         CacheVMAddress machHeaderVMAddr = cacheDylib.cacheLoadAddress;
+
+        // Get the PrebuiltLoader* for this cache dylib
+        const PrebuiltLoader* ldr = cachedDylibsLoaderSet->atIndex(cacheDylib.cacheIndex);
+        //assert(ldr->path(state) == cacheDylib.installName); // can't do assert because state is not passed to this method
+
+        CacheVMAddress ldrVMAddr = getVMAddressInSection(*this->prebuiltLoaderBuilder.cacheDylibsLoaderChunk, ldr);
 
         if ( this->config.layout.is64 ) {
             ObjCOptimizer::header_info_ro_64_t* element = (ObjCOptimizer::header_info_ro_64_t*)arrayElement;
@@ -5546,6 +6382,13 @@
             element->info_offset           = infoOffset;
             // Check for truncation
             assert(element->info_offset == infoOffset);
+
+            // metadata_offset
+            CacheVMAddress metadataOffsetVMAddr = getVMAddressInSection(*this->objcOptimizer.headerInfoReadOnlyChunk, &element->metadata_offset);
+            int64_t metadataOffset         = ldrVMAddr.rawValue() - metadataOffsetVMAddr.rawValue();
+            element->metadata_offset       = metadataOffset;
+            // Check for truncation
+            assert(element->metadata_offset == metadataOffset);
         }
         else {
             ObjCOptimizer::header_info_ro_32_t* element = (ObjCOptimizer::header_info_ro_32_t*)arrayElement;
@@ -5563,15 +6406,25 @@
             element->info_offset           = (int32_t)infoOffset;
             // Check for truncation
             assert(element->info_offset == infoOffset);
+
+            // metadata_offset
+            CacheVMAddress metadataOffsetVMAddr = getVMAddressInSection(*this->objcOptimizer.headerInfoReadOnlyChunk, &element->metadata_offset);
+            int64_t       metadataOffset   = ldrVMAddr.rawValue() - metadataOffsetVMAddr.rawValue();
+            element->metadata_offset       = (int32_t)metadataOffset;
+            // Check for truncation
+            assert(element->metadata_offset == metadataOffset);
         }
 
         // Set the dylib to be optimized, which lets it use this header info
-        struct objc_image_info {
-            int32_t version;
-            uint32_t flags;
-        };
-        objc_image_info* info = (objc_image_info*)cacheImageInfoBuffer;
-        info->flags = info->flags | (1 << 3);
+        objc::objc_image_info* info = (objc::objc_image_info*)cacheImageInfoBuffer;
+        info->flags |= optimizedByDyldFlag;
+        if ( this->objcCategoryOptimizer.preAttachedDylibs.contains(i) ) {
+            if ( !this->objcCategoryOptimizer.excludedDylibs.contains(i) )
+                info->flags |= dyldCategoriesOptimizedFlag;
+        }
+
+        // Also copy in to the contiguous space
+        memcpy(&imageInfoArray[i], info, sizeof(objc::objc_image_info));
     }
 
     // Emit header info RW
@@ -5754,7 +6607,8 @@
             return;
 
         if ( (strncmp(sectInfo.segInfo.segName, "__DATA", 6) != 0)
-            && (strncmp(sectInfo.segInfo.segName, "__AUTH", 6) != 0) )
+            && (strncmp(sectInfo.segInfo.segName, "__AUTH", 6) != 0)
+            && (strncmp(sectInfo.segInfo.segName, "__TPRO_CONST", 12) != 0))
             return;
 
         // Found the section we need.  Now to check if its valid
@@ -5969,22 +6823,44 @@
         if ( (dataConstRegion == nullptr) && (authConstRegion == nullptr) )
             continue;
 
-        for ( bool auth : { false, true } ) {
-            if ( auth && (authConstRegion == nullptr) )
+        for ( UniquedGOTKind sectionKind : { UniquedGOTKind::regular, UniquedGOTKind::authGot, UniquedGOTKind::authPtr } ) {
+
+            Region* region = nullptr;
+            CoalescedGOTSection* subCacheUniquedGOTs = nullptr;
+
+            // Skip sections if their segment doesn't exist
+            switch ( sectionKind ) {
+                case UniquedGOTKind::regular:
+                    if ( dataConstRegion == nullptr )
+                        continue;
+
+                    region = dataConstRegion;
+                    subCacheUniquedGOTs = &subCache.uniquedGOTsOptimizer.regularGOTs;
+                    break;
+                case UniquedGOTKind::authGot:
+                    if ( authConstRegion == nullptr )
+                        continue;
+
+                    region = authConstRegion;
+                    subCacheUniquedGOTs = &subCache.uniquedGOTsOptimizer.authGOTs;
+                    break;
+                case UniquedGOTKind::authPtr:
+                    if ( authConstRegion == nullptr )
+                        continue;
+
+                    region = authConstRegion;
+                    subCacheUniquedGOTs = &subCache.uniquedGOTsOptimizer.authPtrs;
+                    break;
+            }
+
+            if ( subCacheUniquedGOTs->cacheChunk == nullptr )
                 continue;
-            if ( !auth && (dataConstRegion == nullptr) )
-                continue;
-
-            Region& region = auth ? *authConstRegion : *dataConstRegion;
-            CoalescedGOTSection& subCacheUniquedGOTs = auth ? subCache.uniquedGOTsOptimizer.authGOTs : subCache.uniquedGOTsOptimizer.regularGOTs;
-            if ( subCacheUniquedGOTs.cacheChunk == nullptr )
-                continue;
-
-            UniquedGOTsChunk* subCacheGOTChunk = subCacheUniquedGOTs.cacheChunk->isUniquedGOTsChunk();
+
+            UniquedGOTsChunk* subCacheGOTChunk = subCacheUniquedGOTs->cacheChunk->isUniquedGOTsChunk();
 
             std::set<const void*> seenFixups;
             std::vector<PatchInfo::GOTInfo> gots;
-            for ( const Chunk* chunk : region.chunks ) {
+            for ( const Chunk* chunk : region->chunks ) {
                 const DylibSegmentChunk* segmentChunk = chunk->isDylibSegmentChunk();
                 if ( !segmentChunk )
                     continue;
@@ -5995,19 +6871,26 @@
                 // Walk all the binds in this dylib, looking for GOT uses of the bind
                 assert(cacheDylib->bindTargets.size() == dylibPatchInfo.bindGOTUses.size());
                 assert(cacheDylib->bindTargets.size() == dylibPatchInfo.bindAuthGOTUses.size());
+                assert(cacheDylib->bindTargets.size() == dylibPatchInfo.bindAuthPtrUses.size());
                 for ( uint32_t bindIndex = 0; bindIndex != cacheDylib->bindTargets.size(); ++bindIndex ) {
                     const CacheDylib::BindTarget& bindTarget = cacheDylib->bindTargets[bindIndex];
 
-                    std::vector<PatchInfo::GOTInfo>* bindUses = nullptr;
-                    if ( auth ) {
-                        bindUses = &dylibPatchInfo.bindAuthGOTUses[bindIndex];
-                    } else {
-                        bindUses = &dylibPatchInfo.bindGOTUses[bindIndex];
+                    std::span<PatchInfo::GOTInfo> bindUses;
+                    switch ( sectionKind ) {
+                        case UniquedGOTKind::regular:
+                            bindUses = dylibPatchInfo.bindGOTUses[bindIndex];
+                            break;
+                        case UniquedGOTKind::authGot:
+                            bindUses = dylibPatchInfo.bindAuthGOTUses[bindIndex];
+                            break;
+                        case UniquedGOTKind::authPtr:
+                            bindUses = dylibPatchInfo.bindAuthPtrUses[bindIndex];
+                            break;
                     }
 
                     // For absolute binds, just set the pointers and move on
                     if ( bindTarget.kind == CacheDylib::BindTarget::Kind::absolute ) {
-                        for ( const PatchInfo::GOTInfo& got : *bindUses ) {
+                        for ( const PatchInfo::GOTInfo& got : bindUses ) {
                             CacheVMAddress gotVMAddr = got.patchInfo.cacheVMAddr;
                             assert(gotVMAddr >= subCacheGOTChunk->cacheVMAddress);
                             assert(gotVMAddr < (subCacheGOTChunk->cacheVMAddress + subCacheGOTChunk->cacheVMSize));
@@ -6023,7 +6906,7 @@
                         continue;
                     }
 
-                    gots.insert(gots.end(), bindUses->begin(), bindUses->end());
+                    gots.insert(gots.end(), bindUses.begin(), bindUses.end());
                 }
             }
 
@@ -6097,7 +6980,8 @@
 
     for ( CacheDylib* cacheDylib : this->objcOptimizer.objcDylibs ) {
         objcVisitors.push_back(cacheDylib->makeCacheObjCVisitor(config, nullptr,
-                                                                this->objcProtocolOptimizer.canonicalProtocolsChunk));
+                                                                this->objcProtocolOptimizer.canonicalProtocolsChunk,
+                                                                this->objcCategoryOptimizer.categoriesChunk));
     }
 
     // The offset in the protocol buffer for the next protocol to emit
@@ -6263,7 +7147,7 @@
     objcVisitors.reserve(this->cacheDylibs.size());
 
     for ( CacheDylib& cacheDylib : this->cacheDylibs ) {
-        objcVisitors.push_back(cacheDylib.makeCacheObjCVisitor(config, nullptr, nullptr));
+        objcVisitors.push_back(cacheDylib.makeCacheObjCVisitor(config, nullptr, nullptr, nullptr));
     }
 
     // Check for missing superclasses, but only error on customer/universal caches
@@ -6368,7 +7252,7 @@
         worklist.insert(worklist.end(), classInfo->subClasses.begin(), classInfo->subClasses.end());
         bool elidedSomething = false;
         const objc_visitor::Class& objcClass = classInfo->classPos;
-
+        const bool isSwiftClass = objcClass.isSwift(*classInfo->objcVisitor);
         auto& map = objcClass.isMetaClass ? metaclassMap : classMap;
 
         std::optional<VMAddress> superclassVMAddr = objcClass.getSuperclassVMAddr(*classInfo->objcVisitor);
@@ -6409,7 +7293,7 @@
                     continue;
 
                 // skip ivars that swiftc has optimized away
-                if ( ivar.elided(*classInfo->objcVisitor) ) {
+                if ( isSwiftClass && ivar.elided(*classInfo->objcVisitor) ) {
                     if ( log ) {
                         if ( !elidedSomething )
                             printf("adjusting ivars for %s\n", objcClass.getName(*classInfo->objcVisitor));
@@ -6427,6 +7311,396 @@
             objcClass.setInstanceStart(*classInfo->objcVisitor, objcClass.getInstanceStart(*classInfo->objcVisitor) + diff);
             objcClass.setInstanceSize(*classInfo->objcVisitor, objcClass.getInstanceSize(*classInfo->objcVisitor) + diff);
         }
+    }
+
+    return Error();
+}
+
+Error SharedCacheBuilder::emitPreAttachedObjCCategories()
+{
+    
+    if ( this->objcOptimizer.objcDylibs.empty() )
+        return Error();
+
+    Timer::Scope timedScope(this->config, "dylib emitPreAttachedObjCCategories time");
+
+    ObjCPreAttachedCategoriesChunk* categoriesChunk = this->objcCategoryOptimizer.categoriesChunk;
+
+    // Build reverse map of categories for fast lookup during optimization
+    __block std::unordered_multimap<size_t, uint64_t> dylibsToClasses;
+    __block std::unordered_multimap<uint64_t, const ObjCCategoryOptimizer::Category*> classMap;
+    __block std::unordered_multimap<uint64_t, const ObjCCategoryOptimizer::Category*> metaClassMap;
+    auto& categories = this->objcCategoryOptimizer.categories;
+    for (size_t i = 0; i < categories.size(); i++) {
+        dylibsToClasses.insert({ categories[i].classDylibIndex.value(), categories[i].classVMAddress.value().rawValue() });
+        classMap.insert({ categories[i].classVMAddress.value().rawValue(), &categories[i] });
+    }
+
+    __block dyld3::MachOFile::PointerMetaData methodListPMD;
+    if ( this->config.layout.hasAuthRegion ) {
+        methodListPMD.diversity         = 0xC310;
+        methodListPMD.high8             = 0;
+        methodListPMD.authenticated     = 1;
+        methodListPMD.key               = ptrauth_key_asda;
+        methodListPMD.usesAddrDiversity = 1;
+    }
+
+    __block uint64_t chunkOffset = 0;
+
+    // The very first thing we want to do is make an empty class method list
+    // All classes which have no method lists will have this added as the last entry
+    // in their list
+    assert(categoriesChunk->subCacheFileSize.rawValue() >= 8);
+    void* emptyMethodListLocation = categoriesChunk->subCacheBuffer;
+    CacheVMAddress emptyMethodListVMAddr = categoriesChunk->cacheVMAddress;
+    chunkOffset += objc_visitor::MethodList::makeEmptyMethodList(emptyMethodListLocation);
+
+    auto visitCategoryMetaClass = ^(objc_visitor::Visitor &objCVisitor,
+                                size_t classCacheIndex, size_t classObjcIndex,
+                                objc_visitor::Class &objcClass,
+                                std::span<DylibSegmentChunk> cacheDylibSegments) {
+        auto metaClassRange = metaClassMap.equal_range(objcClass.getVMAddress().rawValue());
+        if ( metaClassRange.first == metaClassRange.second )
+            return;
+
+        //fprintf(stderr, "meta class %s %s 0x%llx\n", this->cacheDylibs[classCacheIndex].installName.data(), objcClass.getName(objCVisitor), objcClass.getVMAddress().rawValue());
+        // class method lists
+        {
+            uint8_t* listBuffer = categoriesChunk->subCacheBuffer + chunkOffset;
+            uint64_t firstEntryOffset = chunkOffset;
+            ListOfListsEntry* listHeader = (ListOfListsEntry*)listBuffer;
+            ListOfListsEntry* listEntry = listHeader + 1;
+
+            bool createdNewList = false;
+            for (auto classEntry = metaClassRange.first; classEntry != metaClassRange.second; classEntry++) {
+                const ObjCCategoryOptimizer::Category* category = classEntry->second;
+                if ( !category->cMethodListVMAddress.has_value() )
+                    continue;
+
+                //fprintf(stderr, "  meta cat %s 0x%llx %s (%llu) => %zu\n", category->name.data(), category->vmAddress.value().rawValue(), this->objcOptimizer.objcDylibs[category->dylibObjcIndex.value()]->installName.data(), category->dylibObjcIndex.value(), classObjcIndex);
+                if ( !createdNewList ) {
+                    createdNewList = true;
+                    // New list with count and entry size
+                    listHeader->entsize = sizeof(ListOfListsEntry);
+                    listHeader->count = 0;
+                    chunkOffset += sizeof(ListOfListsEntry);
+                }
+
+                listEntry->imageIndex = category->dylibObjcIndex.value();
+                int64_t destVMAddr = (int64_t)category->cMethodListVMAddress.value().rawValue();
+                listEntry->offset = destVMAddr - getVMAddressInSection(*categoriesChunk, listEntry).rawValue();
+                listEntry++;
+                listHeader->count++;
+                chunkOffset += sizeof(ListOfListsEntry);
+            }
+
+            if ( createdNewList ) {
+                objc_visitor::MethodList methodList = objcClass.getBaseMethods(objCVisitor);
+                // add original method list
+                if ( methodList.numMethods() > 0 ) {
+                    listEntry->imageIndex = classObjcIndex;
+                    int64_t destVMAddr = (int64_t)methodList.getVMAddress().value().rawValue();
+                    listEntry->offset = destVMAddr - getVMAddressInSection(*categoriesChunk, listEntry).rawValue();
+                } else {
+                    // add an empty list entry if no original was found
+                    listEntry->imageIndex = classObjcIndex;
+                    int64_t destVMAddr = (int64_t)emptyMethodListVMAddr.rawValue();
+                    listEntry->offset = destVMAddr - getVMAddressInSection(*categoriesChunk, listEntry).rawValue();
+                }
+                listEntry++;
+                listHeader->count++;
+                chunkOffset += sizeof(ListOfListsEntry);
+                //fprintf(stderr, "Total cMethod list lists: %d\n", listHeader->count);
+                // store new base methods pointer to update
+                CacheVMAddress newBaseMethods = categoriesChunk->cacheVMAddress + VMOffset(firstEntryOffset);
+                metadata_visitor::ResolvedValue field = objcClass.setBaseMethodsVMAddr(objCVisitor,
+                                                                                       VMAddress(newBaseMethods.rawValue() | 1),
+                                                                                       methodListPMD);
+                cacheDylibSegments[field.segmentIndex()].tracker.add(field.value());
+            }
+        }
+        // class property lists
+        {
+            uint8_t* listBuffer = categoriesChunk->subCacheBuffer + chunkOffset;
+            uint64_t firstEntryOffset = chunkOffset;
+            ListOfListsEntry* listHeader = (ListOfListsEntry*)listBuffer;
+            ListOfListsEntry* listEntry = listHeader + 1;
+
+            bool createdNewList = false;
+            for (auto classEntry = metaClassRange.first; classEntry != metaClassRange.second; classEntry++) {
+                const ObjCCategoryOptimizer::Category* category = classEntry->second;
+                if ( !category->cPropertyListVMAddress.has_value() )
+                    continue;
+
+                //fprintf(stderr, "  meta cat %s 0x%llx %s (%llu) => %zu\n", category->name.data(), category->vmAddress.value().rawValue(), this->objcOptimizer.objcDylibs[category->dylibObjcIndex.value()]->installName.data(), category->dylibObjcIndex.value(), classObjcIndex);
+                if ( !createdNewList ) {
+                    createdNewList = true;
+                    // New list with count and entry size
+                    listHeader->entsize = sizeof(ListOfListsEntry);
+                    listHeader->count = 0;
+                    chunkOffset += sizeof(ListOfListsEntry);
+                }
+
+                listEntry->imageIndex = category->dylibObjcIndex.value();
+                int64_t destVMAddr = (int64_t)category->cPropertyListVMAddress.value().rawValue();
+                listEntry->offset = destVMAddr - getVMAddressInSection(*categoriesChunk, listEntry).rawValue();
+                listEntry++;
+                listHeader->count++;
+                chunkOffset += sizeof(ListOfListsEntry);
+            }
+
+            if ( createdNewList ) {
+                objc_visitor::PropertyList propertyList = objcClass.getBaseProperties(objCVisitor);
+                // add original method list
+                if ( propertyList.numProperties() > 0 ) {
+                    listEntry->imageIndex = classObjcIndex;
+                    int64_t destVMAddr = (int64_t)propertyList.getVMAddress().value().rawValue();
+                    listEntry->offset = destVMAddr - getVMAddressInSection(*categoriesChunk, listEntry).rawValue();
+                } else {
+                    // add an empty list entry if no original was found
+                    // Zeroing the entry will make the offset point to itself
+                    // That will then be interpreted as a ListOfListsEntry of count 0
+                    // This also means that the image at index 0 needs to always be libobjc.A.dylib
+                    listEntry->imageIndex = 0;
+                    listEntry->offset = 0;
+                }
+                listEntry++;
+                listHeader->count++;
+                chunkOffset += sizeof(ListOfListsEntry);
+
+                //fprintf(stderr, "Total cProperty list lists: %d\n", listHeader->count);
+                // store new base properties pointer to update
+                CacheVMAddress newBaseProperties = categoriesChunk->cacheVMAddress + VMOffset(firstEntryOffset);
+                metadata_visitor::ResolvedValue field = objcClass.setBasePropertiesVMAddr(objCVisitor,
+                                                                                          VMAddress(newBaseProperties.rawValue() | 1));
+                cacheDylibSegments[field.segmentIndex()].tracker.add(field.value());
+            }
+        }
+        return;
+    };
+    auto visitCategoryClass = ^(objc_visitor::Visitor &objCVisitor,
+                                size_t classCacheIndex, size_t classObjcIndex,
+                                objc_visitor::Class &objcClass,
+                                std::span<DylibSegmentChunk> cacheDylibSegments) {
+        auto classRange = classMap.equal_range(objcClass.getVMAddress().rawValue());
+        if ( classRange.first == classRange.second )
+            return;
+
+        //fprintf(stderr, "class %s %s 0x%llx\n", this->cacheDylibs[classCacheIndex].installName.data(), objcClass.getName(objCVisitor), objcClass.getVMAddress().rawValue());
+        // instance method lists
+        {
+            uint8_t* listBuffer = categoriesChunk->subCacheBuffer + chunkOffset;
+            uint64_t firstEntryOffset = chunkOffset;
+            ListOfListsEntry* listHeader = (ListOfListsEntry*)listBuffer;
+            ListOfListsEntry* listEntry = listHeader + 1;
+
+            bool createdNewList = false;
+            for (auto classEntry = classRange.first; classEntry != classRange.second; classEntry++) {
+                const ObjCCategoryOptimizer::Category* category = classEntry->second;
+                if ( !category->iMethodListVMAddress.has_value() )
+                    continue;
+
+                //fprintf(stderr, "  cat %s 0x%llx %s (%llu) => %zu\n", category->name.data(), category->vmAddress.value().rawValue(), this->objcOptimizer.objcDylibs[category->dylibObjcIndex.value()]->installName.data(), category->dylibObjcIndex.value(), classObjcIndex);
+                if ( !createdNewList ) {
+                    createdNewList = true;
+                    // New list with count and entry size
+                    listHeader->entsize = sizeof(ListOfListsEntry);
+                    listHeader->count = 0;
+                    chunkOffset += sizeof(ListOfListsEntry);
+                }
+
+                listEntry->imageIndex = category->dylibObjcIndex.value();
+                int64_t destVMAddr = (int64_t)category->iMethodListVMAddress.value().rawValue();
+                listEntry->offset = destVMAddr - getVMAddressInSection(*categoriesChunk, listEntry).rawValue();
+                listEntry++;
+                listHeader->count++;
+                chunkOffset += sizeof(ListOfListsEntry);
+            }
+
+            if ( createdNewList ) {
+                objc_visitor::MethodList methodList = objcClass.getBaseMethods(objCVisitor);
+                // add original method list
+                if ( methodList.numMethods() > 0 ) {
+                    listEntry->imageIndex = classObjcIndex;
+                    int64_t destVMAddr = (int64_t)methodList.getVMAddress().value().rawValue();
+                    listEntry->offset = destVMAddr - getVMAddressInSection(*categoriesChunk, listEntry).rawValue();
+                } else {
+                    // add an empty list entry if no original was found
+                    listEntry->imageIndex = classObjcIndex;
+                    int64_t destVMAddr = (int64_t)emptyMethodListVMAddr.rawValue();
+                    listEntry->offset = destVMAddr - getVMAddressInSection(*categoriesChunk, listEntry).rawValue();
+                }
+                listEntry++;
+                listHeader->count++;
+                chunkOffset += sizeof(ListOfListsEntry);
+                //fprintf(stderr, "Total iMethod list lists: %d\n", listHeader->count);
+                // store new base methods pointer to update
+                CacheVMAddress newBaseMethods = categoriesChunk->cacheVMAddress + VMOffset(firstEntryOffset);
+                metadata_visitor::ResolvedValue field = objcClass.setBaseMethodsVMAddr(objCVisitor,
+                                                                                       VMAddress(newBaseMethods.rawValue() | 1),
+                                                                                       methodListPMD);
+                cacheDylibSegments[field.segmentIndex()].tracker.add(field.value());
+            }
+        }
+
+        // protocol lists
+        {
+            uint8_t* listBuffer = categoriesChunk->subCacheBuffer + chunkOffset;
+            uint64_t firstEntryOffset = chunkOffset;
+            ListOfListsEntry* listHeader = (ListOfListsEntry*)listBuffer;
+            ListOfListsEntry* listEntry = listHeader + 1;
+
+            bool createdNewList = false;
+            for (auto classEntry = classRange.first; classEntry != classRange.second; classEntry++) {
+                const ObjCCategoryOptimizer::Category* category = classEntry->second;
+                if ( !category->protocolListVMAddress.has_value() )
+                    continue;
+
+                //fprintf(stderr, "  cat %s 0x%llx %s (%llu) => %zu\n", category->name.data(), category->vmAddress.value().rawValue(), this->objcOptimizer.objcDylibs[category->dylibObjcIndex.value()]->installName.data(), category->dylibObjcIndex.value(), classObjcIndex);
+                if ( !createdNewList ) {
+                    createdNewList = true;
+                    // New list with count and entry size
+                    listHeader->entsize = sizeof(ListOfListsEntry);
+                    listHeader->count = 0;
+                    chunkOffset += sizeof(ListOfListsEntry);
+                }
+
+                listEntry->imageIndex = category->dylibObjcIndex.value();
+                int64_t destVMAddr = (int64_t)category->protocolListVMAddress.value().rawValue();
+                listEntry->offset = destVMAddr - getVMAddressInSection(*categoriesChunk, listEntry).rawValue();
+                listEntry++;
+                listHeader->count++;
+                chunkOffset += sizeof(ListOfListsEntry);
+            }
+
+            if ( createdNewList ) {
+                objc_visitor::ProtocolList protocolList = objcClass.getBaseProtocols(objCVisitor);
+                // add original protocol list
+                if ( protocolList.numProtocols(objCVisitor) > 0 ) {
+                    listEntry->imageIndex = classObjcIndex;
+                    int64_t destVMAddr = (int64_t)protocolList.getVMAddress().value().rawValue();
+                    listEntry->offset = destVMAddr - getVMAddressInSection(*categoriesChunk, listEntry).rawValue();
+                } else {
+                    // add an empty list entry if no original was found
+                    // Zeroing the entry will make the offset point to itself
+                    // That will then be interpreted as a ListOfListsEntry of count 0
+                    // This also means that the image at index 0 needs to always be libobjc.A.dylib
+                    listEntry->imageIndex = 0;
+                    listEntry->offset = 0;
+                }
+                listEntry++;
+                listHeader->count++;
+                chunkOffset += sizeof(ListOfListsEntry);
+
+                //fprintf(stderr, "Total Protocol list lists: %d\n", listHeader->count);
+                // store new base protocols pointer to update
+                CacheVMAddress newBaseProtocols = categoriesChunk->cacheVMAddress + VMOffset(firstEntryOffset);
+                metadata_visitor::ResolvedValue field = objcClass.setBaseProtocolsVMAddr(objCVisitor,
+                                                                                         VMAddress(newBaseProtocols.rawValue() | 1));
+                cacheDylibSegments[field.segmentIndex()].tracker.add(field.value());
+            }
+        }
+
+        // instance properties
+        {
+            uint8_t* listBuffer = categoriesChunk->subCacheBuffer + chunkOffset;
+            uint64_t firstEntryOffset = chunkOffset;
+            ListOfListsEntry* listHeader = (ListOfListsEntry*)listBuffer;
+            ListOfListsEntry* listEntry = listHeader + 1;
+
+            bool createdNewList = false;
+            for (auto classEntry = classRange.first; classEntry != classRange.second; classEntry++) {
+                const ObjCCategoryOptimizer::Category* category = classEntry->second;
+                if ( !category->iPropertyListVMAddress.has_value() )
+                    continue;
+
+                //fprintf(stderr, "  cat %s 0x%llx %s (%llu) => %zu\n", category->name.data(), category->vmAddress.value().rawValue(), this->objcOptimizer.objcDylibs[category->dylibObjcIndex.value()]->installName.data(), category->dylibObjcIndex.value(), classObjcIndex);
+                if ( !createdNewList ) {
+                    createdNewList = true;
+                    // New list with count and entry size
+                    listHeader->entsize = sizeof(ListOfListsEntry);
+                    listHeader->count = 0;
+                    chunkOffset += sizeof(ListOfListsEntry);
+                }
+
+                listEntry->imageIndex = category->dylibObjcIndex.value();
+                int64_t destVMAddr = (int64_t)category->iPropertyListVMAddress.value().rawValue();
+                listEntry->offset = destVMAddr - getVMAddressInSection(*categoriesChunk, listEntry).rawValue();
+                listEntry++;
+                listHeader->count++;
+                chunkOffset += sizeof(ListOfListsEntry);
+            }
+
+            if ( createdNewList ) {
+                objc_visitor::PropertyList propertyList = objcClass.getBaseProperties(objCVisitor);
+                // add original property list
+                if ( propertyList.numProperties() > 0 ) {
+                    listEntry->imageIndex = classObjcIndex;
+                    int64_t destVMAddr = (int64_t)propertyList.getVMAddress().value().rawValue();
+                    listEntry->offset = destVMAddr - getVMAddressInSection(*categoriesChunk, listEntry).rawValue();
+                } else {
+                    // add an empty list entry if no original was found
+                    // Zeroing the entry will make the offset point to itself
+                    // That will then be interpreted as a ListOfListsEntry of count 0
+                    // This also means that the image at index 0 needs to always be libobjc.A.dylib
+                    listEntry->imageIndex = 0;
+                    listEntry->offset = 0;
+                }
+                listEntry++;
+                listHeader->count++;
+                chunkOffset += sizeof(ListOfListsEntry);
+                //fprintf(stderr, "Total iProperty list lists: %d\n", listHeader->count);
+                // store new base properties pointer to update
+                CacheVMAddress newBaseProperties = categoriesChunk->cacheVMAddress + VMOffset(firstEntryOffset);
+                metadata_visitor::ResolvedValue field = objcClass.setBasePropertiesVMAddr(objCVisitor,
+                                                                                          VMAddress(newBaseProperties.rawValue() | 1));
+                cacheDylibSegments[field.segmentIndex()].tracker.add(field.value());
+            }
+        }
+
+        // class method lists and class properties
+        {
+            // check if we need to update the meta class
+            bool isPatchableClass;
+            VMAddress metaClassVMAddr = objcClass.getISA(objCVisitor, isPatchableClass).vmAddress();
+
+            bool visitMetaClassMap = false;
+            for (auto classEntry = classRange.first; classEntry != classRange.second; classEntry++) {
+                const ObjCCategoryOptimizer::Category* category = classEntry->second;
+                if ( category->cPropertyListVMAddress.has_value() || category->cMethodListVMAddress.has_value() ) {
+                    metaClassMap.insert({ metaClassVMAddr.rawValue(), category });
+                    visitMetaClassMap = true;
+                }
+            }
+            if ( visitMetaClassMap ) {
+                metadata_visitor::ResolvedValue metaClassValue = objCVisitor.getValueFor(metaClassVMAddr);
+                objc_visitor::Class objcMetaClass(metaClassValue, /*isMetaClass*/ true, /*isPatchable*/ false);
+                visitCategoryMetaClass(objCVisitor, classCacheIndex, classObjcIndex, objcMetaClass, cacheDylibSegments);
+            }
+        }
+    };
+
+    size_t objcIndex = 0;
+    for (size_t cacheIndex = 0; cacheIndex < this->cacheDylibs.size(); cacheIndex++) {
+        CacheDylib& cacheDylib = this->cacheDylibs[cacheIndex];
+        if ( !cacheDylib.inputMF->hasObjC() )
+            continue;
+
+        this->objcCategoryOptimizer.preAttachedDylibs.insert(objcIndex);
+        __block objc_visitor::Visitor objCVisitor = cacheDylib.makeCacheObjCVisitor(config, nullptr, nullptr, categoriesChunk);
+
+        // dylibsToClasses can contain multiple entries with the same pair of key/values
+        std::set<uint64_t> visitedClasses;
+        auto dylibToClassRange = dylibsToClasses.equal_range(cacheIndex);
+        for (auto dylibToClass = dylibToClassRange.first; dylibToClass != dylibToClassRange.second; dylibToClass++) {
+            if ( visitedClasses.contains(dylibToClass->second) )
+                continue;
+            visitedClasses.insert(dylibToClass->second);
+            metadata_visitor::ResolvedValue classValue = objCVisitor.getValueFor(VMAddress(dylibToClass->second));
+            objc_visitor::Class objcClass(classValue, /*isMetaClass*/ false, /*isPatchable*/ false);
+            visitCategoryClass(objCVisitor, cacheIndex, objcIndex, objcClass, cacheDylib.segments);
+        }
+        objcIndex++;
     }
 
     return Error();
@@ -6461,13 +7735,13 @@
 
     Diagnostics diag;
     auto objcClassOpt = (objc::ClassHashTable*)this->objcClassOptimizer.classHashTableChunk->subCacheBuffer;
-    buildSwiftHashTables(this->config, diag, this->cacheDylibs,
+    buildSwiftHashTables(this->config, diag, this->objcOptimizer.objcDylibs,
                          extraRegions, objcClassOpt,
                          this->objcOptimizer.headerInfoReadOnlyChunk->subCacheBuffer,
                          this->objcOptimizer.headerInfoReadWriteChunk->subCacheBuffer,
                          this->objcOptimizer.headerInfoReadOnlyChunk->cacheVMAddress,
+                         swiftPrespecializedDylib,
                          this->swiftProtocolConformanceOptimizer);
-
     if ( diag.hasError() )
         return Error("Couldn't build Swift protocol opts because: %s", diag.errorMessageCStr());
 
@@ -6488,7 +7762,7 @@
     Timer::Scope timedScope(this->config, "computeSlideInfo time");
 
     if ( !this->config.slideInfo.slideInfoFormat.has_value() ) {
-        assert(this->options.isSimultor());
+        assert(this->options.isSimulator());
     }
 
     Error err = parallel::forEach(this->subCaches, ^(size_t index, SubCache& subCache) {
@@ -6524,11 +7798,14 @@
                 switch ( region.kind ) {
                     case Region::Kind::text:
                     case Region::Kind::dynamicConfig:
+                    case Region::Kind::readOnly:
                     case Region::Kind::linkedit:
                         maxSlide = std::min(maxSlide, subCacheLimit - region.subCacheVMSize);
                         break;
+                    case Region::Kind::tproConst:
                     case Region::Kind::data:
                     case Region::Kind::dataConst:
+                    case Region::Kind::tproAuthConst:
                     case Region::Kind::auth:
                     case Region::Kind::authConst:
                         if ( firstDataRegion == nullptr )
@@ -6553,6 +7830,11 @@
     // We must be a largeContiguous cache. Others were dealt with above in the x86_64 and/or sim cases
     assert(this->config.layout.contiguous.has_value());
 
+    // Some caches have a fixed max slide
+    if ( this->config.layout.cacheFixedSlide.has_value() ) {
+        return this->config.layout.cacheFixedSlide.value();
+    }
+
     // Start off making sure we can't slide past the end of the cache
     CacheVMAddress maxVMAddress(0ULL);
     for ( const Region& region : this->subCaches.back().regions ) {
@@ -6583,6 +7865,10 @@
     if ( sizeUpToTextEnd <= twoGB )
         maxSlide = CacheVMSize(twoGB - sizeUpToTextEnd);
 
+    if ( this->config.layout.cacheMaxSlide.has_value() ) {
+        maxSlide = std::min(maxSlide, CacheVMSize(this->config.layout.cacheMaxSlide.value()));
+    }
+
     return maxSlide.rawValue();
 }
 
@@ -6596,10 +7882,35 @@
         Diagnostics diag;
         cacheDylib.addObjcSegments(diag, aggregateTimer,
                                    this->objcOptimizer.headerInfoReadOnlyChunk,
+                                   this->objcOptimizer.imageInfoChunk,
                                    this->objcProtocolOptimizer.protocolHashTableChunk,
+                                   this->objcCategoryOptimizer.categoriesChunk,
                                    this->objcOptimizer.headerInfoReadWriteChunk,
                                    this->objcProtocolOptimizer.canonicalProtocolsChunk);
     }
+}
+
+Error SharedCacheBuilder::patchLinkedDylibs(CacheDylib& cacheDylib)
+{
+    if ( swiftPrespecializedDylib == nullptr )
+        return Error::none();
+
+    Timer::Scope timedScope(this->config, "patchLinkedDylibs time");
+    Timer::AggregateTimer aggregateTimerOwner(this->config);
+
+    Diagnostics diag;
+    if ( &cacheDylib == swiftPrespecializedDylib ) {
+        // remove all but libSystem
+        cacheDylib.removeLinkedDylibs(diag);
+    } else if ( cacheDylib.installName.find("libswiftCore.dylib") != std::string_view::npos ) {
+        // add Swift prespecialized dylib dependency to libswiftCore
+        cacheDylib.addLinkedDylib(diag, *swiftPrespecializedDylib);
+    }
+
+    if ( diag.hasError() )
+        return Error("%s", diag.errorMessageCStr());
+
+    return Error::none();
 }
 
 void SharedCacheBuilder::computeCacheHeaders()
@@ -6730,6 +8041,16 @@
     return buff;
 }
 
+std::span<const std::string_view> SharedCacheBuilder::getEvictedDylibs() const
+{
+    return this->evictedDylibs;
+}
+
+std::string_view SharedCacheBuilder::getSwiftPrespecializedDylibBuildError() const
+{
+    return swiftPrespecializedDylibBuildError;
+}
+
 void SharedCacheBuilder::getResults(std::vector<CacheBuffer>& results) const
 {
     for ( const SubCache& subCache : this->subCaches ) {
@@ -6767,12 +8088,15 @@
                 case Region::Kind::text:
                     prot = "EX";
                     break;
+                case Region::Kind::tproConst:
                 case Region::Kind::data:
                 case Region::Kind::dataConst:
+                case Region::Kind::tproAuthConst:
                 case Region::Kind::auth:
                 case Region::Kind::authConst:
                     prot = "RW";
                     break;
+                case Region::Kind::readOnly:
                 case Region::Kind::linkedit:
                     prot = "RO";
                     break;