/*
 * Author: Mike Ash <mikeash@plausiblelabs.com>
 *
 * Copyright (c) 2012-2013 Plausible Labs Cooperative, Inc.
 * All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

#include "PLCrashAsyncObjCSection.h"
#include "PLCrashCompatConstants.h"

#include <Foundation/Foundation.h>

/**
 * @internal
 * @ingroup plcrash_async_image
 * @defgroup plcrash_async_image_objc Objective-C Metadata Parsing
 *
 * Implements async-safe Objective-C binary parsing, for use at crash time when extracting binary information
 * from the crashed process.
 * @{
 */

static const char * const kObjCSegmentName = "__OBJC";
static const char * const kDataSegmentName = "__DATA";
static const char * const kDataConstSegmentName = "__DATA_CONST";
static const char * const kDataDirtySegmentName = "__DATA_DIRTY";

static const char * const kObjCModuleInfoSectionName = "__module_info";
static const char * const kClassListSectionName = "__objc_classlist";
static const char * const kCategoryListSectionName = "__objc_catlist";
static const char * const kObjCConstSectionName = "__objc_const";
static const char * const kObjCConstAxSectionName = "__objc_const_ax";
static const char * const kObjCDataSectionName = "__objc_data";
static const char * const kDataSectionName = "__data";

static uint32_t CLS_NO_METHOD_ARRAY = 0x4000;
static uint32_t END_OF_METHODS_LIST = -1;

/**
 * @internal
 * Flag set for non-ptr ISAs. This flag is not ABI stable, and may change.
 */
#define PLCRASH_ASYNC_OBJC_ISA_NONPTR_FLAG 0x1

/**
 * Return true if the given CPU uses non-pointer isa values.
 *
 * On x64 architectures, isa pointers are masked to
 * allow for refcounting and maintaining bit flags when the LSB is 0x1.
 *
 * @warning  ISA tagging is handled entirely in libobjc, and could be changed in any future release. There
 * are variables vended from libobjc that return the isa pointer mask; we validate these in our
 * tests, but we can't validate the meaning of bitfield checked here.
 *
 * The tagged isa pointers seem to be used even within the writable class data; as such, we must
 * perform masking here, as well. This is another reason we should migrate the code to
 * work directly on the backing unmodified pages, as that provides us with a stable ABI. In
 * the worst case scenario, we'll simply fail to symbolicate a class should the ABI
 * change incompatibly.
 *
 * @sa http://www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html
 * @sa https://github.com/opensource-apple/objc4/blob/master/runtime/objc-config.h
 */
#if !__LP64__ || TARGET_IPHONE_SIMULATOR
#define PLCRASH_ASYNC_OBJC_SUPPORT_NONPTR_ISA 0
#else
#define PLCRASH_ASYNC_OBJC_SUPPORT_NONPTR_ISA 1
#endif

/**
 * @internal
 * The pointer mask for non-pointer ISAs. This flag is not ABI stable, and may change; it is validated
 * at development time via our unit tests. As per our API invariants, if this flag becomes out-of-sync
 * with the host OS, our symbolication implementation will either fail to find some symbols, or will
 * return incorrect symbols, but it will not crash.
 *
 * @sa https://github.com/opensource-apple/objc4/blob/master/runtime/objc-private.h
 */
#if defined(__arm64__)
#define PLCRASH_ASYNC_OBJC_ISA_NONPTR_CLASS_MASK 0x0000000ffffffff8ULL
#elif defined(__x86_64__)
#define PLCRASH_ASYNC_OBJC_ISA_NONPTR_CLASS_MASK 0x00007ffffffffff8ULL
#else
#define PLCRASH_ASYNC_OBJC_ISA_NONPTR_CLASS_MASK ~1UL
#endif

/**
 * @internal
 * @see https://opensource.apple.com/source/objc4/objc4-750/runtime/objc-runtime-new.h
 *
 * Class's rw data structure has been realized.
 ** If set, data pointers to RW data instead of RO.
 */
static const uint32_t RW_REALIZED = (1U<<31);

/**
 * @internal
 * @see https://opensource.apple.com/source/objc4/objc4-750/runtime/objc-runtime-new.h
 *
 * A realized class' data pointer is a heap-copied copy of class_ro_t.
 */
static const uint32_t RW_COPIED_RO = (1<<27);

/**
 * @internal
 * @see https://opensource.apple.com/source/objc4/objc4-750/runtime/objc-runtime-new.h
 *
 * A class' data pointer mask.
 */
#if !__LP64__
#define FAST_DATA_MASK 0xfffffffcUL
#else
#define FAST_DATA_MASK 0x00007ffffffffff8UL
#endif

struct pl_objc1_module {
    uint32_t version;
    uint32_t size;
    uint32_t name;
    uint32_t symtab;
};

struct pl_objc1_symtab {
    uint32_t sel_ref_cnt;
    uint32_t refs;
    uint16_t cls_def_count;
    uint16_t cat_def_count;
};

struct pl_objc1_class {
    uint32_t isa;
    uint32_t super;
    uint32_t name;
    uint32_t version;
    uint32_t info;
    uint32_t instance_size;
    uint32_t ivars;
    uint32_t methods;
    uint32_t cache;
    uint32_t protocols;
};

struct pl_objc1_method_list {
    uint32_t obsolete;
    uint32_t count;
};

struct pl_objc1_method {
    uint32_t name;
    uint32_t types;
    uint32_t imp;
};

struct pl_objc2_class_32 {
    uint32_t isa;
    uint32_t superclass;
    uint32_t cache;
    uint32_t vtable;
    uint32_t data_rw;
};

struct pl_objc2_class_64 {
    uint64_t isa;
    uint64_t superclass;
    uint64_t cache;
    uint64_t vtable;
    uint64_t data_rw;
};

struct pl_objc2_class_data_rw_32 {
    uint32_t flags;
    uint32_t version;
    uint32_t data_ro;
};

struct pl_objc2_class_data_rw_64 {
    uint32_t flags;
    uint32_t version;
    uint64_t data_ro;
};

struct pl_objc2_class_data_ro_32 {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    uint32_t ivarLayout;
    uint32_t name;
    uint32_t baseMethods;
    uint32_t baseProtocols;
    uint32_t ivars;
    uint32_t weakIvarLayout;
    uint32_t baseProperties;
};

struct pl_objc2_class_data_ro_64 {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    uint32_t reserved;
    uint64_t ivarLayout;
    uint64_t name;
    uint64_t baseMethods;
    uint64_t baseProtocols;
    uint64_t ivars;
    uint64_t weakIvarLayout;
    uint64_t baseProperties;
};

/** @internal
 * Category list entry (32-bit representation).
 */
struct pl_objc2_category_32 {
    uint32_t name;
    uint32_t cls;
    uint32_t instanceMethods;
    uint32_t classMethods;
    uint32_t protocols;
    uint32_t instanceProperties;
};

/** @internal
 * Category list entry (64-bit representation).
 */
struct pl_objc2_category_64 {
    uint64_t name;
    uint64_t cls;
    uint64_t instanceMethods;
    uint64_t classMethods;
    uint64_t protocols;
    uint64_t instanceProperties;
};

struct pl_objc2_method_32 {
    uint32_t name;
    uint32_t types;
    uint32_t imp;
};

struct pl_objc2_method_64 {
    uint64_t name;
    uint64_t types;
    uint64_t imp;
};

