The causes of

A wild pointer is a pointer to a deleted object or restricted memory region. When we write C++, we emphasize that the pointer is initialized to NULL, and we emphasize that when we use up the pointer, we also assign it to NULL, and whoever allocates it will reclaim it, to avoid the problem of wild Pointers. The common thing is that the memory that the pointer points to is reclaimed elsewhere, but the pointer still points to that memory. In the MRC era, reference counting was controlled manually, so memory could easily be reclaimed elsewhere. ARC solves most of these problems. Before iOS9, the delegate and target-action of the system library were partly assigned (unsafe_unretain), and wild Pointers also appear if the memory is reclaimed elsewhere. So after iOS9, we changed these places to the weak memory decorator, and when memory is reclaimed by the weak table, we set these Pointers to nil. It also significantly reduces the occurrence of wild Pointers.

If wild Pointers are still common in projects today, it is almost certainly a mistake to use memory.

Performance: Crash

For Mach, Unix, and NSException three different levels of crash,NSException is easier to say, can be directly located to the OC code. The main problem is the exception EXC_BAD_ACCESS(SIGSEGV), which is difficult to locate in our application code.

  • SIGILL executed an illegal instruction, usually an error in the executable file
  • The SIGTRAP breakpoint instruction or other trap instruction is generated
  • SIGABRT call Abort is generated
  • SIGBUS Indicates an invalid address. For example, incorrect memory type access, memory address alignment, etc
  • SIGSEGV Indicates an invalid address. Access unallocated memory or write to memory that has no write permission
  • SIGFPE deadly arithmetic operation. For example, numeric overflow, NaN values, and so on

In fact, we encountered Mach exceptions that were mostly wild Pointers. SIGSEGV/SIGABRT/SIGTRAP are more common. Wild pointer problems exhibit a variety of quirks, and because the crash is not where the wild pointer was created and is difficult to reproduce, the problem is often difficult to locate.

Mach Exception

The positioning tool

Zoombie Object

This is by far the most helpful debugging mode. The idea is to hook the object’s dealloc method and zombie the object by calling its own __dealloc_zombie method.

id object_dispose(id obj)
{
    if(! obj)return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}
Copy the code

Destructinstance: zombies are no longer free(obj); zombies are no longer free(obj). . Also generate a “_NSZombie_” + clsName class name and call objc_setClass(self, zombieCls); Modify the object’s ISA pointer to point to a particular zombie class. If the object receives a message again, when objc_msgsend calls abort(), it crashes and prints out the method called.

A crash does not necessarily occur when the memory to which the wild pointer points is not overwritten, or when it is overwritten into accessible memory. Sending a message to an object at this point does not necessarily crash (there might be a method), or sending a message to an object that has been released. But if the wild pointer is pointing to a zombie, it will crash, it will crash the first time the zombie is accessed by another message.

Zombie Object without Xcode

Zombie objects must be used when connecting to debug in Xcode, and if we want to integrate with our crash collection tool, we need to implement something like Zombie Objects ourselves. The logic is to hook up to the dealloc method of NSObject’s root class, and then change the ISA pointer of the object that was going to be released to a new zombie class that we created in the new dealloc method.

IOS uses code to detect wild pointer errors and develops its own NSZombie. The two articles describe how to implement something like Zoombie Object in code, but it is not actually available. There is a big difference between these two implementations and Zombie Object implementations. There are many cases of misjudgment in practical application.

The main reason for this miscalculation is that the implementation of Dealloc and Zombie class is different from the Zombie Object. Referring to Apple’s source code, you can see that Apple completely calls the objc_destructInstance function. Other implementations either didn’t call this function or did only part of it. There are two main parts to an OC object dealloc, one is objc_destructInstance, and the other is free(self). Objc_destructInstance includes removal of weak references, removal of associated objects, c++ destructors, etc. These logic cannot be omitted.

