[iOS研習記]——聊聊野指針與殭屍對象定位

[iOS研習記]——聊聊野指針與殭屍對象定位

一、從一個異常說起

在iOS項目開發中,或多或少的我們都會遇到一些Crash的情況,大部分Crash拋出的異常都是NSException層的,這類異常是OC層代碼問題造成的,通常堆棧信息和異常的提示信息都非常明確,可以直接定位到出問題的代碼,從而使這類問題的解決並不困難。可以引起Crash的異常除了NSException外,還有Unix層和Mach層的異常。

Mach異常一般是底層內核級的異常,我們可以通過一些底層的API來對這類異常進行捕獲,這不是本文的討論內容,這裏不再贅述。Unix層是指對於開發者沒有捕獲的Mach異常,會被轉換成對應的Unix信號傳遞給出錯線程。

如果你在iOS項目在線上收集的異常中,有類似EXC_BAD_ACCESS的異常,則大概率是由於內存問題產生的野指針引起的。這也是本文我們要討論的核心內容。

1. 什麼是野指針?

當前我們在編寫iOS程序時大多會採用ARC來進行內存管理,通常情況下我們無需過多的對內存管理進行關心。但是這並不代表不會產生內存問題。從原理上講,我們在創建任何對象的時候,首先都會通過操作系統從內存中申請出一塊內存空間供此對象使用,並將此內存空間的地址保存到指針中供我們在代碼中方便的引用到此內存。那麼當這個對象被銷燬的時候,原則上我們需要做兩件事,一是將這塊內存還回去,之後操作系統可以重複利用這塊內存分配給其他申請者使用,二是將代碼中的指針清空回收。這樣可以保證程序能夠可持續化的健康運行。工作過程如下圖所示:

但是無論在生活中還是編程中,意外總會發生,通常情況下,在向操作系統申請內存這一步很少會出現問題,操作系統本身的穩定性比應用程序要強很多。問題大多出現在內存釋放的時候。問題可能有兩種:

一種是已經不需要使用的對象我們將指針變量直接清除了,但卻沒有告訴操作系統回收這塊內存,此後程序中沒有地方存儲這塊內存的地址,這塊內存將永遠無法使用和回收。這種情況下,這塊內存就變成了無主內存且操作系統並不知道,就產生了我們常說的內存泄露問題,隨着應用的運行時間越來越長,內存泄露可能越來越多最終導致內存不夠用,程序無法再正常運行。

另一種是我們告訴操作系統要回收這塊內存,並且這塊內存也真正的被回收了,但是程序中依然有指針變量存儲着這個地址沒有清空,此時這個指針就變成了也指針,因爲它所指向的內存已經回收,這塊內存具體是又被利用了還是依然存放着原來的數據我們都一無所知。此後如果不小心又通過這個指針使用了這塊內存的數據,無論讀寫都將產生各種千奇百怪的問題,且我們很難定位。本文我們主要聊的就是這類野指針問題的產生原因與定位方法。

2. 野指針會產生哪些問題?

開發中我們遇到的大部分的EXC_BAD_ACCESS問題都是由野指針導致的,主要有兩種信號:SIGSEGV和SIGBUS。其中SIGSEGV表示操作的地址非法,訪問了未分配的內存或者寫入了沒有寫權限的內存。SIGBUS表示錯誤的內存類型訪問。

野指針會產生的問題千奇百怪,難以定位。當程序中使用到了野指針時,可能存在兩大種場景:

1> 訪問的內存沒有被覆蓋

如果原對象依賴的其他對象沒有被刪除,則看上去程序的運行好像任何問題,但是實際上卻很危險,程序邏輯上的表現已經不可控。

如果原對象依賴的其他對象有刪除情況,則內部可能還有有其他野指針生成,依然會出現各種複雜的異常場景。

2> 訪問的內存重新被覆蓋了

這種從場景會更加麻煩,如果當前內存區域的可訪問性發生了變化,則會產生許多類型的異常,例如objc_msgSend失敗,SIGBUS地址類型異常,SIGFPE運算符異常,SIGILL指令異常等等。

如果當前內存是可以訪問的,則可能違揹我們本意的寫壞其他地方在使用的內存,使其他地方在使用時產生異常。也可能要使用的數據類型和我們原對象對不上,導致未實現的選擇器類的錯誤,查找方法類的錯誤,各種底層邏輯錯誤,以及malloc錯誤等。這時要排查問題就非常難了。

綜上所述,野指針的危害是非常大,除了其本身會造成異常Crash外,還可能會使其他正常使用的代碼產生異常,並且有不可復現性與隨機性,例如你可能發現某個Crash的堆棧是調用了某個對象的某個方法找不大,但是你搜遍代碼也沒有找到類似的方法調用,其實就是其他地方出現了野指針問題,之後這個正確的對象剛好分配到了野指針所指向的內存,野指針將此內存的數據破壞了。對於這種Crash問題,我們幾乎是束手無策的。

3. 動手造一個野指針場景試試看

通過前面的介紹,我們瞭解了野指針問題的產生原因與危害。現在可以動手一試。使用Xcode新建一個iOS工程。在其中新建一個名爲MyObject的類,爲其添加一個屬性,如下:

#import <Foundation/Foundation.h>

@interface MyObject : NSObject

@property(copy) NSString *name;

@end

在ViewController類中編寫如下測試代碼:

#import "ViewController.h"
#import "MyObject.h"
@interface ViewController ()

@property (nonatomic, unsafe_unretained)MyObject *object;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    MyObject *object = [[MyObject alloc] init];
    self.object = object;
    self.object.name = @"HelloWorld";
    void *p = (__bridge void *)(self.object);
    NSLog(@"%p,%@",self.object,self.object.name);
    NSLog(@"%p,%@",p, [(__bridge MyObject *)p name]);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%p",self->_object);
    NSLog(@"%@",self.object.name);
}

@end

這裏我們手動的造出了一個會出現野指針問題的場景,ViewController類的object屬性聲明爲的unsafe_unretained,這個修飾符的意思是當前屬性不被ARC所管理,其所引用的對象釋放後,此指針也不會被置空。上面代碼我們在viewDidLoad方法中創建了一個MyObject對象,並複製給了當前控制器的object屬性,由於棧內對象的生命週期爲當前代碼塊內有效,因此當viewDidLoad方法結束後,此內存就會被回收,此時object指針就成了野指針。

我們可以在viewDidLoad方法的最後打上斷點,觀察當前MyOject對象的內存分配地址,如下:

可以看到,當次運行時,object對象分配的內存地址爲0x600001e542d0(每次運行都會不同),後面訪問對象的屬性實際上就是對此內存中數據的訪問,我們如果知道了內存地址,也可以直接使用地址進行訪問,不一定要有變量,例如上圖中通過LLDB中的po指令可以直接向內存地址發送消息,效果和通過變量調用對象方法是一樣的。

之後,我們可以在運行後點擊一下當前頁面,大部分情況下都會出現地址異常Crash,我們可以通過LLDB輸出下線程的堆棧信息,如下:

還有時候,程序可能會直接Crash到main方法中,輸入更奇怪的堆棧信息,如下:

如上圖所示,堆棧信息提示我們調用了數組的name方法,這其實就是因爲此塊內存被重新分配了。

我們只創建了一個沒有任何邏輯的Demo項目,野指針的問題都如此多樣,如果是在實際項目中,出了野指針問題我們更難找到問題源頭。並且在ARC環境下,上面示例的場景其實很好排查到,更多產生野指針的原因是多線程不安全的讀寫數據造成的,結合多線程使用,野指針的問題則更加難查。

二、從原理看野指針的監控