struct pl_objc2_list_header {
    uint32_t entsize;
    uint32_t count;
};

/**
 * Returns the pointer value for a non-pointer isa. This assumes that the lsb flag of 0x1 will continue to be
 * used to designate a non-pointer isa; see the PLCRASH_ASYNC_OBJC_SUPPORT_NONPTR_ISA documentation for more details.
 */
static pl_vm_address_t plcrash_async_objc_isa_pointer (pl_vm_address_t isa) {
#if PLCRASH_ASYNC_OBJC_SUPPORT_NONPTR_ISA
    if (isa & PLCRASH_ASYNC_OBJC_ISA_NONPTR_FLAG) {
        return isa & PLCRASH_ASYNC_OBJC_ISA_NONPTR_CLASS_MASK;
    }
#endif
    return isa;
}

/**
 * Get the index into the context's cache for the given key. Must only be called
 * if the cache size has been set.
 *
 * @param context The context.
 * @param key The key.
 * @return The index.
 */
static size_t cache_index (plcrash_async_objc_cache_t *context, pl_vm_address_t key) {
    return (key >> 2) % context->classCacheSize;
}

/**
 * Get the context's cache's total memory allocation size, including both keys and values.
 *
 * @param context The context.
 * @return The total number of bytes allocated for the cache.
 */
static size_t cache_allocation_size (plcrash_async_objc_cache_t *context) {
    size_t size = context->classCacheSize;
    return size * sizeof(*context->classCacheKeys) + size * sizeof(*context->classCacheValues);
}

/**
 * Look up a key within the cache.
 *
 * @param context The context.
 * @param key The key to look up.
 * @return The value stored in the cache for that key, or 0 if none was found.
 */
static pl_vm_address_t cache_lookup (plcrash_async_objc_cache_t *context, pl_vm_address_t key) {
    if (context->classCacheSize > 0) {
        size_t index = cache_index(context, key);
        if (context->classCacheKeys[index] == key) {
            return context->classCacheValues[index];
        }
    }
    return 0;
}

/**
 * Store a key/value pair in the cache. The cache is not guaranteed storage so storing may
 * silently fail, and the association can be evicted at any time. It's a CACHE.
 *
 * @param context The context.
 * @param key The key to store.
 * @param value The value to store.
 */
static void cache_set (plcrash_async_objc_cache_t *context, pl_vm_address_t key, pl_vm_address_t value) {
    /* If nothing has used the cache yet, allocate the memory. */
    if (context->classCacheKeys == NULL) {
        size_t size = 1024;
        context->classCacheSize = size;
        
        size_t allocationSize = cache_allocation_size(context);
        
        vm_address_t addr;
        kern_return_t err = vm_allocate(mach_task_self_, &addr, allocationSize, VM_FLAGS_ANYWHERE);
        /* If it fails, just bail out. We don't need the cache for correct operation. */
        if (err != KERN_SUCCESS) {
            PLCF_DEBUG("vm_allocate failed with error %x, the class cache could not be initialized and ObjC parsing will be substantially slower", err);
            context->classCacheSize = 0;
            return;
        }
        
        context->classCacheKeys = (pl_vm_address_t *)addr;
        context->classCacheValues = (pl_vm_address_t *)(context->classCacheKeys + size);
    }
    
    /* Treat the cache as a simple hash table with no chaining whatsoever. If the bucket is already
     * occupied, then don't do anything. The existing entry wins. */
    size_t index = cache_index(context, key);
    if (context->classCacheKeys[index] == 0) {
        context->classCacheKeys[index] = key;
        context->classCacheValues[index] = value;
    }
}

/**
 * Free any initialized memory objects in an ObjC context object.
 *
 * @param context The context.
 */
static void free_mapped_sections (plcrash_async_objc_cache_t *context) {
    if (context->objcConstMobjInitialized) {
        plcrash_async_mobject_free(&context->objcConstMobj);
        context->objcConstMobjInitialized = false;
    }
    if (context->objcConstAxMobjInitialized) {
        plcrash_async_mobject_free(&context->objcConstAxMobj);
        context->objcConstAxMobjInitialized = false;
    }
    if (context->classMobjInitialized) {
        plcrash_async_mobject_free(&context->classMobj);
        context->classMobjInitialized = false;
    }
    if (context->catMobjInitialized) {
        plcrash_async_mobject_free(&context->catMobj);
        context->catMobjInitialized = false;
    }
    if (context->objcDataMobjInitialized) {
        plcrash_async_mobject_free(&context->objcDataMobj);
        context->objcDataMobjInitialized = false;
    }
    if (context->dataMobjInitialized) {
        plcrash_async_mobject_free(&context->dataMobj);
        context->dataMobjInitialized = false;
    }
}

/**
 * @internal
 * @see https://opensource.apple.com/source/objc4/objc4-750/runtime/objc-file.mm
 *
 * Find and map a named section within a __DATA or __DATA_CONST or __DATA_DIRTY segment, initializing @a mobj.
 * It is the caller's responsibility to dealloc @a mobj after a successful initialization.
 *
 * @param image The image to search for @a segname.
 * @param sectname The name of the section to map.
 * @param mobj The mobject to be initialized with a mapping of the section's data. It is the caller's responsibility to dealloc @a mobj after
 * a successful initialization.
 *
 * @return Returns PLCRASH_ESUCCESS on success, PLCRASH_ENOTFOUND if the section is not found, or an error result on failure.
 */
static plcrash_error_t map_data_section (plcrash_async_macho_t *image, const char **segname, const char *sectname, plcrash_async_mobject_t *mobj) {
    *segname = kDataSegmentName;
    plcrash_error_t err = plcrash_async_macho_map_section(image, *segname, sectname, mobj);
    if (err == PLCRASH_ENOTFOUND) {
        *segname = kDataConstSegmentName;
        err = plcrash_async_macho_map_section(image, *segname, sectname, mobj);
    }
    if (err == PLCRASH_ENOTFOUND) {
        *segname = kDataDirtySegmentName;
        err = plcrash_async_macho_map_section(image, *segname, sectname, mobj);
    }
    return err;
}

/**
 * Set up the memory objects in an ObjC context object for the given image. This will
 * map the memory objects in the context to the appropriate sections in the image.
 *
 * @param image The MachO image to map.
 * @param context The context.
 * @return An error code.
 */
static plcrash_error_t map_sections (plcrash_async_macho_t *image, plcrash_async_objc_cache_t *context) {
    if (image == context->lastImage)
        return PLCRASH_ESUCCESS;
    
    /* Clean up the info from the previous image. Free the memory objects and reset the
     * image pointer. The image pointer is reset so that it's not stale in case we return
     * early due to an error. */
    free_mapped_sections(context);
    context->lastImage = NULL;
    
    plcrash_error_t err;
    const char *segname;
    
    /* Map in the __objc_const section, which is where all the read-only class data lives. */
    err = map_data_section(image, &segname, kObjCConstSectionName, &context->objcConstMobj);
    if (err != PLCRASH_ESUCCESS) {
        if (err != PLCRASH_ENOTFOUND)
            PLCF_DEBUG("pl_async_macho_map_section(%p, %s, %s, %p) failure %d", image, segname, kObjCConstSectionName, &context->objcConstMobj, err);
        goto cleanup;
    }
    context->objcConstMobjInitialized = true;

    /* Map in the __objc_const_ax section. */
    err = plcrash_async_macho_map_section(image, kDataSegmentName, kObjCConstAxSectionName, &context->objcConstAxMobj);
    if (err != PLCRASH_ESUCCESS && err != PLCRASH_ENOTFOUND)
        PLCF_DEBUG("pl_async_macho_map_section(%s, %s, %s, %p) failure %d", PLCF_DEBUG_IMAGE_NAME(image), kDataSegmentName, kObjCConstAxSectionName, &context->objcConstAxMobj, err);
    context->objcConstAxMobjInitialized = err == PLCRASH_ESUCCESS;
    
    /* Map in the class list section. */
    err = map_data_section(image, &segname, kClassListSectionName, &context->classMobj);
    if (err != PLCRASH_ESUCCESS) {
        if (err != PLCRASH_ENOTFOUND)
            PLCF_DEBUG("pl_async_macho_map_section(%s, %s, %s, %p) failure %d", PLCF_DEBUG_IMAGE_NAME(image), segname, kClassListSectionName, &context->classMobj, err);
        goto cleanup;
    }
    context->classMobjInitialized = true;
    
    /* Map in the category list section. */
    err = map_data_section(image, &segname, kCategoryListSectionName, &context->catMobj);
    if (err != PLCRASH_ESUCCESS) {
        if (err != PLCRASH_ENOTFOUND)
            PLCF_DEBUG("pl_async_macho_map_section(%s, %s, %s, %p) failure %d", PLCF_DEBUG_IMAGE_NAME(image), segname, kCategoryListSectionName, &context->catMobj, err);
        goto cleanup;
    }
    context->catMobjInitialized = true;
    
    /* Map in the __objc_data section, which is where the actual classes live. */
    err = plcrash_async_macho_map_section(image, kDataSegmentName, kObjCDataSectionName, &context->objcDataMobj);
    if (err != PLCRASH_ESUCCESS) {
        /* If the class list was found, the data section must also be found. */
        PLCF_DEBUG("pl_async_macho_map_section(%s, %s, %s, %p) failure %d", PLCF_DEBUG_IMAGE_NAME(image), kDataSegmentName, kObjCDataSectionName, &context->objcDataMobj, err);
        goto cleanup;
    }
    context->objcDataMobjInitialized = true;

    /* Map in the __data section. */
    err = plcrash_async_macho_map_section(image, kDataSegmentName, kDataSectionName, &context->dataMobj);
    if (err != PLCRASH_ESUCCESS && err != PLCRASH_ENOTFOUND)
        PLCF_DEBUG("pl_async_macho_map_section(%s, %s, %s, %p) failure %d", PLCF_DEBUG_IMAGE_NAME(image), kDataSegmentName, kDataSectionName, &context->dataMobj, err);
    context->dataMobjInitialized = err == PLCRASH_ESUCCESS;
    
    /* Only after all mappings succeed do we set the image. If any failed, the image won't be set,
     * and any mappings that DO succeed will be cleaned up on the next call (or when freeing the
     * context. */
    context->lastImage = image;
    
cleanup:
    return err;
}

static plcrash_error_t pl_async_parse_obj1_class(plcrash_async_macho_t *image, struct pl_objc1_class *cls, bool isMetaClass, plcrash_async_objc_found_method_cb callback, void *ctx) {
    plcrash_error_t err;
    
    /* Get the class's name. */
    pl_vm_address_t namePtr = image->byteorder->swap32(cls->name);
    bool classNameInitialized = false;
    plcrash_async_macho_string_t className;
    err = plcrash_async_macho_string_init(&className, image, namePtr);
    if (err != PLCRASH_ESUCCESS) {
        PLCF_DEBUG("plcrash_async_macho_string_init at 0x%llx error %d", (long long)namePtr, err);
        return err;
    }
    classNameInitialized = true;
    
    /* Grab the method list pointer. This is either a pointer to
     * a single method_list structure, OR a pointer to an array
     * of pointers to method_list structures, depending on the
     * flag in the .info field. Argh. */
    pl_vm_address_t methodListPtr = image->byteorder->swap32(cls->methods);
    
    /* If CLS_NO_METHOD_ARRAY is set, then methodListPtr points to
     * one method_list. If it's not set, then it points to an
     * array of pointers to method lists. */
    bool hasMultipleMethodLists = (image->byteorder->swap32(cls->info) & CLS_NO_METHOD_ARRAY) == 0;
    pl_vm_address_t methodListCursor = methodListPtr;
    
    while (true) {
        /* Grab a method list pointer. How to do that depends on whether
         * CLS_NO_METHOD_ARRAY is set. Once done, thisListPtr contains
         * a pointer to the method_list structure to read. */
        pl_vm_address_t thisListPtr;
        if (hasMultipleMethodLists) {
            /* If there are multiple method lists, then read the list pointer
             * from the current cursor, and advance the cursor. */
            uint32_t ptr;
            err = plcrash_async_task_memcpy(image->task, methodListCursor, 0, &ptr, sizeof(ptr));
            if (err != PLCRASH_ESUCCESS) {
                PLCF_DEBUG("plcrash_async_task_memcpy at 0x%llx error %d", (long long)methodListCursor, err);
                goto cleanup;
            }
            
            thisListPtr = image->byteorder->swap32(ptr);
            /* The end of the list is indicated with NULL or
             * END_OF_METHODS_LIST (the ObjC runtime source checks both). */
            if (thisListPtr == 0 || thisListPtr == END_OF_METHODS_LIST)
                break;
            
            methodListCursor += sizeof(ptr);
        } else {
            /* If CLS_NO_METHOD_ARRAY is set, then the single method_list
             * is pointed to by the cursor. */
            thisListPtr = methodListCursor;
            
            /* The pointer may be NULL, in which case there are no methods. */
            if (thisListPtr == 0)
                break;
        }
        
        /* Read a method_list structure from the current list pointer. */
        struct pl_objc1_method_list methodList;
        err = plcrash_async_task_memcpy(image->task, thisListPtr, 0, &methodList, sizeof(methodList));
        if (err != PLCRASH_ESUCCESS) {
            PLCF_DEBUG("plcrash_async_task_memcpy at 0x%llx error %d", (long long)methodListPtr, err);
            goto cleanup;
        }
        
        /* Find out how many methods are in the list, and iterate. */
        uint32_t count = image->byteorder->swap32(methodList.count);
        for (uint32_t i = 0; i < count; i++) {
            /* Method structures are laid out directly following the
             * method_list structure. */
            struct pl_objc1_method method;
            pl_vm_address_t methodPtr = thisListPtr + sizeof(methodList) + i * sizeof(method);
            err = plcrash_async_task_memcpy(image->task, methodPtr, 0, &method, sizeof(method));
            if (err != PLCRASH_ESUCCESS) {
                PLCF_DEBUG("plcrash_async_task_memcpy at 0x%llx error %d", (long long)methodPtr, err);
                goto cleanup;
            }
            
            /* Load the method name from the .name field pointer. */
            pl_vm_address_t methodNamePtr = image->byteorder->swap32(method.name);
            plcrash_async_macho_string_t methodName;
            err = plcrash_async_macho_string_init(&methodName, image, methodNamePtr);
            if (err != PLCRASH_ESUCCESS) {
                PLCF_DEBUG("plcrash_async_macho_string_init at 0x%llx error %d", (long long)methodNamePtr, err);
                goto cleanup;
            }
            
            /* Grab the method's IMP as well. */
            pl_vm_address_t imp = image->byteorder->swap32(method.imp);
            
            /* Callback! */
            callback(isMetaClass, &className, &methodName, imp, ctx);
            
            /* Clean up the method name object. */
            plcrash_async_macho_string_free(&methodName);
        }
        
        /* Bail out of the loop after a single iteration if
         * CLS_NO_METHOD_ARRAY is set, because there's no need
         * to iterate in that case. */
        if (!hasMultipleMethodLists)
            break;
    }
    
cleanup:
    if (classNameInitialized)
        plcrash_async_macho_string_free(&className);
    
    return err;
}

