Loading...
testing/unit-tests/MockO.cpp /dev/null dyld-957
--- /dev/null
+++ dyld/dyld-957/testing/unit-tests/MockO.cpp
@@ -0,0 +1,1157 @@
+/* -*- mode: C++; c-basic-offset: 4; tab-width: 4 -*-
+ *
+ * Copyright (c) 2020 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 <stdio.h>
+#include <stdlib.h>
+#include <mach-o/loader.h>
+#include <mach-o/dyld.h>
+#include <mach-o/nlist.h>
+
+#include "Trie.hpp"
+#include "MockO.h"
+
+using dyld3::MachOFile;
+using dyld3::Platform;
+using dyld3::GradedArchs;
+
+
+//
+// MARK: --- methods for configuring a mach-o image ---
+//
+
+MockO::MockO(uint32_t filetype, const char* archName, Platform platform, const char* minOsStr, const char* sdkStr)
+{
+    _header.cputype     = MachOFile::cpuTypeFromArchName(archName);
+    _header.cpusubtype  = MachOFile::cpuSubtypeFromArchName(archName);
+    _header.magic       = (_header.cputype & CPU_ARCH_ABI64) ? MH_MAGIC_64 : MH_MAGIC;
+    _header.filetype    = filetype;
+    _header.ncmds       = 0;
+    _header.sizeofcmds  = 0;
+    _header.flags       = 0;
+
+    if ( filetype == MH_EXECUTE )
+        addSegment("__PAGEZERO", 0);
+
+    addSegment("__TEXT",     VM_PROT_READ|VM_PROT_EXECUTE);
+    addSegment("__DATA",     VM_PROT_READ|VM_PROT_WRITE);
+    addSegment("__LINKEDIT", VM_PROT_READ);
+    customizeAddSection("__TEXT", "__text", S_REGULAR);
+    customizeAddSection("__DATA", "__data", S_REGULAR);
+
+    _platform     = platform;
+    _minOSVersion = 0;
+    _sdkVersion   = 0;
+    if ( minOsStr != nullptr )
+        _minOSVersion = parseVersionNumber32(minOsStr);
+    if ( sdkStr != nullptr )
+        _sdkVersion = parseVersionNumber32(sdkStr);
+    if (_sdkVersion == 0 ) {
+        if ( _minOSVersion == 0 ) {
+            // if minOS not specified, use defaults for macOS and iOS
+            if ( _platform == Platform::macOS )
+                _minOSVersion = 0x000C0000;  // default is macOS 12.0
+            else if ( _platform == Platform::iOS )
+                _minOSVersion = 0x000F0000;  // default is iOS 15.0
+            else
+                assert(0 && "no default SDK/minOS for platform");
+        }
+        // if sdk version not specified, use same as minOS version
+        _sdkVersion = _minOSVersion;
+    }
+    if ( _minOSVersion == 0 ) {
+        _minOSVersion = _sdkVersion;
+    }
+
+    // give dylibs a default install name
+    if ( filetype == MH_DYLIB )
+        _installName.push_back({"/usr/lib/libfoo.dylib", 1, 1, LC_ID_DYLIB});
+
+    // add platform/minOS info
+    if ( targetIsAtLeast(epochFall2018) )
+        addBuildVersion(_platform, _minOSVersion, _sdkVersion);
+    else
+        addVersionMin(_platform, _minOSVersion, _sdkVersion);
+
+    // add a uuid to the binary
+    addUniqueUUID();
+
+    if ( filetype == MH_EXECUTE )
+       _baseAddress = (_header.cputype & CPU_ARCH_ABI64) ? 0x100000000ULL : 0x1000;
+
+    if ( (filetype == MH_EXECUTE) && (_platform != Platform::driverKit) )
+        _mainOffset = 0x1000;
+
+    // make main executable dynamic by default
+    if ( filetype == MH_EXECUTE )
+        _dynamicLinker.push_back("/usr/lib/dyld");
+
+    // all binaries link with libSystem by default
+    if ( _dependents.empty() )
+        _dependents.push_back({"/usr/lib/libSystem.B.dylib", 1, 1, LC_LOAD_DYLIB});
+}
+
+//
+// Parses number of form X[.Y[.Z]] into a uint32_t where the nibbles are xxxx.yy.zz
+//
+uint32_t MockO::parseVersionNumber32(const char* versionString)
+{
+	unsigned long x = 0;
+	unsigned long y = 0;
+	unsigned long z = 0;
+	char* end;
+	x = strtoul(versionString, &end, 10);
+	if ( *end == '.' ) {
+		y = strtoul(&end[1], &end, 10);
+		if ( *end == '.' ) {
+			z = strtoul(&end[1], &end, 10);
+		}
+	}
+	assert((*end == '\0') && (x <= 0xffff) && (y <= 0xff) && (z <= 0xff) && "malformed 32-bit x.y.z version number");
+	return (uint32_t)((x << 16) | ( y << 8 ) | z);
+}
+
+
+void MockO::customizeMakeZippered()
+{
+    addBuildVersion(Platform::iOSMac, 0x000E0000, 0x000E0000);
+}
+
+void MockO::customizeInstallName(const char* path, uint32_t compatVers, uint32_t curVers)
+{
+    assert(_header.filetype == MH_DYLIB);
+    assert(_installName.size() == 1);
+    _installName[0] = { path, compatVers, curVers, LC_ID_DYLIB };
+}
+
+void MockO::customizeAddDependentDylib(const char* path, bool isWeak, bool isUpward, bool isReexport, uint32_t compatVers, uint32_t curVers)
+{
+    PathWithVersions pv;
+    pv.path       = path;
+    pv.compatVers = compatVers;
+    pv.curVers    = curVers;
+    if ( isWeak )
+        pv.cmd    = LC_LOAD_WEAK_DYLIB;
+    else if ( isReexport )
+        pv.cmd    = LC_REEXPORT_DYLIB;
+    else if ( isUpward )
+        pv.cmd    = LC_LOAD_UPWARD_DYLIB;
+    else
+        pv.cmd    = LC_LOAD_DYLIB;
+
+    _dependents.push_back(pv);
+}
+
+void MockO::customizeAddDyldEnvVar(const char* envVar)
+{
+    _dyldEnvVars.push_back(envVar);
+}
+
+void MockO::customizeAddRPath(const char* path)
+{
+    _rpaths.push_back(path);
+}
+
+MockO::SectInfo* MockO::findSection(const char* segName, const char* sectionName)
+{
+    for (SegInfo& segInfo : _segments) {
+        if ( strcmp(segInfo.name, segName) == 0 ) {
+            for (SectInfo& sectInfo : segInfo.sections) {
+                if ( strcmp(sectInfo.name, sectionName) == 0 ) {
+                    return &sectInfo;
+                }
+            }
+        }
+    }
+    return nullptr;
+}
+
+void MockO::customizeAddSegment(const char* segName)
+{
+    addSegment(segName, VM_PROT_READ);
+}
+
+void* MockO::customizeAddSection(const char* segName, const char* sectionName, uint32_t sectFlags)
+{
+   for (SegInfo& segInfo : _segments) {
+        if ( strcmp(segInfo.name, segName) == 0 ) {
+            segInfo.sections.emplace_back(sectionName, sectFlags);
+        }
+    }
+    return nullptr;
+}
+
+void MockO::customizeAddZeroFillSection(const char* segName, const char* sectionName)
+{
+   for (SegInfo& segInfo : _segments) {
+        if ( strcmp(segInfo.name, segName) == 0 ) {
+            segInfo.sections.emplace_back(sectionName, S_ZEROFILL);
+        }
+    }
+}
+
+void MockO::customizeAddFunction(const char* functionName, bool global)
+{
+    SectInfo* text = this->findSection("__TEXT", "__text");
+    assert(text != nullptr);
+    const std::vector<uint8_t> bytes = {0x90, 0x90, 0x90, 0x90};
+    text->content.emplace_back(functionName, global, bytes);
+}
+
+void MockO::customizeAddData(const char* dataName, bool global)
+{
+    SectInfo* data = this->findSection("__DATA", "__data");
+    assert(data != nullptr);
+    const std::vector<uint8_t> bytes = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+    data->content.emplace_back(dataName, global, bytes);
+}
+
+void MockO::customizeAddZeroFillData(const char* dataName, uint64_t size, bool global)
+{
+    if ( global ) {
+        SectInfo* data = this->findSection("__DATA", "__common");
+        if ( data == nullptr ) {
+            customizeAddZeroFillSection("__DATA", "__common");
+            data = this->findSection("__DATA", "__common");
+        }
+        data->content.emplace_back(dataName, true, size);
+    }
+    else {
+        SectInfo* bss = this->findSection("__DATA", "__bss");
+        if ( bss == nullptr ) {
+            customizeAddZeroFillSection("__DATA", "__bss");
+            bss = this->findSection("__DATA", "__bss");
+        }
+        bss->content.emplace_back(dataName, false, size);
+    }
+}
+
+void MockO::customizeAddInitializer()
+{
+    const std::vector<uint8_t> bytes4 = { 0, 0, 0, 0 };
+    const std::vector<uint8_t> bytes8 = { 0, 0, 0, 0, 0, 0, 0, 0 };
+    customizeAddFunction("myinit", false);
+    if ( targetIsAtLeast(epochFall2021) ) {
+        customizeAddSection("__TEXT", "__init_offsets", S_INIT_FUNC_OFFSETS);
+        SectInfo* sect = this->findSection("__TEXT", "__init_offsets");
+        sect->content.emplace_back("", false, bytes4);
+    }
+    else {
+        customizeAddSection("__DATA", "__mod_init_func", S_MOD_INIT_FUNC_POINTERS);
+        SectInfo* sect = this->findSection("__DATA", "__mod_init_func");
+        if ( is64() )
+            sect->content.emplace_back("", false, bytes8);
+        else
+            sect->content.emplace_back("", false, bytes4);
+   }
+    // FIXME
+}
+
+
+
+void MockO::addSegment(const char* segName, uint32_t perms)
+{
+    _segments.emplace_back(segName, perms);
+}
+
+void MockO::addSection(const char* segName, const char* sectionName, uint32_t sectFlags)
+{
+    for (SegInfo& info : _segments) {
+        if ( strcmp(info.name, segName) == 0 ) {
+            info.sections.emplace_back(sectionName, sectFlags);
+            return;
+        }
+    }
+    assert(0 && "segment not found");
+}
+
+
+
+#if 0
+uint64_t MockO::getSegmentSize(const char* segName)
+{
+    for (SegInfo& info : _segments) {
+        if ( strcmp(info.name, segName) == 0 ) {
+            return info.size;
+        }
+    }
+    assert(0 && "segment not found");
+}
+
+void MockO::setSegmentPerm(const char* segName, uint32_t protections)
+{
+    for (SegInfo& info : _segments) {
+        if ( strcmp(info.name, segName) == 0 ) {
+            info.perms = protections;
+            return;
+        }
+    }
+    assert(0 && "segment not found");
+}
+
+uint64_t MockO::getSegmentOffset(const char* segName)
+{
+    for (SegInfo& info : _segments) {
+        if ( strcmp(info.name, segName) == 0 ) {
+            return info.fileOffset;
+        }
+    }
+    assert(0 && "segment not found");
+}
+
+void MockO::setSegmentOffset(const char* segName, uint64_t offset)
+{
+    for (SegInfo& info : _segments) {
+        if ( strcmp(info.name, segName) == 0 ) {
+            info.fileOffset = offset;
+            return;
+        }
+    }
+    assert(0 && "segment not found");
+}
+
+
+
+void MockO::setSectionOffset(const char* segName, const char* sectionName, uint64_t offset)
+{
+    for (SegInfo& segInfo : _segments) {
+        if ( strcmp(segInfo.name, segName) == 0 ) {
+            for (SectInfo& sectInfo : segInfo.sections) {
+                if ( strcmp(sectInfo.name, sectionName) == 0 )
+                    sectInfo.fileOffset = offset;
+            }
+            return;
+        }
+    }
+    assert(0 && "section not found");
+}
+#endif
+
+bool MockO::is64() const
+{
+    return (_header.magic == MH_MAGIC_64);
+}
+
+
+void MockO::addUniqueUUID()
+{
+    uuid_t aUUID;
+    uuid_generate_random(aUUID);
+    uuid_command uc;
+    uc.cmd      = LC_UUID;
+    uc.cmdsize  = sizeof(uuid_command);
+    memcpy(uc.uuid, aUUID, sizeof(uuid_t));
+    _uuid.push_back(uc);
+}
+
+void MockO::addBuildVersion(Platform platform, uint32_t minOS, uint32_t sdk)
+{
+    build_version_command bv;
+    bv.cmd      = LC_BUILD_VERSION;
+    bv.cmdsize  = sizeof(build_version_command);
+    bv.platform = (uint32_t)platform;
+    bv.minos    = minOS;
+    bv.sdk      = sdk;
+    bv.ntools   = 0;
+    _buildVersions.push_back(bv);
+}
+
+void MockO::addVersionMin(Platform platform, uint32_t minOS, uint32_t sdk)
+{
+    version_min_command vc;
+    vc.cmdsize = sizeof(version_min_command);
+    vc.version = minOS;
+    vc.sdk     = sdk;
+    switch (platform) {
+        case Platform::macOS:
+            vc.cmd = LC_VERSION_MIN_MACOSX;
+            break;
+        case Platform::iOS:
+            vc.cmd = LC_VERSION_MIN_IPHONEOS;
+            break;
+        case Platform::watchOS:
+            vc.cmd = LC_VERSION_MIN_WATCHOS;
+            break;
+        case Platform::tvOS:
+            vc.cmd = LC_VERSION_MIN_TVOS;
+            break;
+        default:
+            assert(0 && "invalid platform for min version load command");
+    }
+    _versionMin.push_back(vc);
+}
+
+#define VERS(major, minor) ((((major)&0xffff) << 16) | (((minor)&0xff) << 8))
+
+// Fall 2018 introduced LC_BUILD_VERSION
+const MockO::PlatformEpoch MockO::epochFall2018[] = {
+    { Platform::macOS,   VERS(10,14) },
+    { Platform::iOS,     VERS(12,0)  },
+    { Platform::watchOS, VERS( 5,0)  },
+    { Platform::tvOS,    VERS(12,0)  },
+    { Platform::unknown, VERS( 0,0)  }  // zero terminate
+};
+
+// Fall 2019 introduced __DATA_CONST and zippering
+const MockO::PlatformEpoch MockO::epochFall2019[] = {
+    { Platform::macOS,   VERS(10,15) },
+    { Platform::iOS,     VERS(13,0)  },
+    { Platform::watchOS, VERS( 6,0)  },
+    { Platform::tvOS,    VERS(13,0)  },
+    { Platform::unknown, VERS( 0,0)  }  // zero terminate
+};
+
+// Fall 2020 introduced relative method lists
+const MockO::PlatformEpoch MockO::epochFall2020[] = {
+    { Platform::macOS,     VERS(11,0) },
+    { Platform::iOS,       VERS(14,0) },
+    { Platform::watchOS,   VERS( 7,0) },
+    { Platform::tvOS,      VERS(14,0) },
+    { Platform::driverKit, VERS(20,0) },
+    { Platform::unknown,   VERS( 0,0) }  // zero terminate
+};
+
+// Fall 2021 introduced chained fixups and initializer offsets
+const MockO::PlatformEpoch MockO::epochFall2021[] = {
+    { Platform::macOS,     VERS(12,0) },
+    { Platform::iOS,       VERS(15,0) },
+    { Platform::watchOS,   VERS( 8,0) },
+    { Platform::tvOS,      VERS(15,0) },
+    { Platform::driverKit, VERS(21,0) },
+    { Platform::unknown,   VERS( 0,0) }  // zero terminate
+};
+
+
+bool MockO::targetIsAtLeast(const PlatformEpoch epoch[])
+{
+    for (const PlatformEpoch* e=epoch; e->osVersion != 0; ++e) {
+        if ( _platform == e->platform ) {
+            return ( _minOSVersion >= e->osVersion );
+        }
+    }
+    return true;
+}
+
+
+
+
+//
+// MARK: --- methods for malforming mach-o ---
+//
+
+void MockO::wrenchRemoveDyld()
+{
+    _dynamicLinker.clear();
+}
+
+void MockO::wrenchRemoveInstallName()
+{
+    _installName.clear();
+}
+
+void MockO::wrenchAddExtraInstallName(const char* path)
+{
+    _installName.push_back({ path, 1, 1, LC_ID_DYLIB });
+}
+
+void MockO::wrenchSetNoDependentDylibs()
+{
+    _dependents.clear();
+}
+
+void MockO::wrenchRemoveUUID()
+{
+    _uuid.clear();
+}
+
+void MockO::wrenchAddUUID()
+{
+    addUniqueUUID();
+}
+
+void MockO::wrenchRemoveVersionInfo()
+{
+    _versionMin.clear();
+    _buildVersions.clear();
+}
+
+void MockO::wrenchAddMain()
+{
+    _mainOffset = 0x1000;
+}
+
+load_command* MockO::wrenchFindLoadCommand(uint32_t cmd)
+{
+    const MachOAnalyzer*  ma     = header();
+    __block load_command* result = nullptr;
+    Diagnostics           diag;
+    ma->forEachLoadCommand(diag, ^(const load_command* lc, bool& stop) {
+        if ( lc->cmd == cmd ) {
+            result = (load_command*)lc;
+            stop = true;
+        }
+    });
+    return result;
+}
+
+
+//
+// MARK: --- methods for building actual mach-o image ---
+//
+
+uint32_t MockO::alignLC(uint32_t value)
+{
+    // mach-o requires all load command sizes to be a multiple the pointer size
+    if ( is64() )
+        return ((value + 7) & (-8));
+    else
+        return ((value + 3) & (-4));
+}
+
+
+MockO::~MockO()
+{
+}
+
+const MachOAnalyzer* MockO::header()
+{
+    if ( _mf == nullptr )
+        buildMachO();
+
+    return _mf;
+}
+
+const size_t MockO::size() const
+{
+    return _size;
+}
+
+void MockO::save(char savedPath[PATH_MAX])
+{
+    const MachOAnalyzer* ma = this->header();
+
+    ::strcpy(savedPath, "/tmp/mocko-XXXXXX");
+    int fd = ::mkstemp(savedPath);
+    ::pwrite(fd, ma, _size, 0);
+    ::close(fd);
+}
+
+void MockO::buildMachO()
+{
+    // assign addresses/offsets to segments, sections, symbols
+    layout();
+
+    // allocate space for mach-o image
+    assert(::vm_allocate(mach_task_self(), (vm_address_t*)&_mf, (vm_size_t)_size, VM_FLAGS_ANYWHERE) == KERN_SUCCESS);
+
+
+    writeHeaderAndLoadCommands();
+
+    writeLinkEdit();
+}
+
+
+void MockO::writeHeaderAndLoadCommands()
+{
+    // copy header
+    *((mach_header*)_mf) = _header;
+
+    // add segment load commands
+    for (const SegInfo& info : _segments)
+        appendSegmentLoadCommand(info);
+
+    // add fixup info
+    appendFixupLoadCommand();
+
+    // add nlist symbol table
+    appendSymbolTableLoadCommand();
+
+    // if set, add install name
+    for ( const PathWithVersions& install : _installName )
+        appendPathVersionLoadCommand(install);
+
+    // add dyld load command
+    for ( const std::string& pth : _dynamicLinker )
+        appendStringLoadCommand(LC_LOAD_DYLINKER,pth);
+
+    // add uuid
+    for ( const uuid_command& id : _uuid )
+        appendLoadCommand((load_command*)&id);
+
+    // if set, add version-min load command
+    for ( const version_min_command& vc : _versionMin )
+        appendLoadCommand((load_command*)&vc);
+
+    // add build-version load command(s)
+    for (const build_version_command& bv : _buildVersions)
+        appendLoadCommand((load_command*)&bv);
+
+    // add entry
+    if ( (_header.filetype == MH_EXECUTE) && (_mainOffset != 0) )
+        appendEntryLoadCommand();
+
+    // add dyld info
+    if ( _dyldInfo.has_value() )
+        appendLoadCommand((load_command*)&_dyldInfo.value());
+
+    if ( _routinesInitOffset )
+        appendRoutinesLoadCommand(_routinesInitOffset.value());
+
+
+    // add dependent dylibs
+    for (const PathWithVersions& dep : _dependents)
+        appendPathVersionLoadCommand(dep);
+
+    // add any dyld env var load commands
+    for (const std::string& str : _dyldEnvVars)
+        appendStringLoadCommand(LC_DYLD_ENVIRONMENT, str);
+
+    // add any rpath load commands
+    for (const std::string& str : _rpaths)
+        appendStringLoadCommand(LC_RPATH, str);
+
+}
+
+static int segmentOrder(const char* name)
+{
+    if ( strcmp(name, "__PAGEZERO") == 0 )
+        return 1;
+    if ( strcmp(name, "__TEXT") == 0 )
+        return 2;
+    if ( strcmp(name, "__DATA_CONST") == 0 )
+        return 3;
+    if ( strcmp(name, "__DATA") == 0 )
+        return 4;
+    if ( strcmp(name, "__LINKEDIT") == 0 )
+        return 999;
+    return 10;
+}
+
+void MockO::pageAlign(uint64_t& value)
+{
+    // FIXME: is there any case where we want 4KB pages?
+    value = ((value + 0x3FFF) & (-0x4000));
+}
+
+void MockO::pageAlign(uint32_t& value)
+{
+    // FIXME: is there any case where we want 4KB pages?
+    value = ((value + 0x3FFF) & (-0x4000));
+}
+
+void MockO::layout()
+{
+    // sort segments
+    std::sort(_segments.begin(), _segments.end(), [](const SegInfo& s1, const SegInfo& s2) {
+        return segmentOrder(s1.name) < segmentOrder(s2.name);
+    });
+
+    // assign address to segments
+    SegInfo* leSegInfo     = nullptr;
+    uint32_t curFileOffset = 0;
+    uint64_t curVMAddr     = 0;
+    for (SegInfo& segInfo : _segments) {
+        pageAlign(curFileOffset);
+        pageAlign(curVMAddr);
+        segInfo.fileOffset = curFileOffset;
+        segInfo.vmAddr     = curVMAddr;
+        if ( strcmp(segInfo.name, "__TEXT") == 0 ) {
+            curVMAddr = _baseAddress;
+            segInfo.fileOffset = 0;
+            segInfo.vmAddr     = curVMAddr;
+            // reverse layout TEXT so padding is after load commands and before __text
+            uint64_t totalSectionsSize = 0;
+            for (SectInfo& sectInfo : segInfo.sections) {
+                sectInfo.size = 0;
+                for (const Content& cont : sectInfo.content) {
+                    sectInfo.size += cont.bytes.size();
+                }
+                totalSectionsSize += sectInfo.size;
+            }
+            uint32_t textPageSize = (uint32_t)totalSectionsSize+2048; // FIXME: guestimate of load commands size
+            pageAlign(textPageSize);
+            segInfo.fileSize   = textPageSize;
+            segInfo.vmSize     = textPageSize;
+            uint64_t addr        = segInfo.vmAddr + segInfo.vmSize;
+            uint64_t off         = textPageSize;
+            for (int i=(int)segInfo.sections.size(); i > 0; --i) {
+                SectInfo& sectInfo = segInfo.sections[i-1];
+                addr -= sectInfo.size;
+                off  -= sectInfo.size;
+                uint64_t symAddr = addr;
+                for (const Content& cont : sectInfo.content) {
+                    if ( cont.symbolName[0] != '\0' ) {
+                        if ( cont.global )
+                            _exportedSymbols.push_back({cont.symbolName, symAddr});
+                        else
+                            _localSymbols.push_back({cont.symbolName, symAddr});
+                    }
+                    symAddr += cont.bytes.size();
+                }
+                sectInfo.vmAddr     = addr;
+                sectInfo.fileOffset = off;
+            }
+            curFileOffset = textPageSize;
+            curVMAddr     = segInfo.vmAddr + segInfo.vmSize;
+        }
+        else if ( strcmp(segInfo.name, "__PAGEZERO") == 0 ) {
+            segInfo.fileOffset = 0;
+            segInfo.fileSize   = 0;
+            segInfo.vmAddr     = 0;
+            segInfo.vmSize     = _baseAddress;
+        }
+        else if ( strcmp(segInfo.name, "__LINKEDIT") == 0 ) {
+            // LINKEDIT size set later
+            leSegInfo = &segInfo;
+        }
+        else {
+            // sort sections so zero-fill at end
+            std::sort(segInfo.sections.begin(), segInfo.sections.end(), [](const SectInfo& l, const SectInfo& r) {
+                bool lzf = (l.flags == S_ZEROFILL);
+                bool rzf = (r.flags == S_ZEROFILL);
+                if ( lzf != rzf )
+                    return rzf;
+                return (strcmp(l.name, r.name) == 0);
+            });
+            for (SectInfo& sectInfo : segInfo.sections) {
+                sectInfo.fileOffset = curFileOffset;
+                sectInfo.vmAddr     = curVMAddr;
+                sectInfo.size       = 0;
+                uint64_t symVmOffset = curVMAddr - _baseAddress;
+                if ( sectInfo.flags == S_ZEROFILL ) {
+                    sectInfo.fileOffset = 0; // all zero-fill sections have no file offset
+                    for (const Content& cont : sectInfo.content) {
+                        assert(cont.zeroFillSize != 0);
+                        assert(cont.bytes.empty());
+                        if ( cont.symbolName[0] != '\0' ) {
+                            if ( cont.global )
+                                _exportedSymbols.push_back({cont.symbolName, symVmOffset});
+                            else
+                                _localSymbols.push_back({cont.symbolName, symVmOffset});
+                        }
+                        sectInfo.size  += cont.zeroFillSize;
+                        segInfo.vmSize += cont.zeroFillSize;
+                        symVmOffset    += cont.zeroFillSize;
+                        curVMAddr      += cont.zeroFillSize;
+                    }
+                }
+                else {
+                    for (const Content& cont : sectInfo.content) {
+                        assert(cont.zeroFillSize == 0);
+                        assert(!cont.bytes.empty());
+                        // FIXME: support alignment
+                        if ( cont.symbolName[0] != '\0' ) {
+                            if ( cont.global )
+                                _exportedSymbols.push_back({cont.symbolName, symVmOffset});
+                            else
+                                _localSymbols.push_back({cont.symbolName, symVmOffset});
+                        }
+                        sectInfo.size    += cont.bytes.size();
+                        segInfo.fileSize += sectInfo.size;
+                        segInfo.vmSize   += sectInfo.size;
+                        curVMAddr        += sectInfo.size;
+                        curFileOffset    += sectInfo.size;
+                   }
+                }
+            }
+            pageAlign(segInfo.fileSize);
+            pageAlign(segInfo.vmSize);
+        }
+    }
+
+    // layout linkedit
+
+    // build exports trie
+    std::vector<ExportInfoTrie::Entry> trieEntrys;
+    for (const Symbol& exp : _exportedSymbols) {
+        ExportInfoTrie::Entry entry;
+        entry.name            = exp.name;
+        entry.info.address    = exp.vmOffset;
+        entry.info.flags      = 0; // FIXME
+        entry.info.other      = 0;
+        entry.info.importName = "";
+        trieEntrys.push_back(entry);
+    }
+    ExportInfoTrie programTrie(trieEntrys);
+    programTrie.emit(_leContent.exportsTrieBytes);
+    while ( (_leContent.exportsTrieBytes.size() % 8) != 0 )
+        _leContent.exportsTrieBytes.push_back(0);
+    _leLayout.exportsTrieOffset = curFileOffset;
+    _leLayout.exportsTrieSize   = (uint32_t)_leContent.exportsTrieBytes.size();
+    curFileOffset += _leLayout.exportsTrieSize;
+
+    // nlist symbol table
+    _leLayout.symbolTableOffset   = curFileOffset;
+    _leLayout.symbolTableCount    = (uint32_t)(_exportedSymbols.size() + _localSymbols.size());
+    _leContent.symbolTableStringPool.push_back('\0');
+    for (const Symbol& exp : _localSymbols) {
+        if ( is64() ) {
+            nlist_64 sym;
+            sym.n_un.n_strx = (uint32_t)_leContent.symbolTableStringPool.size();
+            sym.n_type      = N_SECT;
+            sym.n_sect      = 1; // FIXME
+            sym.n_desc      = 0;
+            sym.n_value     = exp.vmOffset;
+            _leContent.symbolTable64.push_back(sym);
+        }
+        else {
+            struct nlist sym;
+            sym.n_un.n_strx = (uint32_t)_leContent.symbolTableStringPool.size();
+            sym.n_type      = N_SECT;
+            sym.n_sect      = 1; // FIXME
+            sym.n_desc      = 0;
+            sym.n_value     = (uint32_t)exp.vmOffset;
+            _leContent.symbolTable32.push_back(sym);
+        }
+        const char* str = exp.name.c_str();
+        _leContent.symbolTableStringPool.insert(_leContent.symbolTableStringPool.end(), str, str+strlen(str)+1);
+    }
+    for (const Symbol& exp : _exportedSymbols) {
+        if ( is64() ) {
+            nlist_64 sym;
+            sym.n_un.n_strx = (uint32_t)_leContent.symbolTableStringPool.size();
+            sym.n_type      = N_SECT | N_EXT;;
+            sym.n_sect      = 1; // FIXME
+            sym.n_desc      = 0;
+            sym.n_value     = exp.vmOffset;
+            _leContent.symbolTable64.push_back(sym);
+        }
+        else {
+            struct nlist sym;
+            sym.n_un.n_strx = (uint32_t)_leContent.symbolTableStringPool.size();
+            sym.n_type      = N_SECT | N_EXT;;
+            sym.n_sect      = 1; // FIXME
+            sym.n_desc      = 0;
+            sym.n_value     = (uint32_t)exp.vmOffset;
+            _leContent.symbolTable32.push_back(sym);
+        }
+        const char* str = exp.name.c_str();
+        _leContent.symbolTableStringPool.insert(_leContent.symbolTableStringPool.end(), str, str+strlen(str)+1);
+    }
+    while ( (_leContent.symbolTableStringPool.size() % 8) != 0 )
+        _leContent.symbolTableStringPool.push_back('\0');
+
+    curFileOffset += _leLayout.symbolTableCount * (is64() ? sizeof(nlist_64) : sizeof(struct nlist));
+    _leLayout.symbolStringsOffset = curFileOffset;
+    _leLayout.symbolStringsSize   = (uint32_t)_leContent.symbolTableStringPool.size();
+    curFileOffset += _leLayout.symbolStringsSize;
+
+    _size = curFileOffset;
+
+    leSegInfo->fileSize = curFileOffset - leSegInfo->fileOffset;
+    leSegInfo->vmSize   = leSegInfo->fileSize;
+    pageAlign(leSegInfo->vmSize);
+}
+
+
+void MockO::writeLinkEdit()
+{
+    // write exports trie
+    ::memcpy((char*)_mf + _leLayout.exportsTrieOffset, &_leContent.exportsTrieBytes[0], _leLayout.exportsTrieSize);
+
+
+    // write symbol table
+    if ( is64() )
+        ::memcpy((char*)_mf + _leLayout.symbolTableOffset, &_leContent.symbolTable64[0], _leLayout.symbolTableCount*sizeof(nlist_64));
+    else
+        ::memcpy((char*)_mf + _leLayout.symbolTableOffset, &_leContent.symbolTable32[0], _leLayout.symbolTableCount*sizeof(struct nlist));
+
+    // write symbol table string pool
+    ::memcpy((char*)_mf + _leLayout.symbolStringsOffset, &_leContent.symbolTableStringPool[0], _leLayout.symbolStringsSize);
+
+}
+
+load_command* MockO::firstLoadCommand() const
+{
+    if ( _mf->magic == MH_MAGIC_64 )
+        return (load_command*)((uint8_t*)_mf + sizeof(mach_header_64));
+    else if ( _mf->magic == MH_MAGIC )
+        return (load_command*)((uint8_t*)_mf + sizeof(mach_header));
+    assert(0 && "no mach-o magic");
+}
+
+// creates space for a new load command, but does not fill in its payload
+load_command* MockO::appendLoadCommand(uint32_t cmd, uint32_t cmdSize)
+{
+    assert(cmdSize == alignLC(cmdSize)); // size must be multiple of 4
+    load_command* thisCmd = (load_command*)((uint8_t*)firstLoadCommand() + _mf->sizeofcmds);
+    thisCmd->cmd     = cmd;
+    thisCmd->cmdsize = cmdSize;
+    _mf->ncmds       += 1;
+    _mf->sizeofcmds  += cmdSize;
+
+    return thisCmd;
+}
+
+// copies a new load command from another
+void MockO::appendLoadCommand(const load_command* lc)
+{
+    assert(lc->cmdsize == alignLC(lc->cmdsize)); // size must be multiple of 4
+    load_command* thisCmd = (load_command*)((uint8_t*)firstLoadCommand() + _mf->sizeofcmds);
+    ::memcpy(thisCmd, lc, lc->cmdsize);
+    _mf->ncmds       += 1;
+    _mf->sizeofcmds  += lc->cmdsize;
+}
+
+void MockO::appendEntryLoadCommand()
+{
+    // FIXME: old macOS binaries use different load command
+    entry_point_command* mlc = (entry_point_command*)appendLoadCommand(LC_MAIN, sizeof(entry_point_command));
+    mlc->entryoff  = _mainOffset;
+    mlc->stacksize = 0;
+}
+
+void MockO::appendFixupLoadCommand()
+{
+    if ( targetIsAtLeast(epochFall2021) ) {
+        linkedit_data_command* etlc = (linkedit_data_command*)appendLoadCommand(LC_DYLD_EXPORTS_TRIE, sizeof(linkedit_data_command));
+        etlc->dataoff   = _leLayout.exportsTrieOffset;
+        etlc->datasize  = _leLayout.exportsTrieSize;
+    }
+    else {
+        dyld_info_command* dilc = (dyld_info_command*)appendLoadCommand(LC_DYLD_INFO_ONLY, sizeof(dyld_info_command));
+        dilc->rebase_off        = 0;
+        dilc->rebase_size       = 0;
+        dilc->bind_off          = 0;
+        dilc->bind_size         = 0;
+        dilc->weak_bind_off     = 0;
+        dilc->weak_bind_size    = 0;
+        dilc->lazy_bind_off     = 0;
+        dilc->lazy_bind_size    = 0;
+        dilc->export_off        = _leLayout.exportsTrieOffset;
+        dilc->export_size       = _leLayout.exportsTrieSize;;
+    }
+
+}
+
+
+void MockO::appendSymbolTableLoadCommand()
+{
+    symtab_command* stlc = (symtab_command*)appendLoadCommand(LC_SYMTAB, sizeof(symtab_command));
+    stlc->symoff  = _leLayout.symbolTableOffset;
+    stlc->nsyms   = _leLayout.symbolTableCount;
+    stlc->stroff  = _leLayout.symbolStringsOffset;
+    stlc->strsize = _leLayout.symbolStringsSize;
+
+    dysymtab_command* dlc = (dysymtab_command*)appendLoadCommand(LC_DYSYMTAB, sizeof(dysymtab_command));
+    dlc->ilocalsym      = 0;
+    dlc->nlocalsym      = (uint32_t)_localSymbols.size();
+    dlc->iextdefsym     = dlc->nlocalsym;
+    dlc->nextdefsym     = (uint32_t)_exportedSymbols.size();
+    dlc->iundefsym      = 0;
+    dlc->nundefsym      = 0;
+    dlc->tocoff         = 0;
+    dlc->ntoc           = 0;
+    dlc->modtaboff      = 0;
+    dlc->nmodtab        = 0;
+    dlc->extrefsymoff   = 0;
+    dlc->nextrefsyms    = 0;
+    dlc->indirectsymoff = 0;
+    dlc->nindirectsyms  = 0;
+    dlc->extreloff      = 0;
+    dlc->nextrel        = 0;
+    dlc->locreloff      = 0;
+    dlc->nlocrel        = 0;
+}
+
+void MockO::appendPathVersionLoadCommand(const PathWithVersions& pv)
+{
+    uint32_t cmdSize = alignLC((uint32_t)(sizeof(dylib_command)+pv.path.size()+1));
+    dylib_command* lc = (dylib_command*)appendLoadCommand(pv.cmd, cmdSize);
+    lc->dylib.name.offset           = sizeof(dylib_command);
+    lc->dylib.timestamp             = 1;
+    lc->dylib.current_version       = pv.curVers;
+    lc->dylib.compatibility_version = pv.compatVers;
+    strcpy((char*)lc + sizeof(dylib_command), pv.path.c_str());
+}
+
+
+void MockO::appendSegmentLoadCommand(const SegInfo& info)
+{
+    if ( is64() ) {
+        uint32_t cmdSize = (uint32_t)(sizeof(segment_command_64)+info.sections.size()*sizeof(section_64));
+        segment_command_64* newCmd = (segment_command_64*)appendLoadCommand(LC_SEGMENT_64, cmdSize);
+        strlcpy(newCmd->segname, info.name, 16);
+        newCmd->vmaddr   = info.vmAddr;
+        newCmd->vmsize   = info.vmSize;
+        newCmd->fileoff  = info.fileOffset;
+        newCmd->filesize = info.fileSize;
+        newCmd->maxprot  = info.perms;
+        newCmd->initprot = info.perms;
+        newCmd->nsects   = (uint32_t)info.sections.size();
+        newCmd->flags    = 0;
+        section_64* sect = (section_64*)((uint8_t*)newCmd + sizeof(segment_command_64));
+        for (const SectInfo& sectInfo : info.sections) {
+            size_t sectSize = 0;
+            for (const Content& cont : sectInfo.content) {
+                sectSize += cont.bytes.size();
+            }
+            strlcpy(sect->sectname, sectInfo.name, 17); // section name can be 16 chars, may overflow into segname, but about to set that
+            strlcpy(sect->segname, info.name, 16);
+            sect->addr   = sectInfo.vmAddr;
+            sect->size   = sectInfo.size;
+            sect->offset = (uint32_t)sectInfo.fileOffset;
+            sect->flags  = sectInfo.flags;
+            ++sect;
+        }
+    }
+    else {
+        uint32_t cmdSize = (uint32_t)(sizeof(segment_command)+info.sections.size()*sizeof(section));
+        segment_command* newCmd = (segment_command*)appendLoadCommand(LC_SEGMENT, cmdSize);
+        strlcpy(newCmd->segname, info.name, 16);
+        newCmd->vmaddr   = (uint32_t)info.vmAddr;
+        newCmd->vmsize   = (uint32_t)info.vmSize;
+        newCmd->fileoff  = (uint32_t)info.fileOffset;
+        newCmd->filesize = (uint32_t)info.fileSize;
+        newCmd->maxprot  = info.perms;
+        newCmd->initprot = info.perms;
+        newCmd->nsects   = (uint32_t)info.sections.size();
+        newCmd->flags    = 0;
+        section* sect = (section*)((uint8_t*)newCmd + sizeof(segment_command));
+        for (const SectInfo& sectInfo : info.sections) {
+            size_t sectSize = 0;
+            for (const Content& cont : sectInfo.content)
+                sectSize += cont.bytes.size();
+            strlcpy(sect->sectname, sectInfo.name, 17); // section name can be 16 chars, may overflow into segname, but about to set that
+            strlcpy(sect->segname, info.name, 16);
+            sect->addr  = newCmd->vmaddr;
+            sect->size  = (uint32_t)sectSize;
+            sect->offset = (uint32_t)sectInfo.fileOffset;
+            sect->flags = sectInfo.flags;
+           ++sect;
+        }
+    }
+}
+
+void MockO::appendStringLoadCommand(uint32_t cmd, const std::string& str)
+{
+    uint32_t size = alignLC((uint32_t)(sizeof(dylinker_command)+str.size()+1));
+    dylinker_command* newCmd = (dylinker_command*)appendLoadCommand(cmd, size);
+    newCmd->name.offset = sizeof(dylinker_command);
+    strcpy((char*)newCmd + newCmd->name.offset, str.c_str());
+}
+
+void MockO::appendRoutinesLoadCommand(uint64_t offset)
+{
+    if ( is64() ) {
+        uint32_t cmdSize = sizeof(routines_command_64);
+        routines_command_64* newCmd = (routines_command_64*)appendLoadCommand(LC_ROUTINES_64, cmdSize);
+        newCmd->init_address = offset;
+        newCmd->init_module = 0;
+        newCmd->reserved1 = 0;
+        newCmd->reserved2 = 0;
+        newCmd->reserved3 = 0;
+        newCmd->reserved4 = 0;
+        newCmd->reserved5 = 0;
+        newCmd->reserved6 = 0;
+    }
+    else {
+        uint32_t cmdSize = sizeof(routines_command);
+        routines_command* newCmd = (routines_command*)appendLoadCommand(LC_ROUTINES, cmdSize);
+        newCmd->init_address = (uint32_t)offset;
+        newCmd->init_module = 0;
+        newCmd->reserved1 = 0;
+        newCmd->reserved2 = 0;
+        newCmd->reserved3 = 0;
+        newCmd->reserved4 = 0;
+        newCmd->reserved5 = 0;
+        newCmd->reserved6 = 0;
+    }
+}
+
+
+//
+// MARK: --- methods for configuring a FAT image ---
+//
+
+Muckle::Muckle() { }
+
+Muckle::~Muckle() { }
+
+void Muckle::addMockO(MockO *mock)
+{
+    _mockos.push_back(mock);
+}
+
+const FatFile* Muckle::header()
+{
+    if ( _fat == nullptr )
+        buildFATFile();
+
+    return _fat;
+}
+
+static uint32_t alignPage(uint32_t value)
+{
+    return ((value + 16383) & (-16384));
+}
+
+void Muckle::buildFATFile()
+{
+    // Add a page for the FAT header
+    _size = 16384;
+
+    for ( MockO* mock : _mockos ) {
+        // Force the MockO to build
+        mock->header();
+        _size += alignPage((uint32_t)mock->size());
+    }
+
+    vm_address_t loadAddress = 0;
+    assert(::vm_allocate(mach_task_self(), &loadAddress, (vm_size_t)_size, VM_FLAGS_ANYWHERE) == KERN_SUCCESS);
+    _fat = (FatFile*)loadAddress;
+
+    // Add the FAT header to the start of the buffer
+    fat_header* header = (fat_header*)_fat;
+    header->magic       = OSSwapHostToBigInt32(FAT_MAGIC);
+    header->nfat_arch   = OSSwapHostToBigInt32((uint32_t)_mockos.size());
+
+    size_t offsetInBuffer = 16384;
+    for (uint32_t i = 0; i != _mockos.size(); ++i) {
+        MockO* mock = _mockos[i];
+        const MachOAnalyzer* ma = mock->header();
+
+        fat_arch* archBuffer = (fat_arch*)((uint8_t*)header + sizeof(fat_header));
+        archBuffer[i].cputype       = OSSwapHostToBigInt32(ma->cputype);
+        archBuffer[i].cpusubtype    = OSSwapHostToBigInt32(ma->cpusubtype);
+        archBuffer[i].offset        = OSSwapHostToBigInt32(offsetInBuffer);
+        archBuffer[i].size          = OSSwapHostToBigInt32(mock->size());
+        archBuffer[i].align         = OSSwapHostToBigInt32(14);
+
+        uint32_t alignedSize = alignPage((uint32_t)mock->size());
+        memcpy((uint8_t*)_fat + offsetInBuffer, ma, mock->size());
+
+        offsetInBuffer += alignedSize;
+        assert(offsetInBuffer <= _size);
+    }
+}
+
+void Muckle::save(char savedPath[PATH_MAX])
+{
+    const FatFile* ff = this->header();
+
+    ::strcpy(savedPath, "/tmp/muckle-XXXXXX");
+    int fd = ::mkstemp(savedPath);
+    ::pwrite(fd, ff, _size, 0);
+    ::close(fd);
+}