要解決由野指針產生的問題,除了編程時儘量注意一些,避免危險的寫法外。更重要的是能總結出一套方案來流程化的對此類問題進行監控。由於野指針問題的特性所致,我們在內存釋放時其實是並不知道是否會產生野指針問題的,發生了野指針問題後也無法回溯。因此我們要用預設的思路來找這類問題的監控方案,即假設當前內存釋放後依然有野指針要訪問它,在設計時,我們可以不真正的將這塊內存釋放,而是將這塊內存標記成有問題的,之後如果又發現有對這塊有問題內存的訪問出現,則表明出現了野指針問題。在標記內存時,我們也可以記錄一下原對象的某些信息,例如類名,這樣在發生野指針問題時,無論具體Crash的堆棧情況如何,我們都可以知道是具體哪個類的對象釋放問題產生的野指針,能極大的縮小問題的排查範圍。

因此處理野指針問題的核心點在於兩點:

1.預設標記內存,被動等待野指針問題觸發。

2.記錄產生野指針問題的類,從類對象的使用入手排查而不是Crash時的堆棧入手排查。

針對如上兩點,我們來看下如何實現。

1. 殭屍對象

將要釋放的對象內存不真正回收,而是僅僅進行標記,我們會形象的成此時的對象爲“殭屍對象”。Xcode默認支持開啓殭屍對象,當我們向一個殭屍對象進行訪問時,就會必然產生Crash,並且控制檯輸出相關提示信息。在Xcode中對要運行的scheme進行編輯,打開殭屍對象功能,如下所示:

再次運行工程,程序Crash後將輸出如下信息:

*** -[MyObject retain]: message sent to deallocated instance 0x600000670670

我們可以明確的知道是MyObject對象的內存問題導致了野指針崩潰。

Xcode的殭屍對象功能雖然好用,但是隻能調試時使用,更多時候我們產生的野指針問題都是線上環境的,而且無法復現,這個功能就顯得非常雞肋的。我們能否不依賴Xcode來實現野指針的監控呢?首先我們需要先搞明白Xcode中殭屍對象的實現原理。

2. Apple殭屍對象的實現原理探究

首先我們大致可以知道,要實現殭屍對象大概率是要對dealloc方法做些事情的,我們可以從這個方法入手找線索,查看objc的源代碼,在其NSObject.m中可以看到如下代碼:

// Replaced by NSZombies
- (void)dealloc {
    _objc_rootDealloc(self);
}

從註釋可以看到,系統實現的殭屍對象的確是處理dealloc方法了,推測其實通過Runtime替換了NSObject的dealloc方法。在CoreFoundation的源碼中也有部分關於Zombies的內容,在CFRuntime.c中可以看到如下代碼:

extern void __CFZombifyNSObject(void);  // from NSObject.m

void _CFEnableZombies(void) {
}

其中,_CFEnableZombies比較好理解,它應該是來表示是否開啓殭屍對象功能的,應該和我們在Xcode中設置的環境變量功能一致,__CFZombifyNSObject從註釋可以知道,應該是對殭屍對象的實現。我們在Xcode中添加一個__CFZombifyNSObject的符號斷點,斷點後內容如下所示:

看到這裏的彙編,你應該不會太陌生,我們把核心的僞代碼提出來,大致如下:

// 定義字符串
define "NSObject"
// 用來獲取NSObject類
objc_lookUpClass "NSObject"
// 定義字符串
define "dealloc"
define "__dealloc_zombie"
// 獲取dealloc方法的實現
class_getInstanceMethod "NSObject" "dealloc"
// 獲取__dealloc_zombie方法的實現
class_getInstanceMethod "NSObject" "__dealloc_zombie"
// 交換dealloc與__dealloc_zombie的方法實現
method_exchangeImplementations "dealloc" "__dealloc_zombie"

和我們想的差不多,下面我們可以再添加一個__dealloc_zombie的符號斷點,看一看__dealloc_zombie方法是怎麼實現的,如下:

