Loading...
cache_builder/CacheDylib.cpp /dev/null dyld-1330
--- /dev/null
+++ dyld/dyld-1330/cache_builder/CacheDylib.cpp
@@ -0,0 +1,3061 @@
+/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil -*-
+*
+* Copyright (c) 2017 Apple Inc. All rights reserved.
+*
+* @APPLE_LICENSE_HEADER_START@
+*
+* This file contains Original Code and/or Modifications of Original Code
+* as defined in and that are subject to the Apple Public Source License
+* Version 2.0 (the 'License'). You may not use this file except in
+* compliance with the License. Please obtain a copy of the License at
+* http://www.opensource.apple.com/apsl/ and read it before using this
+* file.
+*
+* The Original Code and all software distributed under the License are
+* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+* Please see the License for the specific language governing rights and
+* limitations under the License.
+*
+* @APPLE_LICENSE_HEADER_END@
+*/
+
+#include "Array.h"
+#include "BuilderConfig.h"
+#include "BuilderOptions.h"
+#include "CacheDylib.h"
+#include "Chunk.h"
+#include "MachOFile.h"
+#include "MachOFileAbstraction.hpp"
+#include "Header.h"
+#include "ObjCVisitor.h"
+#include "Optimizers.h"
+#include "OptimizerObjC.h"
+#include "StringUtils.h"
+#include "Trie.hpp"
+
+// mach_o
+#include "Header.h"
+#include "Image.h"
+#include "FunctionVariants.h"
+
+#include <CommonCrypto/CommonHMAC.h>
+#include <CommonCrypto/CommonDigest.h>
+#include <CommonCrypto/CommonDigestSPI.h>
+
+#include <optional>
+#include <vector>
+
+// mach_o_writer
+#include "HeaderWriter.h"
+
+// FIXME: We should get this from cctools
+#define DYLD_CACHE_ADJ_V2_FORMAT 0x7F
+
+#define DYLD_CACHE_ADJ_V2_POINTER_32            0x01
+#define DYLD_CACHE_ADJ_V2_POINTER_64            0x02
+#define DYLD_CACHE_ADJ_V2_DELTA_32                0x03
+#define DYLD_CACHE_ADJ_V2_DELTA_64                0x04
+#define DYLD_CACHE_ADJ_V2_ARM64_ADRP            0x05
+#define DYLD_CACHE_ADJ_V2_ARM64_OFF12            0x06
+#define DYLD_CACHE_ADJ_V2_ARM64_BR26            0x07
+#define DYLD_CACHE_ADJ_V2_ARM_MOVW_MOVT            0x08
+#define DYLD_CACHE_ADJ_V2_ARM_BR24                0x09
+#define DYLD_CACHE_ADJ_V2_THUMB_MOVW_MOVT        0x0A
+#define DYLD_CACHE_ADJ_V2_THUMB_BR22            0x0B
+#define DYLD_CACHE_ADJ_V2_IMAGE_OFF_32            0x0C
+#define DYLD_CACHE_ADJ_V2_THREADED_POINTER_64   0x0D
+
+using namespace cache_builder;
+using dyld3::MachOFile;
+using error::Error;
+using mach_o::Header;
+using mach_o::Version32;
+using mach_o::Image;
+using mach_o::FunctionVariantFixups;
+
+//
+// MARK: --- cache_builder::CacheDylib methods ---
+//
+
+#if BUILDING_CACHE_BUILDER_UNIT_TESTS
+CacheDylib::CacheDylib()
+{
+}
+#endif
+
+CacheDylib::CacheDylib(InputFile& inputFile)
+    : inputFile(&inputFile)
+    , inputMF(inputFile.mf)
+    , inputHdr((const Header*)inputFile.mf)
+    , inputLoadAddress(this->inputHdr->preferredLoadAddress())
+    , installName(this->inputHdr->installName())
+{
+    if ( inputFile.mf )
+        inputImage = std::make_unique<mach_o::Image>(inputFile.mf, inputFile.size, Image::MappingKind::wholeSliceMapped);
+}
+
+CacheDylib::CacheDylib(std::string_view installName)
+    : inputFile(nullptr)
+    , inputMF(nullptr)
+    , inputHdr(nullptr)
+    , inputLoadAddress(0ull)
+    , installName(installName)
+{
+}
+
+// If you want to watch a location, set a breakpoint here.  The way to use this is to work out
+// the segment you want, and the address of the location in the *source* dylib.  This will then
+// compute the equivalent location in the cache builder buffers
+#if DEBUG
+__attribute__((noinline))
+void CacheDylib::watchMemory(const DylibSegmentChunk& segment,
+                             std::string_view dylibInstallName,
+                             std::string_view dylibSegmentName,
+                             uint64_t dylibAddressInSegment) const
+{
+    if ( this->installName != dylibInstallName )
+        return;
+    if ( segment.segmentName != dylibSegmentName )
+        return;
+
+    printf("watchpoint set expression -w w -s 8 -- %p\n",
+           segment.subCacheBuffer + dylibAddressInSegment - segment.inputVMAddress.rawValue());
+    printf("watchpoint set expression -w w -s 4 -- %p\n",
+           segment.subCacheBuffer + dylibAddressInSegment - segment.inputVMAddress.rawValue());
+    printf("");
+}
+#endif
+
+static bool hasUnalignedFixups(const MachOFile* mf)
+{
+    // arm64e chained fixup formats are always 8-byte aligned
+    if ( mf->isArch("arm64e") )
+        return false;
+
+    uint32_t pointerMask = mf->pointerSize() - 1;
+
+    __block Diagnostics diag;
+    __block bool foundUnalignedFixup = false;
+    mf->withFileLayout(diag, ^(const mach_o::Layout &layout) {
+        mach_o::Fixups fixups(layout);
+
+        if ( mf->hasChainedFixups() ) {
+            fixups.withChainStarts(diag, ^(const dyld_chained_starts_in_image* starts) {
+                fixups.forEachFixupInAllChains(diag, starts, false, ^(mach_o::ChainedFixupPointerOnDisk* fixupLoc, uint64_t fixupSegmentOffset, const dyld_chained_starts_in_segment* segInfo, bool& stop) {
+                    if ( (fixupSegmentOffset & pointerMask) != 0 ) {
+                        foundUnalignedFixup = true;
+                        stop = true;
+                        return;
+                    }
+                });
+            });
+        } else {
+            fixups.forEachRebaseLocation_Opcodes(diag, ^(uint64_t runtimeOffset, uint32_t segmentIndex, bool &stop) {
+                if ( (runtimeOffset & pointerMask) != 0 ) {
+                    foundUnalignedFixup = true;
+                    stop = true;
+                    return;
+                }
+            });
+            fixups.forEachBindLocation_Opcodes(diag, ^(uint64_t runtimeOffset, uint32_t segmentIndex, unsigned int targetIndex, bool &stop) {
+                if ( (runtimeOffset & pointerMask) != 0 ) {
+                    foundUnalignedFixup = true;
+                    stop = true;
+                    return;
+                }
+            }, ^(uint64_t runtimeOffset, uint32_t segmentIndex, unsigned int overrideBindTargetIndex, bool &stop) {
+                if ( (runtimeOffset & pointerMask) != 0 ) {
+                    foundUnalignedFixup = true;
+                    stop = true;
+                    return;
+                }
+            });
+        }
+    });
+
+    diag.assertNoError();
+
+    return foundUnalignedFixup;
+}
+
+static const bool segmentHasAuthFixups(const MachOFile* mf, uint32_t segmentIndexToSearch)
+{
+    // non-arm64e cannot have auth fixups
+    if ( !mf->isArch("arm64e") )
+        return false;
+
+    __block Diagnostics diag;
+    __block bool foundAuthFixup = false;
+    mf->withFileLayout(diag, ^(const mach_o::Layout &layout) {
+        mach_o::Fixups fixups(layout);
+
+        if ( mf->hasChainedFixups() ) {
+            fixups.withChainStarts(diag, ^(const dyld_chained_starts_in_image* starts) {
+                fixups.forEachFixupChainSegment(diag, starts, ^(const dyld_chained_starts_in_segment *segInfo, uint32_t segIndex, bool &stopSegment) {
+                    if ( segIndex != segmentIndexToSearch )
+                        return;
+                    fixups.forEachFixupInSegmentChains(diag, segInfo, segIndex, true,
+                                                       ^(dyld3::MachOFile::ChainedFixupPointerOnDisk *fixupLocation, uint64_t fixupSegmentOffset, bool &stopChain) {
+                        if ( fixupLocation->arm64e.rebase.auth ) {
+                            foundAuthFixup = true;
+                            stopChain = true;
+                            stopSegment = true;
+                        }
+                    });
+                });
+            });
+        }
+
+        // Move to auth if __objc_const or __objc_data is present.
+        // This allows new method lists added by the category optimizer to be signed.
+        // Note the linker eagerly moves these sections to AUTH, as of rdar://111858154,
+        // so it is not expected that this code ever finds anything to move, but we'll keep it to be safe
+        ((const Header*)mf)->forEachSection(^(const Header::SegmentInfo &segInfo, const Header::SectionInfo &sectInfo, bool &stop) {
+            if ( segInfo.segmentIndex != segmentIndexToSearch )
+                return;
+            if ( (sectInfo.sectionName == "__objc_const") || (sectInfo.sectionName == "__objc_data") ) {
+                foundAuthFixup = true;
+                stop = true;
+            }
+        });
+    });
+
+    return foundAuthFixup;
+}
+
+void CacheDylib::categorizeSegments(const BuilderConfig& config,
+                                    objc_visitor::Visitor& objcVisitor)
+{
+    bool hasUnalignedFixups = ::hasUnalignedFixups(this->inputMF);
+    this->inputHdr->forEachSegment(^(const Header::SegmentInfo& info, uint64_t sizeOfSections, uint32_t maxAlignOfSections, bool& stop) {
+        auto addSegment = [&](DylibSegmentChunk::Kind kind) {
+            // TODO: Cache VMSize/fileSize might be less than input VMSize if we deduplicate strings for example
+            uint64_t inputFileSize = std::min((uint64_t)info.fileSize, sizeOfSections);
+            uint64_t cacheFileSize = sizeOfSections;
+            uint64_t vmSize        = sizeOfSections;
+
+            // LINKEDIT doesn't get space any more. Its individual chunks will get their own space
+            if ( info.segmentName == "__LINKEDIT" ) {
+                inputFileSize = 0;
+                cacheFileSize = 0;
+                vmSize        = 0;
+            }
+
+            uint64_t minAlignment = 1 << maxAlignOfSections;
+            // Always align __TEXT to a page as split seg can't handle less
+            if ( info.segmentName == "__TEXT" )
+                minAlignment = config.layout.machHeaderAlignment;
+            else if ( hasUnalignedFixups )
+                minAlignment = (this->inputHdr->uses16KPages() ? 0x4000 : 0x1000);
+
+            DylibSegmentChunk segment(kind, minAlignment);
+            segment.segmentName     = info.segmentName;
+            segment.inputFile       = this->inputFile;
+            segment.inputFileOffset = InputDylibFileOffset((uint64_t)info.fileOffset);
+            segment.inputFileSize   = InputDylibFileSize(inputFileSize);
+            segment.inputVMAddress  = InputDylibVMAddress(info.vmaddr);
+            segment.inputVMSize     = InputDylibVMSize(info.vmsize);
+
+            segment.cacheVMSize      = CacheVMSize(vmSize);
+            segment.subCacheFileSize = CacheFileSize(cacheFileSize);
+
+            // Santify check.  The cache buffer adds zero fill so VMSize should always be the largest.
+            assert(segment.inputFileSize.rawValue() <= segment.cacheVMSize.rawValue());
+            assert(segment.subCacheFileSize.rawValue() <= segment.cacheVMSize.rawValue());
+
+            this->segments.push_back(std::move(segment));
+        };
+
+        // __TEXT
+        if ( info.initProt == (VM_PROT_READ | VM_PROT_EXECUTE) ) {
+            addSegment(DylibSegmentChunk::Kind::dylibText);
+            return;
+        }
+
+        // DATA*
+        if ( info.initProt == (VM_PROT_READ | VM_PROT_WRITE) ) {
+            // If we don't have split seg v2, then all __DATA* segments must look like __DATA so that they
+            // stay contiguous
+            __block bool isSplitSegV2 = false;
+            Diagnostics diag;
+            this->inputMF->withFileLayout(diag, ^(const mach_o::Layout& layout) {
+                mach_o::SplitSeg splitSeg(layout);
+
+                isSplitSegV2 = splitSeg.isV2();
+            });
+            diag.assertNoError();
+
+            if ( !isSplitSegV2 ) {
+                addSegment(DylibSegmentChunk::Kind::dylibData);
+                return;
+            }
+
+            if ( info.segmentName == "__TPRO_CONST" ) {
+                addSegment(DylibSegmentChunk::Kind::tproDataConst);
+                return;
+            }
+
+            if ( info.segmentName == "__OBJC_CONST" ) {
+                // In arm64e, "__OBJC_CONST __objc_class_ro" contains authenticated values
+                if ( config.layout.hasAuthRegion )
+                    addSegment(DylibSegmentChunk::Kind::dylibAuthConst);
+                else
+                    addSegment(DylibSegmentChunk::Kind::dylibDataConst);
+                return;
+            }
+
+            if ( info.segmentName == "__DATA_DIRTY" ) {
+                addSegment(DylibSegmentChunk::Kind::dylibDataDirty);
+                return;
+            }
+
+            bool hasAuthFixups = false;
+            if ( (info.segmentName == "__AUTH") || (info.segmentName == "__AUTH_CONST") ) {
+                hasAuthFixups = true;
+            } else if ( config.layout.hasAuthRegion ) {
+                // HACK: Some dylibs don't get __AUTH segments.  This matches ld64
+                hasAuthFixups = segmentHasAuthFixups(this->inputMF, info.segmentIndex);
+            }
+
+            bool isConst = info.segmentName.ends_with("_CONST");
+            if ( hasAuthFixups ) {
+                // AUTH/AUTH_CONST
+                if ( isConst ) {
+                    // AUTH_CONST
+                    addSegment(DylibSegmentChunk::Kind::dylibAuthConst);
+                    return;
+                } else {
+                    // AUTH
+                    addSegment(DylibSegmentChunk::Kind::dylibAuth);
+                    return;
+                }
+            } else {
+                // DATA/DATA_CONST
+                if ( isConst ) {
+                    // DATA_CONST
+                    addSegment(DylibSegmentChunk::Kind::dylibDataConst);
+                    return;
+                } else {
+                    // DATA
+                    addSegment(DylibSegmentChunk::Kind::dylibData);
+                    return;
+                }
+            }
+        }
+
+        // LINKEDIT/readOnly
+        if ( info.initProt == (VM_PROT_READ) ) {
+            if ( info.segmentName != "__LINKEDIT" ) {
+                addSegment(DylibSegmentChunk::Kind::dylibReadOnly);
+                return;
+            }
+
+            addSegment(DylibSegmentChunk::Kind::dylibLinkedit);
+            return;
+        }
+
+        // Not text/data/linkedit.  This should have been caught by canBePlacedInDyldCache()
+        assert(0);
+    });
+}
+
+// The export trie might grow, as addresses outside of __TEXT will need more uleb bytes to encode when their
+// addresses grow.  Estimate how much space we need to grow the given trie
+static uint32_t estimateExportTrieSize(const uint8_t* start, const uint8_t* end)
+{
+
+    // FIXME: This is terrible.  We could actually estimate the result, not just calculate it
+    // Eg, just assume all nodes outside __TEXT will grow by however many bytes it takes to encode about 2GB
+    std::vector<uint8_t> newTrieBytes;
+    Diagnostics          diag;
+
+    if ( start == end )
+        return 0;
+
+    // since export info addresses are offsets from mach_header, everything in __TEXT is fine
+    // only __DATA addresses need to be updated
+    std::vector<ExportInfoTrie::Entry> originalExports;
+    if ( !ExportInfoTrie::parseTrie(start, end, originalExports) ) {
+        diag.error("malformed exports trie in");
+        assert(0);
+        return 0;
+    }
+
+    std::vector<ExportInfoTrie::Entry> newExports;
+    newExports.reserve(originalExports.size());
+
+    // Assume dylibs start at 0, and will slide to 2GB
+    uint64_t baseAddress      = 0;
+    uint64_t baseAddressSlide = 1ULL << 31;
+    for ( auto& entry : originalExports ) {
+        // remove symbols used by the static linker only
+        // FIXME: This can result in the cache export-trie being smaller than the input dylib
+        // But then the initial linkedit chunk doesn't contain the whole trie and adjustExportsTrie() fails
+        // If we are going to allow a smaller true in the cache, then we need adjustExportsTrie() to consume the
+        // trie from the input dylib, and emit a trie in to the cache.
+#if 0
+        if (   (strncmp(entry.name.c_str(), "$ld$", 4) == 0)
+            || (strncmp(entry.name.c_str(), ".objc_class_name",16) == 0)
+            || (strncmp(entry.name.c_str(), ".objc_category_name",19) == 0) ) {
+            continue;
+        }
+#endif
+        // adjust symbols in slid segments
+        if ( (entry.info.flags & EXPORT_SYMBOL_FLAGS_KIND_MASK) != EXPORT_SYMBOL_FLAGS_KIND_ABSOLUTE )
+            entry.info.address += (baseAddressSlide - baseAddress);
+        newExports.push_back(entry);
+    }
+
+    // rebuild export trie
+    newTrieBytes.reserve(end - start);
+
+    ExportInfoTrie(newExports).emit(newTrieBytes);
+    // align
+    while ( (newTrieBytes.size() % sizeof(uint64_t)) != 0 )
+        newTrieBytes.push_back(0);
+
+    // HACK: copyRawSegments() is going to first copy the original trie in to the buffer, so make
+    // sure we have at least that much space
+    size_t requiredSize = std::max((uint64_t)newTrieBytes.size(), (uint64_t)end - (uint64_t)start);
+
+    return (uint32_t)requiredSize;
+}
+
+void CacheDylib::categorizeLinkedit(const BuilderConfig& config)
+{
+    uint32_t pointerSize = config.layout.is64 ? 8 : 4;
+
+    __block Diagnostics diag;
+    this->inputMF->forEachLoadCommand(diag, ^(const load_command* cmd, bool& stop) {
+        typedef Chunk::Kind Kind;
+        auto addLinkedit = [&](Kind kind, InputDylibFileOffset inputFileOffset,
+                               InputDylibFileSize inputFileSize, CacheVMSize estimatedCacheVMSize,
+                               uint64_t minAlignment) {
+            LinkeditDataChunk chunk(kind, minAlignment);
+            chunk.inputFile       = this->inputFile;
+            chunk.inputFileOffset = inputFileOffset;
+            chunk.inputFileSize   = inputFileSize;
+
+            chunk.cacheVMSize      = estimatedCacheVMSize;
+            chunk.subCacheFileSize = CacheFileSize(estimatedCacheVMSize.rawValue());
+            this->linkeditChunks.push_back(std::move(chunk));
+        };
+
+        switch ( cmd->cmd ) {
+            case LC_SYMTAB: {
+                const symtab_command* symTabCmd = (const symtab_command*)cmd;
+
+                // NList
+                uint64_t nlistEntrySize  = config.layout.is64 ? sizeof(struct nlist_64) : sizeof(struct nlist);
+                uint64_t symbolTableSize = symTabCmd->nsyms * nlistEntrySize;
+                addLinkedit(Kind::linkeditSymbolNList, InputDylibFileOffset((uint64_t)symTabCmd->symoff),
+                            InputDylibFileSize(symbolTableSize), CacheVMSize(symbolTableSize),
+                            pointerSize);
+
+                // Symbol strings
+                addLinkedit(Kind::linkeditSymbolStrings, InputDylibFileOffset((uint64_t)symTabCmd->stroff),
+                            InputDylibFileSize((uint64_t)symTabCmd->strsize), CacheVMSize((uint64_t)symTabCmd->strsize),
+                            1);
+                break;
+            }
+            case LC_DYSYMTAB: {
+                const dysymtab_command* dynSymTabCmd = (const dysymtab_command*)cmd;
+
+                assert(dynSymTabCmd->tocoff == 0);
+                assert(dynSymTabCmd->ntoc == 0);
+                assert(dynSymTabCmd->modtaboff == 0);
+                assert(dynSymTabCmd->nmodtab == 0);
+                assert(dynSymTabCmd->extrefsymoff == 0);
+                assert(dynSymTabCmd->nextrefsyms == 0);
+
+                if ( dynSymTabCmd->indirectsymoff != 0 ) {
+                    assert(dynSymTabCmd->nindirectsyms != 0);
+
+                    // Indirect symbols
+                    uint64_t entrySize = sizeof(uint32_t);
+                    uint64_t tableSize = dynSymTabCmd->nindirectsyms * entrySize;
+                    addLinkedit(Kind::linkeditIndirectSymbols, InputDylibFileOffset((uint64_t)dynSymTabCmd->indirectsymoff),
+                                InputDylibFileSize(tableSize), CacheVMSize(tableSize),
+                                4);
+                }
+                else {
+                    assert(dynSymTabCmd->nindirectsyms == 0);
+                }
+
+                assert(dynSymTabCmd->extreloff == 0);
+                assert(dynSymTabCmd->nextrel == 0);
+                assert(dynSymTabCmd->locreloff == 0);
+                assert(dynSymTabCmd->nlocrel == 0);
+                break;
+            }
+            case LC_DYLD_INFO:
+            case LC_DYLD_INFO_ONLY: {
+                // Most things should be chained fixups, but some old dylibs exist for back deployment
+                const dyld_info_command* linkeditCmd = (const dyld_info_command*)cmd;
+
+                this->inputMF->withFileLayout(diag, ^(const mach_o::Layout &layout) {
+                    this->inputDylibRebaseStart   = layout.linkedit.rebaseOpcodes.buffer;
+                    this->inputDylibRebaseEnd     = this->inputDylibRebaseStart + layout.linkedit.rebaseOpcodes.bufferSize;
+                    this->inputDylibBindStart     = layout.linkedit.regularBindOpcodes.buffer;
+                    this->inputDylibBindEnd       = this->inputDylibBindStart + layout.linkedit.regularBindOpcodes.bufferSize;
+                    this->inputDylibLazyBindStart = layout.linkedit.lazyBindOpcodes.buffer;
+                    this->inputDylibLazyBindEnd   = this->inputDylibLazyBindStart + layout.linkedit.lazyBindOpcodes.bufferSize;
+                    this->inputDylibWeakBindStart = layout.linkedit.weakBindOpcodes.buffer;
+                    this->inputDylibWeakBindEnd   = this->inputDylibWeakBindStart + layout.linkedit.weakBindOpcodes.bufferSize;
+
+                    // The export trie is going to change size, as it might grow/shrink based on removing elements
+                    // but addresses growing in size
+                    const uint8_t* trieStart = layout.linkedit.exportsTrie.buffer;
+                    const uint8_t* trieEnd   = trieStart + layout.linkedit.exportsTrie.bufferSize;
+                    uint32_t estimatedSize = estimateExportTrieSize(trieStart, trieEnd);
+
+                    addLinkedit(Kind::linkeditExportTrie, InputDylibFileOffset((uint64_t)linkeditCmd->export_off),
+                                InputDylibFileSize((uint64_t)linkeditCmd->export_size), CacheVMSize((uint64_t)estimatedSize),
+                                pointerSize);
+                });
+                break;
+            }
+            case LC_SEGMENT_SPLIT_INFO: {
+                // The final cache dylib won't have split seg, but keep a pointer to the source dylib split seg, for use later
+                this->inputMF->withFileLayout(diag, ^(const mach_o::Layout &layout) {
+                    this->inputDylibSplitSegStart = layout.linkedit.splitSegInfo.buffer;
+                    this->inputDylibSplitSegEnd   = this->inputDylibSplitSegStart + layout.linkedit.splitSegInfo.bufferSize;
+                });
+                break;
+            }
+            case LC_FUNCTION_STARTS: {
+                const linkedit_data_command* linkeditCmd = (const linkedit_data_command*)cmd;
+
+                addLinkedit(Kind::linkeditFunctionStarts, InputDylibFileOffset((uint64_t)linkeditCmd->dataoff),
+                            InputDylibFileSize((uint64_t)linkeditCmd->datasize), CacheVMSize((uint64_t)linkeditCmd->datasize),
+                            pointerSize);
+                break;
+            }
+            case LC_DATA_IN_CODE: {
+                const linkedit_data_command* linkeditCmd = (const linkedit_data_command*)cmd;
+
+                addLinkedit(Kind::linkeditDataInCode, InputDylibFileOffset((uint64_t)linkeditCmd->dataoff),
+                            InputDylibFileSize((uint64_t)linkeditCmd->datasize), CacheVMSize((uint64_t)linkeditCmd->datasize),
+                            pointerSize);
+                break;
+            }
+            case LC_DYLD_CHAINED_FIXUPS: {
+                // Drop chained fixups
+                break;
+            }
+            case LC_DYLD_EXPORTS_TRIE: {
+                const linkedit_data_command* linkeditCmd = (const linkedit_data_command*)cmd;
+
+                this->inputMF->withFileLayout(diag, ^(const mach_o::Layout &layout) {
+                    // The export trie is going to change size, as it might grow/shrink based on removing elements
+                    // but addresses growing in size
+                    const uint8_t* trieStart = layout.linkedit.exportsTrie.buffer;
+                    const uint8_t* trieEnd   = trieStart + layout.linkedit.exportsTrie.bufferSize;
+                    uint32_t estimatedSize = estimateExportTrieSize(trieStart, trieEnd);
+
+
+                    addLinkedit(Kind::linkeditExportTrie, InputDylibFileOffset((uint64_t)linkeditCmd->dataoff),
+                                InputDylibFileSize((uint64_t)linkeditCmd->datasize), CacheVMSize((uint64_t)estimatedSize),
+                                pointerSize);
+                });
+                break;
+            }
+            case LC_FUNCTION_VARIANTS: {
+                const linkedit_data_command* linkeditCmd = (const linkedit_data_command*)cmd;
+
+                addLinkedit(Kind::linkeditFunctionVariants, InputDylibFileOffset((uint64_t)linkeditCmd->dataoff),
+                            InputDylibFileSize((uint64_t)linkeditCmd->datasize), CacheVMSize((uint64_t)linkeditCmd->datasize),
+                            pointerSize);
+                break;
+            }
+        }
+    });
+    diag.assertNoError();
+}
+
+void CacheDylib::copyRawSegments(const BuilderConfig& config, Timer::AggregateTimer& timer)
+{
+    const bool log = config.log.printDebugCacheLayout;
+
+    Timer::AggregateTimer::Scope timedScope(timer, "dylib copyRawSegments time");
+
+    for ( const DylibSegmentChunk& segment : this->segments ) {
+        const uint8_t* srcSegment = (uint8_t*)segment.inputFile->mf + segment.inputFileOffset.rawValue();
+
+        if ( segment.subCacheBuffer == nullptr ) {
+            // Note, Linkedit isn't copied here, so will have no buffer, even though it has a size
+            assert( (segment.cacheVMSize == CacheVMSize(0ULL)) || (segment.segmentName == "__LINKEDIT") );
+            if ( log ) {
+                config.log.log("Skipping empty segment %s\n", segment.segmentName.data());
+            }
+        }
+        else {
+            if ( log ) {
+                config.log.log("Copying %s from %p to (%p..%p)\n", segment.segmentName.data(), srcSegment, segment.subCacheBuffer, segment.subCacheBuffer + segment.inputFileSize.rawValue());
+            }
+            memcpy(segment.subCacheBuffer, srcSegment, segment.inputFileSize.rawValue());
+        }
+
+#if DEBUG
+        watchMemory(segment, "install name", "segment name", 0x0);
+#endif
+    }
+
+    // Also copy linkedit in to place
+    for ( const LinkeditDataChunk& chunk : this->linkeditChunks ) {
+        const uint8_t* srcChunk = (uint8_t*)chunk.inputFile->mf + chunk.inputFileOffset.rawValue();
+        if ( log ) {
+            config.log.log("Copying from %p to (%p..%p)\n", srcChunk, chunk.subCacheBuffer, chunk.subCacheBuffer + chunk.inputFileSize.rawValue());
+        }
+        memcpy(chunk.subCacheBuffer, srcChunk, chunk.inputFileSize.rawValue());
+    }
+
+    // The nlist was optimized.  Its not in the linkeditChunks
+    if ( !this->optimizedSymbols.nlist64.empty() ) {
+        memcpy(this->optimizedSymbols.subCacheBuffer, this->optimizedSymbols.nlist64.data(),
+               sizeof(struct nlist_64) * this->optimizedSymbols.nlist64.size());
+    } else {
+        memcpy(this->optimizedSymbols.subCacheBuffer, this->optimizedSymbols.nlist32.data(),
+               sizeof(struct nlist) * this->optimizedSymbols.nlist32.size());
+    }
+}
+
+void CacheDylib::applySplitSegInfo(Diagnostics& diag, const BuilderOptions& options,
+                                   const BuilderConfig& config, Timer::AggregateTimer& timer,
+                                   UnmappedSymbolsOptimizer& unmappedSymbolsOptimizer)
+{
+    Timer::AggregateTimer::Scope timedScope(timer, "dylib applySplitSegInfo time");
+
+    __block const uint8_t* chainedFixupsStart = nullptr;
+    __block const uint8_t* chainedFixupsEnd   = nullptr;
+    __block const uint8_t* rebaseOpcodesStart = nullptr;
+    __block const uint8_t* rebaseOpcodesEnd   = nullptr;
+
+    this->inputMF->withFileLayout(diag, ^(const mach_o::Layout &layout) {
+        if ( layout.linkedit.regularBindOpcodes.hasValue() ) {
+            rebaseOpcodesStart = layout.linkedit.rebaseOpcodes.buffer;
+            rebaseOpcodesEnd = rebaseOpcodesStart + layout.linkedit.rebaseOpcodes.bufferSize;
+        } else if ( layout.linkedit.chainedFixups.hasValue() ) {
+            chainedFixupsStart = layout.linkedit.chainedFixups.buffer;
+            chainedFixupsEnd = chainedFixupsStart + layout.linkedit.chainedFixups.bufferSize;
+        }
+    });
+
+    adjustor->adjustDylib(diag, config.layout.cacheBaseAddress, this->cacheMF, this->installName,
+                          chainedFixupsStart, chainedFixupsEnd, this->inputDylibSplitSegStart, this->inputDylibSplitSegEnd,
+                          rebaseOpcodesStart, rebaseOpcodesEnd, &this->optimizedSections);
+
+    // Not strictly part of the dylib any more, but the unmapped locals also need adjusting
+    if ( options.localSymbolsMode == cache_builder::LocalSymbolsMode::unmap ) {
+        typedef UnmappedSymbolsOptimizer::LocalSymbolInfo LocalSymbolInfo;
+        LocalSymbolInfo& symbolInfo = unmappedSymbolsOptimizer.symbolInfos[this->cacheIndex];
+        for ( uint32_t i = 0; i != symbolInfo.nlistCount; ++i ) {
+            uint32_t symbolIndex = symbolInfo.nlistStartIndex + i;
+
+            if ( config.layout.is64 ) {
+                struct nlist_64& sym = unmappedSymbolsOptimizer.symbolNlistChunk.nlist64[symbolIndex];
+                InputDylibVMAddress inputVMAddr(sym.n_value);
+                CacheVMAddress cacheVMAddr = adjustor->adjustVMAddr(inputVMAddr);
+                sym.n_value = cacheVMAddr.rawValue();
+            } else {
+                struct nlist& sym = unmappedSymbolsOptimizer.symbolNlistChunk.nlist32[symbolIndex];
+                InputDylibVMAddress inputVMAddr((uint64_t)sym.n_value);
+                CacheVMAddress cacheVMAddr = adjustor->adjustVMAddr(inputVMAddr);
+                sym.n_value = (uint32_t)cacheVMAddr.rawValue();
+            }
+        }
+    }
+}
+
+void CacheDylib::updateSymbolTables(Diagnostics& diag, const BuilderConfig& config, Timer::AggregateTimer& timer)
+{
+    Timer::AggregateTimer::Scope timedScope(timer, "dylib updateSymbolTables time");
+
+    for ( LinkeditDataChunk& chunk : this->linkeditChunks ) {
+        if ( !chunk.isIndirectSymbols() )
+            continue;
+
+        // We found the indirect symbol table, now make sure the updated table we cached from earlier
+        // is the correct size
+        uint64_t newTableSize = this->indirectSymbolTable.size() * sizeof(uint32_t);
+        if ( newTableSize != chunk.cacheVMSize.rawValue() ) {
+            diag.error("Wrong indirect symbol table size (%lld != %lld)",
+                       newTableSize, chunk.cacheVMSize.rawValue());
+            return;
+        }
+
+        memcpy(chunk.subCacheBuffer, this->indirectSymbolTable.data(), newTableSize);
+    }
+}
+
+std::optional<CacheDylib::BindTargetAndName> CacheDylib::findDyldMagicSymbolAddress(const char* fullSymbolName, std::string_view name) const
+{
+    auto nextString = [&]() -> std::string_view {
+        auto pos = name.find('$');
+        if ( pos == std::string_view::npos ) {
+            auto str = name;
+            name = "";
+            return str;
+        }
+        auto str = name.substr(0, pos);
+        name = name.substr(pos + 1);
+        return str;
+    };
+
+    std::string_view type = nextString();
+    if ( type == "segment" ) {
+        std::string_view segmentType = nextString();
+        std::string_view segmentName = nextString();
+
+        bool isStart = (segmentType == "start");
+        bool isEnd   = (segmentType == "end");
+
+        __block std::optional<VMAddress> vmAddr;
+
+        this->inputHdr->forEachSegment(^(const Header::SegmentInfo& info, bool& stop) {
+            if ( info.segmentName == segmentName ) {
+                if ( isStart )
+                    vmAddr = VMAddress(info.vmaddr);
+                else if ( isEnd )
+                    vmAddr = VMAddress(info.vmaddr) + VMOffset(info.vmsize);
+
+                stop = true;
+            }
+        });
+
+        if ( !vmAddr )
+            return std::nullopt;
+
+        VMOffset vmOff = *vmAddr - VMAddress(inputLoadAddress.rawValue());
+        return std::make_pair(BindTarget{ BindTarget::Kind::inputImage, { .inputImage = { vmOff, this, /* weak def */ false }  } }, std::string(fullSymbolName));
+    }
+
+    return std::nullopt;
+}
+
+// FIXME: This was stolen from Loader.  try unify them again
+CacheDylib::BindTargetAndName CacheDylib::resolveSymbol(Diagnostics& diag, int libOrdinal, const char* symbolName,
+                                                        bool weakImport, const std::vector<const CacheDylib*>& cacheDylibs) const
+{
+    const CacheDylib* targetDylib = nullptr;
+
+    BindTarget nullBindTarget = { BindTarget::Kind::absolute, { .absolute = { 0 } } };
+
+    if ( (libOrdinal > 0) && ((unsigned)libOrdinal <= this->inputDependents.size()) ) {
+        targetDylib = this->inputDependents[libOrdinal - 1].dylib;
+    }
+    else if ( libOrdinal == BIND_SPECIAL_DYLIB_SELF ) {
+        targetDylib = this;
+    }
+    else if ( libOrdinal == BIND_SPECIAL_DYLIB_MAIN_EXECUTABLE ) {
+        diag.error("shared cache dylibs bind to the main executable: %s\n  Referenced from: %s", symbolName, this->installName.data());
+        return { nullBindTarget, "" };
+    }
+    else if ( libOrdinal == BIND_SPECIAL_DYLIB_FLAT_LOOKUP ) {
+        for ( const CacheDylib* cacheDylib : cacheDylibs ) {
+            std::optional<BindTargetAndName> bindTargetAndName = cacheDylib->hasExportedSymbol(diag, symbolName, SearchMode::onlySelf);
+            if ( bindTargetAndName.has_value() )
+                return bindTargetAndName.value();
+        }
+
+        if ( weakImport ) {
+            // ok to be missing, bind to NULL
+            return { nullBindTarget, "" };
+        }
+
+        // missing symbol, but not weak-import or lazy-bound, so error
+        diag.error("symbol not found in flat namespace '%s'\n  Referenced from: %s", symbolName, this->installName.data());
+        return { nullBindTarget, "" };
+    }
+    else if ( libOrdinal == BIND_SPECIAL_DYLIB_WEAK_LOOKUP ) {
+        // when dylibs in cache are build, we don't have real load order, so do weak binding differently
+
+        // look first in /usr/lib/libc++, most will be here
+        for ( const CacheDylib* cacheDylib : cacheDylibs ) {
+            if ( cacheDylib->inputHdr->hasWeakDefs() && startsWith(cacheDylib->installName, "/usr/lib/libc++.") ) {
+                std::optional<BindTargetAndName> bindTargetAndName = cacheDylib->hasExportedSymbol(diag, symbolName, SearchMode::onlySelf);
+                if ( bindTargetAndName.has_value() )
+                    return bindTargetAndName.value();
+
+                // We found libc++, but not this symbol.  Break out of the loop as we don't need to look in other images
+                break;
+            }
+        }
+
+        // if not found, try looking in the images itself, most custom weak-def symbols have a copy in the image itself
+        std::optional<BindTargetAndName> sellBindTargetAndName = this->hasExportedSymbol(diag, symbolName, SearchMode::onlySelf);
+        if ( sellBindTargetAndName.has_value() )
+            return sellBindTargetAndName.value();
+
+        // if this image directly links with something that also defines this weak-def, use that because we know it will be loaded
+        for ( const CacheDylib::DependentDylib& dependentDylib : this->inputDependents ) {
+            if ( dependentDylib.kind == DependentDylib::Kind::upward )
+                continue;
+
+            // Skip missing weak dylibs
+            if ( dependentDylib.kind == DependentDylib::Kind::weakLink ) {
+                if ( dependentDylib.dylib == nullptr )
+                    continue;
+            }
+
+            std::optional<BindTargetAndName> bindTargetAndName = dependentDylib.dylib->hasExportedSymbol(diag, symbolName, SearchMode::selfAndReexports);
+            if ( bindTargetAndName.has_value() )
+                return bindTargetAndName.value();
+        }
+
+        // no impl??
+        diag.error("weak-def symbol (%s) not found in dyld cache\n  Referenced from: %s", symbolName, this->installName.data());
+        return { nullBindTarget, "" };
+    }
+    else {
+        diag.error("unknown library ordinal %d in %s when binding '%s'", libOrdinal, this->installName.data(), symbolName);
+        return { nullBindTarget, "" };
+    }
+    if ( targetDylib != nullptr ) {
+        if ( const char* dyldMagic = strstr(symbolName, "$dyld$") ) {
+            std::string_view name = dyldMagic + 6;
+            std::optional<BindTargetAndName> target;
+
+            // only synthetic dylibs without a need for the patch table can use magic dyld symbols
+            // dyld itself does not know about them so it won't be able to bind them
+            if ( !needsPatchTable )
+                target = targetDylib->findDyldMagicSymbolAddress(symbolName, name);
+            if ( target )
+                return target.value();
+
+            const char* expectedInDylib = "unknown";
+            if ( targetDylib != nullptr )
+                expectedInDylib = targetDylib->installName.data();
+
+            diag.error("Symbol not found: %s\n  Referenced from: %s\n  Expected in: %s", symbolName, this->installName.data(), expectedInDylib);
+            return { nullBindTarget, "" };
+        }
+
+        std::optional<BindTargetAndName> bindTargetAndName = targetDylib->hasExportedSymbol(diag, symbolName, SearchMode::selfAndReexports);
+        if ( diag.hasError() )
+            return { nullBindTarget, "" };
+
+        if ( bindTargetAndName.has_value() )
+            return bindTargetAndName.value();
+    }
+    if ( weakImport ) {
+        // ok to be missing, bind to NULL
+        return { nullBindTarget, "" };
+    }
+
+    const char* expectedInDylib = "unknown";
+    if ( targetDylib != nullptr )
+        expectedInDylib = targetDylib->installName.data();
+
+    diag.error("Symbol not found: %s\n  Referenced from: %s\n  Expected in: %s", symbolName, this->installName.data(), expectedInDylib);
+    return { nullBindTarget, "" };
+}
+
+std::optional<CacheDylib::BindTargetAndName> CacheDylib::hasExportedSymbol(Diagnostics& diag, const char* symbolName, SearchMode mode) const
+{
+    bool canSearchDependentReexports = false;
+    bool searchSelf                  = false;
+    switch ( mode ) {
+        case SearchMode::onlySelf:
+            canSearchDependentReexports = false;
+            searchSelf                  = true;
+            break;
+        case SearchMode::selfAndReexports:
+            canSearchDependentReexports = true;
+            searchSelf                  = true;
+            break;
+    }
+
+    __block const uint8_t* trieStart = nullptr;
+    __block const uint8_t* trieEnd   = nullptr;
+    this->inputMF->withFileLayout(diag, ^(const mach_o::Layout &layout) {
+        if ( layout.linkedit.exportsTrie.hasValue() ) {
+            trieStart = layout.linkedit.exportsTrie.buffer;
+            trieEnd   = trieStart + layout.linkedit.exportsTrie.bufferSize;
+        }
+    });
+
+    if ( trieStart == nullptr ) {
+        diag.error("shared cache dylibs must have an export trie");
+        return {};
+    }
+    const uint8_t* node = MachOFile::trieWalk(diag, trieStart, trieEnd, symbolName);
+    //state.log("    trieStart=%p, trieSize=0x%08X, node=%p, error=%s\n", trieStart, trieSize, node, diag.errorMessage());
+    if ( (node != nullptr) && searchSelf ) {
+        const uint8_t* p     = node;
+        const uint64_t flags = MachOFile::read_uleb128(diag, p, trieEnd);
+        if ( flags & EXPORT_SYMBOL_FLAGS_REEXPORT ) {
+            // re-export from another dylib, lookup there
+            const uint64_t ordinal      = MachOFile::read_uleb128(diag, p, trieEnd);
+            const char*    importedName = (char*)p;
+            if ( importedName[0] == '\0' ) {
+                importedName = symbolName;
+            }
+            if ( (ordinal == 0) || (ordinal > this->inputDependents.size()) ) {
+                diag.error("re-export ordinal %lld in %s out of range for %s", ordinal, this->installName.data(), symbolName);
+                return {};
+            }
+            uint32_t depIndex = (uint32_t)(ordinal - 1);
+            if ( const CacheDylib* dependentDylib = this->inputDependents[depIndex].dylib )
+                return dependentDylib->hasExportedSymbol(diag, importedName, mode);
+
+            // re-exported symbol from weak-linked dependent which is missing
+            return {};
+        }
+        else {
+            if ( diag.hasError() )
+                return {};
+            bool     isAbsoluteSymbol = ((flags & EXPORT_SYMBOL_FLAGS_KIND_MASK) == EXPORT_SYMBOL_FLAGS_KIND_ABSOLUTE);
+            bool     isWeakDef        = (flags & EXPORT_SYMBOL_FLAGS_WEAK_DEFINITION);
+            bool     isFuncVariant    = (flags & EXPORT_SYMBOL_FLAGS_FUNCTION_VARIANT);
+            uint64_t value            = MachOFile::read_uleb128(diag, p, trieEnd);
+
+            if ( isAbsoluteSymbol ) {
+                BindTarget result = { BindTarget::Kind::absolute, { .absolute = { value } } };
+                return (BindTargetAndName) { result, symbolName };
+            }
+
+            uint16_t fvTableIndex = 0;
+            if ( isFuncVariant ) {
+                // next uleb128 is func-variant table index
+                fvTableIndex = (uint16_t)MachOFile::read_uleb128(diag, p, trieEnd);
+            }
+
+            // Bind to image
+            BindTarget result = { BindTarget::Kind::inputImage, { .inputImage = { VMOffset(value), this, isWeakDef, isFuncVariant, fvTableIndex } } };
+            return (BindTargetAndName) { result, symbolName };
+        }
+    }
+
+    if ( canSearchDependentReexports ) {
+        // Search re-exported dylibs
+        for ( const CacheDylib::DependentDylib& dependentDylib : this->inputDependents ) {
+            if ( dependentDylib.kind != DependentDylib::Kind::reexport )
+                continue;
+
+            // No need for a weak check here as re-exports can't be weak
+
+            std::optional<BindTargetAndName> bindTargetAndName = dependentDylib.dylib->hasExportedSymbol(diag, symbolName, mode);
+            if ( diag.hasError() )
+                return {};
+
+            if ( bindTargetAndName.has_value() )
+                return bindTargetAndName.value();
+        }
+    }
+    return {};
+}
+
+std::vector<Error> CacheDylib::calculateBindTargets(Diagnostics& diag,
+                                                    const BuilderConfig& config, Timer::AggregateTimer& timer,
+                                                    const std::vector<const CacheDylib*>& cacheDylibs,
+                                                    PatchInfo& dylibPatchInfo)
+{
+    Timer::AggregateTimer::Scope timedScope(timer, "dylib calculateBindTargets time");
+
+    // As we are running in parallel, addresses in other dylibs may not have been shifted yet.  We may also
+    // race looking at the export trie in a target dylib, while it is being shifted by AdjustDylibSegments.
+    // Given that, we'll do all the analysis on the input dylibs, with knowledge of where they'll shift to
+
+    __block std::vector<Error> errors;
+    auto handleBindTarget = ^(int libOrdinal, const char* symbolName, uint64_t addend, bool weakImport, bool& stop) {
+        Diagnostics symbolDiag;
+        BindTargetAndName bindTargetAndName = this->resolveSymbol(symbolDiag, libOrdinal, symbolName, weakImport, cacheDylibs);
+        BindTarget&       bindTarget        = bindTargetAndName.first;
+        if ( symbolDiag.hasError() ) {
+            errors.push_back(Error("%s", symbolDiag.errorMessageCStr()));
+            return;
+        }
+
+        bindTarget.addend = addend;
+        bindTarget.isWeakImport = weakImport;
+#if DEBUG
+        bindTarget.name = symbolName;
+#endif
+        this->bindTargets.push_back(std::move(bindTarget));
+        dylibPatchInfo.bindTargetNames.push_back(std::move(bindTargetAndName.second));
+    };
+
+    if ( this->inputMF->hasChainedFixups() ) {
+        // Ideally we'd just walk the chained fixups command, but the macOS simulator support dylibs use
+        // the old threaded rebase format, not chained fixups
+        this->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) {
+                handleBindTarget(info.libOrdinal, info.symbolName, info.addend, info.weakImport, stop);
+            }, ^(const mach_o::Fixups::BindTargetInfo& info, bool& stop) {
+                // This should never happen on chained fixups
+                assert(0);
+            });
+        });
+    }
+    else if ( this->inputMF->hasOpcodeFixups() ) {
+        // Use the fixups from the source dylib
+        this->inputMF->withFileLayout(diag, ^(const mach_o::Layout &layout) {
+            mach_o::Fixups fixups(layout);
+
+            bool allowLazyBinds = false;
+            fixups.forEachBindTarget(diag, allowLazyBinds, 0,
+                ^(const mach_o::Fixups::BindTargetInfo& info, bool& stop) {
+                    handleBindTarget(info.libOrdinal, info.symbolName, info.addend, info.weakImport, stop);
+                },
+                ^(const mach_o::Fixups::BindTargetInfo& info, bool& stop) {
+                    if ( !this->weakBindTargetsStartIndex.has_value() )
+                        this->weakBindTargetsStartIndex = this->bindTargets.size();
+                    handleBindTarget(info.libOrdinal, info.symbolName, info.addend, info.weakImport, stop);
+                });
+        });
+    }
+    else {
+        // Cache dylibs shouldn't use old style fixups.
+    }
+
+    if ( !errors.empty() )
+        diag.error("missing symbols");
+
+    return std::move(errors);
+}
+
+void CacheDylib::bindLocation(Diagnostics& diag, const BuilderConfig& config,
+                              const BindTarget& bindTarget, uint64_t addend,
+                              uint32_t bindOrdinal, uint32_t segIndex,
+                              dyld3::MachOFile::ChainedFixupPointerOnDisk* fixupLoc,
+                              CacheVMAddress fixupVMAddr, MachOFile::PointerMetaData pmd,
+                              CoalescedGOTsMap& coalescedGOTs, CoalescedGOTsMap& coalescedAuthGOTs,
+                              CoalescedGOTsMap& coalescedAuthPtrs, PatchInfo& dylibPatchInfo,
+                              FunctionVariantsOptimizer& functionVariantsOptimizer)
+{
+    switch ( bindTarget.kind ) {
+        case BindTarget::Kind::absolute: {
+            uint64_t targetValue = bindTarget.absolute.value + addend;
+
+            if ( config.layout.is64 ) {
+                fixupLoc->raw64 = targetValue;
+            }
+            else {
+                fixupLoc->raw32 = (uint32_t)targetValue;
+            }
+
+            // Tell the slide info emitter to ignore this location
+            this->segments[segIndex].tracker.remove(fixupLoc);
+            return;
+        }
+        case BindTarget::Kind::inputImage: {
+            diag.error("Input binds should have been converted to cache binds in %s: %d",
+                       this->installName.data(), bindOrdinal);
+            return;
+        }
+        case BindTarget::Kind::cacheImage: {
+            CacheVMAddress targetDylibLoadAddress = bindTarget.cacheImage.targetDylib->cacheLoadAddress;
+            CacheVMAddress targetVMAddr = targetDylibLoadAddress + bindTarget.cacheImage.targetRuntimeOffset;
+            uint64_t       finalVMAddrWithAddend  = targetVMAddr.rawValue() + addend;
+            if ( config.layout.is64 ) {
+                uint64_t finalVMAddr = finalVMAddrWithAddend;
+
+                uint8_t high8 = (uint8_t)(finalVMAddr >> 56);
+                if ( high8 != 0 ) {
+                    // Remove high8 from the vmAddr
+                    finalVMAddr = finalVMAddr & 0x00FFFFFFFFFFFFFFULL;
+                }
+
+                Fixup::Cache64::setLocation(config.layout.cacheBaseAddress,
+                                            fixupLoc, CacheVMAddress(finalVMAddr),
+                                            high8,
+                                            pmd.diversity, pmd.usesAddrDiversity, pmd.key,
+                                            pmd.authenticated);
+            } else {
+                Fixup::Cache32::setLocation(config.layout.cacheBaseAddress,
+                                            fixupLoc, CacheVMAddress(finalVMAddrWithAddend));
+            }
+
+            // Tell the slide info emitter to slide this location
+            this->segments[segIndex].tracker.add(fixupLoc);
+
+            // Work out if the location we just wrote is a coalesced GOT.  If so, NULL the current location and
+            // note down the fixup to the GOT.  We can't just apply the GOT fixup, as we might be running in parallel with
+            // other threads all trying to do the same thing
+            if( needsPatchTable ) {
+                // The GOT map is keyed by the input VMAddr, so convert back to that
+                VMOffset segmentVMOffset = fixupVMAddr - this->segments[segIndex].cacheVMAddress;
+                InputDylibVMAddress inputFixupVMAddr = this->segments[segIndex].inputVMAddress + segmentVMOffset;
+                auto checkGOTs = ^(CoalescedGOTsMap& gotMap) {
+                    auto gotIt = gotMap.find(inputFixupVMAddr);
+                    if ( gotIt != gotMap.end() ) {
+
+                        // NULL out this entry
+                        if ( config.layout.is64 ) {
+                            fixupLoc->raw64 = 0;
+                        } else {
+                            fixupLoc->raw32 = 0;
+                        }
+
+                        // Tell the slide info emitter to ignore this location
+                        this->segments[segIndex].tracker.remove(fixupLoc);
+                        return true;
+                    }
+                    return false;
+                };
+                if ( checkGOTs(coalescedGOTs) || checkGOTs(coalescedAuthGOTs) || checkGOTs(coalescedAuthPtrs) ) {
+                    // normal GOT/auth GOT/auth ptr
+                } else {
+                    // if target is a function variant, record that dyld may need to update pointer at launch
+                    if ( bindTarget.cacheImage.isFunctionVariant ) {
+                        uint64_t fvTableVmAddr = 0;
+                        uint32_t fvTableVmSize = 0;
+                        for ( const LinkeditDataChunk& chunk :  bindTarget.cacheImage.targetDylib->linkeditChunks ) {
+                            if ( chunk.isFunctionVariantsTable() ) {
+                                fvTableVmAddr = chunk.cacheVMAddress.rawValue();
+                                fvTableVmSize = (uint32_t)chunk.cacheVMSize.rawValue();
+                                break;
+                            }
+                        }
+                        dyld_cache_function_variant_entry entry;
+                        entry.fixupLocVmAddr               = fixupVMAddr.rawValue();
+                        entry.functionVariantTableVmAddr   = fvTableVmAddr;
+                        entry.functionVariantTableSizeDiv4 = fvTableVmSize/4;
+                        entry.dylibHeaderVmAddr            = bindTarget.cacheImage.targetDylib->cacheLoadAddress.rawValue();
+                        entry.variantIndex                 = bindTarget.cacheImage.functionVariantTableIndex;
+                        entry.pacAuth                      = pmd.authenticated;
+                        entry.pacAddress                   = pmd.usesAddrDiversity;
+                        entry.pacKey                       = pmd.key;
+                        entry.pacDiversity                 = pmd.diversity;
+                        entry.targetDylibIndex             = bindTarget.cacheImage.targetDylib->cacheIndex;
+                        assert(entry.variantIndex == bindTarget.cacheImage.functionVariantTableIndex);
+                        functionVariantsOptimizer.infos.push_back(entry);
+                    }
+                }
+            }
+            break;
+        }
+    }
+}
+
+void CacheDylib::bindWithChainedFixups(Diagnostics& diag, const BuilderConfig& config,
+                                       CoalescedGOTsMap& coalescedGOTs, CoalescedGOTsMap& coalescedAuthGOTs,
+                                       CoalescedGOTsMap& coalescedAuthPtrs, PatchInfo& dylibPatchInfo,
+                                       FunctionVariantsOptimizer& functionVariantsOptimizer)
+{
+    auto fixupHandler = ^(MachOFile::ChainedFixupPointerOnDisk* fixupLoc, uint16_t chainedFormat,
+                          uint32_t segIndex, CacheVMAddress fixupVMAddr,
+                          bool& stopChain) {
+        MachOFile::PointerMetaData pmd(fixupLoc, chainedFormat);
+        uint32_t bindOrdinal;
+        int64_t  embeddedAddend;
+        if ( !fixupLoc->isBind(chainedFormat, bindOrdinal, embeddedAddend) ) {
+            // Rebases might be stored in a side table from applying split seg.  If so, we
+            // can copy their values in to place now
+            if ( config.layout.is64 ) {
+                uint64_t targetVMAddr;
+                if ( this->segments[segIndex].tracker.hasRebaseTarget64(fixupLoc, &targetVMAddr) ) {
+                    // The value is now stored in targetVMAddr.
+                    // We'll use it later
+                    // We should never get high8 from hasRebaseTarget64()
+                    uint64_t high8 = targetVMAddr >> 56;
+                    assert(high8 == 0);
+                } else {
+                    uint64_t runtimeOffset;
+                    bool isRebase = fixupLoc->isRebase(chainedFormat, this->cacheLoadAddress.rawValue(),
+                                                       runtimeOffset);
+                    assert(isRebase);
+
+                    targetVMAddr = this->cacheLoadAddress.rawValue() + runtimeOffset;
+
+                    // Remove high8 if we have it.  The PMD has it too
+                    uint64_t high8 = targetVMAddr >> 56;
+                    assert(pmd.high8 == high8);
+                    targetVMAddr &= 0x00FFFFFFFFFFFFFFULL;
+                }
+
+                CacheVMAddress targetCacheAddress(targetVMAddr);
+                Fixup::Cache64::setLocation(config.layout.cacheBaseAddress,
+                                            fixupLoc, targetCacheAddress,
+                                            pmd.high8,
+                                            pmd.diversity, pmd.usesAddrDiversity, pmd.key,
+                                            pmd.authenticated);
+            }
+            else {
+                uint32_t targetVMAddr;
+                assert(this->segments[segIndex].tracker.hasRebaseTarget32(fixupLoc, &targetVMAddr)
+                       && "32-bit archs always store target in side table");
+
+                CacheVMAddress targetCacheAddress((uint64_t)targetVMAddr);
+                Fixup::Cache32::setLocation(config.layout.cacheBaseAddress,
+                                            fixupLoc, targetCacheAddress);
+            }
+            return;
+        }
+
+        if ( bindOrdinal >= this->bindTargets.size() ) {
+            diag.error("out of range bind ordinal %d (max %lu)", bindOrdinal, this->bindTargets.size());
+            stopChain   = true;
+            return;
+        }
+
+        const BindTarget& targetInTable = this->bindTargets[bindOrdinal];
+        uint64_t          addend        = targetInTable.addend + embeddedAddend;
+
+        this->bindLocation(diag, config, targetInTable, addend, bindOrdinal, segIndex,
+                           fixupLoc, fixupVMAddr, pmd,
+                           coalescedGOTs, coalescedAuthGOTs, 
+                           coalescedAuthPtrs, dylibPatchInfo,
+                           functionVariantsOptimizer);
+    };
+
+    this->inputMF->withFileLayout(diag, ^(const mach_o::Layout &layout) {
+        mach_o::Fixups fixups(layout);
+
+        // Use the chained fixups header from the input dylib
+        fixups.withChainStarts(diag, ^(const dyld_chained_starts_in_image* starts) {
+            MachOFile::forEachFixupChainSegment(diag, starts, ^(const dyld_chained_starts_in_segment* segInfo,
+                                                                uint32_t segIndex, bool& stopSegment) {
+
+                // We now have the dyld_chained_starts_in_segment from the input dylib, but
+                // we want to walk the chain in the cache dylib
+                DylibSegmentChunk& segmentInfo = this->segments[segIndex];
+                uint8_t* cacheDylibSegment = segmentInfo.subCacheBuffer;
+
+                auto adaptor = ^(MachOFile::ChainedFixupPointerOnDisk* fixupLocation, bool& stop) {
+                    uint64_t fixupOffsetInSegment = (uint64_t)fixupLocation - (uint64_t)cacheDylibSegment;
+                    CacheVMAddress fixupVMAddr = segmentInfo.cacheVMAddress + VMOffset(fixupOffsetInSegment);
+                    fixupHandler(fixupLocation, segInfo->pointer_format,
+                                 segIndex, fixupVMAddr, stop);
+                    if ( stop )
+                        stopSegment = true;
+                };
+
+                MachOFile::forEachFixupInSegmentChains(diag, segInfo, false, cacheDylibSegment, adaptor);
+            });
+        });
+    });
+}
+
+void CacheDylib::bindWithOpcodeFixups(Diagnostics& diag, const BuilderConfig& config,
+                                      CoalescedGOTsMap& coalescedGOTs, CoalescedGOTsMap& coalescedAuthGOTs,
+                                      CoalescedGOTsMap& coalescedAuthPtrs, PatchInfo& dylibPatchInfo,
+                                      FunctionVariantsOptimizer& functionVariantsOptimizer)
+{
+    auto handleFixup = ^(uint64_t fixupRuntimeOffset, int bindOrdinal, uint32_t segmentIndex, bool& stopSegment) {
+        DylibSegmentChunk& segmentInfo = this->segments[segmentIndex];
+
+        CacheVMAddress fixupVMAddr  = this->cacheLoadAddress + VMOffset(fixupRuntimeOffset);
+        VMOffset segmentOffset      = fixupVMAddr - segmentInfo.cacheVMAddress;
+        uint8_t* fixupLoc           = segmentInfo.subCacheBuffer + segmentOffset.rawValue();
+
+        if ( bindOrdinal >= this->bindTargets.size() ) {
+            diag.error("out of range bind ordinal %d (max %lu)", bindOrdinal, this->bindTargets.size());
+            stopSegment   = true;
+            return;
+        }
+
+        const BindTarget& targetInTable = this->bindTargets[bindOrdinal];
+        uint64_t          addend        = targetInTable.addend;
+
+        this->bindLocation(diag, config, targetInTable, addend, bindOrdinal, segmentIndex,
+                           (dyld3::MachOFile::ChainedFixupPointerOnDisk*)fixupLoc,
+                           fixupVMAddr, dyld3::MachOFile::PointerMetaData(),
+                           coalescedGOTs, coalescedAuthGOTs, coalescedAuthPtrs, dylibPatchInfo,
+                           functionVariantsOptimizer);
+    };
+
+    // Use the fixups from the source dylib
+    mach_o::LinkeditLayout linkedit;
+    if ( !this->inputMF->getLinkeditLayout(diag, linkedit) ) {
+        diag.error("Couldn't get dylib layout");
+        return;
+    }
+
+    // Use the segment layout from the cache dylib so that VMAddresses are correct
+    __block std::vector<mach_o::SegmentLayout> segmentLayout;
+    segmentLayout.reserve(this->segments.size());
+    for ( const DylibSegmentChunk& dylibSegment : this->segments ) {
+        mach_o::SegmentLayout segment;
+        segment.vmAddr      = dylibSegment.cacheVMAddress.rawValue();
+        segment.vmSize      = dylibSegment.cacheVMSize.rawValue();
+        segment.fileOffset  = dylibSegment.subCacheFileOffset.rawValue();
+        segment.fileSize    = dylibSegment.subCacheFileSize.rawValue();
+        segment.buffer      = dylibSegment.subCacheBuffer;
+
+        segment.kind        = mach_o::SegmentLayout::Kind::unknown;
+        if ( dylibSegment.segmentName == "__TEXT" ) {
+            segment.kind    = mach_o::SegmentLayout::Kind::text;
+        } else if ( dylibSegment.segmentName == "__LINKEDIT" ) {
+            segment.kind    = mach_o::SegmentLayout::Kind::linkedit;
+        }
+        segmentLayout.push_back(segment);
+    }
+
+    // The cache segments don't have the permissions.  Get that from the load commands
+    this->cacheHdr->forEachSegment(^(const Header::SegmentInfo& info, bool& stop) {
+        segmentLayout[info.segmentIndex].protections = info.initProt;
+    });
+
+    mach_o::Layout layout(this->inputMF, { segmentLayout.data(), segmentLayout.data() + segmentLayout.size() }, linkedit);
+    mach_o::Fixups fixups(layout);
+
+    fixups.forEachRebaseLocation_Opcodes(diag, ^(uint64_t fixupRuntimeOffset, uint32_t segmentIndex, bool& stop) {
+        DylibSegmentChunk& segmentInfo = this->segments[segmentIndex];
+
+        uint64_t fixupCacheVMAddr = layout.textUnslidVMAddr() + fixupRuntimeOffset;
+        uint64_t segmentOffset    = fixupCacheVMAddr - segmentInfo.cacheVMAddress.rawValue();
+        uint8_t* fixupLoc         = segmentInfo.subCacheBuffer + segmentOffset;
+
+        // Convert from rebase vmAddr to the internal cache format
+        if ( config.layout.is64 ) {
+            uint64_t targetVMAddr = *(uint64_t*)fixupLoc;
+            CacheVMAddress targetCacheAddress(targetVMAddr);
+
+            uint8_t high8 = (uint8_t)(targetVMAddr >> 56);
+            if ( high8 != 0 ) {
+                // Remove high8 from the vmAddr
+                targetVMAddr = targetVMAddr & 0x00FFFFFFFFFFFFFFULL;
+            }
+
+            // Unused PointerMetadata, but just use here to get all the fields
+            dyld3::MachOFile::PointerMetaData pmd;
+            Fixup::Cache64::setLocation(config.layout.cacheBaseAddress,
+                                        fixupLoc, CacheVMAddress(targetVMAddr),
+                                        high8,
+                                        pmd.diversity, pmd.usesAddrDiversity, pmd.key,
+                                        pmd.authenticated);
+        }
+        else {
+            uint32_t targetVMAddr = *(uint32_t*)fixupLoc;
+
+            CacheVMAddress targetCacheAddress((uint64_t)targetVMAddr);
+            Fixup::Cache32::setLocation(config.layout.cacheBaseAddress,
+                                        fixupLoc, CacheVMAddress((uint64_t)targetVMAddr));
+        }
+    });
+
+    // Do binds after rebases, in case we have lazy binds which override the rebase
+    fixups.forEachBindLocation_Opcodes(diag,
+        ^(uint64_t runtimeOffset, uint32_t segmentIndex, unsigned int targetIndex, bool& stop) {
+            handleFixup(runtimeOffset, targetIndex, segmentIndex, stop);
+        },
+        ^(uint64_t runtimeOffset, uint32_t segmentIndex, unsigned int overrideBindTargetIndex, bool& stop) {
+            assert(this->weakBindTargetsStartIndex.has_value());
+            handleFixup(runtimeOffset, this->weakBindTargetsStartIndex.value() + overrideBindTargetIndex, segmentIndex, stop);
+        });
+}
+
+void CacheDylib::calculateBindLocationPatchInfo(Diagnostics& diag, const BuilderConfig& config,
+                                                const BindTarget& bindTarget, uint64_t addend,
+                                                uint32_t bindOrdinal, uint32_t segIndex,
+                                                InputDylibVMAddress fixupVMAddr, MachOFile::PointerMetaData pmd,
+                                                CoalescedGOTsMap& coalescedGOTs, CoalescedGOTsMap& coalescedAuthGOTs,
+                                                CoalescedGOTsMap& coalescedAuthPtrs, PatchInfo& dylibPatchInfo)
+{
+    switch ( bindTarget.kind ) {
+        case BindTarget::Kind::absolute: {
+            uint64_t targetValue = bindTarget.absolute.value + addend;
+
+            auto checkGOTs = ^(CoalescedGOTsMap& gotMap, std::vector<std::vector<PatchInfo::GOTInfo>>& gotInfo) {
+                auto gotIt = gotMap.find(fixupVMAddr);
+                if ( gotIt != gotMap.end() ) {
+                    // Probably a missing weak import.  Rewrite the original GOT anyway, but also the coalesced one
+                    const ChunkPlusOffset gotPlusOffset = gotIt->second;
+                    DyldCachePatchableGOTLocation patchLoc(gotPlusOffset.first, gotPlusOffset.second, pmd, addend, bindTarget.isWeakImport);
+                    auto& gotUses = gotInfo[bindOrdinal];
+                    gotUses.emplace_back((PatchInfo::GOTInfo){ patchLoc, targetValue });
+                    return true;
+                }
+                return false;
+            };
+            if ( checkGOTs(coalescedGOTs, dylibPatchInfo.bindGOTUses) ) {
+                // normal GOT
+            } else if ( checkGOTs(coalescedAuthGOTs, dylibPatchInfo.bindAuthGOTUses) ) {
+                // auth GOT
+            } else if ( checkGOTs(coalescedAuthPtrs, dylibPatchInfo.bindAuthPtrUses) ) {
+                // auth ptr
+            }
+            return;
+        }
+        case BindTarget::Kind::inputImage: {
+            InputDylibVMAddress targetDylibLoadAddress = bindTarget.inputImage.targetDylib->inputLoadAddress;
+            InputDylibVMAddress targetVMAddr = targetDylibLoadAddress + bindTarget.inputImage.targetRuntimeOffset;
+            uint64_t            finalTargetVMAddrWithAddend  = targetVMAddr.rawValue() + addend;
+
+            // Work out if the location we just wrote is a coalesced GOT.  If so, NULL the current location and
+            // note down the fixup to the GOT.  We can't just apply the GOT fixup, as we might be running in parallel with
+            // other threads all trying to do the same thing
+            uint64_t                   patchTableAddend = addend;
+            MachOFile::PointerMetaData patchTablePMD    = pmd;
+            uint64_t                   addendHigh8      = addend >> 56;
+            if ( addendHigh8 != 0 ) {
+                // Put the high8 from the addend in to the high8 of the patch
+                assert(patchTablePMD.high8 == 0);
+                patchTablePMD.high8 = (uint32_t)addendHigh8;
+
+                // Remove high8 from the addend
+                patchTableAddend = patchTableAddend & 0x00FFFFFFFFFFFFFFULL;
+            }
+
+            InputDylibVMOffset finalTargetVMOffset = InputDylibVMAddress(finalTargetVMAddrWithAddend) - targetDylibLoadAddress;
+
+            auto checkGOTs = ^(CoalescedGOTsMap& gotMap, std::vector<std::vector<PatchInfo::GOTInfo>>& gotInfo) {
+                auto gotIt = gotMap.find(fixupVMAddr);
+                if ( gotIt != gotMap.end() ) {
+                    const ChunkPlusOffset gotPlusOffset = gotIt->second;
+                    DyldCachePatchableGOTLocation patchLoc(gotPlusOffset.first, gotPlusOffset.second, patchTablePMD, patchTableAddend, bindTarget.isWeakImport);
+                    auto& gotUses = gotInfo[bindOrdinal];
+                    DylibOffset dylibOffset = { bindTarget.inputImage.targetDylib, finalTargetVMOffset };
+                    gotUses.emplace_back((PatchInfo::GOTInfo){ patchLoc, dylibOffset });
+                    return true;
+                }
+                return false;
+            };
+            if ( checkGOTs(coalescedGOTs, dylibPatchInfo.bindGOTUses) ) {
+                // normal GOT
+            } else if ( checkGOTs(coalescedAuthGOTs, dylibPatchInfo.bindAuthGOTUses) ) {
+                // auth GOT
+            } else if ( checkGOTs(coalescedAuthPtrs, dylibPatchInfo.bindAuthPtrUses) ) {
+                // auth ptr
+            } else {
+                // Location wasn't coalesced.  So add to the regular list of uses
+                InputDylibVMOffset fixupVMOffset = fixupVMAddr - this->inputLoadAddress;
+                DyldCachePatchableLocation patchLoc = { fixupVMOffset, patchTablePMD, patchTableAddend, bindTarget.isWeakImport };
+                dylibPatchInfo.bindUses[bindOrdinal].push_back(patchLoc);
+            }
+            break;
+        }
+        case BindTarget::Kind::cacheImage: {
+            diag.error("Input binds should not have been converted to cache binds in %s: %d",
+                       this->installName.data(), bindOrdinal);
+            return;
+        }
+    }
+}
+
+void CacheDylib::calcuatePatchInfo(Diagnostics& diag, const BuilderConfig& config, Timer::AggregateTimer& timer,
+                                   PatchInfo& dylibPatchInfo)
+{
+    Timer::AggregateTimer::Scope timedScope(timer, "dylib patch info calculation time");
+
+    __block CoalescedGOTsMap coalescedGOTs      = optimizedSections.gots.getCoalescedGOTsMap();
+    __block CoalescedGOTsMap coalescedAuthGOTs  = optimizedSections.auth_gots.getCoalescedGOTsMap();
+    __block CoalescedGOTsMap coalescedAuthPtrs  = optimizedSections.auth_ptrs.getCoalescedGOTsMap();
+
+    // Track which locations this dylib uses in other dylibs.  One per bindTarget
+    dylibPatchInfo.bindUses.resize(this->bindTargets.size());
+    dylibPatchInfo.bindGOTUses.resize(this->bindTargets.size());
+    dylibPatchInfo.bindAuthGOTUses.resize(this->bindTargets.size());
+    dylibPatchInfo.bindAuthPtrUses.resize(this->bindTargets.size());
+
+    if ( !needsPatchTable )
+        return;
+
+    auto handleFixup = ^(InputDylibVMAddress fixupVMAddr, int64_t embeddedAddend, int bindOrdinal, dyld3::MachOFile::PointerMetaData pmd,
+                         uint32_t segmentIndex, bool& stopSegment) {
+        if ( bindOrdinal >= this->bindTargets.size() ) {
+            diag.error("out of range bind ordinal %d (max %lu)", bindOrdinal, this->bindTargets.size());
+            stopSegment   = true;
+            return;
+        }
+
+        const BindTarget& targetInTable = this->bindTargets[bindOrdinal];
+        uint64_t          addend        = targetInTable.addend + embeddedAddend;
+
+        this->calculateBindLocationPatchInfo(diag, config, targetInTable, addend, bindOrdinal, segmentIndex,
+                                             fixupVMAddr, pmd,
+                                             coalescedGOTs, coalescedAuthGOTs, coalescedAuthPtrs, dylibPatchInfo);
+    };
+
+    this->inputMF->withFileLayout(diag, ^(const mach_o::Layout& layout) {
+        mach_o::Fixups fixups(layout);
+        if ( this->inputMF->hasChainedFixups() ) {
+            fixups.withChainStarts(diag, ^(const dyld_chained_starts_in_image* starts) {
+                fixups.forEachFixupChainSegment(diag, starts, ^(const dyld_chained_starts_in_segment *segInfo, uint32_t segIndex, bool &stopSegment) {
+                    InputDylibVMAddress segmentVMAddr = this->segments[segIndex].inputVMAddress;
+                    fixups.forEachFixupInSegmentChains(diag, segInfo, segIndex, true,
+                                                       ^(dyld3::MachOFile::ChainedFixupPointerOnDisk *fixupLocation, uint64_t fixupSegmentOffset, bool &stopChain) {
+                        uint32_t bindOrdinal = 0;
+                        int64_t embeddedAddend = 0;
+                        if ( fixupLocation->isBind(segInfo->pointer_format, bindOrdinal, embeddedAddend) ) {
+                            MachOFile::PointerMetaData pmd(fixupLocation, segInfo->pointer_format);
+                            handleFixup(segmentVMAddr + VMOffset(fixupSegmentOffset), embeddedAddend, bindOrdinal, pmd, segIndex, stopChain);
+                        }
+                    });
+                });
+            });
+        } else if ( this->inputMF->hasOpcodeFixups() ) {
+            const dyld3::MachOFile::PointerMetaData pmd;
+            fixups.forEachBindLocation_Opcodes(diag,
+                                               ^(uint64_t runtimeOffset, uint32_t segmentIndex, unsigned int targetIndex, bool& stop) {
+                InputDylibVMAddress fixupVMAddr = this->inputLoadAddress + VMOffset(runtimeOffset);
+                handleFixup(fixupVMAddr, 0, targetIndex, pmd, segmentIndex, stop);
+            },
+            ^(uint64_t runtimeOffset, uint32_t segmentIndex, unsigned int overrideBindTargetIndex, bool& stop) {
+                assert(this->weakBindTargetsStartIndex.has_value());
+                InputDylibVMAddress fixupVMAddr = this->inputLoadAddress + VMOffset(runtimeOffset);
+                handleFixup(fixupVMAddr, 0, this->weakBindTargetsStartIndex.value() + overrideBindTargetIndex, pmd, segmentIndex, stop);
+            });
+        } else {
+            // Cache dylibs shouldn't use old style fixups.
+        }
+    });
+}
+
+void CacheDylib::bind(Diagnostics& diag, const BuilderConfig& config, Timer::AggregateTimer& timer,
+                      PatchInfo& dylibPatchInfo, FunctionVariantsOptimizer& functionVariantsOptimizer)
+{
+    Timer::AggregateTimer::Scope timedScope(timer, "dylib bind time");
+
+    // As we are running in parallel, addresses in other dylibs may not have been shifted yet.  We may also
+    // race looking at the export trie in a target dylib, while it is being shifted by AdjustDylibSegments.
+    // Given that, we'll look at our own cache dylib, but everyone elses input dylib, as those won't mutate
+    CoalescedGOTsMap coalescedGOTs      = optimizedSections.gots.getCoalescedGOTsMap();
+    CoalescedGOTsMap coalescedAuthGOTs  = optimizedSections.auth_gots.getCoalescedGOTsMap();
+    CoalescedGOTsMap coalescedAuthPtrs  = optimizedSections.auth_ptrs.getCoalescedGOTsMap();
+
+    if ( this->inputMF->hasChainedFixups() )
+        bindWithChainedFixups(diag, config, coalescedGOTs, coalescedAuthGOTs, coalescedAuthPtrs, dylibPatchInfo,
+                              functionVariantsOptimizer);
+    else if ( this->inputMF->hasOpcodeFixups() ) {
+        bindWithOpcodeFixups(diag, config, coalescedGOTs, coalescedAuthGOTs, coalescedAuthPtrs, dylibPatchInfo,
+                             functionVariantsOptimizer);
+    } else {
+        // Cache dylibs shouldn't use old style fixups.
+    }
+
+    // Now that we've bound this dylib, we can tell the ASLRTrackers on the segments to clear
+    // any out of band maps
+    for ( DylibSegmentChunk& segment : this->segments )
+        segment.tracker.clearRebaseTargetsMaps();
+}
+
+void CacheDylib::updateObjCSelectorReferences(Diagnostics& diag, const BuilderConfig& config,
+                                              Timer::AggregateTimer& timer, ObjCSelectorOptimizer& objcSelectorOptimizer)
+{
+    if ( !this->inputHdr->hasObjC() )
+        return;
+
+    Timer::AggregateTimer::Scope timedScope(timer, "dylib updateObjCSelectorReferences time");
+
+    __block objc_visitor::Visitor objcVisitor = this->makeCacheObjCVisitor(config,
+                                                                           objcSelectorOptimizer.selectorStringsChunk,
+                                                                           nullptr,
+                                                                           nullptr);
+
+    // Update every selector reference to point to the canonical selectors
+    objcVisitor.forEachSelectorReference(^(metadata_visitor::ResolvedValue& selRefValue) {
+        const char* selString = (const char*)objcVisitor.resolveRebase(selRefValue).value();
+
+        // Find the selector in the map
+        auto it = objcSelectorOptimizer.selectorsMap.find(selString);
+        assert(it != objcSelectorOptimizer.selectorsMap.end());
+
+        VMOffset newSelBufferOffset = it->second;
+        assert(newSelBufferOffset.rawValue() < objcSelectorOptimizer.selectorStringsChunk->cacheVMSize.rawValue());
+        CacheVMAddress newSelCacheVMAddress = objcSelectorOptimizer.selectorStringsChunk->cacheVMAddress + newSelBufferOffset;
+
+        objcVisitor.updateTargetVMAddress(selRefValue, newSelCacheVMAddress);
+    });
+
+    objcVisitor.forEachMethodList(^(objc_visitor::MethodList& objcMethodList,
+                                    std::optional<metadata_visitor::ResolvedValue> extendedMethodTypes) {
+        // Set both relative and pointer based lists to uniqued.  They will be after this method is done
+        objcMethodList.setIsUniqued();
+
+        // Skip uniqing relative method lists.  We know for sure they point to __objc_selrefs which were handled above
+        if ( objcMethodList.usesRelativeOffsets() )
+            return;
+
+        uint32_t numMethods = objcMethodList.numMethods();
+        for ( uint32_t i = 0; i != numMethods; ++i ) {
+            objc_visitor::Method objcMethod = objcMethodList.getMethod(objcVisitor, i);
+
+            // Get the selector reference which is implicit in the name field of the Method.
+            metadata_visitor::ResolvedValue nameRef = objcMethod.getNameField(objcVisitor);
+
+            const char* selString = (const char*)objcVisitor.resolveRebase(nameRef).value();
+
+            // Find the selector in the map
+            auto it = objcSelectorOptimizer.selectorsMap.find(selString);
+            assert(it != objcSelectorOptimizer.selectorsMap.end());
+
+            VMOffset       newSelBufferOffset   = it->second;
+            CacheVMAddress newSelCacheVMAddress = objcSelectorOptimizer.selectorStringsChunk->cacheVMAddress + newSelBufferOffset;
+
+            objcVisitor.updateTargetVMAddress(nameRef, newSelCacheVMAddress);
+        }
+    });
+}
+
+static void sortObjCRelativeMethodList(const BuilderConfig&                         config,
+                                       const objc_visitor::Visitor&                 objcVisitor,
+                                       const objc_visitor::MethodList&              objcMethodList,
+                                       std::optional<metadata_visitor::ResolvedValue>   extendedMethodTypesBase)
+{
+    uint32_t numMethods = objcMethodList.numMethods();
+
+    // Is this possible?  It simplifies code below, so check it anyway
+    if ( numMethods == 0 )
+        return;
+
+    // Don't sort if we have a single method
+    if ( numMethods == 1 )
+        return;
+
+    // At this point we assume we are using offsets directly to selectors.  This
+    // is so that the Method struct can also use direct offsets and not track the
+    // SEL reference VMAddrs
+    assert(objcMethodList.usesOffsetsFromSelectorBuffer());
+
+    // We can't sort relative method lists.  So turn them in to Pointer based lists and sort those instead
+    struct Method
+    {
+        VMAddress selStringVMAddr;
+        VMAddress typeStringVMAddr;
+        std::optional<VMAddress> impVMAddr;
+        VMAddress extendedMethodTypeVMAddr;
+    };
+
+    const uint32_t pointerSize = objcVisitor.mf()->pointerSize();
+
+    Method methods[numMethods];
+    for ( uint32_t i = 0; i != numMethods; ++i ) {
+        objc_visitor::Method objcMethod = objcMethodList.getMethod(objcVisitor, i);
+
+        methods[i].selStringVMAddr  = objcMethod.getNameVMAddr(objcVisitor);
+        methods[i].typeStringVMAddr = objcMethod.getTypesVMAddr(objcVisitor);
+        methods[i].impVMAddr        = objcMethod.getIMPVMAddr(objcVisitor);
+
+        if ( extendedMethodTypesBase.has_value() ) {
+            const uint8_t* methodTypesBase = (uint8_t*)extendedMethodTypesBase->value();
+            methodTypesBase += (pointerSize * i);
+            metadata_visitor::ResolvedValue methodType(extendedMethodTypesBase.value(), methodTypesBase);
+
+            // Get the VMAddr pointed to by this method type
+            VMAddress targetVMAddr              = objcVisitor.resolveRebase(methodType).vmAddress();
+            methods[i].extendedMethodTypeVMAddr = targetVMAddr;
+        }
+    }
+
+    // Sort by selector address (not contents)
+    auto sorter = [](const Method& a, const Method& b) {
+        return a.selStringVMAddr < b.selStringVMAddr;
+    };
+
+    // Stable sort because method lists can contain duplicates when categories have been attached.
+    std::stable_sort(&methods[0], &methods[numMethods], sorter);
+
+    // Replace the relative methods with the sorted ones
+    for ( uint32_t i = 0; i != numMethods; ++i ) {
+        objc_visitor::Method objcMethod = objcMethodList.getMethod(objcVisitor, i);
+
+        objcMethod.setName(objcVisitor, methods[i].selStringVMAddr);
+        objcMethod.setTypes(objcVisitor, methods[i].typeStringVMAddr);
+        objcMethod.setIMP(objcVisitor, methods[i].impVMAddr);
+
+        if ( extendedMethodTypesBase.has_value() ) {
+            const uint8_t* methodTypesBase = (uint8_t*)extendedMethodTypesBase->value();
+            methodTypesBase += (pointerSize * i);
+            metadata_visitor::ResolvedValue methodType(extendedMethodTypesBase.value(), methodTypesBase);
+
+            // Get the VMAddr pointed to by this method type
+            VMAddress targetVMAddr = methods[i].extendedMethodTypeVMAddr;
+            objcVisitor.updateTargetVMAddress(methodType, CacheVMAddress(targetVMAddr.rawValue()));
+        }
+    }
+}
+
+static void sortObjCPointerMethodList(const BuilderConfig&                          config,
+                                      const objc_visitor::Visitor&                  objcVisitor,
+                                      const objc_visitor::MethodList&               objcMethodList,
+                                      std::optional<metadata_visitor::ResolvedValue>    extendedMethodTypesBase)
+{
+    uint32_t numMethods = objcMethodList.numMethods();
+
+    // Is this possible?  It simplifies code below, so check it anyway
+    if ( numMethods == 0 )
+        return;
+
+    // Don't sort if we have a single method
+    if ( numMethods == 1 )
+        return;
+
+    // It's painful to sort both methods and method types at the same time, so
+    // put everything in to a temporary array to sort
+    struct Method
+    {
+        VMAddress selStringVMAddr;
+        VMAddress typeStringVMAddr;
+        std::optional<VMAddress> impVMAddr;
+        VMAddress extendedMethodTypeVMAddr;
+    };
+
+    const uint32_t pointerSize = objcVisitor.mf()->pointerSize();
+
+    Method methods[numMethods];
+    for ( uint32_t i = 0; i != numMethods; ++i ) {
+        objc_visitor::Method objcMethod = objcMethodList.getMethod(objcVisitor, i);
+
+        methods[i].selStringVMAddr  = objcMethod.getNameVMAddr(objcVisitor);
+        methods[i].typeStringVMAddr = objcMethod.getTypesVMAddr(objcVisitor);
+        methods[i].impVMAddr        = objcMethod.getIMPVMAddr(objcVisitor);
+
+        if ( extendedMethodTypesBase.has_value() ) {
+            const uint8_t* methodTypesBase = (uint8_t*)extendedMethodTypesBase->value();
+            methodTypesBase += (pointerSize * i);
+            metadata_visitor::ResolvedValue methodType(extendedMethodTypesBase.value(), methodTypesBase);
+
+            // Get the VMAddr pointed to by this method type
+            VMAddress targetVMAddr              = objcVisitor.resolveRebase(methodType).vmAddress();
+            methods[i].extendedMethodTypeVMAddr = targetVMAddr;
+        }
+    }
+
+    // Sort by selector address (not contents)
+    auto sorter = [](const Method& a, const Method& b) {
+        return a.selStringVMAddr < b.selStringVMAddr;
+    };
+
+    // Stable sort because method lists can contain duplicates when categories have been attached.
+    std::stable_sort(&methods[0], &methods[numMethods], sorter);
+
+    // Replace the methods with the sorted ones
+    for ( uint32_t i = 0; i != numMethods; ++i ) {
+        objc_visitor::Method objcMethod = objcMethodList.getMethod(objcVisitor, i);
+
+        objcMethod.setName(objcVisitor, methods[i].selStringVMAddr);
+        objcMethod.setTypes(objcVisitor, methods[i].typeStringVMAddr);
+        objcMethod.setIMP(objcVisitor, methods[i].impVMAddr);
+
+        if ( extendedMethodTypesBase.has_value() ) {
+            const uint8_t* methodTypesBase = (uint8_t*)extendedMethodTypesBase->value();
+            methodTypesBase += (pointerSize * i);
+            metadata_visitor::ResolvedValue methodType(extendedMethodTypesBase.value(), methodTypesBase);
+
+            // Get the VMAddr pointed to by this method type
+            VMAddress targetVMAddr = methods[i].extendedMethodTypeVMAddr;
+            objcVisitor.updateTargetVMAddress(methodType, CacheVMAddress(targetVMAddr.rawValue()));
+        }
+    }
+}
+
+void CacheDylib::convertObjCMethodListsToOffsets(Diagnostics& diag, const BuilderConfig& config,
+                                                 Timer::AggregateTimer& timer,
+                                                 const Chunk*           selectorStringsChunk)
+{
+    if ( !this->inputHdr->hasObjC() )
+        return;
+
+    Timer::AggregateTimer::Scope timedScope(timer, "dylib convertObjCMethodListsToOffsets time");
+
+    __block objc_visitor::Visitor objcVisitor = this->makeCacheObjCVisitor(config, selectorStringsChunk, nullptr, nullptr);
+
+    // protocols can be listed multiple times in the _objc_protolist section, so we'll visit them multiple times here
+    // We don't want to convert the method list twice, so keep track of all seen method lists
+    // FIXME: Remove this once ld removes the duplicates (rdar://133008657)
+    __block std::unordered_set<const void*> seenMethodLists;
+
+    objcVisitor.forEachMethodList(^(objc_visitor::MethodList& objcMethodList,
+                                    std::optional<metadata_visitor::ResolvedValue> extendedMethodTypes) {
+        // Skip pointer based method lists
+        if ( !objcMethodList.usesRelativeOffsets() )
+            return;
+
+        // Skip method lists we've already converted
+        if ( bool inserted = seenMethodLists.insert(objcMethodList.getLocation()).second; !inserted )
+            return;
+
+        uint32_t numMethods = objcMethodList.numMethods();
+        for ( uint32_t i = 0; i != numMethods; ++i ) {
+            objc_visitor::Method objcMethod = objcMethodList.getMethod(objcVisitor, i);
+
+            const char* selString = objcMethod.getName(objcVisitor);
+
+            uint64_t nameOffset = (uint64_t)selString - (uint64_t)selectorStringsChunk->subCacheBuffer;
+            assert((uint32_t)nameOffset == nameOffset);
+
+            objcMethod.convertNameToOffset(objcVisitor, (uint32_t)nameOffset);
+        }
+
+        objcMethodList.setUsesOffsetsFromSelectorBuffer();
+    });
+}
+
+void CacheDylib::sortObjCMethodLists(Diagnostics& diag, const BuilderConfig& config,
+                                     Timer::AggregateTimer& timer,
+                                     const Chunk*           selectorStringsChunk)
+{
+    if ( !this->inputHdr->hasObjC() )
+        return;
+
+    Timer::AggregateTimer::Scope timedScope(timer, "dylib sortObjCMethodLists time");
+
+    __block objc_visitor::Visitor objcVisitor = this->makeCacheObjCVisitor(config, selectorStringsChunk, nullptr, nullptr);
+
+    objcVisitor.forEachMethodList(^(objc_visitor::MethodList& objcMethodList,
+                                    std::optional<metadata_visitor::ResolvedValue> extendedMethodTypes) {
+        if ( objcMethodList.usesRelativeOffsets() )
+            sortObjCRelativeMethodList(config, objcVisitor, objcMethodList, extendedMethodTypes);
+        else
+            sortObjCPointerMethodList(config, objcVisitor, objcMethodList, extendedMethodTypes);
+
+        objcMethodList.setIsSorted();
+    });
+}
+
+void CacheDylib::forEachReferenceToASelRef(Diagnostics &diags,
+                                           void (^handler)(uint64_t kind, uint32_t* instrPtr, uint64_t selRefVMAddr)) const
+{
+    const uint8_t* infoStart = this->inputDylibSplitSegStart;
+    const uint8_t* infoEnd = this->inputDylibSplitSegEnd;;
+
+    if ( *infoStart++ != DYLD_CACHE_ADJ_V2_FORMAT ) {
+        // Must be split seg v1
+        return;
+    }
+
+    __block uint32_t textSectionIndex = ~0U;
+    __block const uint8_t* textSectionContent = nullptr;
+    __block uint32_t selRefSectionIndex = ~0U;
+    __block uint64_t selRefSectionVMAddr = 0;
+    // The mach_header is section 0
+    __block uint32_t sectionIndex = 1;
+    this->cacheHdr->forEachSection(^(const Header::SegmentInfo &segInfo, const Header::SectionInfo &sectInfo, bool &stop) {
+        if ( (sectInfo.segmentName == "__TEXT" ) && (sectInfo.sectionName == "__text") ) {
+            textSectionIndex = sectionIndex;
+            VMOffset sectionOffsetInSegment(sectInfo.address - segInfo.vmaddr);
+            textSectionContent = this->segments[sectInfo.segIndex].subCacheBuffer;
+            textSectionContent += sectionOffsetInSegment.rawValue();
+        }
+        if ( sectInfo.segmentName.starts_with("__DATA") && (sectInfo.sectionName == "__objc_selrefs") ) {
+            selRefSectionIndex = sectionIndex;
+            selRefSectionVMAddr = sectInfo.address;
+        }
+        ++sectionIndex;
+    });
+
+    if ( (textSectionIndex == ~0U) || (selRefSectionIndex == ~0U) )
+        return;
+
+    // Whole         :== <count> FromToSection+
+    // FromToSection :== <from-sect-index> <to-sect-index> <count> ToOffset+
+    // ToOffset         :== <to-sect-offset-delta> <count> FromOffset+
+    // FromOffset     :== <kind> <count> <from-sect-offset-delta>
+    const uint8_t* p = infoStart;
+    uint64_t sectionCount = dyld3::MachOFile::read_uleb128(diags, p, infoEnd);
+    for (uint64_t i=0; i < sectionCount; ++i) {
+        uint64_t fromSectionIndex = dyld3::MachOFile::read_uleb128(diags, p, infoEnd);
+        uint64_t toSectionIndex = dyld3::MachOFile::read_uleb128(diags, p, infoEnd);
+        uint64_t toOffsetCount = dyld3::MachOFile::read_uleb128(diags, p, infoEnd);
+        uint64_t toSectionOffset = 0;
+        for (uint64_t j=0; j < toOffsetCount; ++j) {
+            uint64_t toSectionDelta = dyld3::MachOFile::read_uleb128(diags, p, infoEnd);
+            uint64_t fromOffsetCount = dyld3::MachOFile::read_uleb128(diags, p, infoEnd);
+            toSectionOffset += toSectionDelta;
+            for (uint64_t k=0; k < fromOffsetCount; ++k) {
+                uint64_t kind = dyld3::MachOFile::read_uleb128(diags, p, infoEnd);
+                if ( kind > 13 ) {
+                    diags.error("bad kind (%llu) value in %s\n", kind, installName.data());
+                }
+                uint64_t fromSectDeltaCount = dyld3::MachOFile::read_uleb128(diags, p, infoEnd);
+                uint64_t fromSectionOffset = 0;
+                for (uint64_t l=0; l < fromSectDeltaCount; ++l) {
+                    uint64_t delta = dyld3::MachOFile::read_uleb128(diags, p, infoEnd);
+                    fromSectionOffset += delta;
+                    if ( (fromSectionIndex == textSectionIndex) && (toSectionIndex == selRefSectionIndex) ) {
+                        uint32_t* instrPtr = (uint32_t*)(textSectionContent + fromSectionOffset);
+                        uint64_t targetVMAddr = selRefSectionVMAddr + toSectionOffset;
+                        handler(kind, instrPtr, targetVMAddr);
+                    }
+                }
+            }
+        }
+    }
+}
+
+void CacheDylib::optimizeLoadsFromConstants(const BuilderConfig& config,
+                                            Timer::AggregateTimer& timer,
+                                            const ObjCStringsChunk* selectorStringsChunk)
+{
+    const bool logSelectors = config.log.printDebugCacheLayout;
+
+    Timer::AggregateTimer::Scope timedScope(timer, "dylib optimizeLoadsFromConstants time");
+
+    if ( !this->cacheHdr->is64() )
+        return;
+
+    __block const uint8_t* textSectionContent = nullptr;
+    __block CacheVMAddress textSectionVMAddr;
+    __block const uint8_t* selRefSectionContent = nullptr;
+    __block CacheVMAddress selRefSectionVMAddr;
+    this->cacheHdr->forEachSection(^(const Header::SegmentInfo &segInfo, const Header::SectionInfo &sectInfo, bool &stop) {
+        VMOffset sectionOffsetInSegment(sectInfo.address - segInfo.vmaddr);
+        if ( ( sectInfo.segmentName == "__TEXT" ) && (sectInfo.sectionName == "__text") ) {
+            textSectionContent = this->segments[sectInfo.segIndex].subCacheBuffer;
+            textSectionContent += sectionOffsetInSegment.rawValue();
+
+            textSectionVMAddr = CacheVMAddress(sectInfo.address);
+        }
+        if ( sectInfo.segmentName.starts_with("__DATA") && (sectInfo.sectionName == "__objc_selrefs") ) {
+            selRefSectionContent = this->segments[sectInfo.segIndex].subCacheBuffer;
+            selRefSectionContent += sectionOffsetInSegment.rawValue();
+
+            selRefSectionVMAddr = CacheVMAddress(sectInfo.address);
+        }
+    });
+
+    __block std::unordered_map<uint64_t, std::set<void*>> lohTracker;
+    Diagnostics diag;
+    this->forEachReferenceToASelRef(diag, ^(uint64_t kind, uint32_t *instrPtr, uint64_t selRefVMAddr) {
+        if ( (kind == DYLD_CACHE_ADJ_V2_ARM64_ADRP) || (kind == DYLD_CACHE_ADJ_V2_ARM64_OFF12) ) {
+            lohTracker[selRefVMAddr].insert(instrPtr);
+        }
+    });
+
+    if ( lohTracker.empty() )
+        return;
+
+    uint64_t lohADRPCount = 0;
+    uint64_t lohLDRCount = 0;
+
+    CacheVMAddress selectorStringsStart = selectorStringsChunk->cacheVMAddress;
+    CacheVMAddress selectorStringsEnd = selectorStringsStart + selectorStringsChunk->cacheVMSize;
+
+    for (auto& targetAndInstructions : lohTracker) {
+        CacheVMAddress selRefVMAddr(targetAndInstructions.first);
+        std::set<void*>& instructions = targetAndInstructions.second;
+
+        VMOffset selRefSectionOffset = selRefVMAddr - selRefSectionVMAddr;
+        const void* selRefContent = selRefSectionContent + selRefSectionOffset.rawValue();
+
+        // Load the selector and make sure its in the selector strings chunk
+        CacheVMAddress selStringVMAddr = Fixup::Cache64::getCacheVMAddressFromLocation(config.layout.cacheBaseAddress,
+                                                                                       selRefContent);
+        const char* selectorString = nullptr;
+        if ( (selStringVMAddr >= selectorStringsStart) && (selStringVMAddr < selectorStringsEnd) ) {
+            VMOffset stringOffset = selStringVMAddr - selectorStringsStart;
+            selectorString = (const char*)selectorStringsChunk->subCacheBuffer + stringOffset.rawValue();
+        } else {
+            // This selRef doesn't point to the strings chunk, so skip it
+            instructions.clear();
+            continue;
+        }
+
+        // We do 2 passes over the instructions.  The first to validate them and the second
+        // to actually update them.
+        for (unsigned pass = 0; pass != 2; ++pass) {
+            uint32_t adrpCount = 0;
+            uint32_t ldrCount = 0;
+            for (void* instructionAddress : instructions) {
+                uint32_t& instruction = *(uint32_t*)instructionAddress;
+                VMOffset instructionSectionOffset((uint64_t)instructionAddress - (uint64_t)textSectionContent);
+                CacheVMAddress instructionVMAddr = textSectionVMAddr + instructionSectionOffset;
+
+                if ( (instruction & 0x9F000000) == 0x90000000 ) {
+                    // ADRP
+                    int64_t pageDistance = ((selStringVMAddr.rawValue() & ~0xFFF) - (instructionVMAddr.rawValue() & ~0xFFF));
+                    int64_t newPage21 = pageDistance >> 12;
+
+                    if (pass == 0) {
+                        if ( (newPage21 > 2097151) || (newPage21 < -2097151) ) {
+                            if (logSelectors)
+                                fprintf(stderr, "Out of bounds ADRP selector reference target\n");
+                            instructions.clear();
+                            break;
+                        }
+                        ++adrpCount;
+                    }
+
+                    if (pass == 1) {
+                        instruction = (instruction & 0x9F00001F) | ((newPage21 << 29) & 0x60000000) | ((newPage21 << 3) & 0x00FFFFE0);
+                        ++lohADRPCount;
+                    }
+                    continue;
+                }
+
+                if ( (instruction & 0x3B000000) == 0x39000000 ) {
+                    // LDR/STR.  STR shouldn't be possible as this is a selref!
+                    if (pass == 0) {
+                        if ( (instruction & 0xC0C00000) != 0xC0400000 ) {
+                            // Not a load, or dest reg isn't xN, or uses sign extension
+                            if (logSelectors)
+                                fprintf(stderr, "Bad LDR for selector reference optimisation\n");
+                            instructions.clear();
+                            break;
+                        }
+                        if ( (instruction & 0x04000000) != 0 ) {
+                            // Loading a float
+                            if (logSelectors)
+                                fprintf(stderr, "Bad LDR for selector reference optimisation\n");
+                            instructions.clear();
+                            break;
+                        }
+                        ++ldrCount;
+                    }
+
+                    if (pass == 1) {
+                        uint32_t ldrDestReg = (instruction & 0x1F);
+                        uint32_t ldrBaseReg = ((instruction >> 5) & 0x1F);
+
+                        // Convert the LDR to an ADD
+                        instruction = 0x91000000;
+                        instruction |= ldrDestReg;
+                        instruction |= ldrBaseReg << 5;
+                        instruction |= (selStringVMAddr.rawValue() & 0xFFF) << 10;
+
+                        ++lohLDRCount;
+                    }
+                    continue;
+                }
+
+                if ( (instruction & 0xFFC00000) == 0x91000000 ) {
+                    // ADD imm12
+                    // We don't support ADDs.
+                    if (logSelectors)
+                        fprintf(stderr, "Bad ADD for selector reference optimisation\n");
+                    instructions.clear();
+                    break;
+                }
+
+                if (logSelectors)
+                    fprintf(stderr, "Unknown instruction for selref optimisation\n");
+                instructions.clear();
+                break;
+            }
+            if (pass == 0) {
+                // If we didn't see at least one ADRP/LDR in pass one then don't optimize this location
+                if ((adrpCount == 0) || (ldrCount == 0)) {
+                    instructions.clear();
+                    break;
+                }
+            }
+        }
+    }
+
+    if ( logSelectors ) {
+        config.log.log("  Optimized %lld ADRP LOHs\n", lohADRPCount);
+        config.log.log("  Optimized %lld LDR LOHs\n", lohLDRCount);
+    }
+}
+
+Error CacheDylib::setObjCImpCachesPointers(const BuilderConfig& config,
+                                           const ObjCIMPCachesOptimizer& objcIMPCachesOptimizer,
+                                           const ObjCStringsChunk* selectorStringsChunk)
+{
+    if ( this->installName != "/usr/lib/libobjc.A.dylib" )
+        return Error();
+
+    Diagnostics diag;
+
+    // New libobjc's have a magic symbol for the offsets
+    std::string_view symbolName = objcIMPCachesOptimizer.sharedCacheOffsetsSymbolName;
+    std::optional<BindTargetAndName> bindTargetAndName;
+    bindTargetAndName = this->hasExportedSymbol(diag, symbolName.data(), SearchMode::onlySelf);
+    if ( diag.hasError() )
+        return Error("Couldn't build IMP caches because: %s", diag.errorMessageCStr());
+
+    if ( !bindTargetAndName )
+        return Error("Couldn't build IMP caches because: couldn't find imp caches symbol");
+
+    BindTarget& bindTarget = bindTargetAndName->first;
+    if ( bindTarget.kind != BindTarget::Kind::inputImage )
+        return Error("Couldn't build IMP caches because: symbol is wrong kind");
+
+    BindTarget::InputImage bindInputImage    = bindTarget.inputImage;
+    InputDylibVMAddress    targetInputVMAddr = bindInputImage.targetDylib->inputLoadAddress + bindInputImage.targetRuntimeOffset;
+    CacheVMAddress         targetCacheVMAddr = bindInputImage.targetDylib->adjustor->adjustVMAddr(targetInputVMAddr);
+
+    // Find the segment for the content
+    for ( DylibSegmentChunk& segment : this->segments ) {
+        if ( targetCacheVMAddr < segment.cacheVMAddress )
+            continue;
+        if ( targetCacheVMAddr >= (segment.cacheVMAddress + segment.cacheVMSize) )
+            continue;
+
+        VMOffset offsetInSegment = targetCacheVMAddr - segment.cacheVMAddress;
+        uint8_t* content = segment.subCacheBuffer + offsetInSegment.rawValue();
+
+        // Section looks like
+        // struct objc_opt_imp_caches_pointerlist_tt {
+        //     T selectorStringVMAddrStart;
+        //     T selectorStringVMAddrEnd;
+        //     T inlinedSelectorsVMAddrStart;
+        //     T inlinedSelectorsVMAddrEnd;
+        // };
+
+        CacheVMAddress selectorStringStartVMAddr = selectorStringsChunk->cacheVMAddress;
+        CacheVMAddress selectorStringEndVMAddr = selectorStringStartVMAddr + selectorStringsChunk->cacheVMSize;
+        if ( config.layout.is64 ) {
+            uint8_t* selectorStringStart = content;
+            uint8_t* selectorStringEnd = content + 8;
+
+            dyld3::MachOFile::PointerMetaData pmd;
+            Fixup::Cache64::setLocation(config.layout.cacheBaseAddress,
+                                        selectorStringStart, selectorStringStartVMAddr,
+                                        pmd.high8, pmd.diversity, pmd.usesAddrDiversity,
+                                        pmd.key, pmd.authenticated);
+            Fixup::Cache64::setLocation(config.layout.cacheBaseAddress,
+                                        selectorStringEnd, selectorStringEndVMAddr,
+                                        pmd.high8, pmd.diversity, pmd.usesAddrDiversity,
+                                        pmd.key, pmd.authenticated);
+
+            segment.tracker.add(selectorStringStart);
+            segment.tracker.add(selectorStringEnd);
+        } else {
+            uint8_t* selectorStringStart = content;
+            uint8_t* selectorStringEnd = content + 4;
+
+            dyld3::MachOFile::PointerMetaData pmd;
+            Fixup::Cache32::setLocation(config.layout.cacheBaseAddress,
+                                        selectorStringStart, selectorStringStartVMAddr);
+            Fixup::Cache32::setLocation(config.layout.cacheBaseAddress,
+                                        selectorStringStart, selectorStringEndVMAddr);
+
+            segment.tracker.add(selectorStringStart);
+            segment.tracker.add(selectorStringEnd);
+        }
+
+        return Error();
+    }
+
+    return Error("Couldn't build IMP caches because: couldn't find section for imp caches symbol");
+}
+
+Error CacheDylib::emitObjCIMPCaches(const BuilderConfig& config, Timer::AggregateTimer& timer,
+                                    const ObjCIMPCachesOptimizer& objcIMPCachesOptimizer,
+                                    const ObjCStringsChunk* selectorStringsChunk)
+{
+    if ( !objcIMPCachesOptimizer.builder )
+        return Error();
+
+    const bool log = config.log.printDebugIMPCaches;
+
+    Timer::AggregateTimer::Scope timedScope(timer, "emitObjCIMPCaches time");
+
+    const ObjCIMPCachesOptimizer::IMPCacheMap& dylibIMPCaches = objcIMPCachesOptimizer.dylibIMPCaches[this->cacheIndex];
+
+    // libobjc needs to know about some offsets, even if it didn't get IMP caches itself
+    Error pointersErr = this->setObjCImpCachesPointers(config, objcIMPCachesOptimizer,
+                                                       selectorStringsChunk);
+    if ( pointersErr.hasError() )
+        return pointersErr;
+
+    // Skip dylibs without chained fixups.  This simplifies binding superclasses across dylibs
+    if ( !this->inputMF->hasChainedFixupsLoadCommand() )
+        return Error();
+
+    __block objc_visitor::Visitor objcVisitor = this->makeCacheObjCVisitor(config, nullptr, nullptr, nullptr);
+
+    // Walk the classes in this dylib, and see if any have an IMP cache
+    objcVisitor.forEachClassAndMetaClass(^(objc_visitor::Class& objcClass, bool& stopClass) {
+        const ObjCIMPCachesOptimizer::ClassKey classKey = { objcClass.getName(objcVisitor), objcClass.isMetaClass };
+        auto it = dylibIMPCaches.find(classKey);
+        if ( it == dylibIMPCaches.end() ) {
+            // No IMP cache for this dylib
+            return;
+        }
+
+        // Get the cache we are going to emit
+        const imp_caches::IMPCache& impCache = it->second.first;
+
+        // Get the offset in the IMPCache buffer for this IMP cache
+        VMOffset impCacheOffset = it->second.second;
+
+        // Skip dylibs where the "vtable" address is set
+        if ( objcClass.getMethodCachePropertiesVMAddr(objcVisitor).has_value() )
+            return;
+
+        MachOFile::PointerMetaData PMD;
+        if ( config.layout.hasAuthRegion && (objcIMPCachesOptimizer.libobjcImpCachesVersion >= 4) ) {
+            PMD.diversity         = 0x9cff; // hash of "originalPreoptCache"
+            PMD.high8             = 0;
+            PMD.authenticated     = 1;
+            PMD.key               = 2;     // DA
+            PMD.usesAddrDiversity = 1;
+        }
+
+        // Set the "vtable" to point to the cache
+        CacheVMAddress impCacheVMAddr = objcIMPCachesOptimizer.impCachesChunk->cacheVMAddress + impCacheOffset;
+        objcClass.setMethodCachePropertiesVMAddr(objcVisitor, VMAddress(impCacheVMAddr.rawValue()), PMD);
+
+        // Tell the slide info emitter to slide this location
+        metadata_visitor::ResolvedValue vtableField = objcClass.getMethodCachePropertiesField(objcVisitor);
+        this->segments[vtableField.segmentIndex()].tracker.add(vtableField.value());
+
+        // TODO: This is where we could check the version if needed.  For now we know objc
+        // is new enough for the V2 format.
+        uint8_t* impCachePos = objcIMPCachesOptimizer.impCachesChunk->subCacheBuffer;
+        impCachePos += impCacheOffset.rawValue();
+
+        // Convert from VMAddress to CacheVMAddress as the objc visitor uses VMAddress internally
+        CacheVMAddress classVMAddr = CacheVMAddress(objcClass.getVMAddress().rawValue());
+
+        ImpCacheHeader_v2* impCacheHeader = (ImpCacheHeader_v2*)impCachePos;
+        VMOffset fallbackOffset;
+        if ( impCache.fallback_class.has_value() ) {
+            auto classIt = objcIMPCachesOptimizer.classMap.find(impCache.fallback_class.value());
+            assert(classIt != objcIMPCachesOptimizer.classMap.end());
+            const ObjCIMPCachesOptimizer::InputDylibLocation& inputDylibClass = classIt->second;
+
+            CacheVMAddress superclassVMAddr = inputDylibClass.first->adjustor->adjustVMAddr(inputDylibClass.second);
+            fallbackOffset = superclassVMAddr - classVMAddr;
+        } else {
+            // The default fallback class is the superclass
+            VMAddress superclassVMAddr(0ULL);
+            std::optional<VMAddress> optionalSuperclassVMAddr = objcClass.getSuperclassVMAddr(objcVisitor);
+            if ( optionalSuperclassVMAddr.has_value() )
+                superclassVMAddr = optionalSuperclassVMAddr.value();
+
+            fallbackOffset = superclassVMAddr - VMAddress(classVMAddr.rawValue());
+        }
+
+        impCacheHeader->fallback_class_offset = fallbackOffset.rawValue();
+        impCacheHeader->cache_shift = impCache.cache_shift;
+        impCacheHeader->cache_mask = impCache.cache_mask;
+        impCacheHeader->occupied = impCache.occupied;
+        impCacheHeader->has_inlines = impCache.has_inlines;
+        impCacheHeader->padding = impCache.padding;
+        impCacheHeader->unused = impCache.unused;
+        impCacheHeader->bit_one = impCache.bit_one;
+
+        // Emit the buckets
+        uint8_t* firstBucketPos = impCachePos + sizeof(*impCacheHeader);
+        ImpCacheEntry_v2* currentBucket = (ImpCacheEntry_v2*)firstBucketPos;
+        for ( const imp_caches::Bucket& bucket : impCache.buckets ) {
+            if ( bucket.isEmptyBucket ) {
+                currentBucket->selOffset = 0x3FFFFFF;
+                currentBucket->impOffset = 0;
+            } else {
+                imp_caches::BucketMethod bucketMethod = {
+                    .className = bucket.className,
+                    .methodName = bucket.methodName,
+                    .isInstanceMethod = bucket.isInstanceMethod
+                };
+                const auto& dylibMethodMap = objcIMPCachesOptimizer.methodMap.at(bucket.installName);
+                auto bucketIt = dylibMethodMap.find(bucketMethod);
+                assert(bucketIt != dylibMethodMap.end());
+
+                const ObjCIMPCachesOptimizer::InputDylibLocation& bucketInputLocation = bucketIt->second;
+                CacheVMAddress methodVMAddr = bucketInputLocation.first->adjustor->adjustVMAddr(bucketInputLocation.second);
+                VMOffset impVMOffset = classVMAddr - methodVMAddr;
+
+                int64_t selOffset = (int64_t)bucket.selOffset;
+                int64_t impOffset = (int64_t)impVMOffset.rawValue();
+
+                assert(impOffset % 4 == 0); // dest and source should be aligned
+                impOffset >>= 2;
+                // objc assumes the imp offset always has
+                // its two bottom bits set to 0, this lets us have
+                // 4x more reach
+
+                assert(impOffset < 1ll << 39);
+                assert(-impOffset < 1ll << 39);
+                assert(selOffset < 0x4000000);
+                currentBucket->selOffset = selOffset;
+                currentBucket->impOffset = impOffset;
+
+                if ( log ) {
+                    const uint8_t* selString = selectorStringsChunk->subCacheBuffer + currentBucket->selOffset;
+                    uint64_t bucketIndex = currentBucket - (ImpCacheEntry_v2*)firstBucketPos;
+                    config.log.log("[IMP Caches] Coder[%lld]: %#08llx (sel: %#08llx, imp %#08llx) %s\n",
+                                   bucketIndex, methodVMAddr.rawValue(),
+                                   selOffset, impOffset, (const char*)selString);
+                }
+            }
+            ++currentBucket;
+        }
+    });
+
+    return Error();
+}
+
+// This dylib may have uniqued GOTs.  This returns a map from the address of the uniqued GOT
+// to the target of that GOT.
+CacheDylib::GOTToTargetMap CacheDylib::getUniquedGOTTargets(const PatchInfo& dylibPatchInfo) const
+{
+    CacheDylib::GOTToTargetMap gotToTargetMap;
+
+    for ( UniquedGOTKind sectionKind : { UniquedGOTKind::regular, UniquedGOTKind::authGot, UniquedGOTKind::authPtr } ) {
+        std::span<const std::vector<PatchInfo::GOTInfo>> bindGOTUses;
+        switch ( sectionKind ) {
+            case UniquedGOTKind::regular:
+                bindGOTUses = dylibPatchInfo.bindGOTUses;
+                break;
+            case UniquedGOTKind::authGot:
+                bindGOTUses = dylibPatchInfo.bindAuthGOTUses;
+                break;
+            case UniquedGOTKind::authPtr:
+                bindGOTUses = dylibPatchInfo.bindAuthPtrUses;
+                break;
+        }
+
+        assert(this->bindTargets.size() == bindGOTUses.size());
+        for ( uint32_t bindIndex = 0; bindIndex != this->bindTargets.size(); ++bindIndex ) {
+            const BindTarget& bindTarget = this->bindTargets[bindIndex];
+
+            // Skip binds with no uses
+            const std::vector<PatchInfo::GOTInfo>& clientUses = bindGOTUses[bindIndex];
+            if ( clientUses.empty() )
+                continue;
+
+            // Skip absolute binds.  Perhaps we should track these, but we lost the information to patch them
+            if ( bindTarget.kind == CacheDylib::BindTarget::Kind::absolute )
+                continue;
+
+            assert(bindTarget.kind == BindTarget::Kind::cacheImage);
+            const BindTarget::CacheImage& cacheImageTarget = bindTarget.cacheImage;
+            CacheVMAddress bindTargetVMAddr = cacheImageTarget.targetDylib->cacheLoadAddress + cacheImageTarget.targetRuntimeOffset;
+
+            for ( const PatchInfo::GOTInfo& gotInfo : clientUses ) {
+                CacheVMAddress gotVMAddr = gotInfo.useLocation.clientGOT->cacheVMAddress + gotInfo.useLocation.clientGOTOffset;
+                gotToTargetMap[gotVMAddr] = bindTargetVMAddr;
+            }
+        }
+    }
+
+    return gotToTargetMap;
+}
+
+CacheDylib::OldToNewStubMap CacheDylib::buildStubMaps(const BuilderConfig& config,
+                                                      const StubOptimizer& stubOptimizer,
+                                                      const PatchInfo& dylibPatchInfo)
+{
+    __block OldToNewStubMap oldToNewStubMap;
+
+    __block Diagnostics diag;
+    __block uint32_t stubsLeftInterposable = 0;
+
+    // Find all the indirect symbol names from the source dylib
+    // Record all the indirect symbols
+    __block std::vector<std::string_view> indirectSymbols;
+    this->inputMF->withFileLayout(diag, ^(const mach_o::Layout& layout) {
+        mach_o::SymbolTable symbols(layout);
+
+        indirectSymbols.reserve(layout.linkedit.indirectSymbolTable.entryCount);
+
+        symbols.forEachIndirectSymbol(diag, ^(const char* symbolName, uint32_t symNum) {
+            indirectSymbols.push_back(symbolName);
+        });
+    });
+    diag.assertNoError();
+
+    GOTToTargetMap uniquedGOTMap = getUniquedGOTTargets(dylibPatchInfo);
+
+    // GOTs may have been optimized.  We'll either end up in a GOT or auth GOT, depending on arch
+    __block metadata_visitor::Visitor visitor = this->makeCacheVisitor(config);
+
+    // Get the target of the GOT.  It might be uniqued so look there too
+    auto getGOTTarget = ^(uint64_t targetLPAddr) {
+        std::optional<VMAddress> targetVMAddr;
+
+        CacheVMAddress gotCacheVMAddr(targetLPAddr);
+        VMAddress gotVMAddr(targetLPAddr);
+        if ( auto it = uniquedGOTMap.find(gotCacheVMAddr); it != uniquedGOTMap.end() ) {
+            targetVMAddr = VMAddress(it->second.rawValue());
+        } else {
+            metadata_visitor::ResolvedValue gotValue = visitor.getValueFor(gotVMAddr);
+            targetVMAddr = visitor.resolveOptionalRebaseToVMAddress(gotValue);
+        }
+
+        return targetVMAddr;
+    };
+
+    // Walk all the stubs in the stubs sections
+    this->cacheHdr->forEachSection(^(const Header::SegmentInfo &segInfo, const Header::SectionInfo &sectInfo, bool &stop) {
+        unsigned sectionType = (sectInfo.flags & SECTION_TYPE);
+        if ( sectionType != S_SYMBOL_STUBS )
+            return;
+
+        // We can only optimize certain stubs sections, depending on the arch
+        if ( sectInfo.sectionName != this->developmentStubs.sectionName )
+            return;
+        if ( sectInfo.segmentName != this->developmentStubs.segmentName )
+            return;
+
+        // reserved1/reserved2 tell us how large stubs are, and our offset in to the symbol table
+        const uint64_t indirectTableOffset = sectInfo.reserved1;
+        const uint64_t stubsSize = sectInfo.reserved2;
+        const uint64_t stubsCount = sectInfo.size / stubsSize;
+
+        CacheVMAddress stubsSectionBaseAddress(sectInfo.address);
+
+        // Work out where the stub buffer is in the cache
+        const DylibSegmentChunk& segment = this->segments[segInfo.segmentIndex];
+        CacheVMAddress segmentBaseAddress = segment.cacheVMAddress;
+        VMOffset sectionOffsetInSegment = stubsSectionBaseAddress - segmentBaseAddress;
+        const uint8_t* sectionBuffer = segment.subCacheBuffer + sectionOffsetInSegment.rawValue();
+
+        for ( uint64_t stubIndex = 0; stubIndex != stubsCount; ++stubIndex ) {
+            uint64_t stubOffset = stubsSize * stubIndex;
+            CacheVMAddress oldStubVMAddr = stubsSectionBaseAddress + CacheVMSize(stubOffset);
+            CacheVMAddress newStubVMAddr = this->developmentStubs.cacheVMAddress + VMOffset(stubOffset);
+            const uint8_t* stubInstrs = sectionBuffer + stubOffset;
+
+            uint64_t symbolIndex = indirectTableOffset + stubIndex;
+            if ( symbolIndex >= indirectSymbolTable.size() ) {
+                diag.warning("Symbol index (%lld) exceeds length of symbol table (%lld)",
+                             symbolIndex, (uint64_t)indirectSymbolTable.size());
+                continue;
+            }
+
+            std::string_view symName = indirectSymbols[symbolIndex];
+            if ( stubOptimizer.neverStubEliminate.count(symName) ) {
+                stubsLeftInterposable++;
+                continue;
+            }
+
+            if ( this->cacheHdr->isArch("arm64") ) {
+                uint64_t targetLPAddr = StubOptimizer::gotAddrFromArm64Stub(diag, this->installName,
+                                                                            stubInstrs,
+                                                                            oldStubVMAddr.rawValue());
+
+                if ( targetLPAddr == 0 )
+                    continue;
+
+                std::optional<VMAddress> gotTargetVMAddr = getGOTTarget(targetLPAddr);
+                if ( !gotTargetVMAddr.has_value() )
+                    continue;
+
+                // Track the stub for later
+                oldToNewStubMap[oldStubVMAddr] = newStubVMAddr;
+
+                // Emit this stub in to the stub islands for this dylib
+                {
+                    // Dev stub
+                    uint8_t* newStubBuffer = developmentStubs.subCacheBuffer + stubOffset;
+                    StubOptimizer::generateArm64StubToGOT(newStubBuffer, newStubVMAddr.rawValue(),
+                                                          targetLPAddr);
+                }
+                {
+                    // Customer stub
+                    uint8_t* newStubBuffer = customerStubs.subCacheBuffer + stubOffset;
+                    StubOptimizer::generateArm64StubTo(newStubBuffer, newStubVMAddr.rawValue(),
+                                                       targetLPAddr, gotTargetVMAddr->rawValue());
+                }
+            } else if ( this->cacheHdr->isArch("arm64e") ) {
+                uint64_t targetLPAddr = StubOptimizer::gotAddrFromArm64eStub(diag, this->installName,
+                                                                             stubInstrs,
+                                                                             oldStubVMAddr.rawValue());
+
+                if ( targetLPAddr == 0 )
+                    continue;
+
+                std::optional<VMAddress> gotTargetVMAddr = getGOTTarget(targetLPAddr);
+                if ( !gotTargetVMAddr.has_value() )
+                    continue;
+
+                // Track the stub for later
+                oldToNewStubMap[oldStubVMAddr] = newStubVMAddr;
+
+                // Emit this stub in to the stub islands for this dylib
+                {
+                    // Dev stub
+                    uint8_t* newStubBuffer = developmentStubs.subCacheBuffer + stubOffset;
+                    StubOptimizer::generateArm64eStubToGOT(newStubBuffer, newStubVMAddr.rawValue(),
+                                                           targetLPAddr);
+                }
+                {
+                    // Customer stub
+                    uint8_t* newStubBuffer = customerStubs.subCacheBuffer + stubOffset;
+                    StubOptimizer::generateArm64eStubTo(newStubBuffer, newStubVMAddr.rawValue(),
+                                                        targetLPAddr, gotTargetVMAddr->rawValue());
+                }
+            } else if ( this->cacheHdr->isArch("arm64_32") ) {
+                uint64_t targetLPAddr = StubOptimizer::gotAddrFromArm64_32Stub(diag, this->installName,
+                                                                               stubInstrs,
+                                                                               oldStubVMAddr.rawValue());
+
+                if ( targetLPAddr == 0 )
+                    continue;
+
+                std::optional<VMAddress> gotTargetVMAddr = getGOTTarget(targetLPAddr);
+                if ( !gotTargetVMAddr.has_value() )
+                    continue;
+
+                // Track the stub for later
+                oldToNewStubMap[oldStubVMAddr] = newStubVMAddr;
+
+                // Emit this stub in to the stub islands for this dylib
+                {
+                    // Dev stub
+                    uint8_t* newStubBuffer = developmentStubs.subCacheBuffer + stubOffset;
+                    StubOptimizer::generateArm64_32StubToGOT(newStubBuffer, newStubVMAddr.rawValue(),
+                                                             targetLPAddr);
+                }
+                {
+                    // Customer stub
+                    uint8_t* newStubBuffer = customerStubs.subCacheBuffer + stubOffset;
+                    StubOptimizer::generateArm64_32StubTo(newStubBuffer, newStubVMAddr.rawValue(),
+                                                          gotTargetVMAddr->rawValue());
+                }
+            } else {
+                // Unknown arch
+                assert(0);
+            }
+        }
+    });
+
+    return oldToNewStubMap;
+}
+
+void CacheDylib::forEachCallSiteToAStub(Diagnostics& diag, const CallSiteHandler handler)
+{
+    // Get the section layout and split seg info from the source dylib
+    __block uint64_t textSectionIndex = ~0U;
+    __block uint64_t stubSectionIndex = ~0U;
+    __block uint8_t* textSectionBuffer = nullptr;
+    __block uint64_t textSectionVMAddr = ~0ULL;
+    __block uint64_t stubSectionVMAddr = ~0ULL;
+
+    // Find the sections
+    {
+        // Section #0 is the mach_header
+        __block uint32_t sectionIndex = 1;
+        this->cacheHdr->forEachSection(^(const Header::SegmentInfo &segInfo, const Header::SectionInfo &sectInfo, bool &stop) {
+            if ( sectInfo.segmentName == "__TEXT" ) {
+                if ( sectInfo.sectionName == "__text" ) {
+                    textSectionIndex = sectionIndex;
+                    textSectionVMAddr = sectInfo.address;
+
+                    // Work out the buffer for the text section
+                    const DylibSegmentChunk& segment = this->segments[segInfo.segmentIndex];
+                    CacheVMAddress segmentBaseAddress = segment.cacheVMAddress;
+                    CacheVMAddress sectionBaseAddress(sectInfo.address);
+                    VMOffset sectionOffsetInSegment = sectionBaseAddress - segmentBaseAddress;
+                    textSectionBuffer = segment.subCacheBuffer + sectionOffsetInSegment.rawValue();
+                } else if ( sectInfo.sectionName == "__stubs" ) {
+                    // On arm64e devices, we ignore __stubs and only handle __auth_stubs
+                    if ( !this->cacheHdr->isArch("arm64e") ) {
+                        stubSectionIndex = sectionIndex;
+                        stubSectionVMAddr = sectInfo.address;
+                    }
+                } else if ( sectInfo.sectionName == "__auth_stubs" ) {
+                    // On arm64e devices, we ignore __stubs and only handle __auth_stubs
+                    if ( this->cacheHdr->isArch("arm64e") ) {
+                        stubSectionIndex = sectionIndex;
+                        stubSectionVMAddr = sectInfo.address;
+                    }
+                }
+            }
+            ++sectionIndex;
+        });
+    }
+
+    if ( textSectionIndex == ~0U )
+        return;
+    if ( stubSectionIndex == ~0U )
+        return;
+
+    this->inputMF->withFileLayout(diag, ^(const mach_o::Layout& layout) {
+        const uint8_t* infoStart = layout.linkedit.splitSegInfo.buffer;
+        const uint8_t* infoEnd   = infoStart + layout.linkedit.splitSegInfo.bufferSize;
+        if ( *infoStart++ != DYLD_CACHE_ADJ_V2_FORMAT ) {
+            diag.error("malformed split seg info in %s", this->installName.data());
+            return;
+        }
+
+        // Whole         :== <count> FromToSection+
+        // FromToSection :== <from-sect-index> <to-sect-index> <count> ToOffset+
+        // ToOffset      :== <to-sect-offset-delta> <count> FromOffset+
+        // FromOffset    :== <kind> <count> <from-sect-offset-delta>
+        const uint8_t* p = infoStart;
+        uint64_t sectionCount = dyld3::MachOFile::read_uleb128(diag, p, infoEnd);
+        for (uint64_t i=0; i < sectionCount; ++i) {
+            uint64_t fromSectionIndex = dyld3::MachOFile::read_uleb128(diag, p, infoEnd);
+            uint64_t toSectionIndex = dyld3::MachOFile::read_uleb128(diag, p, infoEnd);
+            uint64_t toOffsetCount = dyld3::MachOFile::read_uleb128(diag, p, infoEnd);
+            uint64_t toSectionOffset = 0;
+            for (uint64_t j=0; j < toOffsetCount; ++j) {
+                uint64_t toSectionDelta = dyld3::MachOFile::read_uleb128(diag, p, infoEnd);
+                uint64_t fromOffsetCount = dyld3::MachOFile::read_uleb128(diag, p, infoEnd);
+                toSectionOffset += toSectionDelta;
+                for (uint64_t k=0; k < fromOffsetCount; ++k) {
+                    uint64_t kind = dyld3::MachOFile::read_uleb128(diag, p, infoEnd);
+                    if ( kind > 13 ) {
+                        diag.error("bad kind (%llu) value in %s\n", kind, this->installName.data());
+                        return;
+                    }
+                    uint64_t fromSectDeltaCount = dyld3::MachOFile::read_uleb128(diag, p, infoEnd);
+                    uint64_t fromSectionOffset = 0;
+                    for (uint64_t l=0; l < fromSectDeltaCount; ++l) {
+                        uint64_t delta = dyld3::MachOFile::read_uleb128(diag, p, infoEnd);
+                        fromSectionOffset += delta;
+                        if ( (fromSectionIndex == textSectionIndex) && (toSectionIndex == stubSectionIndex) ) {
+                            uint32_t* instrPtr = (uint32_t*)(textSectionBuffer + fromSectionOffset);
+                            uint64_t instrAddr = textSectionVMAddr + fromSectionOffset;
+                            uint64_t stubAddr = stubSectionVMAddr + toSectionOffset;
+                            uint32_t instruction = *instrPtr;
+                            if ( handler(kind, instrAddr, stubAddr, instruction) ) {
+                                *instrPtr = instruction;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    });
+}
+
+// In a universal cache, dylibs should not longer use their own __stubs, but instead redirect to a stubs
+// subCache.  There will be 1 stubs cache for customer and another for development
+void CacheDylib::optimizeStubs(const BuilderOptions& options, const BuilderConfig& config,
+                               Timer::AggregateTimer& timer,
+                               const StubOptimizer& stubOptimizer,
+                               const PatchInfo& dylibPatchInfo)
+{
+    if ( options.kind != CacheKind::universal )
+        return;
+
+    Timer::AggregateTimer::Scope timedScope(timer, "optimizeStubs time");
+
+    OldToNewStubMap oldToNewStubMap = this->buildStubMaps(config, stubOptimizer, dylibPatchInfo);
+
+    __block Diagnostics diag;
+
+    // Walk the split seg info from the input dylib, as its been removed from the cache dylib
+    this->forEachCallSiteToAStub(diag, ^(uint8_t kind, uint64_t callSiteAddr, uint64_t stubAddr,
+                                         uint32_t& instruction) {
+        if ( kind != DYLD_CACHE_ADJ_V2_ARM64_BR26 )
+            return false;
+        // skip all but BL or B
+        if ( (instruction & 0x7C000000) != 0x14000000 )
+            return false;
+        // compute target of branch instruction
+        int32_t brDelta = (instruction & 0x03FFFFFF) << 2;
+        if ( brDelta & 0x08000000 )
+            brDelta |= 0xF0000000;
+        uint64_t targetAddr = callSiteAddr + (int64_t)brDelta;
+        if ( targetAddr != stubAddr ) {
+            diag.warning("stub target mismatch");
+            return false;
+        }
+
+        // ignore branch if not to a stub we want to optimize
+        CacheVMAddress oldStubAddr(stubAddr);
+        auto it = oldToNewStubMap.find(oldStubAddr);
+        if ( it == oldToNewStubMap.end() )
+            return false;
+
+        CacheVMAddress newStubAddr = it->second;
+
+        int64_t deltaToNewStub = newStubAddr.rawValue() - callSiteAddr;
+        static const int64_t b128MegLimit = 0x07FFFFFF;
+        if ( (deltaToNewStub <= -b128MegLimit) || (deltaToNewStub >= b128MegLimit) ) {
+            diag.error("%s call could not reach stub island at offset 0x%llx",
+                       this->installName.data(), deltaToNewStub);
+            return false;
+        }
+
+        instruction = (instruction & 0xFC000000) | ((deltaToNewStub >> 2) & 0x03FFFFFF);
+        return true;
+    });
+}
+
+void CacheDylib::fipsSign(Timer::AggregateTimer& timer)
+{
+    // We only need corecrypto.  Skip everything else
+    if ( this->installName != "/usr/lib/system/libcorecrypto.dylib" )
+        return;
+
+    Timer::AggregateTimer::Scope timedScope(timer, "fipsSign time");
+
+    // find location in libcorecrypto.dylib to store hash of __text section
+    __block const void* textLocation = nullptr;
+    __block CacheVMSize textSize;
+    __block const void* hashStoreLocation = nullptr;
+    __block CacheVMSize hashStoreSize;
+    this->forEachCacheSection(^(std::string_view segmentName, std::string_view sectionName,
+                                uint8_t *sectionBuffer, CacheVMAddress sectionVMAddr,
+                                CacheVMSize sectionVMSize, bool &stop) {
+        if ( (segmentName == "__TEXT") && (sectionName == "__text") ) {
+            textLocation = sectionBuffer;
+            textSize = sectionVMSize;
+        } else if ( (segmentName == "__TEXT") && (sectionName == "__fips_hmacs") ) {
+            hashStoreLocation = sectionBuffer;
+            hashStoreSize = sectionVMSize;
+        }
+    });
+
+    if ( hashStoreLocation == nullptr ) {
+        // FIXME: Plumb up a warning.  We can't make this an error as some platforms don't have this dylib
+        // _diagnostics.warning("Could not find __TEXT/__fips_hmacs section in libcorecrypto.dylib, skipping FIPS sealing");
+        return;
+    }
+
+    if ( hashStoreSize.rawValue() != 32 ) {
+        // FIXME: Plumb up a warning.  We can't make this an error as some platforms don't have this dylib
+        // _diagnostics.warning("__TEXT/__fips_hmacs section in libcorecrypto.dylib is not 32 bytes in size, skipping FIPS sealing");
+        return;
+    }
+
+    if ( textLocation == nullptr ) {
+        // FIXME: Plumb up a warning.  We can't make this an error as some platforms don't have this dylib
+        // _diagnostics.warning("Could not find __TEXT/__text section in libcorecrypto.dylib, skipping FIPS sealing");
+        return;
+    }
+
+    // store hash directly into hashStoreLocation
+    unsigned char hmac_key = 0;
+    CCHmac(kCCHmacAlgSHA256, &hmac_key, 1, textLocation, textSize.rawValue(), (void*)hashStoreLocation);
+}
+
+template <typename P>
+static void addObjcSegments(Diagnostics& diag, const dyld3::MachOFile* objcMF,
+                            CacheVMAddress readOnlyVMAddr, CacheVMSize readOnlyVMSize,
+                            CacheFileOffset readOnlyFileOffset,
+                            CacheVMAddress readWriteVMAddr, CacheVMSize readWriteVMSize,
+                            CacheFileOffset readWriteFileOffset)
+{
+    // validate there is enough free space to add the load commands
+    uint32_t freeSpace = ((const Header*)objcMF)->loadCommandsFreeSpace();
+    const uint32_t segSize = sizeof(macho_segment_command<P>);
+    if ( freeSpace < 2*segSize ) {
+        diag.warning("not enough space in libojbc.dylib to add load commands for objc optimization regions");
+        return;
+    }
+
+    // find location of LINKEDIT LC_SEGMENT load command, we need to insert new segments before it
+    uint32_t linkeditIndex = 0;
+    uint8_t* linkeditSeg = nullptr;
+    linkeditSeg = (uint8_t*)((mach_o::Header*)objcMF)->findLoadCommand(linkeditIndex, ^bool(const load_command *lc) {
+        CString segmentName;
+        if ( lc->cmd == LC_SEGMENT )
+            segmentName = ((const segment_command*)lc)->segname;
+        else if ( lc->cmd == LC_SEGMENT_64 )
+            segmentName = ((const segment_command_64*)lc)->segname;
+        
+        return segmentName == "__LINKEDIT";
+    });
+    
+    if ( linkeditSeg == nullptr ) {
+        diag.warning("__LINKEDIT not found in libojbc.dylib");
+        return;
+    }
+
+    // move load commands to make room to insert two new ones before LINKEDIT segment load command
+    uint8_t* endOfLoadCommands = (uint8_t*)objcMF + sizeof(macho_header<P>) + objcMF->sizeofcmds;
+    uint32_t remainingSize = (uint32_t)(endOfLoadCommands - linkeditSeg);
+    memmove(linkeditSeg+2*segSize, linkeditSeg, remainingSize);
+
+    // insert new segments
+    macho_segment_command<P>* roSeg = (macho_segment_command<P>*)(linkeditSeg);
+    macho_segment_command<P>* rwSeg = (macho_segment_command<P>*)(linkeditSeg+sizeof(macho_segment_command<P>));
+    roSeg->set_cmd(macho_segment_command<P>::CMD);
+    roSeg->set_cmdsize(segSize);
+    roSeg->set_segname("__OBJC_RO");
+    roSeg->set_vmaddr(readOnlyVMAddr.rawValue());
+    roSeg->set_vmsize(readOnlyVMSize.rawValue());
+    roSeg->set_fileoff(readOnlyFileOffset.rawValue());
+    roSeg->set_filesize(readOnlyVMSize.rawValue());
+    roSeg->set_maxprot(VM_PROT_READ);
+    roSeg->set_initprot(VM_PROT_READ);
+    roSeg->set_nsects(0);
+    roSeg->set_flags(0);
+    rwSeg->set_cmd(macho_segment_command<P>::CMD);
+    rwSeg->set_cmdsize(segSize);
+    rwSeg->set_segname("__OBJC_RW");
+    rwSeg->set_vmaddr(readWriteVMAddr.rawValue());
+    rwSeg->set_vmsize(readWriteVMSize.rawValue());
+    rwSeg->set_fileoff(readWriteFileOffset.rawValue());
+    rwSeg->set_filesize(readWriteVMSize.rawValue());
+    rwSeg->set_maxprot(VM_PROT_WRITE|VM_PROT_READ);
+    rwSeg->set_initprot(VM_PROT_WRITE|VM_PROT_READ);
+    rwSeg->set_nsects(0);
+    rwSeg->set_flags(0);
+
+    // update mach_header to account for new load commands
+    macho_header<P>* mh = (macho_header<P>*)objcMF;
+    mh->set_sizeofcmds(mh->sizeofcmds() + 2*segSize);
+    mh->set_ncmds(mh->ncmds()+2);
+}
+
+void CacheDylib::addObjcSegments(Diagnostics& diag, Timer::AggregateTimer& timer,
+                                 const ObjCHeaderInfoReadOnlyChunk* headerInfoReadOnlyChunk,
+                                 const ObjCImageInfoChunk* imageInfoChunk,
+                                 const ObjCProtocolHashTableChunk* protocolHashTableChunk,
+                                 const ObjCPreAttachedCategoriesChunk* preAttachedCategoriesChunk,
+                                 const ObjCHeaderInfoReadWriteChunk* headerInfoReadWriteChunk,
+                                 const ObjCCanonicalProtocolsChunk* canonicalProtocolsChunk)
+{
+    // We only need objc.  Skip everything else
+    if ( this->installName != "/usr/lib/libobjc.A.dylib" )
+        return;
+
+    Timer::AggregateTimer::Scope timedScope(timer, "addObjcSegments time");
+
+    // Find the ranges for OBJC_RO and OBJC_RW
+
+    // Read-only
+    // Note these asserts are just to make sure we use the correct chunks for the start/end
+    static_assert(Chunk::Kind::objcHeaderInfoRO < Chunk::Kind::objcImageInfo);
+    static_assert(Chunk::Kind::objcImageInfo < Chunk::Kind::objcStrings);
+    static_assert(Chunk::Kind::objcStrings < Chunk::Kind::objcSelectorsHashTable);
+    static_assert(Chunk::Kind::objcSelectorsHashTable < Chunk::Kind::objcClassesHashTable);
+    static_assert(Chunk::Kind::objcClassesHashTable < Chunk::Kind::objcProtocolsHashTable);
+    static_assert(Chunk::Kind::objcProtocolsHashTable < Chunk::Kind::objcIMPCaches);
+    static_assert(Chunk::Kind::objcIMPCaches < Chunk::Kind::objcPreAttachedCategories);
+
+    CacheFileOffset readOnlyFileOffset = headerInfoReadOnlyChunk->subCacheFileOffset;
+    CacheVMAddress readOnlyVMAddr = headerInfoReadOnlyChunk->cacheVMAddress;
+    CacheVMSize readOnlyVMSize = (preAttachedCategoriesChunk->cacheVMAddress + preAttachedCategoriesChunk->cacheVMSize) - readOnlyVMAddr;
+
+
+    // Read-write
+    static_assert(Chunk::Kind::objcHeaderInfoRW < Chunk::Kind::objcCanonicalProtocols);
+
+    CacheFileOffset readWriteFileOffset = headerInfoReadWriteChunk->subCacheFileOffset;
+    CacheVMAddress readWriteVMAddr = headerInfoReadWriteChunk->cacheVMAddress;
+    CacheVMSize readWriteVMSize = (canonicalProtocolsChunk->cacheVMAddress + canonicalProtocolsChunk->cacheVMSize) - readWriteVMAddr;
+
+    if ( this->inputHdr->is64() ) {
+        typedef Pointer64<LittleEndian> P;
+        addObjcSegments<P>(diag, this->cacheMF,
+                           readOnlyVMAddr, readOnlyVMSize, readOnlyFileOffset,
+                           readWriteVMAddr, readWriteVMSize, readWriteFileOffset);
+    } else {
+        typedef Pointer32<LittleEndian> P;
+        addObjcSegments<P>(diag, this->cacheMF,
+                           readOnlyVMAddr, readOnlyVMSize, readOnlyFileOffset,
+                           readWriteVMAddr, readWriteVMSize, readWriteFileOffset);
+    }
+}
+
+void CacheDylib::removeLinkedDylibs(Diagnostics& diag)
+{
+    mach_o::HeaderWriter* header = (mach_o::HeaderWriter*)cacheHdr;
+    uint32_t        lcLibSystemIndex = 0;
+    if ( !header->findLoadCommand(lcLibSystemIndex, ^bool(const load_command *lc) {
+        const dylib_command* dyliblc = mach_o::Header::isDylibLoadCommand(lc);
+        if ( !dyliblc ) return false;
+
+        const char* loadPath = (char*)dyliblc + dyliblc->dylib.name.offset;
+        return strstr(loadPath, "libSystem");
+    }) ) {
+        diag.error("can't remove linked dylibs from %s, expected to find libSystem dependency", header->installName());
+        return;
+    }
+
+    uint32_t lcDylibStart = 0;
+    uint32_t lcDylibEnd = 0;
+    header->findLoadCommandRange(lcDylibStart, lcDylibEnd, ^bool(const load_command *lc) {
+        return mach_o::Header::isDylibLoadCommand(lc) != nullptr;
+    });
+    // libSystem was found, so the range of dylib load commands also must not be empty
+    assert(lcDylibStart != lcDylibEnd);
+    assert(lcLibSystemIndex >= lcDylibStart);
+
+    if ( lcDylibStart != lcLibSystemIndex ) {
+        diag.error("expected libSystem to be the first linked dylib of %s, but it's ordinal is: %u",
+                header->installName(), lcLibSystemIndex-lcDylibStart);
+        return;
+    }
+
+    // This removes all load commands after LC_LOAD_DYLIB of libSystem
+    if ( mach_o::Error err = header->removeLoadCommands(lcLibSystemIndex+1, lcDylibEnd) )
+        diag.error(err);
+}
+
+void CacheDylib::addLinkedDylib(Diagnostics& diag, const CacheDylib& dylib)
+{
+    const char* dylibInstallName = nullptr;
+    Version32   compatVersion;
+    Version32   currentVersion;
+    dylib.inputHdr->getDylibInstallName(&dylibInstallName, &compatVersion, &currentVersion);
+
+    // find the range of all LC_LOAD* commands, new dylib will be added as last
+    uint32_t lcLoadStart = 0;
+    uint32_t lcLoadEnd = 0;
+
+    mach_o::HeaderWriter* header = (mach_o::HeaderWriter*)this->cacheHdr;
+    header->findLoadCommandRange(lcLoadStart, lcLoadEnd, ^bool(const load_command *lc) {
+        return mach_o::Header::isDylibLoadCommand(lc) != nullptr;
+    });
+
+    if ( lcLoadEnd == 0 ) {
+        // there should be at least one already
+        diag.error("%s has no linked dylibs", header->installName());
+        return;
+    }
+
+    // determine command size
+    mach_o::LinkedDylibAttributes attr = mach_o::LinkedDylibAttributes::regular;
+    uint32_t traditionalCmd = 0;
+    uint32_t cmdSize = header->sizeForLinkedDylibCommand(dylibInstallName, attr, traditionalCmd);
+
+    // insert command
+    load_command* lc = header->insertLoadCommand(lcLoadEnd, cmdSize);
+    if ( lc == nullptr ) {
+        diag.error("not enough space in %s to add %s load command", header->installName(), dylibInstallName);
+        return;
+    }
+    header->setLinkedDylib(lc, dylibInstallName, attr, mach_o::Version32(compatVersion), mach_o::Version32(currentVersion));
+}
+
+objc_visitor::Visitor CacheDylib::makeCacheObjCVisitor(const BuilderConfig& config,
+                                                       const Chunk* selectorStringsChunk,
+                                                       const ObjCCanonicalProtocolsChunk* canonicalProtocolsChunk,
+                                                       const ObjCPreAttachedCategoriesChunk* categoriesChunk) const
+{
+    // Get the segment ranges.  We need this as the dylib's segments are in different buffers, not in VM layout
+    std::vector<metadata_visitor::Segment> cacheSegments;
+    cacheSegments.reserve(this->segments.size());
+    for ( uint32_t segIndex = 0; segIndex != this->segments.size(); ++segIndex ) {
+        const DylibSegmentChunk& segmentInfo = this->segments[segIndex];
+
+        metadata_visitor::Segment segment;
+        segment.startVMAddr = VMAddress(segmentInfo.cacheVMAddress.rawValue());
+        segment.endVMAddr   = VMAddress((segmentInfo.cacheVMAddress + segmentInfo.cacheVMSize).rawValue());
+        segment.bufferStart = segmentInfo.subCacheBuffer;
+
+        // Cache dylibs never have a chained format. They always use the Fixup struct
+        segment.onDiskDylibChainedPointerFormat = { };
+
+        // We need to know what segment we are in, so that we can find the ASLRTracker for the segment
+        segment.segIndex = segIndex;
+
+        cacheSegments.push_back(std::move(segment));
+    }
+
+    // Add the selector strings chunk too.  That way we can resolve references which land on it
+    if ( selectorStringsChunk != nullptr ) {
+        metadata_visitor::Segment segment;
+        segment.startVMAddr = VMAddress(selectorStringsChunk->cacheVMAddress.rawValue());
+        segment.endVMAddr   = VMAddress((selectorStringsChunk->cacheVMAddress + selectorStringsChunk->cacheVMSize).rawValue());
+        segment.bufferStart = selectorStringsChunk->subCacheBuffer;
+
+        // Note we don't have a chainedPointerFormat as the selectors don't slide
+        segment.onDiskDylibChainedPointerFormat = { };
+
+        cacheSegments.push_back(std::move(segment));
+    }
+
+    // Add the canonical protocols chunk too.  That way we can resolve references which land on it
+    if ( canonicalProtocolsChunk != nullptr ) {
+        metadata_visitor::Segment segment;
+        segment.startVMAddr = VMAddress(canonicalProtocolsChunk->cacheVMAddress.rawValue());
+        segment.endVMAddr   = VMAddress((canonicalProtocolsChunk->cacheVMAddress + canonicalProtocolsChunk->cacheVMSize).rawValue());
+        segment.bufferStart = canonicalProtocolsChunk->subCacheBuffer;
+
+        // Cache segments never have a chained format. They always use the Fixup struct
+        segment.onDiskDylibChainedPointerFormat = { };
+
+        cacheSegments.push_back(std::move(segment));
+    }
+
+    // Add the categories data chunk too.  That way we can resolve references which land on it
+    if ( categoriesChunk != nullptr ) {
+        metadata_visitor::Segment segment;
+        segment.startVMAddr = VMAddress(categoriesChunk->cacheVMAddress.rawValue());
+        segment.endVMAddr   = VMAddress((categoriesChunk->cacheVMAddress + categoriesChunk->cacheVMSize).rawValue());
+        segment.bufferStart = categoriesChunk->subCacheBuffer;
+
+        // Cache segments never have a chained format. They always use the Fixup struct
+        segment.onDiskDylibChainedPointerFormat = { };
+
+        cacheSegments.push_back(std::move(segment));
+    }
+
+    VMAddress selectorStringsAddress;
+    if ( selectorStringsChunk != nullptr )
+        selectorStringsAddress = VMAddress(selectorStringsChunk->cacheVMAddress.rawValue());
+
+    std::vector<uint64_t> unusedBindTargets;
+    objc_visitor::Visitor objcVisitor(config.layout.cacheBaseAddress, this->cacheMF,
+                                      std::move(cacheSegments), selectorStringsAddress, std::move(unusedBindTargets));
+    return objcVisitor;
+}
+
+metadata_visitor::SwiftVisitor CacheDylib::makeCacheSwiftVisitor(const BuilderConfig& config,
+                                                                 std::span<metadata_visitor::Segment> extraRegions) const
+{
+    // Get the segment ranges.  We need this as the dylib's segments are in different buffers, not in VM layout
+    std::vector<metadata_visitor::Segment> cacheSegments;
+    cacheSegments.reserve(this->segments.size());
+    for ( uint32_t segIndex = 0; segIndex != this->segments.size(); ++segIndex ) {
+        const DylibSegmentChunk& segmentInfo = this->segments[segIndex];
+
+        metadata_visitor::Segment segment;
+        segment.startVMAddr = VMAddress(segmentInfo.cacheVMAddress.rawValue());
+        segment.endVMAddr   = VMAddress((segmentInfo.cacheVMAddress + segmentInfo.cacheVMSize).rawValue());
+        segment.bufferStart = segmentInfo.subCacheBuffer;
+
+        // Cache dylibs never have a chained format. They always use the Fixup struct
+        segment.onDiskDylibChainedPointerFormat = { };
+
+        // We need to know what segment we are in, so that we can find the ASLRTracker for the segment
+        segment.segIndex = segIndex;
+
+        cacheSegments.push_back(std::move(segment));
+    }
+
+    cacheSegments.insert(cacheSegments.end(), extraRegions.begin(), extraRegions.end());
+
+    std::vector<uint64_t> unusedBindTargets;
+    metadata_visitor::SwiftVisitor swiftVisitor(config.layout.cacheBaseAddress, this->cacheMF,
+                                                std::move(cacheSegments),
+                                                VMAddress(0ULL),
+                                                std::move(unusedBindTargets));
+    return swiftVisitor;
+}
+
+metadata_visitor::Visitor CacheDylib::makeCacheVisitor(const BuilderConfig& config) const
+{
+    // Get the segment ranges.  We need this as the dylib's segments are in different buffers, not in VM layout
+    __block std::vector<metadata_visitor::Segment> cacheSegments;
+    cacheSegments.reserve(this->segments.size());
+    for ( uint32_t segIndex = 0; segIndex != this->segments.size(); ++segIndex ) {
+        const DylibSegmentChunk& segmentInfo = this->segments[segIndex];
+
+        metadata_visitor::Segment segment;
+        segment.startVMAddr = VMAddress(segmentInfo.cacheVMAddress.rawValue());
+        segment.endVMAddr   = VMAddress((segmentInfo.cacheVMAddress + segmentInfo.cacheVMSize).rawValue());
+        segment.bufferStart = segmentInfo.subCacheBuffer;
+
+        // Cache dylibs never have a chained format. They always use the Fixup struct
+        segment.onDiskDylibChainedPointerFormat = { };
+
+        // We need to know what segment we are in, so that we can find the ASLRTracker for the segment
+        segment.segIndex = segIndex;
+
+        cacheSegments.push_back(std::move(segment));
+    }
+
+    // Add the GOTs too, if we have them
+    optimizedSections.forEachCacheGOTChunk(^(const cache_builder::Chunk* chunk) {
+        metadata_visitor::Segment segment;
+        segment.startVMAddr = VMAddress(chunk->cacheVMAddress.rawValue());
+        segment.endVMAddr   = VMAddress((chunk->cacheVMAddress + chunk->cacheVMSize).rawValue());
+        segment.bufferStart = chunk->subCacheBuffer;
+
+        // Cache segments never have a chained format. They always use the Fixup struct
+        segment.onDiskDylibChainedPointerFormat = { };
+
+        cacheSegments.push_back(std::move(segment));
+    });
+
+    std::vector<uint64_t> unusedBindTargets;
+    metadata_visitor::Visitor visitor(config.layout.cacheBaseAddress, this->cacheMF,
+                                      std::move(cacheSegments), { },
+                                      std::move(unusedBindTargets));
+    return visitor;
+}
+
+void CacheDylib::forEachCacheSection(void (^callback)(std::string_view segmentName,
+                                                      std::string_view sectionName,
+                                                      uint8_t* sectionBuffer,
+                                                      CacheVMAddress sectionVMAddr,
+                                                      CacheVMSize sectionVMSize,
+                                                      bool& stop))
+{
+    this->inputHdr->forEachSection(^(const Header::SegmentInfo &segInfo, const Header::SectionInfo &sectInfo,
+                                    bool &stop) {
+        const DylibSegmentChunk& segment = this->segments[sectInfo.segIndex];
+
+        VMAddress sectionVMAddr(sectInfo.address);
+        VMAddress segmentVMAddr(segInfo.vmaddr);
+        VMOffset sectionOffsetInSegment = sectionVMAddr - segmentVMAddr;
+        uint8_t* sectionBuffer = segment.subCacheBuffer + sectionOffsetInSegment.rawValue();
+        CacheVMAddress cacheVMAddr = segment.cacheVMAddress + sectionOffsetInSegment;
+
+        callback(sectInfo.segmentName, sectInfo.sectionName,
+                 sectionBuffer, cacheVMAddr, CacheVMSize(sectInfo.size), stop);
+    });
+}