iOS AOP Techniques

2022, Nov 14    

Aspect Ortiented Programming is one of the popular topic in past years, as the promoters claims this technique effectively mitigates the challenges co-exists with modern Objective Oriented Programming -> entanglement & dispersion.

  • Entanglement: happens when a business module implements cross-cutting concerns which involves multiple use cases. For example a module involves providing both network as well as logging capabilities. Making the module less focusable on its original design intentions.
  • Dispersion: happens when the calls to the above modules spread through out the entire application.

Now via adopting the AOP concept, the execution code no longer requires the developer to attentionally inserting the interface call of the modules into the code body. Instead, by declaring the suitable insertion point, the relevant aspecs interface invocation is "weaved" into the code execution. Some of the core concepts are listed below:

  • JoinPoint: Defines the joint point where the aspect invocation can be inserted into the execution code body.
  • PointCut: Defines how the joint point is recgonised and how the aspects is inserted.
  • Advice: Defines the implementation of the aspects invocation to be inserted.
  • Weaving: Describes the overall process of identify, insert, and forming of the eventual code. The weaving can happen in either compile time or runtime dynammically.

In the world of iOS programming, ObjC and Swift relies on different techniques to achive the concepts. For Swift, due to the nature of the strong typing, extension and property wrapper are among those limited choices. For ObjC since it’s strong runtime capability, the ways to achive AOP appears to be more versatile.

This article discuss those typical frameworks/techniques can be used in ObjC: fishhook and Aspects.

2. Techniques

2.1 Fishhook

Fishhook is implemented based on the dyld(the dynamic linker), the dyld is responsible to load the app macho file into segements. In fishhook, the library use the following code to conduct inject:

// If this was the first call, 
// register callback for image additions 
// which is also invoked for existing images, otherwise, 
// just run on existing images

if (!_rebindings_head->next) {
  // listens for initial dylb loading
  _dyld_register_func_for_add_image(
    _rebind_symbols_for_image
    );
} else {
  // if loaded, swap the func pointers
  uint32_t c = _dyld_image_count();
  for (uint32_t i = 0; i < c; i++) {
    _rebind_symbols_for_image(
      _dyld_get_image_header(i), 
      _dyld_get_image_vmaddr_slide(i)
    );
  }
}

In the function rebind_symbols_for_image, the strtab, symtab, indirect_symtab is calculated via scanning the macho LOAD_COMMAND.

For each of the segements with type equals to “SEG_TEXT” or “SECT_TEXT” (which refers to the executable code), scan its corresponding sections via:

for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
 
  cur_seg_cmd = (segment_command_t *)cur;
  if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {

    // excluding out `DATA` type segements. (data/stack/heap subtypes).
    if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
        strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
      continue;
    }

    // iterate through sections in current Text/text segement
    for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
      section_t *sect = (section_t *)(cur + sizeof(segment_command_t)) + j;
      // scanning for lazy symbol pointers
      if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
        perform_rebinding_with_section(
          rebindings, sect, 
          slide, symtab, 
          strtab, indirect_symtab
        );
      }
      // scanning for non-lazy symbol pointers
      if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
        perform_rebinding_with_section(
          rebindings, sect, 
          slide, symtab, 
          strtab, indirect_symtab
        );
      }
    }
  }
}

Now let’s take a look at the final step, the function pointer & rebinding: for each bindings in the section, based on the indexes of the section in the indrect symbol table, calculate its symbol table offset and hence deduce its symbol table name. The symbol name is compared against rebinding target, if there is the match, replace the binding’s function pointer with the destination function pointer.

uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);