- (void)dealloc { const char *className = object_getClassName(self); char *zombieClassName = NULL; do { //... Class zombieClass = objc_getClass(zombieClassName); objc_destructInstance(self); // key object_setClass(self, zombieClass); } while (0); if (zombieClassName ! = NULL) { free(zombieClassName); }}Copy the code

For the implementation of Zombie class, Zombie Object implementation is concise and effective. It’s not as bloated as other people’s implementations. It simply declares a root class with no methods, so any message sent to it will crash.

NS_ROOT_CLASS
@interface _NSZombie_ {
    Class isa;
}

@end
Copy the code

So the set of implementations that I pulled out of the Apple source code, NSZombie, is exactly the same as the Zombie Object implementation guarantees, and solves the misjudgment case.

Scribble

The Scribble tool can be used to alloc 0xAA and dealloc 0x55, which means that an object is released and then fills the memory with inaccessible data. If the object is revisited, it will crash.

In Bugly’s article on how to locate the RANDOM Crash of Obj-C wild pointer, the Crash rate is improved in this way to facilitate the problem location. In order not to restrict its use in Xcode, I implemented similar logic in my own code. To hook free by fishhook function, the implementation is as follows:

void safe_free(void* p){
    size_tmemSiziee=malloc_size(p);
    memset(p,0x55, memSiziee);
    orig_free(p);
    return;
}
Copy the code

Although 0x55 has been written for the freed object, if the memory was overwritten before it was accessed (triggering the crash), the crash may not be triggered. This is not uncommon. So Bugly will no longer call free to free the memory in order not to overwrite it. Keep this memory there all the time. It’s very much like a Zombie Object.

Crash is also made using the RSA pointer that is abort() when the object receives the message.

Address Sanitizer

The malloc/free function has been replaced. Additional memory is allocated in the malloc function for the inaccessible region. In the free function, all allocated memory areas are set as inaccessible and placed in the queue of the isolated area (guaranteed that they will not be allocated again by the malloc function for a certain period of time). If you access an inaccessible area, crash directly.

The CPU is affected by two to five attempts and the memory consumption is increased by two to three attempts.

Problems that can be detected:

  • Access already dealloc’s memory /dealloc already dealloc’s memory
  • Dealloc does not yet have alloc memory (but cannot check to access uninitialized memory)
  • The access function returns subsequent stack memory/access stack memory out of scope
  • Overflow over or down buffer,C++ container overflow (but cannot check integer overflow)

Cannot be used to check for memory leaks. Some articles that say ASan can check for memory leaks are not true. Google’s LSan does, but Xcode’s ASan does not.

Malloc Stack

All of the tools I’ve introduced have been to increase the crash probability to get the crashed object and memory address. It is also difficult to locate the crashed object once it has been obtained, because the crash is far away from the release. And some objects in the project initialization of many, do not know the corresponding place out of the problem. So it would be nice to know where the object was initialized. The Malloc Stack records the Stack information of all the objects when Malloc is called. Then we execute the command:

script import lldb.macosx.heap
malloc_info --stack-history 0x7fbf0dd4f5c0
Copy the code

The stack information about where the object was initialized can be printed in the LLDB. The Malloc Stack, however, has two huge disadvantages. One is that it can only be used on the emulator, and the second is that it does not print out the dealloc information. To use it on a real plane, you have to jailbreak it.

lzMalloc

The LLDB plug-in, developed by the company’s internal master, is based on the Malloc Stack. It calls private functions to retrieve the data recorded by the Malloc Stack. Can support real machine debugging, can print dealloc stack information. We can print dealloc because we hook the -dealloc method and call __disk_stack_logging_log_stack to record the current stack information.

A few examples of wild Pointers

Incorrect memory modifier

The example encountered could be a classic wild pointer, showing up in various manifestations in the crash log.

The first is when dealloc objects crash:

0 libsystem_kernel.dylib 0x252fac5c __pthread_kill + 4
1 libsystem_c.dylib 0x2528f0ac abort + 103
2 libsystem_malloc.dylib 0x25324ef6 free + 431
3 libobjc.A.dylib 0x24e13e08 object_dispose + 19
4 Foundation 0x25de3cf2 -[NSIndexPath dealloc] + 66
5 libobjc.A.dylib 0x24e24f66 objc_object::sidetable_release(bool) + 150
6 libsystem_blocks.dylib 0x25243ac2 _Block_release + 215
7 CoreFoundation 0x25583384 -[__NSArrayI dealloc] + 64
5 libobjc.A.dylib 0x24e24f66 objc_object::sidetable_release(bool) + 150
9 UIKit 0x29e934f2 __runAfterCACommitDeferredBlocks + 310
10 UIKit 0x29e9f7da __cleanUpAfterCAFlushAndRunDeferredBlocks + 90
11 UIKit 0x29bddb1c __afterCACommitHandler + 84
Copy the code

As you can see, this is a complete crash of the system library, which has nothing to do with the engineering code, and it was completely confusing at first. There are only two clues here, one is NSIndexPath and the other only happened on iphone5 models before 10.3.3. Since 10.3.3 is the last version of the iphone5 to be supported, there are not many users.

The second representation is objc_msgsend, isEqual: it’s the method name that we get by reading the ARM register LR, which Bugly found for us.

0 libobjc.A.dylib 0x1a1b0dd6 objc_msgSend (isEqual:) + 15
1 UIKit 0x201afdfa -[UICollectionReusableView _setLayoutAttributes:] + 60
2 UIKit 0x209d0280 -[UICollectionView _applyLayoutAttributes:toView:] + 138
3 UIKit 0x209daf26 ___88-[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:viewCategory:]_block_invoke + 28
4 UIKit 0x2015b5c2 +[UIView(Animation) performWithoutAnimation:] + 84
5 UIKit 0x209dae40 -[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:viewCategory:] + 2156
6 UIKit 0x201af68a -[UICollectionView dequeueReusableCellWithReuseIdentifier:forIndexPath:] + 160
7 XXXXXXProject 0x00404c02 -[XXXXXXCollectionView collectionView:cellForItemAtIndexPath:] (XXXXXXClass.m:77)
8 UIKit 0x209cf850 -[UICollectionView _createPreparedCellForItemAtIndexPath:withLayoutAttributes:applyAttributes:isFocused:notify:] + 420
9 UIKit 0x201af5e0 -[UICollectionView _createPreparedCellForItemAtIndexPath:withLayoutAttributes:applyAttributes:] + 42
10 UIKit 0x201ad7f6 -[UICollectionView _updateVisibleCellsNow:] + 4076
11 UIKit 0x201a83d6 -[UICollectionView layoutSubviews] + 398
12 UIKit 0x2014b482 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1224
Copy the code

Here the clue is rich, can find the corresponding class, XXXXXXProject is our project, apparently crashed in the UICollectionView. During the reuse of the collectionViewCell, call the _setLayoutAttributes method, calling isEqual at position +60:, After decompiling the method that calls isEqual: object is UICollectionViewLayoutAttributes (omit decompiled process). Again, this only happened on iphone5 models before 10.3.3. So it’s basically the same problem.

But that’s not what’s going on. As said earlier, the wild pointer breaks down in a very different way than what went wrong. The only thing we know for sure is that the object that caused the crash was NSIndexPath.

UITransitionView Initialize unrecognized Selector. I don’t know where it was used

Exception Type: NSInvalidArgumentException(SIGABRT)
Exception Codes: -[UITransitionView initialize]: unrecognized selector sent to instance 0x165f22c0 at 0x1c4d1acc
Crashed Thread: 0
0 CoreFoundation 0x1cd03b3d ___exceptionPreprocess + 129
1 libobjc.A.dylib 0x1bf8b067 objc_exception_throw + 31
2 CoreFoundation 0x1cd08fd1 ___methodDescriptionForSelector + 1
3 CoreFoundation 0x1cd070c3 ____forwarding___ + 697
4 CoreFoundation 0x1cc2fdc8 _CF_forwarding_prep_0 + 24
5 libobjc.A.dylib 0x1bf8bbad _CALLING_SOME_+initialize_METHOD + 23
6 libobjc.A.dylib 0x1bf8bdf3 __class_initialize + 579
7 libobjc.A.dylib 0x1bf92c15 _lookUpImpOrForward + 173
8 libobjc.A.dylib 0x1bf92b65 __class_lookupMethodAndLoadCache3 + 27
9 libobjc.A.dylib 0x1bf991af __objc_msgSend_uncached + 15
10 UIKit 0x21f98167 -[UICollectionViewLayoutAttributes isEqual:] + 95
11 UIKit 0x21f97dfb -[UICollectionReusableView _setLayoutAttributes:] + 61
12 UIKit 0x227b8281 -[UICollectionView _applyLayoutAttributes:toView:] + 139
13 UIKit 0x227c2f27 ___88-[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:viewCategory:]_block_invoke + 29
14 UIKit 0x21f435c3 +[UIView(Animation) performWithoutAnimation:] + 85
15 UIKit 0x227c2e41 -[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:viewCategory:] + 2157
16 UIKit 0x21f9768b -[UICollectionView dequeueReusableCellWithReuseIdentifier:forIndexPath:] + 161
Copy the code

Look at the stack below and see the same problem, but why this strange error? This is what a wild pointer looks like. This piece of memory is overwritten by something else.

There are actually other manifestations, but these are the three that are representative. Only limited information can be obtained from the crash log. One is that this is a wild pointer problem. The second is that the wild pointer object is probably an NSIndexPath object (which is not entirely certain).

If you don’t know it’s a wild pointer problem, it’s easy to go astray and spend a lot of time studying UICollectionView or studying UITransitionView. It’s a waste of time, because it’s a long way from where the wild pointer is.

As Bugly’s article says, the most important thing about locating wild Pointers is to increase the probability of their occurrence. So this time I’m using a Zombie Object and limiting it to iPhone5 and iOS10.3.3.

And after a couple of repetitions, it’s NSIndexPath, and all of the uicollectionviews and all of the uitableviews are affected. So I began to wonder if there were global code hooks in the project. And so it was:

- (void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation invokeWithTarget:self.target];
    if (kiOS9Later) {
        if ([NSStringFromSelector(invocation.selector) isEqualToString:@"collectionView:didSelectItemAtIndexPath:"]) {
            // No trace dot
            __unsafe_unretained UICollectionView *collectionView = nil;
            id indexPath;
            [invocation getArgument:&collectionView atIndex:2];
            [invocation getArgument:&indexPath atIndex:3];
            [FPPVHelper reportMTAEventId:[collectionView hotTagId] Index:[indexPath row] info:nil]; }}}Copy the code

This is some magical dot code, I don’t know who wrote. IndexPath must be used as a modifier for __unsafe_unretained. If it was strong, the object would have been freed by ARC, but the C pointer was passed, and some other pointer would have pointed to it. The wild pointer is generated.

The delegate crashed before iOS9

Before iOS9, tableView’s delegate and tableView’s datasource were assigned memory modifiers. Weak is not used until iOS9.

// Before iOS 8
@property(nonatomic.assign) id<UITableViewDataSource> dataSource
@property(nonatomic.assign) id<UITableViewDelegate> delegate
// After iOS 9
@property(nonatomic.weak.nullable) id<UITableViewDataSource> dataSource
@property(nonatomic.weak.nullable) id<UITableViewDelegate> delegate
Copy the code

In this case, if the delegate is released before the TableView itself, then the dataSource will become an wild pointer. A common situation such as a block call that extends the life of the TableView can happen, causing the wild pointer to crash. The usual crash log is the crash of objc_msgSend + 15, which is in the delegate or datasource method.

The solution is as simple as setting the dataSource and delegate to nil at dealloc.

- (void)dealloc
{
    _tableView.delegate = nil;
    _tableView.dataSource = nil;
}
Copy the code

Target-action crashes before iOS9

The crash stack is also the most common objc_msgSend, as you can see here is a project where a method of the hook crashes

libobjc.A.dylib objc_msgSend (pv_gestureRecongizerAction:)
UIKit -[UIGestureRecognizer _updateGestureWithEvent:buttonEvent:]
UIKit ____UIGestureRecognizerUpdate_block_invoke662
UIKit __UIGestureRecognizerRemoveObjectsFromArrayAndApplyBlocks
UIKit __UIGestureRecognizerUpdate
SEGV_ACCERR
Copy the code

Our own code looks like this, adding a layer of calls to the addGestureRecognizer method and a layer of target-action. This is equivalent to adding two target-actions to the gestureRecognizer

- (void)pv_addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {
    [gestureRecognizer addTarget:self action:@selector(pv_gestureRecongizerAction:)];
    [self pv_addGestureRecognizer:gestureRecognizer];
}
Copy the code

Since target is similarly assigned in iOS8 for gesture, the gestureRecognizer’s target is still pointing to self’s memory. This happens when self has been released, but gestureRecognizer has not been released.

conclusion

There are several keys to wild pointer positioning:

  • The first is realizing that this is a problem with wild Pointers:Mach ExceptionMost of these are wild pointer problems, most often seen in crash logsobjc_msgSendandunrecognized selector sent toAnd so on. It often depends on the iOS SDK version and the iPhone model. Once you recognize the problem of wild Pointers, you don’t need to stick to the crash log because the crash is far away from the cause of the crash.
  • The second is to reproduce as much as possible. usingZombie Object/Scribble/AasnWill do. Personally, I think the Zombie Object I implemented is the best. It can not only break away from the limitations of Xcode debug, but also is relatively simple to use.
  • The third is based on the wild pointer to the object to determine the location of the error, rather than crash method. Because the method that crashed is far away from the cause of the crash, the object that the wild pointer points to is more likely to be the object that failed (and may sometimes be overridden).
  • The fourth is to usemalloc stack/lzMallocFind out where the wild pointer points to the object initialization position and the dealloc position, determine whether premature release, and so on.