Last active
March 3, 2025 18:46
-
-
Save EthanArbuckle/b04e5a2825d99f2fd5efe071be5cb27f to your computer and use it in GitHub Desktop.
Watch for changes to an ivar
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// main.m | |
// | |
// Created by ethanarbuckle | |
// | |
#import <Foundation/Foundation.h> | |
#import <objc/runtime.h> | |
#import <mach/mach.h> | |
#import <execinfo.h> | |
#import <pthread.h> | |
#import <dlfcn.h> | |
#import "mach_excServer.h" | |
static void *watched_address; | |
static mach_port_t exception_port; | |
static void print_thread_backtrace(thread_t thread) { | |
arm_thread_state64_t thread_state; | |
mach_msg_type_number_t thread_state_count = ARM_THREAD_STATE64_COUNT; | |
kern_return_t kr = thread_get_state(thread, ARM_THREAD_STATE64, (thread_state_t)&thread_state, &thread_state_count); | |
if (kr != KERN_SUCCESS) { | |
NSLog(@"Failed to get thread state: %d", kr); | |
return; | |
} | |
void *frame_ptr = (void *)thread_state.__fp; | |
void *link_register = (void *)thread_state.__lr; | |
void *frames[32]; | |
int frame_count = 0; | |
frames[frame_count++] = (void *)thread_state.__pc; | |
if (link_register) { | |
frames[frame_count++] = link_register; | |
} | |
while (frame_ptr && frame_count < 32) { | |
void **fp = (void **)frame_ptr; | |
void *saved_lr = fp[1]; | |
if (!saved_lr) { | |
break; | |
} | |
frames[frame_count++] = saved_lr; | |
frame_ptr = *fp; | |
} | |
char **symbols = backtrace_symbols(frames, frame_count); | |
if (symbols) { | |
for (int i = 0; i < frame_count; i++) { | |
printf("%s\n", symbols[i]); | |
} | |
free(symbols); | |
} | |
} | |
kern_return_t catch_mach_exception_raise(mach_port_t exception_port, mach_port_t thread, mach_port_t task, exception_type_t exception, mach_exception_data_t code, mach_msg_type_number_t codeCnt) { | |
return KERN_FAILURE; | |
} | |
kern_return_t catch_mach_exception_raise_state_identity(mach_port_t exception_port, mach_port_t thread, mach_port_t task, exception_type_t exception, mach_exception_data_t code, mach_msg_type_number_t codeCnt, int *flavor, thread_state_t old_state, mach_msg_type_number_t old_stateCnt, thread_state_t new_state, mach_msg_type_number_t *new_stateCnt) { | |
NSLog(@"Caught ivar modification from:"); | |
uintptr_t fault_address = code[1]; | |
if (fault_address == (uintptr_t)watched_address) { | |
print_thread_backtrace(thread); | |
memcpy(new_state, old_state, old_stateCnt * sizeof(natural_t)); | |
*new_stateCnt = old_stateCnt; | |
arm_thread_state64_t *state = (arm_thread_state64_t *)new_state; | |
state->__pc += 4; | |
return KERN_SUCCESS; | |
} | |
return KERN_FAILURE; | |
} | |
kern_return_t catch_mach_exception_raise_state(mach_port_t exception_port, exception_type_t exception, const mach_exception_data_t code, mach_msg_type_number_t codeCnt, int *flavor, const thread_state_t old_state, mach_msg_type_number_t old_stateCnt, thread_state_t new_state, mach_msg_type_number_t *new_stateCnt) { | |
return KERN_FAILURE; | |
} | |
void *exception_handler(void *unused) { | |
mach_msg_server(mach_exc_server, sizeof(union __RequestUnion__catch_mach_exc_subsystem), exception_port, MACH_MSG_OPTION_NONE); | |
abort(); | |
} | |
static kern_return_t set_thread_debug_state(thread_act_t thread, void *addr) { | |
arm_debug_state64_t debug_state = {}; | |
mach_msg_type_number_t count = ARM_DEBUG_STATE64_COUNT; | |
kern_return_t kr = thread_get_state(thread, ARM_DEBUG_STATE64, (thread_state_t)&debug_state, &count); | |
if (kr != KERN_SUCCESS) { | |
NSLog(@"Failed to get debug state: %d", kr); | |
return kr; | |
} | |
for (int i = 0; i < 16; i++) { | |
debug_state.__wcr[i] = 0; | |
debug_state.__wvr[i] = 0; | |
} | |
debug_state.__wvr[0] = (uint64_t)addr; | |
debug_state.__bcr[0] = 0xe5; | |
debug_state.__bvr[0] = (uint64_t)addr; | |
debug_state.__wcr[0] = (0xf << 5) | (2 << 3) | (3 << 1) | 1; | |
kr = thread_set_state(thread, ARM_DEBUG_STATE64, (thread_state_t)&debug_state, ARM_DEBUG_STATE64_COUNT); | |
if (kr != KERN_SUCCESS) { | |
NSLog(@"Failed to set debug state: %d", kr); | |
return kr; | |
} | |
arm_debug_state64_t verify = {}; | |
count = ARM_DEBUG_STATE64_COUNT; | |
kr = thread_get_state(thread, ARM_DEBUG_STATE64, (thread_state_t)&verify, &count); | |
if (kr == KERN_SUCCESS) { | |
if (verify.__bvr[0] != debug_state.__bvr[0] || verify.__bcr[0] != debug_state.__bcr[0]) { | |
printf("state mismatch:\n"); | |
printf("BVR0: got 0x%llx, wanted 0x%llx\n", verify.__bvr[0], debug_state.__bvr[0]); | |
printf("BCR0: got 0x%llx, wanted 0x%llx\n", verify.__bcr[0], debug_state.__bcr[0]); | |
} | |
} | |
return kr; | |
} | |
void watch_ivar(id instance, const char *ivarName) { | |
Ivar ivar = class_getInstanceVariable([instance class], ivarName); | |
if (ivar == NULL) { | |
NSLog(@"Ivar %s not found", ivarName); | |
return; | |
} | |
ptrdiff_t offset = ivar_getOffset(ivar); | |
void *addr = (__bridge void *)instance + offset; | |
watched_address = addr; | |
kern_return_t kr = mach_port_allocate(mach_task_self_, MACH_PORT_RIGHT_RECEIVE, &exception_port); | |
if (kr != KERN_SUCCESS) { | |
NSLog(@"Failed to allocate exception port: %d", kr); | |
return; | |
} | |
kr = mach_port_insert_right(mach_task_self(), exception_port, exception_port, MACH_MSG_TYPE_MAKE_SEND); | |
if (kr != KERN_SUCCESS) { | |
NSLog(@"Failed to insert port right: %d", kr); | |
return; | |
} | |
kr = task_set_exception_ports(mach_task_self_, EXC_MASK_BREAKPOINT, exception_port, EXCEPTION_STATE_IDENTITY | MACH_EXCEPTION_CODES, ARM_THREAD_STATE64); | |
if (kr != KERN_SUCCESS) { | |
NSLog(@"Failed to set exception ports: %d", kr); | |
return; | |
} | |
pthread_t exc_thread; | |
pthread_create(&exc_thread, NULL, exception_handler, NULL); | |
thread_act_array_t threads; | |
mach_msg_type_number_t thread_count; | |
kr = task_threads(mach_task_self(), &threads, &thread_count); | |
if (kr != KERN_SUCCESS) { | |
NSLog(@"Failed to get task threads: %d", kr); | |
return; | |
} | |
for (int i = 0; i < thread_count; i++) { | |
kr = set_thread_debug_state(threads[i], addr); | |
if (kr != KERN_SUCCESS) { | |
NSLog(@"Failed to set debug state for thread %d: %d", i, kr); | |
} | |
mach_port_deallocate(mach_task_self(), threads[i]); | |
} | |
vm_deallocate(mach_task_self(), (vm_address_t)threads, thread_count * sizeof(*threads)); | |
} | |
@interface TestClass : NSObject { | |
@public | |
NSString *_testIvar; | |
} | |
@end | |
@implementation TestClass | |
- (instancetype)init { | |
if (self = [super init]) { | |
_testIvar = @"1st value"; | |
int delay = 5; | |
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ | |
self->_testIvar = @"4th value"; | |
}); | |
} | |
return self; | |
} | |
@end | |
int main(int argc, const char * argv[]) { | |
@autoreleasepool { | |
TestClass *obj = [[TestClass alloc] init]; | |
watch_ivar(obj, "_testIvar"); | |
obj->_testIvar = @"2nd value"; | |
sleep(1); | |
[obj setValue:@"3rd value" forKey:@"_testIvar"]; | |
CFRunLoopRun(); | |
} | |
return 0; | |
} | |
``` | |
Caught ivar modification from: | |
0 libobjc.A.dylib 0x000000019d7824a0 objc_storeStrong + 48 | |
1 libobjc.A.dylib 0x000000019d7824a0 objc_storeStrong + 48 | |
2 ivarmon 0x0000000100488da4 main + 100 | |
3 dyld 0x000000019d7cc274 start + 2840 | |
Caught ivar modification from: | |
0 libobjc.A.dylib 0x000000019d7824a0 objc_storeStrong + 48 | |
1 libobjc.A.dylib 0x000000019d7824a0 objc_storeStrong + 48 | |
2 Foundation 0x000000019edc846c -[NSObject(NSKeyValueCoding) setValue:forKey:] + 324 | |
3 ivarmon 0x0000000100488dc8 main + 136 | |
4 dyld 0x000000019d7cc274 start + 2840 | |
Caught ivar modification from: | |
0 libobjc.A.dylib 0x000000019d7824a0 objc_storeStrong + 48 | |
1 libobjc.A.dylib 0x000000019d7824a0 objc_storeStrong + 48 | |
2 ivarmon 0x0000000100488ca0 __17-[TestClass init]_block_invoke + 48 | |
3 libdispatch.dylib 0x00000001006ea8a4 _dispatch_client_callout + 20 | |
4 libdispatch.dylib 0x00000001006ee108 _dispatch_continuation_pop + 700 | |
5 libdispatch.dylib 0x00000001007099c4 _dispatch_source_latch_and_call + 488 | |
6 libdispatch.dylib 0x0000000100708068 _dispatch_source_invoke + 872 | |
7 libdispatch.dylib 0x00000001006fdbf4 _dispatch_main_queue_drain + 768 | |
8 libdispatch.dylib 0x00000001006fd8e4 _dispatch_main_queue_callback_4CF + 44 | |
9 CoreFoundation 0x000000019dc75680 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16 | |
10 CoreFoundation 0x000000019dc3517c __CFRunLoopRun + 1996 | |
11 CoreFoundation 0x000000019dc34334 CFRunLoopRunSpecific + 572 | |
12 CoreFoundation 0x000000019dcafa10 CFRunLoopRun + 64 | |
13 ivarmon 0x0000000100488dcc main + 140 | |
14 dyld 0x000000019d7cc274 start + 2840 | |
``` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment