preface

This paper is divided into two parts

  • Probe into the principle of Instruments – Time Profiler
  • What should I do to implement a time-checking tool at the method level? (hook objc_msgSend)

Probe into the principle of Time Profiler

Time Profiler is a built-in tool in The Xcode tool suite. We can use it to easily monitor the Time consuming of methods and optimize our App.

First of all, I will list a picture taken from the official website to show the working process of Time Profiler in detail. Let’s take this picture a step further

From left to right on the graph, five vertical dashed lines represent the call stack that the TimeProfiler obtains from the application at intervals (the default is 1ms). Analyze the first two dashed lines:

  • Main calls method1, method1 calls method2, and TimeProfiler gets a call stack from the application (marked at the top of the dotted line)
  • Next, method1 calls method3 -> method3. End -> method1 calls method2. Then TimeProfiler gets the call stack again, looks the same as it did last time, and increments the number of calls to the methods involved
  • And the same goes for the next three dotted lines
  • The results can be seen in the call tree shown in the lower part of the figure

From this process, we can see that the Time Profiler does not actually measure the actual execution Time of a method. It does not record the start Time and end Time of the method and then calculate the execution Time by taking the difference. The time we see in the TimeProfiler tool is actually the sample interval x number of samples.

So the principle can be summed up in one sentence: calculate method time by regularly fetching the method call stack on the main thread

What’s wrong with this mechanism

TimeProfile does not distinguish whether a method is called repeatedly or only once. In the final call tree generated in the figure above,method1 and method2 appear in the same number of samples (4), even though method1 was actually called twice and for a long time; Method2 was called four times and ran very short; Method3 was not caught because it was over so quickly.

thinking

Although the TimeProfiler mechanism is flawed in its accuracy, it is still sufficient for everyday use. Moreover, it is more convenient to use.

But what if you want to implement a time-checking tool at the method level?

How to implement a method level time checking tool

Train of thought

Before that, let’s review the message sending mechanism of OC:

- Generates a unique code to identify the method based on the selector method name and parameters. - Looks for the function pointer in descending order of priority: the current class's cache table, methodList, and super Class's methodList If not, dynamic method resolution and message forwarding mechanisms are enteredCopy the code

The second step is to use objc_msgSend to find the function pointer, so if we control objc_msgSend, we can control all OC methods.

⭐️ Conclusion: Hook the objc_msgSend method to understand the execution time of all methods

I learned from Deming’s GCDFetchFeed and ObjCHook, which are suitable for learning, and read the source code and some other materials. I will share and record the learning process here (the relevant learning materials will be posted at the end of the article).

Here I learn by ObjCHook, I found it difficult when I first came into contact, and then reviewed some knowledge of compilation. Here I share my notes, which are the knowledge to master before making Hook objc_msgSend.

register

Since objc_msgSend is implemented in assembly, the first thing we need to understand about assembly is the concept of registers. The CPU itself is only responsible for computing, not storing data. Registers are small storage areas inside the CPU used to store data. They are used to temporarily store the data and results involved in the operation. Arm64 has 31 registers to handle integers and Pointers, which are represented by x0 ~ x31 in 64bits and w0 ~ w31 in 32bits. So what do they do

register explain
x0~x7 Pass arguments and return values during the function call. For objc_msgSend, the first argument to x0 is the passed object (type ID), and the second argument to x1 is the selector (type SEL).
x8 Used to save the subroutine return address
x9~x15 A temporary register that does not need to be saved when used
x16~x17 Internal call register
x18 Platform registers, whose use is platform dependent
x19~x28 A temporary register that must be saved when used
x29 Frame Pointer (FP) connects stack frames and saves them before use
x30 LR (Link Register) saves the return address of the subroutine
x31 SP (Stack Pointer) Indicates the offset address of the Stack, pointing to the top of the Stack

Commonly used instructions

instruction role
mov The data transfer instruction, also the most basic programming instruction, is used to transfer a data from the source address to the target address. Example :(“mov lr, x0”) transfers the contents of the x0 register to the lr register
lrd/ldp Example :(“LDR x1,[x2],#8”) stores the contents of the x2 address to x1, and x2 = x2 + 8
str/stp Write instruction, format STR{condition} source register, < memory address >, read from the source register and write to the memory address (STP can operate both registers simultaneously)(Note: the source register is in front, as opposed to the MOV)
bl/blr Put the next value of the PC register value into the return address (link register LR). BLR is similar to BL except that the value of the new PC is taken from the specific register
ret The function returns an instruction that assigns the value in the LR register to the PC register
* Note: PC: Program counter, which holds the memory address of the next instruction to be executed

Supplementary: The ObjCHook source also uses pthread_setSpecific. It provides a way to share data between different functions in multiple threads.

  • Create a pthread_key_t with pthread_key_create().
  • Pthread_setspecific (pthread_key_t, const void * _Nullable) is used to store shared data
  • Pthread_getspecific (pthread_KEY_t) is used to get data shared by different methods in the same thread with a specified key

General train of thought

With that in mind, combined with the comments in the ObjCHook source code, you can get:

The general idea is to save the information of objc_msgSend to the register before hook, including SEL, class name, start/end time, the address of the next function on the current call stack (ensure that the LR register can execute properly after hook), and so on. Then BLR jumps to method before_objc_msgSend, gets the parameter from register, executes the function, saves the parameter, and then BLR jumps to after_objc_msgSend, which prints the method execution information. Finally, restore the content of LR register before hook.

The last

Thanks to the following authors for sharing

Reference links:

https://developer.apple.com/videos/play/wwdc2016/418/ (WWDC- Using Time Profiler in Instruments) https://juejin.cn/post/6844903875795763213 (arm64 invoke rule) Introduction to http://www.ruanyifeng.com/blog/2018/01/assembly-language-primer.html (assembly) https://juejin.cn/post/6844903847039598600 (iOS penetrate into function calls) https://github.com/czqasngit/objc_msgSend_hook (ObjCHook)Copy the code