/**
 * Parse Objective-C class data from an old-style __module_info section containing
 * ObjC1 metadata.
 *
 * @param image The Mach-O image to read from.
 * @param callback The callback to invoke for each method found.
 * @param ctx The context pointer to pass to the callback.
 * @return PLCRASH_ESUCCESS on success, PLCRASH_ENOTFOUND if the image doesn't
 * contain ObjC1 metadata, or another error code if a different error occurred.
 */
static plcrash_error_t pl_async_objc_parse_from_module_info (plcrash_async_macho_t *image, plcrash_async_objc_found_method_cb callback, void *ctx) {
    plcrash_error_t err = PLCRASH_EUNKNOWN;
    struct pl_objc1_module *moduleData;

    /* Map the __module_info section. */
    bool moduleMobjInitialized = false;
    plcrash_async_mobject_t moduleMobj;

    err = plcrash_async_macho_map_section(image, kObjCSegmentName, kObjCModuleInfoSectionName, &moduleMobj);
    if (err != PLCRASH_ESUCCESS) {
        if (err != PLCRASH_ENOTFOUND)
            PLCF_DEBUG("pl_async_macho_map_section(%p, %s, %s, %p) failure %d", image, kObjCSegmentName, kObjCModuleInfoSectionName, &moduleMobj, err);
        goto cleanup;
    }

    /* Successful mapping, so mark the memory object as needing cleanup. */
    moduleMobjInitialized = true;
    
    /* Get a pointer to the module info data. */
    moduleData = (struct pl_objc1_module *) plcrash_async_mobject_remap_address(&moduleMobj, moduleMobj.task_address, 0, sizeof(*moduleData));
    if (moduleData == NULL) {
        PLCF_DEBUG("Failed to obtain pointer from %s memory object", kObjCModuleInfoSectionName);
        err = PLCRASH_ENOTFOUND;
        goto cleanup;
    }
    
    /* Read successive module structs from the section until we run out of data. */
    for (unsigned moduleIndex = 0; moduleIndex < moduleMobj.length / sizeof(*moduleData); moduleIndex++) {
        /* Grab the pointer to the symtab for this module struct. */
        pl_vm_address_t symtabPtr = image->byteorder->swap32(moduleData[moduleIndex].symtab);
        if (symtabPtr == 0)
            continue;
        
        /* Read a symtab struct from that pointer. */
        struct pl_objc1_symtab symtab;
        err = plcrash_async_task_memcpy(image->task, symtabPtr, 0, &symtab, sizeof(symtab));
        if (err != PLCRASH_ESUCCESS) {
            PLCF_DEBUG("plcrash_async_task_memcpy at 0x%llx error %d", (long long)symtabPtr, err);
            goto cleanup;
        }
        
        /* Iterate over the classes in the symtab. */
        uint16_t classCount = image->byteorder->swap16(symtab.cls_def_count);
        for (unsigned i = 0; i < classCount; i++) {
            /* Classes are indicated by pointers laid out sequentially after the
             * symtab structure. */
            uint32_t classPtr;
            pl_vm_address_t cursor = symtabPtr + sizeof(symtab) + i * sizeof(classPtr);
            err = plcrash_async_task_memcpy(image->task, cursor, 0, &classPtr, sizeof(classPtr));
            if (err != PLCRASH_ESUCCESS) {
                PLCF_DEBUG("plcrash_async_task_memcpy at 0x%llx error %d", (long long)cursor, err);
                goto cleanup;
            }
            classPtr = image->byteorder->swap32(classPtr);
            
            /* Read a class structure from the class pointer. */
            struct pl_objc1_class cls;
            err = plcrash_async_task_memcpy(image->task, classPtr, 0, &cls, sizeof(cls));
            if (err != PLCRASH_ESUCCESS) {
                PLCF_DEBUG("plcrash_async_task_memcpy at 0x%llx error %d", (long long)classPtr, err);
                goto cleanup;
            }
            
            err = pl_async_parse_obj1_class(image, &cls, false, callback, ctx);
            if (err != PLCRASH_ESUCCESS) {
                PLCF_DEBUG("pl_async_parse_obj1_class error %d while parsing class", err);
                goto cleanup;
            }
            
            /* Read a class structure for the metaclass. */
            pl_vm_address_t isa = plcrash_async_objc_isa_pointer(image->byteorder->swap(cls.isa));
            struct pl_objc1_class metaclass;
            err = plcrash_async_task_memcpy(image->task, isa, 0, &metaclass, sizeof(metaclass));
            if (err != PLCRASH_ESUCCESS) {
                PLCF_DEBUG("plcrash_async_task_memcpy at 0x%llx error %d", (long long)isa, err);
                goto cleanup;
            }
            
            err = pl_async_parse_obj1_class(image, &metaclass, true, callback, ctx);
            if (err != PLCRASH_ESUCCESS) {
                PLCF_DEBUG("pl_async_parse_obj1_class error %d while parsing metaclass", err);
                goto cleanup;
            }
        }
    }
    
cleanup:
    /* Clean up the memory objects before returning if they're initialized. */
    if (moduleMobjInitialized)
        plcrash_async_mobject_free(&moduleMobj);
    
    return err;
}

/**
 * Parse an ObjC 2.0 method_list_t structure at @a method_list_addr and call @a callback with all
 * parsed methods.
 *
 * @param image The image to read from.
 * @param objc_cache The Objective-C cache object.
 * @param class_name The name of the class being parsed.
 * @param is_meta_class true if this is a metaclass.
 * @param method_list_addr The address of the method list data.
 * @param callback The callback to be invoked for each method found.
 * @param ctx A context pointer to pass to the callback.
 * @return Returns PLCRASH_ESUCCESS on success, or an appropriate error on failure.
 */
static plcrash_error_t pl_async_objc_parse_objc2_method_list (plcrash_async_macho_t *image,
                                                              plcrash_async_objc_cache_t *objc_cache,
                                                              plcrash_async_macho_string_t *class_name,
                                                              bool is_meta_class,
                                                              pl_vm_address_t method_list_addr,
                                                              plcrash_async_objc_found_method_cb callback,
                                                              void *ctx)
{
    PLCF_ASSERT(method_list_addr != 0);
    
    /* Read the method list header. */
    struct pl_objc2_list_header *header;
    plcrash_async_mobject_t *section = &objc_cache->objcConstMobj;
    header = (struct pl_objc2_list_header *) plcrash_async_mobject_remap_address(section, method_list_addr, 0, sizeof(*header));
    if (header == NULL && objc_cache->objcConstAxMobjInitialized) {
        section = &objc_cache->objcConstAxMobj;
        header = (struct pl_objc2_list_header *) plcrash_async_mobject_remap_address(section, method_list_addr, 0, sizeof(*header));
    }
    if (header == NULL) {
        PLCF_DEBUG("plcrash_async_mobject_remap_address in __objc_const and __objc_const_ax failed to map methods pointer 0x%llx", (long long) method_list_addr);
        return PLCRASH_EINVALID_DATA;
    }
    
    /* Extract the entry size and count from the list header. */
    uint32_t entsize = image->byteorder->swap32(header->entsize) & ~(uint32_t)3;
    uint32_t count = image->byteorder->swap32(header->count);
    
    /* Compute the method list start position and length. */
    pl_vm_address_t method_list_start = method_list_addr + sizeof(*header);
    pl_vm_size_t method_list_length = (pl_vm_size_t)entsize * count;
    
    const char *cursor = (const char *) plcrash_async_mobject_remap_address(section, method_list_start, 0, method_list_length);
    if (cursor == NULL) {
        PLCF_DEBUG("plcrash_async_mobject_remap_address at 0x%llx length %llu returned NULL", (long long)method_list_start, (unsigned long long)method_list_length);
        return PLCRASH_EINVALID_DATA;
    }
    
    /* Extract methods from the list. */
    for (uint32_t i = 0; i < count; i++) {
        plcrash_error_t err;

        /* Read an architecture-appropriate method structure from the
         * current cursor. */
        const struct pl_objc2_method_32 *method_32 = (const struct pl_objc2_method_32 *)cursor;
        const struct pl_objc2_method_64 *method_64 = (const struct pl_objc2_method_64 *)cursor;
        
        /* Extract the method name pointer. */
        pl_vm_address_t methodNamePtr = (image->m64
                                         ? (pl_vm_address_t) image->byteorder->swap64(method_64->name)
                                         : image->byteorder->swap32(method_32->name));
        
        /* Read the method name. */
        plcrash_async_macho_string_t method_name;
        if ((err = plcrash_async_macho_string_init(&method_name, image, methodNamePtr)) != PLCRASH_ESUCCESS) {
            PLCF_DEBUG("plcrash_async_macho_string_init at 0x%llx error %d", (long long)methodNamePtr, err);
            return err;
        }
    
        /* Extract the method IMP. */
        pl_vm_address_t imp = (image->m64
                               ? (pl_vm_address_t) image->byteorder->swap64(method_64->imp)
                               : image->byteorder->swap32(method_32->imp));
        
        /* Call the callback. */
        callback(is_meta_class, class_name, &method_name, imp, ctx);

        /* Clean up the method name. */
        plcrash_async_macho_string_free(&method_name);
        
        /* Increment the cursor by the entry size for the next iteration of the loop. */
        cursor += entsize;
    }

    return PLCRASH_ESUCCESS;
}

/**
 * Parse a single ObjC 2.0 class structure and return the results.
 *
 * @param image The image to read from.
 * @param objc_cache The Objective-C cache object.
 * @param cls A pointer to the class structure to be parsed
 * @param[out] class_name On success, will be initialized with the class name. It is the caller's responsibility to free
 * the returned value via plcrash_async_macho_string_free(). If the function returns an error, no class_name value
 * will be provided and the caller is not responsible for freeing any associated resources.
 * @param[out] cls_data_ro A buffer to which the class_ro data will be written. Must be at least sizeof(class_ro_t).
 *
 * @tparam class_t The class type, one of pl_objc2_class_32 or pl_objc2_class_64.
 * @tparam class_ro_t The read-only class type, one of pl_objc2_class_data_ro_32 or pl_objc2_class_data_ro_64.
 * @tparam class_rw_t The read-write class type, one of pl_objc2_class_data_rw_32 or pl_objc2_class_data_rw_64.
 *
 * @return Returns PLCRASH_ESUCCESS on success, PLCRASH_ENOTFOUND if the class is unrealized, PLCRASH_EINVALID_DATA if
 * the input format is invalid (including failed pointer dereferencing), or another appropriate error.
 */
template<typename class_t, typename class_ro_t, typename class_rw_t, typename machine_ptr_t>
static plcrash_error_t pl_async_objc_parse_objc2_class (plcrash_async_macho_t *image, plcrash_async_objc_cache_t *objc_cache, class_t *cls, plcrash_async_macho_string_t *class_name, class_ro_t *cls_data_ro) {
    plcrash_error_t err;
    
    /* Grab the class's data_rw pointer. This needs masking because it also
     * can contain flags. */
    pl_vm_address_t data_ptr = (pl_vm_address_t) image->byteorder->swap(cls->data_rw);
    data_ptr &= FAST_DATA_MASK;

    /* Grab the data RO pointer from the cache. If unavailable, we'll fetch the data and populate the class. */
    pl_vm_address_t cached_data_ro_addr = cache_lookup(objc_cache, data_ptr);
    if (cached_data_ro_addr == 0) {
        class_rw_t cls_data_rw;
        
        /* Read the class_rw structure. */
        err = plcrash_async_task_memcpy(image->task, data_ptr, 0, &cls_data_rw, sizeof(cls_data_rw));
        if (err != PLCRASH_ESUCCESS) {
            PLCF_DEBUG("plcrash_async_task_memcpy at 0x%llx error %d", (long long)data_ptr, err);
            return PLCRASH_EINVALID_DATA;
        }

        /* Check the flags. If it's not yet realized, then we need to skip the class. */
        uint32_t flags = image->byteorder->swap(cls_data_rw.flags);
        if ((flags & RW_REALIZED) == 0)  {
            // PLCF_DEBUG("Found unrealized class with RO data at 0x%llx, skipping it", (long long)data_ptr);
            return PLCRASH_ENOTFOUND;
        }

        /* Grab the data_ro pointer. The RO data (read-only) contains the class name
         * and method list. */
        cached_data_ro_addr = (pl_vm_address_t) image->byteorder->swap(cls_data_rw.data_ro);

        /* When objc_class_abi_version >= 1, it's a tagged union based on the low bit:
         * 0: class_ro_t  1: class_rw_ext_t
         * @see https://opensource.apple.com/source/objc4/objc4-781/runtime/objc-runtime-new.h */
        if (cached_data_ro_addr & 0x1) {
            cached_data_ro_addr &= ~0x1;
            /* class_rw_ext_t has a link to R/O data without any offset, so just grab this pointer. */
            machine_ptr_t data_ro_addr;
            err = plcrash_async_task_memcpy(image->task, cached_data_ro_addr, 0, &data_ro_addr, sizeof(data_ro_addr));
            if (err != PLCRASH_ESUCCESS) {
                PLCF_DEBUG("plcrash_async_task_memcpy at 0x%llx error %d", (long long)cached_data_ro_addr, err);
                return PLCRASH_EINVALID_DATA;
            }
            cached_data_ro_addr = (pl_vm_address_t) image->byteorder->swap(data_ro_addr);
        }

        /* Copy the R/O data; it will either be heap allocated (RW_COPIED_RO), found within the __objc_const section of the current cached image,
         * or found within the __objc_const section of a non-cached image. */
        if ((flags & RW_COPIED_RO) != 0 || !plcrash_async_macho_contains_address(image, cached_data_ro_addr)) {
            if ((err = plcrash_async_task_memcpy(image->task, cached_data_ro_addr, 0, cls_data_ro, sizeof(*cls_data_ro))) != PLCRASH_ESUCCESS) {
                PLCF_DEBUG("plcrash_async_task_memcpy at 0x%llx returned NULL", (long long)cached_data_ro_addr);
                return PLCRASH_EINVALID_DATA;
            }
        } else {
            void *ptr = plcrash_async_mobject_remap_address(&objc_cache->objcConstMobj, cached_data_ro_addr, 0, sizeof(*cls_data_ro));
            if (ptr == NULL) {
                PLCF_DEBUG("plcrash_async_mobject_remap_address in __objc_const for pointer 0x%llx returned NULL", (long long)cached_data_ro_addr);
                return PLCRASH_EINVALID_DATA;
            }
            
            plcrash_async_memcpy(cls_data_ro, ptr, sizeof(*cls_data_ro));
        }
        
        /* Add a new cache entry. */
        cache_set(objc_cache, data_ptr, cached_data_ro_addr);
    } else {
        void *ptr;
        
        /* We know that the address is valid (it wouldn't be in the cache otherwise). We try the cheaper memory mapping first,
         * and then fall back to a memory copy. */
        if ((ptr = plcrash_async_mobject_remap_address(&objc_cache->objcConstMobj, cached_data_ro_addr, 0, sizeof(*cls_data_ro))) != NULL) {
            plcrash_async_memcpy(cls_data_ro, ptr, sizeof(*cls_data_ro));
        } else if (plcrash_async_task_memcpy(image->task, cached_data_ro_addr, 0, cls_data_ro, sizeof(*cls_data_ro)) == PLCRASH_ESUCCESS) {
            /* Do nothing -- success! */
        } else {
            PLCF_DEBUG("Failed to read validated class_ro data at 0x%llx", (long long)cached_data_ro_addr);
            return PLCRASH_EINVALID_DATA;
        }
    }
    
    /* Fetch the pointer to the class name, and make the string. */
    pl_vm_address_t class_name_ptr = (pl_vm_address_t) image->byteorder->swap(cls_data_ro->name);
    err = plcrash_async_macho_string_init(class_name, image, class_name_ptr);
    if (err != PLCRASH_ESUCCESS) {
        PLCF_DEBUG("plcrash_async_macho_string_init at 0x%llx error %d", (long long)class_name_ptr, err);
        return PLCRASH_EINVALID_DATA;
    }

    return err;
}