for (uint i = 0; i < section->size / sizeof(void *); i++) {
  uint32_t symtab_index = indirect_symbol_indices[i];
  if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
      symtab_index == (INDIRECT_SYMBOL_LOCAL   | INDIRECT_SYMBOL_ABS)) {
    continue;
  }
  uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
  char *symbol_name = strtab + strtab_offset;
  bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
  struct rebindings_entry *cur = rebindings;
  while (cur) {
    for (uint j = 0; j < cur->rebindings_nel; j++) {
      if (symbol_name_longer_than_1
          && strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
        kern_return_t err;

        if (cur->rebindings[j].replaced != NULL
            && indirect_symbol_bindings[i] != cur->rebindings[j].replacement)
          *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];

        /**
         * 1. Moved the vm protection modifying codes to here to reduce the
         *    changing scope.
         * 2. Adding VM_PROT_WRITE mode unconditionally because vm_region
         *    API on some iOS/Mac reports mismatch vm protection attributes.
         * -- Lianfu Hao Jun 16th, 2021
         **/
        err = vm_protect (
                          mach_task_self (),
                          (uintptr_t)indirect_symbol_bindings,
                          section->size,
                          0,
                          VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY
                          );
        if (err == KERN_SUCCESS) {
          /**
           * Once we failed to change the vm protection, we
           * MUST NOT continue the following write actions!
           * iOS 15 has corrected the const segments prot.
           * -- Lionfore Hao Jun 11th, 2021
           **/
          indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
        }
        goto symbol_loop;
      }
    }
    cur = cur->next;
  }
symbol_loop:;
}

The fishhook’s readme page provides a quite abstract description about the above process:

image

2.2 Aspects

Aspects utilizes ObjC message forwarding to encapusulate the “cross-cutting” concerns. It is similar to the OCMock framework, internally compared to the fishhook, the implementation is more straight forward.

static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError * __autoreleasing *error) {

  // parameter check 
  NSCParameterAssert(self);
  NSCParameterAssert(selector);
  NSCParameterAssert(block);

  // start perform hook
  __block AspectIdentifier *identifier = nil;
  // lock
  aspect_performLocked(^{
  // avoid of hook in: runtime methods, dealloct methods, classes that are already hooked.
    if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
  // generate the container in a lazy manner, the container contains all the hooked method for a selector (it means one selector can be hooked for multiple times.)
      AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
  // assemble the identifer with `@xxx_prefix_@selector_name`
      identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
  // insert the identifier into container and ensure the hook works~
      if (identifier) {
        [aspectContainer addAspect:identifier withOptions:options];
  // modify the class to allow message interception.
        aspect_prepareClassAndHookSelector(self, selector, error);
      }
    }
  });
  return identifier;
}

Inside the aspect_prepareClassAndHookSelector, a hookClass is created via aspect_hookClass, which creates a subclass of the current object’s class and resign the class type of the current object to the hooked subclass. In the hooked class, all the forwardInvocation: is routed to the internal method: __ASPECTS_ARE_BEING_CALLED__.

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
  NSCParameterAssert(selector);
  Class klass = aspect_hookClass(self, error);
  Method targetMethod = class_getInstanceMethod(klass, selector);
  IMP targetMethodIMP = method_getImplementation(targetMethod);
  if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
    
  // Make a method alias for the existing method implementation, it not already copied.
    const char *typeEncoding = method_getTypeEncoding(targetMethod);
    SEL aliasSelector = aspect_aliasForSelector(selector);
    if (![klass instancesRespondToSelector:aliasSelector]) {
        __unused BOOL addedAlias = class_addMethod(klass, aliasSelector,method_getImplementation(targetMethod), typeEncoding);
          NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector),
          NSStringFromSelector(aliasSelector), klass);
    } 
    
  // We use forwardInvocation to hook in, which calls
  // into the `__ASPECTS_ARE_BEING_CALLED__` method.
    class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
    AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
  }
}

There is a trick in the method aspect_getMsgForwardIMP. the message forward implementation can be two IMPs: _objc_msgForward & _objc_msgForward_stret. Please refer to this article on the differences between the two.

3. References

3.1 Fishhook

3.2 Aspects

TOC