Resources to prepare

Official Document Address

Introduction to the

In a project, for the OC method, you can HOOK the objc_msgSend method. This only applies to OC methods, C functions, blocks, Swift methods/functions, cannot be intercepted

LLVM has a simple code coverage tool built into it. It inserts calls to user-defined functions at the function level, base Block level, and edge level. In this way, OC methods, C functions, blocks, Swift methods/functions can be fully hooked smoothly.

ClangInsert zhuang

configuration

-fsanitize-coverage=trace- PC-guard = set up test project in Build Setting -> Other C Flags

Add sample code to the project as documented:

#import "ViewController.h" #include <stdint.h> #include <stdio.h> #include <sanitizer/coverage_interface.h> @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; } void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) { static uint64_t N; if (start == stop || *start) return; printf("INIT: %p %p\n", start, stop); for (uint32_t *x = start; x < stop; x++) *x = ++N; } void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { if (! *guard) return; void *PC = __builtin_return_address(0); char PcDescr[1024]; // printf("guard: %p %x PC %s\n", guard, *guard, PcDescr); } @endCopy the code

__sanitizer_cov_trace_pc_guard_init

Run the project and print the following:

INIT: 0x100bbd4c0 0x100bbd4f8
Copy the code
  • Prints from the __sanitizer_cov_trace_pc_guard_init function;

  • As you can see from the loop in the for code, the uint32_t values are stored in the address from start to stop.

  • In the loop, x is the uint32_t pointer type, x++ represents pointer operation, and step size +1 will increase the length of data type.

  • Uint32_t takes up 4 bytes, so the code in the loop means that it records a value of ++N every 4 bytes.

Verify with LLDB:

Start (LLDB) x 0x100bbd4c0 0x100bbD4C0:01 00 00 00 00 02 00 00 00 03 00 00 00 00 04 00 00 00 00................ 0x100bbd4d0: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00 ................ // read stop (LLDB) x 0x100bbd4F8-4 0x100bbD4F4:0E 00 00 00 c0 d2 e0 00 01 00 00 00 00 00 00 00 00................ 0x100bbd504: 00 00 00 00 66 73 bb 00 01 00 00 00 00 00 00 00 .... fs..........Copy the code
  • To read the last value, subtract 4 bytes from the stop address;

  • From start to stop, the values are 01 to 0E. These values represent the number of symbols of methods/functions in the current project.

__sanitizer_cov_trace_pc_guard

Set a breakpoint in the __sanitizer_cov_trace_pc_guard function to run the project. Go to the breakpoint and look at the function call stack:

  • bymainFunction call.

If we continue executing the program, we will enter the breakpoint of the function again:

  • bydidFinishLaunchingWithOptionsMethod call.

We can see that __sanitizer_cov_trace_pc_guard breakpoints are triggered for every method and function called in the project, and are called by the currently executing method/function.

Write the test code:

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { NSLog(@"__sanitizer_cov_trace_pc_guard"); } - (void)touchesBegan:(NSSet< uittouch *> *)touches withEvent:(UIEvent *)event {NSLog(@' touchesBegan '); test(); } void ^ (block) (void) = ^ (void) {NSLog (@ "block executes"); }; Void test() {NSLog(@"test "); block(); }Copy the code

Output result:

  • From the running results, methods and functions are all HOOK;

  • Intercepting methods and functions, only the symbols in the current project, such as NSLog and other external symbols will not be HOOK;

  • The idea of binary reordering is to place the method/function symbols in the binary code implemented at startup in the first order. Method/function implementations of external symbols are not in the current project, so their symbols are not in the scope of the rearrangement.

The principle of

View assembly code:

  • In the assembly code of each method and function, there is an additional BL instruction calling the __sanitizer_cov_trace_pc_guard function.

  • Implementation principle of Clang interpolation: The compiler inserts a line of __sanitizer_cov_trace_pc_guard at the edge of all methods, functions, and Block implementations in the current project, providing 100% method/function /Block coverage.

  • The compiler modifies the current binary at compile time.

  • The timing of the modification, possibly after parsing, was modified when IR intermediate code was generated (not verified).

Get symbol name

In the sample code, a __builtin_return_address function is used:

  • The getaddress () function returns the current return address of the caller.

Get the function address of the caller, get the symbol name:

#include <dlfcn.h> void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { NSLog(@"__sanitizer_cov_trace_pc_guard"); if (! *guard) return; void *PC = __builtin_return_address(0); Dl_info info; dladdr(PC, &info); NSLog(@"%s", info.dli_fname); NSLog(@"%p", info.dli_fbase); NSLog(@"%s", info.dli_sname); NSLog(@"%p", info.dli_saddr); }Copy the code
  • usedladdrFunction, pass the function address, get basic information, saveDl_infoStructure.

Definition of Dl_info structure:

typedef struct dl_info {
        const char      *dli_fname;     /* Pathname of shared object */
        void            *dli_fbase;     /* Base address of shared object */
        const char      *dli_sname;     /* Name of nearest symbol */
        void            *dli_saddr;     /* Address of nearest symbol */
} Dl_info;
Copy the code
  • dli_fname: the currentMachOThe path
  • dli_fbase: the currentMachOBase address
  • dli_sname: function name
  • dli_saddr: function address

Run the project and test the printed results:

__sanitizer_cov_trace_pc_guard dli_fname: /private/var/containers/Bundle/Application/E4DBCC4F-B132-4462-A148-03B398B476F5/SanitizerCoverage.app/SanitizerCoverage Dli_fbase: 0x104CB0000 dli_sname: -[ViewController touchesBegan:withEvent:] dli_saddr: 0x104CB5a64Copy the code
  • throughdli_snameYou get the function name.

Modify the test code to run the project:

#import "ViewController.h" #include <stdint.h> #include <stdio.h> #include <sanitizer/coverage_interface.h> #include <dlfcn.h> @interface ViewController () @end@implementation ViewController + (void)load {// NSLog(@"load "); } - (void)viewDidLoad { [super viewDidLoad]; test(); } void ^ (block) (void) = ^ (void) {/ / NSLog (@ "block executes"); }; Void test(){// NSLog(@"test "); block(); } void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) { static uint64_t N; if (start == stop || *start) return; for (uint32_t *x = start; x < stop; x++) *x = ++N; } void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { void *PC = __builtin_return_address(0); Dl_info info; dladdr(PC, &info); NSLog(@"%s", info.dli_sname); } @endCopy the code

Print result:

+[ViewController load] 
main 
-[AppDelegate application:didFinishLaunchingWithOptions:] 
-[SceneDelegate window] 
-[SceneDelegate setWindow:] 
-[SceneDelegate window] 
-[SceneDelegate window]
-[SceneDelegate scene:willConnectToSession:options:] 
-[SceneDelegate window] 
-[SceneDelegate window] 
-[SceneDelegate window] 
-[ViewController viewDidLoad] 
test 
block_block_invoke 
-[SceneDelegate sceneWillEnterForeground:] 
-[SceneDelegate sceneDidBecomeActive:]
Copy the code
  • Get all called methods, functions,BlockThe function name of. Some functions are called many times, with repeated symbols, and need to be reloaded.

The practical application

In daily development, we often use multi-threaded development. If the function is on a child thread, the __sanitizer_cov_trace_pc_guard function also calls back on the child thread.

So when we collect function names through callbacks, we also want to be thread-safe.

Collect return address

In the following example, we use a thread-safe atomic queue for the collection of return addresses:

Static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT; // define struct typedef struct {void * PC; void *next; } SYNode; void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { void *PC = __builtin_return_address(0); SYNode *node = malloc(sizeof(SYNode)); *node = (SYNode){PC, NULL}; Parameter 1 passes in the type and returns the address of the next node to parameter 2OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next)); } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { while (YES) { SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next)); If (node == NULL){break; } Dl_info info; dladdr(node->pc, &info); NSLog(@"%s", info.dli_sname); }}Copy the code
  • Definition:

    • Defining atomic queues

    • Define a structure where PC stores the current return address and next stores the next node address

  • collect

    • Create a structure, assign to PC, and set next to NULL

    • The structure is pushed

    • Offsetof: macro, parameter 1 passes in type, returns the address of the next node to parameter 2

  • test

    • The loop reads node and stops when empty

    • Write the return address to the Dl_info structure

    • Print symbol name

Cyclic crater

Run the above cases:

  • touchesBeganMethod has dead recursion.

Set a breakpoint in the touchesBegan method, run the project, and look at the assembly code:

  • Method is inserted three times__sanitizer_cov_trace_pc_guardFunction call.

This is the loop-induced pit where Santizer Coverage not only intercepts methods, functions, and blocks, but also hooks loops.

In this case, the while loop is hooked, and the execution of the loop goes into the callback function. The callback will still queue the address of the ‘touchesBegan’ function, which will result in one or two ‘touchesBegan’ forever in the queue, and next will never run out of ‘touchesBegan’.

-fsanitize-coverage=func,trace-pc-guard =func, Build Setting –> Other C Flags

Run the project again, click the screen, and output the following:

-[ViewController touchesBegan:withEvent:] -[SceneDelegate sceneDidBecomeActive:] -[SceneDelegate sceneWillEnterForeground:] block_block_invoke test -[ViewController viewDidLoad] -[SceneDelegate window] -[SceneDelegate  window] -[SceneDelegate window] -[SceneDelegate scene:willConnectToSession:options:] -[SceneDelegate window] -[SceneDelegate window] -[SceneDelegate setWindow:] -[SceneDelegate window] -[AppDelegate application:didFinishLaunchingWithOptions:] main +[ViewController load]Copy the code
  • Modify the configuration item to intercept only method calls, successfully resolving the loop-induced pit.

Gets the function symbol side by side weight

The case also solves several problems:

  • Filter out the name of the function that ‘touchesBegan’ itself;

  • Symbols for functions and blocks, which require an _ before the function name;

  • The same function symbol, need to carry out the weight;

  • Queue rule, first in, last out. So we need to reverse the sign order.