/**
 * Parse a single ObjC 2.0 class structure and call @a callback with all parsed methods.
 *
 * @param image The image to read from.
 * @param objc_cache The Objective-C cache object.
 * @param cls A pointer to the class structure to be parsed
 * @param is_meta_class true if this is a metaclass.
 * @param callback The callback to invoke for each method found.
 * @param ctx A context pointer to pass to the callback.
 *
 * @tparam class_t The class type, one of pl_objc2_class_32 or pl_objc2_class_64.
 * @tparam class_ro_t The read-only class type, one of pl_objc2_class_data_ro_32 or pl_objc2_class_data_ro_64.
 * @tparam class_rw_t The read-write class type, one of pl_objc2_class_data_rw_32 or pl_objc2_class_data_rw_64.
 *
 * @return An error code.
 */
template<typename class_t, typename class_ro_t, typename class_rw_t, typename machine_ptr_t>
static plcrash_error_t pl_async_objc_parse_objc2_class_methods (plcrash_async_macho_t *image, plcrash_async_objc_cache_t *objc_cache, class_t *cls, bool is_meta_class, plcrash_async_objc_found_method_cb callback, void *ctx) {
    plcrash_async_macho_string_t class_name;
    class_ro_t cls_data_ro;
    plcrash_error_t err;
    
    /* Parse the class */
    if ((err = pl_async_objc_parse_objc2_class<class_t, class_ro_t, class_rw_t, machine_ptr_t>(image, objc_cache, cls, &class_name, &cls_data_ro)) != PLCRASH_ESUCCESS)
        return err;

    /* Fetch and parse the method list. */
    pl_vm_address_t methods_ptr = (pl_vm_address_t) image->byteorder->swap(cls_data_ro.baseMethods);
    if (methods_ptr != 0) {
        err = pl_async_objc_parse_objc2_method_list(image, objc_cache, &class_name, is_meta_class, methods_ptr, callback, ctx);
    } else {
        /* The base method list will be NULL if no methods are defined for the class/metaclass; in that case, we simply skip the class. */
        err = PLCRASH_ESUCCESS;
    }

    /* Clean up */
    plcrash_async_macho_string_free(&class_name);
    return err;
}

/**
 * Parse a single ObjC 2.0 category_t structure and call @a callback with all parsed methods.
 *
 * @param image The image to read from.
 * @param objc_cache The Objective-C cache object.
 * @param category A pointer to a category structure.
 * @param callback The callback to invoke for each method found.
 * @param ctx A context pointer to pass to the callback.
 *
 * @tparam category_t The category type, one of pl_objc2_category_32 or pl_objc2_category_64.
 * @tparam class_t The class type, one of pl_objc2_class_32 or pl_objc2_class_64.
 * @tparam class_ro_t The read-only class type, one of pl_objc2_class_data_ro_32 or pl_objc2_class_data_ro_64.
 * @tparam class_rw_t The read-write class type, one of pl_objc2_class_data_rw_32 or pl_objc2_class_data_rw_64.
 *
 * @return Returns PLCRASH_ESUCCESS on success, PLCRASH_ENOTFOUND if the class referenced by the category is unrealized, PLCRASH_EINVALID_DATA if
 * the input format is invalid (including failed pointer dereferencing), or another appropriate error.
 */
template <typename category_t, typename class_t, typename class_ro_t, typename class_rw_t, typename machine_ptr_t>
static plcrash_error_t pl_async_objc_parse_objc2_category_methods (plcrash_async_macho_t *image, plcrash_async_objc_cache_t *objc_cache, category_t *category, plcrash_async_objc_found_method_cb callback, void *ctx) {
    plcrash_async_macho_string_t class_name;
    class_ro_t cls_data_ro;
    plcrash_error_t err;
    
    /* Grab the class reference and parse the class. We try to simply remap the pointer, but if that fails, we perform a more
     * expensive read. */
    pl_vm_address_t ptr = (pl_vm_address_t) image->byteorder->swap(category->cls);
    class_t *classPtr = (class_t *) plcrash_async_mobject_remap_address(&objc_cache->objcDataMobj, ptr, 0, sizeof(*classPtr));
    class_t class_data;

    if (classPtr == NULL) {
        if ((err = plcrash_async_task_memcpy(image->task, ptr, 0, &class_data, sizeof(class_data))) != PLCRASH_ESUCCESS) {
            PLCF_DEBUG("plcrash_async_task_memcpy() for pointer 0x%llx returned NULL", (long long)ptr);
            return PLCRASH_EINVALID_DATA;
        }
        
        classPtr = &class_data;
    }
    
    if ((err = pl_async_objc_parse_objc2_class<class_t, class_ro_t, class_rw_t, machine_ptr_t>(image, objc_cache, classPtr, &class_name, &cls_data_ro)) != PLCRASH_ESUCCESS)
        return err;
    
    /* Fetch and parse the instance and class method lists. The method list will be NULL if no methods are defined for the category; in that case, we simply skip the category. */
    pl_vm_address_t methods_ptr = (pl_vm_address_t) image->byteorder->swap(category->instanceMethods);
    if (methods_ptr != 0) {
        if ((err = pl_async_objc_parse_objc2_method_list(image, objc_cache, &class_name, false, methods_ptr, callback, ctx)) != PLCRASH_ESUCCESS)
            goto cleanup;
    }
    
    methods_ptr = (pl_vm_address_t) image->byteorder->swap(category->classMethods);
    if (methods_ptr != 0) {
        if ((err = pl_async_objc_parse_objc2_method_list(image, objc_cache, &class_name, true, methods_ptr, callback, ctx)) != PLCRASH_ESUCCESS)
            goto cleanup;
    }
    
    err = PLCRASH_ESUCCESS;

cleanup:
    /* Clean up */
    plcrash_async_macho_string_free(&class_name);
    return err;
}