CoreFoundation`-[NSObject(NSObject) __dealloc_zombie]:
->  0x10ef77c49 <+0>:   pushq  %rbp
    0x10ef77c4a <+1>:   movq   %rsp, %rbp
    0x10ef77c4d <+4>:   pushq  %r14
    0x10ef77c4f <+6>:   pushq  %rbx
    0x10ef77c50 <+7>:   subq   $0x10, %rsp
    0x10ef77c54 <+11>:  movq   0x2e04fd(%rip), %rax      ; (void *)0x0000000110021970: __stack_chk_guard
    0x10ef77c5b <+18>:  movq   (%rax), %rax
    0x10ef77c5e <+21>:  movq   %rax, -0x18(%rbp)
    0x10ef77c62 <+25>:  testq  %rdi, %rdi
    0x10ef77c65 <+28>:  js     0x10ef77d04               ; <+187>
    0x10ef77c6b <+34>:  movq   %rdi, %rbx
    0x10ef77c6e <+37>:  cmpb   $0x0, 0x488703(%rip)      ; __CFConstantStringClassReferencePtr + 7
    0x10ef77c75 <+44>:  je     0x10ef77d1d               ; <+212>
    0x10ef77c7b <+50>:  movq   %rbx, %rdi
    0x10ef77c7e <+53>:  callq  0x10eff4b52               ; symbol stub for: object_getClass
    0x10ef77c83 <+58>:  leaq   -0x20(%rbp), %r14
    0x10ef77c87 <+62>:  movq   $0x0, (%r14)
    0x10ef77c8e <+69>:  movq   %rax, %rdi
    0x10ef77c91 <+72>:  callq  0x10eff464e               ; symbol stub for: class_getName
    0x10ef77c96 <+77>:  leaq   0x242db5(%rip), %rsi      ; "_NSZombie_%s"
    0x10ef77c9d <+84>:  movq   %r14, %rdi
    0x10ef77ca0 <+87>:  movq   %rax, %rdx
    0x10ef77ca3 <+90>:  xorl   %eax, %eax
    0x10ef77ca5 <+92>:  callq  0x10eff4570               ; symbol stub for: asprintf
    0x10ef77caa <+97>:  movq   (%r14), %rdi
    0x10ef77cad <+100>: callq  0x10eff4ab0               ; symbol stub for: objc_lookUpClass
    0x10ef77cb2 <+105>: movq   %rax, %r14
    0x10ef77cb5 <+108>: testq  %rax, %rax
    0x10ef77cb8 <+111>: jne    0x10ef77cd7               ; <+142>
    0x10ef77cba <+113>: leaq   0x2427aa(%rip), %rdi      ; "_NSZombie_"
    0x10ef77cc1 <+120>: callq  0x10eff4ab0               ; symbol stub for: objc_lookUpClass
    0x10ef77cc6 <+125>: movq   -0x20(%rbp), %rsi
    0x10ef77cca <+129>: movq   %rax, %rdi
    0x10ef77ccd <+132>: xorl   %edx, %edx
    0x10ef77ccf <+134>: callq  0x10eff4a62               ; symbol stub for: objc_duplicateClass
    0x10ef77cd4 <+139>: movq   %rax, %r14
    0x10ef77cd7 <+142>: movq   -0x20(%rbp), %rdi
    0x10ef77cdb <+146>: callq  0x10eff482e               ; symbol stub for: free
    0x10ef77ce0 <+151>: movq   %rbx, %rdi
    0x10ef77ce3 <+154>: callq  0x10eff4a5c               ; symbol stub for: objc_destructInstance
    0x10ef77ce8 <+159>: movq   %rbx, %rdi
    0x10ef77ceb <+162>: movq   %r14, %rsi
    0x10ef77cee <+165>: callq  0x10eff4b6a               ; symbol stub for: object_setClass
    0x10ef77cf3 <+170>: cmpb   $0x0, 0x48867f(%rip)      ; __CFZombieEnabled
    0x10ef77cfa <+177>: je     0x10ef77d04               ; <+187>
    0x10ef77cfc <+179>: movq   %rbx, %rdi
    0x10ef77cff <+182>: callq  0x10eff482e               ; symbol stub for: free
    0x10ef77d04 <+187>: movq   0x2e044d(%rip), %rax      ; (void *)0x0000000110021970: __stack_chk_guard
    0x10ef77d0b <+194>: movq   (%rax), %rax
    0x10ef77d0e <+197>: cmpq   -0x18(%rbp), %rax
    0x10ef77d12 <+201>: jne    0x10ef77d3d               ; <+244>
    0x10ef77d14 <+203>: addq   $0x10, %rsp
    0x10ef77d18 <+207>: popq   %rbx
    0x10ef77d19 <+208>: popq   %r14
    0x10ef77d1b <+210>: popq   %rbp
    0x10ef77d1c <+211>: retq   
    0x10ef77d1d <+212>: movq   0x2e0434(%rip), %rax      ; (void *)0x0000000110021970: __stack_chk_guard
    0x10ef77d24 <+219>: movq   (%rax), %rax
    0x10ef77d27 <+222>: cmpq   -0x18(%rbp), %rax
    0x10ef77d2b <+226>: jne    0x10ef77d3d               ; <+244>
    0x10ef77d2d <+228>: movq   %rbx, %rdi
    0x10ef77d30 <+231>: addq   $0x10, %rsp
    0x10ef77d34 <+235>: popq   %rbx
    0x10ef77d35 <+236>: popq   %r14
    0x10ef77d37 <+238>: popq   %rbp
    0x10ef77d38 <+239>: jmp    0x10eff44c8               ; symbol stub for: _objc_rootDealloc
    0x10ef77d3d <+244>: callq  0x10eff443e               ; symbol stub for: __stack_chk_fail

彙編內容較多,整體流程是比較清晰的,僞代碼如下:

// 獲取當前類
object_getClass
// 通過當前類獲取當前類型
class_getName
// 將_NSZombie_拼接上當前類名
zombiesClsName = "_NSZombie_%s" + className
// 獲取zombiesClsName類
objc_lookUpClass zombiesClsName
// 判斷是否已經存在zombiesCls
if not zombiesCls:
    // 如果不存在 
    // 現獲取"_NSZombie_"類
    cls = objc_lookUpClass "_NSZombie_"
    // 複製出一個cls類,類名爲zombiesClsName
    objc_duplicateClass cls zombiesClsName
// 字符串變量釋放
free zombiesClsName
// objc中原本的對象銷燬方法
objc_destructInstance(self)
// 將當前對象的類修改爲zombiesCls
object_setClass zombiesCls
// 判斷是否開啓了殭屍對象功能
if not __CFZombieEnabled:
    // 如果沒開啓 將當前內存釋放掉
    free

上面的僞代碼基本是__dealloc_zombie方法實現的整體過程,在objc源碼中,NSObject類原本的dealloc方法實現路徑如下:

- (void)dealloc {
    _objc_rootDealloc(self);
}

void _objc_rootDealloc(id obj)
{
    ASSERT(obj);
    obj->rootDealloc();
}

inline void objc_object::rootDealloc()
{
    // taggedPointer無需回收內存
    if (isTaggedPointer()) return;  // fixme necessary?
    // nonpointer爲1表示不只是地址,isa中包含了其他信息
    // weakly_referenced表示是否有弱引用
    // has_assoc 表示是否有關聯屬性
    // has_cxx_dtor 是否需要C++或Objc析構
    // has_sidetable_rc是否有散列表計數引腳
    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    { 
        // 如果都沒有 直接回收內存
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}
id object_dispose(id obj)
{
    if (!obj) return nil;
    // 進行內存回收前的銷燬工作
    objc_destructInstance(obj);    
    free(obj);
    return nil;
}

可以看到,__dealloc_zombie與真正的dealloc的實現其實只差了當前內存的回收部分,objc_destructInstance方法會正常執行的,這個方法實現如下:

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();
        // C++ 析構
        if (cxx) object_cxxDestruct(obj);
        // 移除關聯屬性
        if (assoc) _object_remove_assocations(obj);
        // 弱引用表和散列表的清除
        obj->clearDeallocating();
    }

    return obj;
}

通過上面的分析,我們發現,其實系統實現的殭屍對象非常安全,並不對正常代碼的運行產生負面作用,唯一的影響在於內存不回收會增加內存的使用負擔,但是可以通過某些策略來進行釋放。

三、手動實現線上野指針問題收集

理解了系統殭屍對象的實現原理,即是不依賴Debug環境,我們也可以仿照此思路來實現殭屍對象監控功能。

1. 仿照Apple的殭屍對象思路實現

首先創建一個名爲_YHZombie_的模板類,實現如下:

// _YHZombie_.h
#import <Foundation/Foundation.h>

@interface _YHZombie_ : NSObject

@end


//  _YHZombie_.m
#import "_YHZombie_.h"

@implementation _YHZombie_

// 調用這個對象對的所有方法都hook住進行LOG
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%p-[%@ %@]:%@",self ,[NSStringFromClass(self.class) componentsSeparatedByString:@"_YHZombie_"].lastObject, NSStringFromSelector(aSelector), @"向已經dealloc的對象發送了消息");
    // 結束當前線程
    abort();
}

@end

在新建一個NSObject的類別,用來替換dealloc方法,如下:

//  NSObject+YHZombiesNSObject.h
#import <Foundation/Foundation.h>

@interface NSObject (YHZombiesNSObject)

@end


//  NSObject+YHZombiesNSObject.m
#import "NSObject+YHZombiesNSObject.h"
#import <objc/objc.h>
#import <objc/runtime.h>

@implementation NSObject (YHZombiesNSObject)

+(void)load {
    [self __YHZobiesObject];
}

+ (void)__YHZobiesObject {
    char *clsChars = "NSObject";
    Class cls = objc_lookUpClass(clsChars);
    Method oriMethod = class_getInstanceMethod(cls, NSSelectorFromString(@"dealloc"));
    Method newMethod = class_getInstanceMethod(cls, NSSelectorFromString(@"__YHDealloc_zombie"));
    method_exchangeImplementations(oriMethod, newMethod);
    
}

- (void)__YHDealloc_zombie {
    const char *className = object_getClassName(self);
    char *zombieClassName = NULL;
    asprintf(&zombieClassName, "_YHZombie_%s", className);
    Class zombieClass = objc_getClass(zombieClassName);
    if (zombieClass == Nil) {
        zombieClass = objc_duplicateClass(objc_getClass("_YHZombie_"), zombieClassName, 0);
    }
    objc_destructInstance(self);
    object_setClass(self, zombieClass);
    if (zombieClassName != NULL)
    {
        free(zombieClassName);
    }
}


@end

上面代碼,除了一些容錯判斷沒有加之外,思路和系統的殭屍對象一模一樣。

再次運行我們的測試代碼,在訪問到野指針的時候將百分百的產生異常中斷,並輸出如下:

0x600003a8c2e0-[MyObject name]:向已經dealloc的對象發送了消息

現在,我們已經從原理上簡單實現了一個不依賴於Xcode的野指針監控工具。

2. 將監控推廣到C指針

通過對象的殭屍化,對OC層的野指針問題可以做到很好的監控作用,但是這種方法並不實用與C層的指針,項目中如果用到C相關的指針,由於內存的管理方式不走引用計數,無法通過Hook dealloc的方式來殭屍化對象。例如,我們創建一個如下的結構體:

typedef struct {
    NSString *name;
} MyStruct;

在使用此結構體時,如果初始化之前進行了使用或內存回收後進行了使用都可能會出現野指針問題,如下:

MyStruct *p;
p = malloc(sizeof(MyStruct));
// 此時內存中的數據不可控 可能是之前未擦除的
printf("%x\n", *((int *)p));
// 使用可能會出現野指針問題
NSLog(@"%@", p->name);
// 進行內存數據的初始化
p->name = @"HelloWorld";
// 回收內存
free(p);
// 此時內存中的數據不可控
NSLog(@"%@", p->name);

我們可以思考下,出現上面野指針場景的主要原因是:

1. 獲取到分配的內存後,如果此內存之前有過使用,數據此時是不可控的,當前指針直接使用此數據會有問題。

2.回收內存後,當前內存中的數據是不可控的,可能有別人或之前未清除的指針使用到。

無論是上面哪種場景,此野指針問題都有非常大的隨機性,難以調試。因此我們核心要處理的地方在於把隨機性改爲必然性,即想辦法讓使用到這些有問題的內存時直接Crash,而不是可能Crash。要處理場景1很容易,我們可以hook住C中的malloc方法,分配了內存後直接將一個約定好的異常數據寫入內存,這樣在初始化之前使用到此數據時必然產生Crash。對於場景2,我們可以hook住C中的free方法,回收內存後將一個約定好的異常數據直接寫入此內存,下次如果此內存沒有被再分配,使用到它後也必然產生Crash。Xcode提供的Malloc Scribble調試功能,就是用這種思路實現的。

開啓Xcode的Malloc Scribble選項,運行上面代碼,效果如下圖所示:

可以看到,在malloc分配內存之後,所有字節都被填入了0xAA,未初始化前使用就會必然產生Crash。這與Apple官方文檔的解釋是一致的,但是在free之後,內存數據獲取到的可能並不是文檔所說的0x55,是因爲這塊內存可能被其他內容覆寫了。官網文檔描述如下:

我們也可以手動根據Malloc Scribble的思路來實現一個將野指針問題從隨機變成必然的工具,只需要重寫系統的malloc相關的函數與free函數即可。對於C語言函數的Hook,我們可以直接使用fishhook庫:

https://github.com/facebook/fishhook

導入上面庫後,新建一個命名爲YHMallocScrbble的類,實現如下:

//  YHMallcScrbble.h
#import <Foundation/Foundation.h>

@interface YHMallcScrbble : NSObject

@end

//  YHMallcScrbble.m
#import "YHMallcScrbble.h"
#import "fishhook.h"
#import "malloc/malloc.h"


void * (*orig_malloc)(size_t __size);
void (*orig_free)(void * p);


void *_YHMalloc_(size_t __size) {
    void *p = orig_malloc(__size);
    memset(p, 0xAA, __size);
    return p;
}

void _YHFree_(void * p) {
    size_t size = malloc_size(p);
    memset(p, 0x55, size);
    orig_free(p);
}



@implementation YHMallcScrbble

+ (void)load {
    rebind_symbols((struct rebinding[2]){{"malloc", _YHMalloc_, (void *)&orig_malloc}, {"free", _YHFree_, (void *)&orig_free}}, 2);
}

@end

這樣我們就實現了將野指針問題從隨機變成必然,並且通用C指針。

相比殭屍對象方案,Malloc Scribble方法可以通用C指針,並且真正實現了對對象內存的回收,不會暫用內存。但是也有很大的弊端,比如對於free後寫入的0x55在很多情況下都是無效的,因爲這塊內存可能又被其他地方改寫了,導致Crash依然是隨機的。當然我們也可以在自定義的free方法中不調用原系統的free,使得這塊內存強制不能分配出去,這樣其實和殭屍對象方案就比較類似了。並且相對殭屍對象方案,Malloc Scribble只能一定程度上將隨機變成必然,方便問題的暴露,但是對開發者來說,並沒有太多的信息告訴我們具體是什麼類型的數據出的問題,排查還是有難度。

四、一些擴展

上面只是簡單介紹了對野指針問題監控的一些手段原理。除了殭屍對象和Malloc Scribble外,Xcode中還提供了Address Sanitizer工具來做內存問題的監控,其原理也是對malloc和free函數做了處理,但程序訪問到了有問題的內存時可以及時Crash,同時這個工具可以將對象在malloc時的堆棧信息進行存儲,方面我們定位問題。無論採用哪種方法,如果我們真的要在線上執行,要做的事情其實還有很多,例如數據的收集策略,殭屍對象內存的清理時機,何時判斷出有問題並抓取堆棧等等。

最後,希望本文可以爲你對開發中野指針問題的處理帶來一些思路。本文中所編寫的示例代碼可以在如下地址下載:

https://github.com/ZYHshao/ZombiesDemo

專注技術,懂的熱愛,願意分享,做個朋友

QQ:316045346

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章