Painted painted levels: fostered fostered fostered

Tags: “iOS” “hook” “Time monitoring” “objc_msgSend” author: 647 Review: QiShare team


* preface:

Recently, xiaobian is looking at the technology sharing of Teacher Daiming, feeling a lot of harvest. Based on recent learning, xiaobian summarizes some knowledge points on App startup optimization, and plans to implement a series of App startup optimization articles. IOS App startup optimization (2) — Use “Time Profiler” tool to monitor the startup Time of the App. IOS App startup optimization (3) — make your own tool to monitor the startup Time of the App


The first two articles introduced the iOS App Startup process and the Use of Time Profiler tools. This article will introduce the hook underlying objc_msgSend to understand the execution time of all Objective-C methods.

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, two structures are designed:

One is a structure used to record symbols, and the other is a linked list used to record 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 // The first step is to iterate through all the images in dyld and retrieve the image header and slide. Note that the first call registers the callback if (! _rebindings_head->next) { _dyld_register_func_for_add_image(_rebind_symbols_for_image); } else { uint32_t c = _dyld_image_count(); // Image for (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 find the relevant Segment_command_t in the symbol table from the image, traverse the symbol table to find the segname to be replaced, and 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 related commands, including linkedit_segment command, symtab Command, and dysymtab command. 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.

This article source: Demo

Finally, I finished the App startup optimization (I), (II) and (III) standing on the shoulders of giants in the iOS industry. Thanks to Mr. Daming for his wonderful technology sharing.

Attached is the link of Mr. Daiming’s course: iOS Development Master Course.


Recommended articles:

IOS to UILabel add click events with SwiftUI add animation to view with SwiftUI Write a simple page iOS control log switch iOS App can be detachable a framework two ways to customize WKWebView display content (a) Swift 5.1 (7) – Closure Swift 5.1 (6) – Function Swift 5.1 (5) – Control flow Xcode11 New project SceneDelegate iOS App startup optimization (2) — use “Time” “Profiler” tool monitoring App startup time iOS App startup optimization (a) – understand the App startup process qidance Weekly