/**
 * Parse ObjC2 class data from a __objc_classlist section.
 *
 * @param image The Mach-O image to parse.
 * @param objcContext An ObjC context object.
 * @param callback The callback to invoke for each method found.
 * @param ctx A context pointer to pass to the callback.
 * @return PLCRASH_ESUCCESS on success, PLCRASH_ENOTFOUND if no ObjC2 data exists in the image, and another error code if a different error occurred.
 *
 * @tparam class_t The class type, one of pl_objc2_class_32 or pl_objc2_class_64.
 * @tparam class_ro_t The read-only class type, one of pl_objc2_class_data_ro_32 or pl_objc2_class_data_ro_64.
 * @tparam class_rw_t The read-write class type, one of pl_objc2_class_data_rw_32 or pl_objc2_class_data_rw_64.
 * @tparam category_t The category type, one of pl_objc2_category_32 or pl_objc2_category_64.
 * @tparam machine_ptr_t The target's pointer type (eg, uint64_t).
 */
template<typename class_t, typename class_ro_t, typename class_rw_t, typename category_t, typename machine_ptr_t>
static plcrash_error_t pl_async_objc_parse_from_data_section (plcrash_async_macho_t *image, plcrash_async_objc_cache_t *objcContext, plcrash_async_objc_found_method_cb callback, void *ctx) {
    plcrash_error_t err;
    
    /* Map memory objects. */
    err = map_sections(image, objcContext);
    if (err != PLCRASH_ESUCCESS) {
        /* Don't log an error if ObjC data was simply not found */
        if (err != PLCRASH_ENOTFOUND)
            PLCF_DEBUG("Unable to map relevant sections in %s for ObjC2 class parsing, error %d", PLCF_DEBUG_IMAGE_NAME(image), err);
        return err;
    }
    
    /* Get a pointer out of the mapped class list. */
    machine_ptr_t *classPtrs = (machine_ptr_t *) plcrash_async_mobject_remap_address(&objcContext->classMobj, objcContext->classMobj.task_address, 0, objcContext->classMobj.length);
    if (classPtrs == NULL) {
        PLCF_DEBUG("plcrash_async_mobject_remap_address in __objc_classlist for pointer 0x%llx returned NULL", (long long)objcContext->classMobj.address);
        return PLCRASH_EINVALID_DATA;
    }
    
    /* Figure out how many classes are in the class list based on its length and
     * the size of a pointer in the image. */
    pl_vm_size_t classCount = objcContext->classMobj.length / sizeof(machine_ptr_t);
    
    /* Iterate over all classes. */
    for(unsigned i = 0; i < classCount; i++) {
        /* Read the class structure */
        pl_vm_address_t ptr = (pl_vm_address_t) classPtrs[i];
        class_t *classPtr = (class_t *) plcrash_async_mobject_remap_address(&objcContext->objcDataMobj, ptr, 0, sizeof(*classPtr));
        if (classPtr == NULL) {
            classPtr = (class_t *) plcrash_async_mobject_remap_address(&objcContext->dataMobj, ptr, 0, sizeof(*classPtr));
        }
        if (classPtr == NULL) {
            PLCF_DEBUG("plcrash_async_mobject_remap_address in __objc_data and __data for pointer 0x%llx returned NULL", (long long)ptr);
            return PLCRASH_EINVALID_DATA;
        }
        
        /* Parse the class. */
        err = pl_async_objc_parse_objc2_class_methods<class_t, class_ro_t, class_rw_t, machine_ptr_t>(image, objcContext, classPtr, false, callback, ctx);
        if (err != PLCRASH_ESUCCESS) {
            /* Skip unrealized classes; they'll never appear in a live backtrace. */
            if (err == PLCRASH_ENOTFOUND) {
                /* We should reset the error variable to avoid return it from the function. */
                err = PLCRASH_ESUCCESS;
                continue;
            }
            PLCF_DEBUG("pl_async_objc_parse_objc2_class error %d while parsing class", err);
            return err;
        }
        
        /* Read an architecture-appropriate class structure for the metaclass. */
        pl_vm_address_t isa = plcrash_async_objc_isa_pointer((pl_vm_address_t) image->byteorder->swap(classPtr->isa));
        class_t *metaclass = (class_t *) plcrash_async_mobject_remap_address(&objcContext->objcDataMobj, isa, 0, sizeof(*metaclass));
        if (metaclass == NULL) {
            metaclass = (class_t *) plcrash_async_mobject_remap_address(&objcContext->dataMobj, isa, 0, sizeof(*metaclass));
        }
        if (metaclass == NULL) {
            PLCF_DEBUG("plcrash_async_mobject_remap_address in __objc_data and __data for pointer 0x%llx returned NULL", (long long)isa);
            return PLCRASH_EINVALID_DATA;
        }

        /* Parse the metaclass. */
        err = pl_async_objc_parse_objc2_class_methods<class_t, class_ro_t, class_rw_t, machine_ptr_t>(image, objcContext, metaclass, true, callback, ctx);
        if (err != PLCRASH_ESUCCESS) {
            PLCF_DEBUG("pl_async_objc_parse_objc2_class_methods error %d while parsing metaclass", err);
            return err;
        }
    }
    
    /* Get a pointer out of the mapped category list. */
    machine_ptr_t *catPtrs = (machine_ptr_t *) plcrash_async_mobject_remap_address(&objcContext->catMobj, objcContext->catMobj.task_address, 0, objcContext->catMobj.length);
    if (catPtrs == NULL) {
        PLCF_DEBUG("plcrash_async_mobject_remap_address in __objc_catlist for pointer 0x%llx returned NULL", (long long)objcContext->catMobj.address);
        return PLCRASH_EINVALID_DATA;
    }
    
    /* Figure out how many categories are in the category list based on its length and the size of a pointer in the image. */
    pl_vm_size_t catCount = objcContext->catMobj.length / sizeof(*catPtrs);
    
    /* Iterate over all classes. */
    for(unsigned i = 0; i < catCount; i++) {
        /* Read a category pointer at the current index from the appropriate pointer. */
        pl_vm_address_t ptr = (pl_vm_address_t) image->byteorder->swap(catPtrs[i]);
        
        /* Read the category structure. */
        category_t *category = (category_t *) plcrash_async_mobject_remap_address(&objcContext->objcConstMobj, ptr, 0, sizeof(*category));
        if (category == NULL) {
            PLCF_DEBUG("plcrash_async_mobject_remap_address in __objc_const for pointer 0x%llx returned NULL", (long long)ptr);
            return PLCRASH_EINVALID_DATA;
        }

        /* Parse the category. */
        err = pl_async_objc_parse_objc2_category_methods<category_t, class_t, class_ro_t, class_rw_t, machine_ptr_t>(image, objcContext, category, callback, ctx);
        if (err != PLCRASH_ESUCCESS) {
            /* Skip unrealized classes; they'll never appear in a live backtrace. */
            if (err == PLCRASH_ENOTFOUND)
                continue;

            PLCF_DEBUG("pl_async_objc_parse_objc2_class error %d while parsing class", err);
            return err;
        }
    }
    
    return err;
}

