#define GNUSTEP_LIBOBJC_NO_LEGACY #include "objc/runtime.h" #include "objc/toydispatch.h" #include "class.h" #include "ivar.h" #include "lock.h" #include "objc/objc-auto.h" #include "visibility.h" #include #include #include #include #include "gc_ops.h" #define I_HIDE_POINTERS /** * Dispatch queue used to invoke finalizers. */ static dispatch_queue_t finalizer_queue; /** * Should finalizers be invoked in their own thread? */ static BOOL finalizeThreaded; /** * Should we do some (not 100% reliable) buffer overflow checking. */ static size_t canarySize; /** * The canary value. Used to check for overruns. When an allocation is * finalized, we check whether it ends with this value. */ static uint32_t canary; /** * Destination to write allocation log to. This can be used to implement the * equivalent of malloc_history */ static FILE *allocationLog; struct objc_slot* objc_get_slot(Class cls, SEL selector); /* * Citing boehm-gc's README.linux: * * 3a) Every file that makes thread calls should define GC_LINUX_THREADS and * _REENTRANT and then include gc.h. Gc.h redefines some of the * pthread primitives as macros which also provide the collector with * information it requires. */ #ifdef __linux__ # define GC_LINUX_THREADS # ifndef _REENTRANT # define _REENTRANT # endif #endif #include #include #ifndef __has_builtin # define __has_builtin(x) 0 #endif #if !__has_builtin(__sync_swap) #define __sync_swap __sync_lock_test_and_set #endif void call_cxx_destruct(id obj); #ifdef NO_EXECINFO static inline void dump_stack(char *msg, void *addr) {} #else #include static inline void dump_stack(char *msg, void *addr) { if (NULL == allocationLog) { return; } void *array[30]; int frames = backtrace(array, 30); fprintf(allocationLog, "%s %p\n", msg, addr); fflush(allocationLog); backtrace_symbols_fd(array, frames, fileno(allocationLog)); fflush(allocationLog); } #endif Class dead_class; Class objc_lookup_class(const char*); GC_descr gc_typeForClass(Class cls); void gc_setTypeForClass(Class cls, GC_descr type); static unsigned long collectionType(unsigned options) { // Low 2 bits in GC options are used for the return options & 3; } static size_t CollectRatio = 0x10000; static size_t CollectThreshold = 0x10000; void objc_set_collection_threshold(size_t threshold) { CollectThreshold = threshold; } void objc_set_collection_ratio(size_t ratio) { CollectRatio = ratio; } void objc_collect(unsigned long options) { size_t newAllocations = GC_get_bytes_since_gc(); // Skip collection if we haven't allocated much memory and this is a // collect if needed collection if ((options & OBJC_COLLECT_IF_NEEDED) && (newAllocations < CollectThreshold)) { return; } switch (collectionType(options)) { case OBJC_RATIO_COLLECTION: if (newAllocations >= CollectRatio) { GC_gcollect(); } else { GC_collect_a_little(); } break; case OBJC_GENERATIONAL_COLLECTION: GC_collect_a_little(); break; case OBJC_FULL_COLLECTION: GC_gcollect(); break; case OBJC_EXHAUSTIVE_COLLECTION: { size_t freeBytes = 0; while (GC_get_free_bytes() != freeBytes) { freeBytes = GC_get_free_bytes(); GC_gcollect(); } } } } BOOL objc_collectingEnabled(void) { return GC_dont_gc == 0; } void objc_gc_disable(void) { GC_disable(); } void objc_gc_enable(void) { GC_enable(); } void* objc_gc_collectable_address(void* ptr) { return GC_base(ptr); } BOOL objc_atomicCompareAndSwapPtr(id predicate, id replacement, volatile id *objectLocation) { return __sync_bool_compare_and_swap(objectLocation, predicate, replacement); } BOOL objc_atomicCompareAndSwapPtrBarrier(id predicate, id replacement, volatile id *objectLocation) { return __sync_bool_compare_and_swap(objectLocation, predicate, replacement); } BOOL objc_atomicCompareAndSwapGlobal(id predicate, id replacement, volatile id *objectLocation) { return objc_atomicCompareAndSwapPtr(predicate, replacement, objectLocation); } BOOL objc_atomicCompareAndSwapGlobalBarrier(id predicate, id replacement, volatile id *objectLocation) { return objc_atomicCompareAndSwapPtr(predicate, replacement, objectLocation); } BOOL objc_atomicCompareAndSwapInstanceVariable(id predicate, id replacement, volatile id *objectLocation) { return objc_atomicCompareAndSwapPtr(predicate, replacement, objectLocation); } BOOL objc_atomicCompareAndSwapInstanceVariableBarrier(id predicate, id replacement, volatile id *objectLocation) { return objc_atomicCompareAndSwapPtr(predicate, replacement, objectLocation); } id objc_assign_strongCast(id val, id *ptr) { *ptr = val; return val; } id objc_assign_global(id val, id *ptr) { if (isGCEnabled) { GC_add_roots(ptr, ptr+1); } *ptr = val; return val; } id objc_assign_ivar(id val, id dest, ptrdiff_t offset) { *(id*)((char*)dest+offset) = val; return val; } struct memmove_args { void *dst; const void *src; size_t size; }; static void* callMemmove(void *args) { struct memmove_args *a = args; memmove(a->dst, a->src, a->size); return a->dst; } void *objc_memmove_collectable(void *dst, const void *src, size_t size) { // For small copies, we copy onto the stack then copy from the stack. This // ensures that pointers are always present in a scanned region. For // larger copies, we just lock the GC to prevent it from freeing the memory // while the system memmove() does the real copy. The first case avoids // the need to acquire the lock, but will perform worse for very large // copies since we are copying the data twice (and because stack space is // relatively scarce). if (size < 128) { char buffer[128]; memcpy(buffer, src, size); __sync_synchronize(); memcpy(dst, buffer, size); // In theory, we should zero the on-stack buffer here to prevent the GC // from seeing spurious pointers, but it's not really important because // the contents of the buffer is duplicated on the heap and overwriting // it will typically involve another copy to this function. This will // not be the case if we are storing in a dead object, but that's // probably sufficiently infrequent that we shouldn't worry about // optimising for that case. return dst; } struct memmove_args args = {dst, src, size}; return GC_call_with_alloc_lock(callMemmove, &args); } /** * Weak Pointers: * * To implement weak pointers, we store the hidden pointer (bits all flipped) * in the real address. We tell the GC to zero the pointer when the associated * object is finalized. The read barrier locks the GC to prevent it from * freeing anything, deobfuscates the pointer (at which point it becomes a * GC-visible on-stack pointer), and then returns it. */ static void *readWeakLocked(void *ptr) { void *val = *(void**)ptr; return 0 == val ? val : REVEAL_POINTER(val); } id objc_read_weak(id *location) { if (!isGCEnabled) { return *location; } return GC_call_with_alloc_lock(readWeakLocked, location); } id objc_assign_weak(id value, id *location) { if (!isGCEnabled) { *location = value; return value; } // Temporarily zero this pointer and get the old value id old = __sync_swap(location, 0); if (0 != old) { GC_unregister_disappearing_link((void**)location); } // If the value is not GC'd memory (e.g. a class), the collector will crash // trying to collect it when you add it as the target of a disappearing // link. if (0 != GC_base(value)) { GC_GENERAL_REGISTER_DISAPPEARING_LINK((void**)location, value); } // If some other thread has modified this, then we may have two different // objects registered to make this pointer 0 if either is destroyed. This // would be bad, so we need to make sure that we unregister them and // register the correct one. if (!__sync_bool_compare_and_swap(location, old, (id)HIDE_POINTER(value))) { return objc_assign_weak(value, location); } return value; } static SEL finalize; Class zombie_class; static void runFinalize(void *addr, void *context) { dump_stack("Freeing Object: ", addr); id obj = addr; size_t size = (uintptr_t)context; if ((canarySize > 0) && (*(uint32_t*)((char*)obj + size) != canary)) { fprintf(stderr, "Something wrote past the end of %p\n", addr); if (obj->isa != Nil) { fprintf(stderr, "Instance of %s\n", obj->isa->name); } abort(); } //fprintf(stderr, "FINALIZING %p (%s)\n", addr, ((id)addr)->isa->name); if (Nil == obj->isa) { return; } struct objc_slot *slot = objc_get_slot(obj->isa, finalize); if (NULL != slot) { slot->method(obj, finalize); } call_cxx_destruct(obj); *(void**)addr = zombie_class; } static void collectIvarForClass(Class cls, GC_word *bitmap) { for (unsigned i=0 ; (cls->ivars != 0) && (iivars->count) ; i++) { struct objc_ivar *ivar = &cls->ivars->ivar_list[i]; size_t start = ivar->offset; size_t end = i+1 < cls->ivars->count ? cls->ivars->ivar_list[i+1].offset : cls->instance_size; switch (ivar->type[0]) { case '[': case '{': case '(': // If the structure / array / union type doesn't contain any // pointers, then skip it. We still need to be careful of packed if ((strchr(ivar->type, '^') == 0) && (strchr(ivar->type, '@') == 0)) { break; } // Explicit pointer types case '^': case '@': for (unsigned b=(start / sizeof(void*)) ; b<(end/sizeof(void*)) ; b++) { GC_set_bit(bitmap, b); } } } if (cls->super_class) { collectIvarForClass(cls->super_class, bitmap); } } static GC_descr descriptor_for_class(Class cls) { GC_descr descr = gc_typeForClass(cls); if (0 != descr) { return descr; } LOCK_RUNTIME_FOR_SCOPE(); descr = (GC_descr)gc_typeForClass(cls); if (0 != descr) { return descr; } size_t size = cls->instance_size / 8 + 1; GC_word bitmap[size]; memset(bitmap, 0, size); collectIvarForClass(cls, bitmap); // It's safe to round down here - if a class ends with an ivar that is // smaller than a pointer, then it can't possibly be a pointer. //fprintf(stderr, "Class is %d byes, %d words\n", cls->instance_size, cls->instance_size/sizeof(void*)); descr = GC_make_descriptor(bitmap, cls->instance_size / sizeof(void*)); gc_setTypeForClass(cls, descr); return descr; } static id allocate_class(Class cls, size_t extra) { size_t size = class_getInstanceSize(cls); if (canarySize) { extra += 4; } id obj = 0; // If there are some extra bytes, they may contain pointers, so we ignore // the type if (extra > 0) { size += extra; // FIXME: Overflow checking! obj = GC_MALLOC(size); } else { obj = GC_MALLOC_EXPLICITLY_TYPED(size, descriptor_for_class(cls)); } //fprintf(stderr, "Allocating %p (%s + %d). Base is %p\n", obj, cls->name, extra, GC_base(obj)); // It would be nice not to register a finaliser if the object didn't // implement finalize or .cxx_destruct methods. Unfortunately, this is not // possible, because a class may add a finalize method as it runs. GC_REGISTER_FINALIZER_NO_ORDER(obj, runFinalize, (void*)(uintptr_t)size-canarySize, 0, 0); if (canarySize > 0) { *(uint32_t*)((char*)obj + size - canarySize) = canary; } dump_stack("Allocating object", obj); return obj; } static void free_object(id obj) {} id objc_allocate_object(Class cls, int extra) { return class_createInstance(cls, extra); } static void registerThread(BOOL errorOnNotRegistered) { struct GC_stack_base base; if (GC_get_stack_base(&base) != GC_SUCCESS) { fprintf(stderr, "Unable to find stack base for new thread\n"); abort(); } switch (GC_register_my_thread(&base)) { case GC_SUCCESS: if (errorOnNotRegistered) { fprintf(stderr, "Thread should have already been registered with the GC\n"); } case GC_DUPLICATE: return; case GC_NO_THREADS: case GC_UNIMPLEMENTED: fprintf(stderr, "Unable to register stack\n"); abort(); } } void objc_registerThreadWithCollector(void) { registerThread(NO); } void objc_unregisterThreadWithCollector(void) { GC_unregister_my_thread(); } void objc_assertRegisteredThreadWithCollector() { registerThread(YES); } /** * Structure stored for each GC */ static struct gc_refcount { /** Reference count */ intptr_t refCount; /** Strong pointer */ id ptr; } null_refcount = {0}; static int refcount_compare(const void *ptr, struct gc_refcount rc) { return ptr == rc.ptr; } static uint32_t ptr_hash(const void *ptr) { // Bit-rotate right 4, since the lowest few bits in an object pointer will // always be 0, which is not so useful for a hash value return ((uintptr_t)ptr >> 4) | ((uintptr_t)ptr << ((sizeof(id) * 8) - 4)); } static uint32_t refcount_hash(struct gc_refcount rc) { return ptr_hash(rc.ptr); } static int isEmpty(struct gc_refcount rc) { return rc.ptr == NULL; } #define MAP_TABLE_VALUE_NULL isEmpty #define MAP_TABLE_NAME refcount #define MAP_TABLE_COMPARE_FUNCTION refcount_compare #define MAP_TABLE_HASH_KEY ptr_hash #define MAP_TABLE_HASH_VALUE refcount_hash #define MAP_TABLE_VALUE_TYPE struct gc_refcount #define MAP_TABLE_VALUE_PLACEHOLDER null_refcount #define MAP_TABLE_TYPES_BITMAP (1<<(offsetof(struct gc_refcount, ptr) / sizeof(void*))) #define MAP_TABLE_ACCESS_BY_REFERENCE #include "hash_table.h" static refcount_table *refcounts; id objc_gc_retain(id object) { struct gc_refcount *refcount = refcount_table_get(refcounts, object); if (NULL == refcount) { LOCK_FOR_SCOPE(&(refcounts->lock)); refcount = refcount_table_get(refcounts, object); if (NULL == refcount) { struct gc_refcount rc = { 1, object}; refcount_insert(refcounts, rc); return object; } } __sync_fetch_and_add(&(refcount->refCount), 1); return object; } void objc_gc_release(id object) { struct gc_refcount *refcount = refcount_table_get(refcounts, object); // This object has not been explicitly retained, don't release it if (0 == refcount) { return; } if (0 == __sync_sub_and_fetch(&(refcount->refCount), 1)) { LOCK_FOR_SCOPE(&(refcounts->lock)); refcount->ptr = 0; __sync_synchronize(); // If another thread has incremented the reference count while we were // doing this, then we need to add the count back into the table, // otherwise we can carry on. if (!__sync_bool_compare_and_swap(&(refcount->refCount), 0, 0)) { refcount->ptr = object; } } } int objc_gc_retain_count(id object) { struct gc_refcount *refcount = refcount_table_get(refcounts, object); return (0 == refcount) ? 0 : refcount->refCount; } static void nuke_buffer(void *addr, void *s) { return; dump_stack("Freeing allocation: ", addr); uintptr_t size = (uintptr_t)s; if (canary != *(uint32_t*)((char*)addr + size)) { fprintf(stderr, "Something wrote past the end of memory allocation %p\n", addr); abort(); } memset(addr, 0, size); } void* objc_gc_allocate_collectable(size_t size, BOOL isScanned) { void *buffer; if (isScanned) { buffer = GC_MALLOC(size+canarySize); } else { buffer = GC_MALLOC_ATOMIC(size+canarySize); memset(buffer, 0, size); } if (canarySize > 0) { *(uint32_t*)((char*)buffer + size) = canary; GC_REGISTER_FINALIZER_NO_ORDER(buffer, nuke_buffer, (void*)(uintptr_t)size, 0, 0); } dump_stack("Allocating memory", buffer); return buffer; } void* objc_gc_reallocate_collectable(void *ptr, size_t size, BOOL isScanned) { if (0 == size) { return 0; } void *new = isScanned ? GC_MALLOC(size) : GC_MALLOC_ATOMIC(size); if (0 == new) { return 0; } if (NULL != ptr) { size_t oldSize = GC_size(ptr); if (oldSize < size) { size = oldSize; } memcpy(new, ptr, size); } dump_stack("New allocation from realloc: ", new); return new; } static void collectAndDumpStats(int signalNo) { objc_collect(OBJC_EXHAUSTIVE_COLLECTION); GC_dump(); } static void deferredFinalizer(void) { GC_invoke_finalizers(); } static void runFinalizers(void) { //fprintf(stderr, "RUNNING FINALIZERS\n"); if (finalizeThreaded) { dispatch_async_f(finalizer_queue, deferredFinalizer, NULL); } else { GC_invoke_finalizers(); } } PRIVATE void init_gc(void) { //GC_no_dls = 1; //GC_enable_incremental(); GC_INIT(); char *envValue; // Dump GC stats on exit - uncomment when debugging. if (getenv("LIBOBJC_DUMP_GC_STATUS_ON_EXIT")) { atexit(GC_dump); } if ((envValue = getenv("LIBOBJC_LOG_ALLOCATIONS"))) { allocationLog = fopen(envValue, "a"); } if ((envValue = getenv("LIBOBJC_CANARIES"))) { unsigned s = envValue[0] ? strtol(envValue, NULL, 10) : 123; srandom(s); canarySize = sizeof(uint32_t); canary = random(); } if ((envValue = getenv("LIBOBJC_DUMP_GC_STATUS_ON_SIGNAL"))) { int s = envValue[0] ? (int)strtol(envValue, NULL, 10) : SIGUSR2; signal(s, collectAndDumpStats); } //GC_clear_roots(); } BOOL objc_collecting_enabled(void) { // Lock the GC in the current state once it's been queried. This prevents // the loading of any modules with an incompatible GC mode. current_gc_mode = isGCEnabled ? GC_Required : GC_None; return isGCEnabled; } void objc_startCollectorThread(void) { if (YES == finalizeThreaded) { return; } finalizer_queue = dispatch_queue_create("ObjC finalizeation thread", 0); finalizeThreaded = YES; } void objc_clear_stack(unsigned long options) { // This isn't a very good implementation - we should really be working out // how much stack space is left somehow, but this is not possible to do // portably. int i[1024]; int *addr = &i[0]; memset(addr, 0, 1024); // Tell the compiler that something that it doesn't know about is touching // this memory, so it shouldn't optimise the allocation and memset away. __asm__ volatile ("" : : "m"(addr) : "memory"); } BOOL objc_is_finalized(void *ptr) { return *(Class*)ptr == zombie_class; } // FIXME: Stub implementation that should be replaced with something better void objc_finalizeOnMainThread(Class cls) {} static void *debug_malloc(size_t s) { return GC_MALLOC_UNCOLLECTABLE(s); } static void debug_free(void *ptr) { GC_FREE(ptr); } PRIVATE struct gc_ops gc_ops_boehm = { .allocate_class = allocate_class, .free_object = free_object, .malloc = debug_malloc, .free = debug_free, }; extern struct objc_class _NSConcreteStackBlock; void *_Block_copy(void *src); PRIVATE void enableGC(BOOL exclude) { isGCEnabled = YES; gc = &gc_ops_boehm; refcount_initialize(&refcounts, 4096); finalize = sel_registerName("finalize"); GC_finalizer_notifier = runFinalizers; }