Modify the ‘touchesBegan’ method to resolve remaining issues:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSMutableArray<NSString *> *symbolNames = [NSMutableArray array]; while (YES) { SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next)); if(node == NULL){ break; } Dl_info info; dladdr(node->pc, &info); NSString *name = @(info.dli_sname); if([name isEqualToString:@(__func__)]){ continue; } if(! [name hasPrefix:@"+["] && ![name hasPrefix:@"-["]) { name = [@"_" stringByAppendingString:name]; } if([symbolNames containsObject:name]){ continue; } [symbolNames addObject:name]; } symbolNames = (NSMutableArray<NSString *> *)[[symbolNames reverseObjectEnumerator] allObjects]; for (NSString *symbol in symbolNames) { NSLog(@"%@", symbol); } }Copy the code

Print result:

+[ViewController load] 
_main
-[AppDelegate application:didFinishLaunchingWithOptions:] 
-[SceneDelegate setWindow:] 
-[SceneDelegate scene:willConnectToSession:options:] 
-[SceneDelegate window] 
-[ViewController viewDidLoad]
_test 
_block_block_invoke 
-[SceneDelegate sceneWillEnterForeground:] 
-[SceneDelegate sceneDidBecomeActive:]
Copy the code
  • Filter out the name of the function that ‘touchesBegan’ itself;

  • Gets the symbol name, treated as a function or Block if it does not begin with +[and -[, preceded by _;

  • If the match name exists in the array, skip. Otherwise, add to array;

  • Reverse the array and print in a loop.

Write to the file and configure

Modify the touchesBegan method to write the list of symbols to the.order file:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSMutableArray<NSString *> *symbolNames = [NSMutableArray array]; while (YES) { SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next)); if(node == NULL){ break; } Dl_info info; dladdr(node->pc, &info); NSString *name = @(info.dli_sname); if([name isEqualToString:@(__func__)]) { continue; } if(! [name hasPrefix:@"+["] && ![name hasPrefix:@"-["]) { name = [@"_" stringByAppendingString:name]; } if([symbolNames containsObject:name]){ continue; } [symbolNames addObject:name]; } symbolNames = (NSMutableArray<NSString *> *)[[symbolNames reverseObjectEnumerator] allObjects]; NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hk.order"]; NSString *symbolStr = [symbolNames componentsJoinedByString:@"\n"]; NSData *symbolData = [symbolStr dataUsingEncoding:kCFStringEncodingUTF8]; [[NSFileManager defaultManager] createFileAtPath:filePath contents:symbolData attributes:nil]; NSLog(@"%@", symbolStr); }Copy the code

Get the.Order file and select Add Additional Simulators…

Select the case App and click Downlad Container… In the pop-up box, perform the following operations:

Select a path and download the.xcappData file. Right-click the package contents, go to AppData/ TMP, and find the.order file:

will.orderCopy the files to the project root directory inBuild Setting –> Order FileConfigure:

In Build Settings –> Write Link Map File, set it to YES

Compile the project and open the LinkMap file

  • The configuration takes effect, and the binary rearrangement succeeds.

swiftFunction symbol of

The configuration in Other C Flags only applies to the Clang compiler. Swift uses the SWIFtc compiler. To obtain the Swift function symbol, you need to configure Other Swift Flags:

  • The configuration parameters are slightly different from those of Clang.

  • Add -sanitize-coverage=func, -sanitize=undefined.

Create SwiftTest. Swift file and write the test code:

import Foundation class SwiftTest: NSObject { 
    @objc class func swiftTest1() { 
    
    } 
    
    @objc class func swiftTest2(){
    
    } 
}
Copy the code

Called in the load method and Block of the ViewController respectively:

+ (void)load { 
    [SwiftTest swiftTest1]; 
} 

- (void)viewDidLoad { 
    [super viewDidLoad]; 
    test(); 
} 

void(^block)(void) = ^(void){ 
    [SwiftTest swiftTest2]; 
}; 

void test(){
    block(); 
}
Copy the code

To run the project, click the screen and output the following:

+[ViewController load]
_$s17SanitizerCoverage9SwiftTestC10swiftTest1yyFZTo 
_$s17SanitizerCoverage9SwiftTestC10swiftTest1yyFZ 
_main 
-[AppDelegate application:didFinishLaunchingWithOptions:] 
-[SceneDelegate setWindow:] 
-[SceneDelegate scene:willConnectToSession:options:]
-[SceneDelegate window] 
-[ViewController viewDidLoad]
_test 
_block_block_invoke 
_$s17SanitizerCoverage9SwiftTestC10swiftTest2yyFZTo 
_$s17SanitizerCoverage9SwiftTestC10swiftTest2yyFZ 
-[SceneDelegate sceneWillEnterForeground:] 
-[SceneDelegate sceneDidBecomeActive:]
Copy the code
  • useOCandSwiftMix it up and get itSwiftFunction notation.