/**
 * Initialize an ObjC cache object.
 *
 * @param cache A pointer to the cache object to initialize.
 * @return An error code.
 */
plcrash_error_t plcrash_async_objc_cache_init (plcrash_async_objc_cache_t *cache) {
    cache->gotObjC2Info = false;
    cache->lastImage = NULL;
    cache->objcConstMobjInitialized = false;
    cache->objcConstAxMobjInitialized = false;
    cache->classMobjInitialized = false;
    cache->catMobjInitialized = false;
    cache->objcDataMobjInitialized = false;
    cache->dataMobjInitialized = false;
    cache->classCacheSize = 0;
    cache->classCacheKeys = NULL;
    cache->classCacheValues = NULL;
    return PLCRASH_ESUCCESS;
}

/**
 * Free an ObjC cache object.
 *
 * @param cache A pointer to the cache object to free.
 */
void plcrash_async_objc_cache_free (plcrash_async_objc_cache_t *cache) {
    free_mapped_sections(cache);

    if (cache->classCacheKeys != NULL)
        vm_deallocate(mach_task_self(), (vm_address_t)cache->classCacheKeys, cache_allocation_size(cache));
}

/**
 * @internal
 *
 * Parse Objective-C class data from a Mach-O image, invoking a callback
 * for each method found in the data. This tries both old-style ObjC1
 * class data and new-style ObjC2 data.
 *
 * @param image The image to read class data from.
 * @param cache An ObjC context object.
 * @param callback The callback to invoke for each method.
 * @param ctx The context pointer to pass to the callback.
 * @return An error code.
 */
static plcrash_error_t plcrash_async_objc_parse (plcrash_async_macho_t *image, plcrash_async_objc_cache_t *cache, plcrash_async_objc_found_method_cb callback, void *ctx) {
    plcrash_error_t err;
    
    if (cache == NULL)
        return PLCRASH_EACCESS;
   
    if (!cache->gotObjC2Info) {
        /* Try ObjC1 data. */
        err = pl_async_objc_parse_from_module_info(image, callback, ctx);
    } else {
        /* If it couldn't be found before, don't even bother to try again. */
        err = PLCRASH_ENOTFOUND;
    }
    
    /* If there wasn't any, try ObjC2 data. */
    if (err == PLCRASH_ENOTFOUND) {
        if (image->m64)
            err = pl_async_objc_parse_from_data_section<pl_objc2_class_64, pl_objc2_class_data_ro_64, pl_objc2_class_data_rw_64, pl_objc2_category_64, uint64_t>(image, cache, callback, ctx);
        else
            err = pl_async_objc_parse_from_data_section<pl_objc2_class_32, pl_objc2_class_data_ro_32, pl_objc2_class_data_rw_32, pl_objc2_category_32, uint32_t>(image, cache, callback, ctx);

        if (err == PLCRASH_ESUCCESS) {
            /* ObjC2 info successfully obtained, note that so we can stop trying ObjC1 next time around. */
            cache->gotObjC2Info = true;
        }
    }
    
    return err;
}

struct pl_async_objc_find_method_search_context {
    pl_vm_address_t searchIMP;
    pl_vm_address_t bestIMP;
};

struct pl_async_objc_find_method_call_context {
    pl_vm_address_t searchIMP;
    plcrash_async_objc_found_method_cb outerCallback;
    void *outerCallbackCtx;
};

/**
 * Callback used to search for the method IMP that best matches a search target.
 * The context pointer is a pointer to pl_async_objc_find_method_search_context.
 * The searchIMP field should be set to the IMP to search for. The bestIMP field
 * should be initialized to 0, and will be updated with the best-matching IMP
 * found.
 */
static void pl_async_objc_find_method_search_callback (bool isClassMethod, plcrash_async_macho_string_t *className, plcrash_async_macho_string_t *methodName, pl_vm_address_t imp, void *ctx) {
    struct pl_async_objc_find_method_search_context *ctxStruct = (struct pl_async_objc_find_method_search_context *) ctx;
    
    if (imp >= ctxStruct->bestIMP && imp <= ctxStruct->searchIMP) {
        ctxStruct->bestIMP = imp;
    }
}

/**
 * Callback used to find the method that precisely matches a search target.
 * The context pointer is a pointer to pl_async_objc_find_method_call_context.
 * The searchIMP field should be set to the IMP to search for. The outerCallback
 * will be invoked, passing outerCalblackCtx and the method data for a precise
 * match, if any is found.
 */
static void pl_async_objc_find_method_call_callback (bool isClassMethod, plcrash_async_macho_string_t *className, plcrash_async_macho_string_t *methodName, pl_vm_address_t imp, void *ctx) {
    struct pl_async_objc_find_method_call_context *ctxStruct = (struct pl_async_objc_find_method_call_context *) ctx;
    
    if (imp == ctxStruct->searchIMP && ctxStruct->outerCallback != NULL) {
        ctxStruct->outerCallback(isClassMethod, className, methodName, imp, ctxStruct->outerCallbackCtx);
        ctxStruct->outerCallback = NULL;
    }
}

/**
 * Search for the method that best matches the given code address.
 *
 * @param image The image to search.
 * @param objcContext A pointer to an ObjC context object. Must not be NULL, and must (obviously) be initialized.
 * @param imp The address to search for.
 * @param callback The callback to invoke when the best match is found.
 * @param ctx The context pointer to pass to the callback.
 * @return An error code.
 */
plcrash_error_t plcrash_async_objc_find_method (plcrash_async_macho_t *image, plcrash_async_objc_cache_t *objcContext, pl_vm_address_t imp, plcrash_async_objc_found_method_cb callback, void *ctx) {
    struct pl_async_objc_find_method_search_context searchCtx = {
        .searchIMP = imp
    };

    plcrash_error_t err = plcrash_async_objc_parse(image, objcContext, pl_async_objc_find_method_search_callback, &searchCtx);
    if (err != PLCRASH_ESUCCESS) {
        /* Don't log an error if ObjC data was simply not found */
        if (err != PLCRASH_ENOTFOUND)
            PLCF_DEBUG("pl_async_objc_parse of %p (%s) failure %d", image, PLCF_DEBUG_IMAGE_NAME(image), err);
        return err;
    }
    
    if (searchCtx.bestIMP == 0)
        return PLCRASH_ENOTFOUND;
    
    struct pl_async_objc_find_method_call_context callCtx = {
        .searchIMP = searchCtx.bestIMP,
        .outerCallback = callback,
        .outerCallbackCtx = ctx
    };
    
    return plcrash_async_objc_parse(image, objcContext, pl_async_objc_find_method_call_callback, &callCtx);
}

