Recently, I learned a lot from Mr. Daiming’s technical sharing about “performance monitoring”. Based on recent learning, this article summarizes some performance monitoring related practices and plans to write a series of performance monitoring related articles. The contents are as follows: iOS performance monitoring (1) – CPU power monitoring iOS performance monitoring (2) – Main thread lag monitoring iOS performance monitoring (3) – Method time monitoring


This article will introduce the iOS performance monitoring tool (QiLagMonitor) related to the “method time monitoring” function module.

What is hook?

Definition: A hook is a method that you specify when the original method starts executing. Or add the method you specify before and after the original method. Thus achieve the purpose of changing the specified method.

Such as:

  • useruntimeMethod Swizzle.
  • useFacebookThe open sourcefishhookFramework.

The former is the “method interchange” capability provided by the ObjC runtime. The latter is the dynamic “rebinding” of the symbols of the Mach-O binaries for the purpose of method exchange.

Question 1: What is the general idea behind fishhook?

As mentioned in iOS App Startup Optimization (I) — Understanding App Startup Processes, dyld binds symbols according to the symbol table of the Mach-O binary executable. By using the symbol table and symbol name, we can know the address that the pointer accesses, and then we can replace the specified method by changing the address that the pointer accesses.

Question 2: Why can I hook objc_msgSend to know the time of all objC methods?

Because objc_msgSend is the path through which all Objective-C method calls are made, all Objective-C methods call the underlying objc_msgSend method at runtime. So as long as we can hook objc_msgSend, we know how long all objC methods take. (See point 6 of my previous article writing Quality Objective-C Code for iOS (part 2) — Understanding objc_msgSend for more details.)

In addition, objc_msgSend itself is written in assembly language, and apple has opened source objc_msgSend. Objc_msgSend source code can be downloaded from the official website.

How to hook the underlying objc_msgSend

Stage 1: Similar to the FishHook framework, we need hook capabilities first.
  • First, design two structures: a structure for recording symbols and a linked list for recording symbol lists.
struct rebinding {
    const char *name;
    void *replacement;
    void **replaced;
};

struct rebindings_entry {
    struct rebinding *rebindings;
    size_t rebindings_nel;
    struct rebindings_entry *next;
};
Copy the code
  • Next, iterate over the dynamic linkerdyldIn all theimageAnd take out theheaderandslide. So that we can get the symbol table.
static int fish_rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
    int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
    if (retval < 0) {
        return retval;
    }
    // If this was the first call, register callback for image additions (which is also invoked for// Existing images, otherwise, just run on existing images // Note that the callback is primarily registered on the first callif(! _rebindings_head->next) { _dyld_register_func_for_add_image(_rebind_symbols_for_image); }else{ uint32_t c = _dyld_image_count(); // iterate over all dyld imagesfor(uint32_t i = 0; i < c; i++) { _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i)); // Read header and slider}}return retval;
}
Copy the code
  • In the previous step, we were atdyldNei got it allimage. Next, we go fromimageFind the associated symbol in the symbol tablesegment_command_t, traverse the symbol table to find the one to replacesegnameAnd then perform the next method replacement. The method is implemented as follows:
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
                                     const struct mach_header *header,
                                     intptr_t slide) {
    Dl_info info;
    if (dladdr(header, &info) == 0) {
        return; } // Find the symbol table associatedcommand, including linkedit_segmentcommand, symtabcommandAnd dysymtabcommand. segment_command_t *cur_seg_cmd; segment_command_t *linkedit_segment = NULL; struct symtab_command* symtab_cmd = NULL; struct dysymtab_command* dysymtab_cmd = NULL; uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);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) {
            if(strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) { linkedit_segment = cur_seg_cmd; }}else if (cur_seg_cmd->cmd == LC_SYMTAB) {
            symtab_cmd = (struct symtab_command*)cur_seg_cmd;
        } else if(cur_seg_cmd->cmd == LC_DYSYMTAB) { dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd; }}if(! symtab_cmd || ! dysymtab_cmd || ! linkedit_segment || ! dysymtab_cmd->nindirectsyms) {return; Uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment-> vmaddr-linkedit_segment ->fileoff; nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff); char *strtab = (char *)(linkedit_base + symtab_cmd->stroff); Uint32_t *indirect_symtab = (uint32_t *)(uintdit_base + dysymtab_cmd->indirectsymoff); cur = (uintptr_t)header + sizeof(mach_header_t);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) {
            if(strcmp(cur_seg_cmd->segname, SEG_DATA) ! = 0 && strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) ! = 0) {continue;
            }
            for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
                section_t *sect =
                (section_t *)(cur + sizeof(segment_command_t)) + j;
                if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
                    perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
                }
                if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
                    perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
                }
            }
        }
    }
}
Copy the code
  • Finally, the pointer address replacement is carried out through the symbol table and the implementation of the method we want to replace. This is the related method implementation:
static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
                                           section_t *section,
                                           intptr_t slide,
                                           nlist_t *symtab,
                                           char *strtab,
                                           uint32_t *indirect_symtab) {
    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;
        if (strnlen(symbol_name, 2) < 2) {
            continue;
        }
        struct rebindings_entry *cur = rebindings;
        while (cur) {
            for (uint j = 0; j < cur->rebindings_nel; j++) {
                if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
                    if(cur->rebindings[j].replaced ! = NULL && indirect_symbol_bindings[i] ! = cur->rebindings[j].replacement) { *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]; } indirect_symbol_bindings[i] = cur->rebindings[j].replacement; goto symbol_loop; } } cur = cur->next; } symbol_loop:; }}Copy the code

At this point, we have the basic capabilities of a hook by calling the following methods.

static int fish_rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
Copy the code
Stage two: write ours in assembly languagehook_objc_msgSendmethods

Since objc_msgSend is written in assembly language, we need to replace the objc_msgSend method in assembly language.

Since we are going to make a tool to monitor method time. Now what is our purpose?

We want to hook the original objc_msgSend method to call the clock operation before the objc_msgSend method and end the clock operation after the objc_msgSend method. By calculating the time difference, we can get the exact duration of the method call.

Therefore, we need to add before_objc_msgSend and after_objc_msgSend before and after the call of the original objc_msgSend method for later timing operations.

Arm64 has 31 64-bit integer registers, represented by x0 through x30. The main ideas are as follows:

  • The parameter register is X0 ~ X7. For the objc_msgSend method, the first argument to x0 is the incoming object, and the second argument to x1 is the selector _cmd. Syscall’s number is going to be in x8.
  • Swap the parameters stored in the register and move the data in the returned register LR to x1.
  • Call pushCallRecord using bl label syntax.
  • Execute the original objc_msgSend and save the return value.
  • Call the popCallRecord function with bl label syntax.
  • return

Some of the assembly instructions involved:

instruction meaning
stp Write to both registers simultaneously.
mov Assigns a value to a register.
ldp Read both registers at the same time.
sub Subtract the values of the two registers
add Add the values of the two registers
ret Return from subroutine to main program

The detailed code is as follows:

#define call(b, value) \
__asm volatile ("stp x8, x9, [sp, #-16]! \n"); \
__asm volatile ("mov x12, %0\n": :"r"(value)); \
__asm volatile ("ldp x8, x9, [sp], #16\n"); \
__asm volatile (#b " x12\n");

#define save() \
__asm volatile ( \
"stp x8, x9, [sp, #-16]! \n" \
"stp x6, x7, [sp, #-16]! \n" \
"stp x4, x5, [sp, #-16]! \n" \
"stp x2, x3, [sp, #-16]! \n" \
"stp x0, x1, [sp, #-16]! \n");

#define load() \
__asm volatile ( \
"ldp x0, x1, [sp], #16\n" \
"ldp x2, x3, [sp], #16\n" \
"ldp x4, x5, [sp], #16\n" \
"ldp x6, x7, [sp], #16\n" \
"ldp x8, x9, [sp], #16\n" );

#define link(b, value) \
__asm volatile ("stp x8, lr, [sp, #-16]! \n"); \
__asm volatile ("sub sp, sp, #16\n"); \
call(b, value); \
__asm volatile ("add sp, sp, #16\n"); \
__asm volatile ("ldp x8, lr, [sp], #16\n");

#define ret() __asm volatile ("ret\n");

__attribute__((__naked__))
static void hook_objc_msgSend() {// Save parameters.save () // STP stack instruction stack parameter, parameter register is x0~ x7. For the objc_msgSend method, the first argument to x0 is the incoming object, and the second argument to x1 is the selector _cmd. Syscall's number is going to be in x8. __asm volatile ("mov x2, lr\n");
    __asm volatile ("mov x3, x4\n");
    
    // Call our before_objc_msgSend.
    call(blr, &before_objc_msgSend)
    
    // Load parameters.
    load()
    
    // Call through to the original objc_msgSend.
    call(blr, orig_objc_msgSend)
    
    // Save original objc_msgSend return value.
    save()
    
    // Call our after_objc_msgSend.
    call(blr, &after_objc_msgSend)
    
    // restore lr
    __asm volatile ("mov lr, x0\n");
    
    // Load original objc_msgSend return value.
    load()
    
    // return
    ret()
}
Copy the code

Whenever the underlying hook_objc_msgSend method is called, the before_objc_msgSend method is called, then the hook_objc_msgSend method is called, and finally the after_objc_msgSend method is called.

Single method invocation, flow as shown below:

Reverse the “three” and the flow of multiple method calls looks like this:

In this way, we can get the time taken for each layer of method calls.

How to use this tool?

The first step is to import the QiLagMonitor class library in the project.

Second, import the qicalltrace.h header file in the controller that you want to monitor.

  [QiCallTrace start]; / / 1. The start

  // Your codes (the range of codes you need to test)

  [QiCallTrace stop]; / / 2. Stop
  [QiCallTrace save]; // 3. Save and print the method call stack and the specific method time.
Copy the code

PS: Currently this tool can only hook all objC methods and calculate the time of all methods within the interval. Swift method listening is not supported.

Finally, I finished this series standing on the shoulders of giants in the iOS industry. Thanks to Mr. Daming for his wonderful technology sharing. I wish you success in your studies and success in your work. Attached is the link of Mr. Daiming’s course: iOS Development Master Course, thank you!