天天看點

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

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

一、從一個異常說起

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

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

如果你在iOS項目線上上收集的異常中,有類似EXC\_BAD\_ACCESS的異常,則大機率是由于記憶體問題産生的野指針引起的。這也是本文我們要讨論的核心内容。

1. 什麼是野指針?

目前我們在編寫iOS程式時大多會采用ARC來進行記憶體管理,通常情況下我們無需過多的對記憶體管理進行關心。但是這并不代表不會産生記憶體問題。從原理上講,我們在建立任何對象的時候,首先都會通過作業系統從記憶體中申請出一塊記憶體空間供此對象使用,并将此記憶體空間的位址儲存到指針中供我們在代碼中友善的引用到此記憶體。那麼當這個對象被銷毀的時候,原則上我們需要做兩件事,一是将這塊記憶體還回去,之後作業系統可以重複利用這塊記憶體配置設定給其他申請者使用,二是将代碼中的指針清空回收。這樣可以保證程式能夠可持續化的健康運作。工作過程如下圖所示:

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

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

一種是已經不需要使用的對象我們将指針變量直接清除了,但卻沒有告訴作業系統回收這塊記憶體,此後程式中沒有地方存儲這塊記憶體的位址,這塊記憶體将永遠無法使用和回收。這種情況下,這塊記憶體就變成了無主記憶體且作業系統并不知道,就産生了我們常說的記憶體洩露問題,随着應用的運作時間越來越長,記憶體洩露可能越來越多最終導緻記憶體不夠用,程式無法再正常運作。

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

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對象的記憶體配置設定位址,如下:

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

可以看到,當次運作時,object對象配置設定的記憶體位址為0x600001e542d0(每次運作都會不同),後面通路對象的屬性實際上就是對此記憶體中資料的通路,我們如果知道了記憶體位址,也可以直接使用位址進行通路,不一定要有變量,例如上圖中通過LLDB中的po指令可以直接向記憶體位址發送消息,效果和通過變量調用對象方法是一樣的。

之後,我們可以在運作後點選一下目前頁面,大部分情況下都會出現位址異常Crash,我們可以通過LLDB輸出下線程的堆棧資訊,如下:

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

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

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

如上圖所示,堆棧資訊提示我們調用了數組的name方法,這其實就是因為此塊記憶體被重新配置設定了。

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

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

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

是以處理野指針問題的核心點在于兩點:

1.預設标記記憶體,被動等待野指針問題觸發。

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

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

1. 僵屍對象

将要釋放的對象記憶體不真正回收,而是僅僅進行标記,我們會形象的成此時的對象為“僵屍對象”。Xcode預設支援開啟僵屍對象,當我們向一個僵屍對象進行通路時,就會必然産生Crash,并且控制台輸出相關提示資訊。在Xcode中對要運作的scheme進行編輯,打開僵屍對象功能,如下所示:

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

再次運作工程,程式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的符号斷點,斷點後内容如下所示:

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

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

// 定義字元串
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調試功能,就是用這種思路實作的。

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

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

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

可以看到,在malloc配置設定記憶體之後,所有位元組都被填入了0xAA,未初始化前使用就會必然産生Crash。這與Apple官方文檔的解釋是一緻的,但是在free之後,記憶體資料擷取到的可能并不是文檔所說的0x55,是因為這塊記憶體可能被其他内容覆寫了。官網文檔描述如下:

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

我們也可以手動根據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
專注技術,懂的熱愛,願意分享,做個朋友

繼續閱讀