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.
Clang
Insert 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:
- by
main
Function call.
If we continue executing the program, we will enter the breakpoint of the function again:
- by
didFinishLaunchingWithOptions
Method 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
- use
dladdr
Function, pass the function address, get basic information, saveDl_info
Structure.
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 currentMachO
The pathdli_fbase
: the currentMachO
Base addressdli_sname
: function namedli_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
- through
dli_sname
You 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,
Block
The 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:
touchesBegan
Method 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_guard
Function 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.order
Copy the files to the project root directory inBuild Setting
–> Order File
Configure:
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.
swift
Function 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
- use
OC
andSwift
Mix it up and get itSwift
Function notation.