Is it possible to get class name when implementing malloc_logger function in iOS?

103 views Asked by At

From here, we know if malloc_logger global function is defined, it will be called whenever there is a malloc or free operation. I want to use it to record memory allocations in my app like this:

typedef void(malloc_logger_t)(uint32_t type,
                uintptr_t arg1,
                uintptr_t arg2,
                uintptr_t arg3,
                uintptr_t result,
                uint32_t num_hot_frames_to_skip);
extern malloc_logger_t *malloc_logger;
void my_malloc_stack_logger(uint32_t type, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3, uintptr_t result, uint32_t num_hot_frames_to_skip); 
malloc_logger = my_malloc_stack_logger; 

void my_malloc_stack_logger(uint32_t type, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3, uintptr_t result, uint32_t num_hot_frames_to_skip)
{
    // do my work
}

In my_malloc_stack_logger, I can directly get the allocated size and address. But how about object types? I want to record the class name if it is an NSObject instance. Is it possible to get this information?

1

There are 1 answers

2
The Dreams Wind On

After playing around with the hook, it looks like what you want to achieve is not quite possible.

First problem here is that if you try to read a class name from within this function (by calling any of object_getClassName, class_getName or NSStringFromClass), this action on its own tends to trigger new allocations. That apparently happens because some Cocoa classes load lazily. I noticed however that when requesting all classes with objc_getClassList it makes a lot of preliminary allocations that helps to avoid them later on. So my idea is to cache all class names before subscribing to the allocations hook and refer to the cached values when needed. For the storage I used Apple's CFMutableDictionary:

CFMutableDictionaryRef objc_class_records;

void refresh_objc_class_list(void) {
    
    pthread_mutex_lock(&objc_class_records_mutex);
    if (objc_class_records) {
        CFRelease(objc_class_records);
    }
        
    objc_class_records = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    
    // The buffer needs to accomodate at least 26665 instances
    static const unsigned buffer_length = 100000;
    Class registered_classes[buffer_length];
    objc_getClassList(registered_classes, buffer_length);
    
    for (unsigned i = 0; i < buffer_length; ++i) {
        if (!registered_classes[i]) {
            break;
        }
        const Class class = registered_classes[i];
        const CFStringRef class_name = CFStringCreateWithCString(kCFAllocatorDefault, class_getName(class), kCFStringEncodingUTF8);
        CFDictionarySetValue(objc_class_records, class, class_name);
        CFRelease(class_name);
    }
    
}

Be advised that you don't want to have it called when the malloc logger is enabled (especially from within the hook itself).

Now you need to obtain a Class instance from the Objective-C objects. Depending on the type of allocation, the pointer argument goes to fifth or third parameter:

void my_malloc_logger(uint32_t type, uintptr_t param0, uintptr_t param1, uintptr_t param2,
                      uintptr_t param3, uint32_t frames_to_skip) {
    
    void *ptr = NULL;
    unsigned size = 0;
    
    switch (type) {
        case MALLOC_OP_MALLOC:
        case MALLOC_OP_CALLOC:
            ptr = (void *)param3;
            size = (unsigned)param1;
            break;
        case MALLOC_OP_REALLOC:
            ptr = (void *)param3;
            size = (unsigned)param2;
            break;
        case MALLOC_OP_FREE:
            ptr = (void *)param1;
            break;
    }
    
    id objc_ptr = (id)ptr;
    Class objc_class = object_getClass(objc_ptr);
    if (!objc_class) {
        return;
    }

    const CFStringRef class_name;
    const bool found = CFDictionaryGetValueIfPresent(objc_class_records, objc_class, (const void **)&class_name);

    if (found) {
        const static unsigned name_max_length = 256;
        char c_class_name[name_max_length];
        if (CFStringGetCString(class_name, c_class_name, name_max_length, kCFStringEncodingUTF8)) {
            const char *alloc_name = alloc_type_name(type);
            nomalloc_printf_sync("%7s: Pointer: %p; Size: %u; Obj-C class: \"%s\"\n", alloc_name, objc_ptr, size, c_class_name);
        }
    }
    
}

And now why it won't work as expected:

  1. object_getClass is not able to tell whether a pointer is an object of Cococa classes at the time of allocation (it will find the class, however, when the object is already allocated, e.g. before deallocation). Thus, the following code:
[NSObject new];

Will produce output similar to this:

 CALLOC: Pointer: 0x600000600080; Size: 16
   FREE: Pointer: 0x600000600080; Size: 0; Obj-C class: "NSObject"
  1. Most of the standard Cocoa classes are in fact so-called Class Clusters and under the hood the actual allocation happens for an instance of a private class (which is not always recognisable by its public interface), thus this information is incomplete and sometimes misleading.

There are also many other factors which need to be taken into account (which i didn't cover here because it's beyond the question asked): the way you output data to standard output should not cause allocation by itself; the logging needs synchronisation since allocation happens a lot from any number of threads; if you want to enable/disable recording the Objective-C classes (or update the cache occasionally) access to the storage also needs to be synchronised.

Having that said if you are satisfied with what can be done with it, feel free to refer to the repository I made where this approach is already implemented in a form of a static library.