[iOSスタディノート]-ワイルドポインターとゾンビオブジェクトの配置について話します
1.例外から始める
iOSプロジェクトの開発では、多少のクラッシュが発生します。クラッシュによってスローされる例外のほとんどはNSExceptionレイヤーです。このような例外は、OCレイヤーのコードの問題、通常はスタック情報と例外によって発生します。プロンプト情報は非常に明確です。問題のあるコードを直接見つけることができるので、そのような問題を解決することは難しくありません。NSExceptionに加えて、クラッシュを引き起こす可能性のある例外には、UnixレイヤーとMachレイヤーの例外が含まれます。
Mach例外は通常、低レベルのカーネルレベルの例外です。このような例外は、一部の低レベルAPIを介してキャプチャできます。これはこの記事の内容ではないため、ここでは繰り返さないことにします。Unixレイヤーは、開発者がキャッチしないMach例外を参照し、対応するUnixシグナルに変換され、エラースレッドに渡されます。
iOSプロジェクトでオンラインで収集する例外の中にEXC_BAD_ACCESSと同様の例外がある場合は、メモリの問題によって生成されたワイルドポインタが原因である可能性があります。これは、この記事で説明するコアコンテンツでもあります。
1.ワイルドポインタとは何ですか?
現在、iOSプログラムを作成する際のメモリ管理には、主にARCを使用していますが、通常、メモリ管理についてはあまり気にする必要はありません。しかし、それはメモリの問題が発生しないという意味ではありません。原則として、オブジェクトを作成するときは、最初に、オペレーティングシステムで使用するオブジェクトのメモリからメモリスペースを申請し、このメモリスペースのアドレスをポインタに保存して、コード内でこれを参照できるようにします。メモリー。次に、オブジェクトが破棄された場合、原則として2つのことを行う必要があります。1つはメモリを返すことです。次に、オペレーティングシステムはメモリを再利用して他の申請者に割り当てることができます。もう1つは、コードでポインタを割り当てることです。空のリサイクル。これにより、プログラムを持続可能かつ健全に実行できるようになります。作業プロセスを次の図に示します。
しかし、人生やプログラミングに関係なく、事故は常に発生します。通常、オペレーティングシステムにメモリを適用するステップでは、問題はほとんどありません。オペレーティングシステム自体の安定性は、アプリケーションプログラムの安定性よりもはるかに強力です。この問題は主に、メモリが解放されたときに発生します。2種類の問題が考えられます。
1つは、不要になったオブジェクトのポインタ変数を直接クリアしますが、オペレーティングシステムにこのメモリを再利用するように指示しないことです。その後、このメモリのアドレスをプログラムに格納する場所がなくなり、このメモリが使用されたり、再利用されたりすることはありません。この場合、このメモリは所有されていないメモリになり、オペレーティングシステムはそれを認識しないため、よく言われるメモリリークの問題が発生します。アプリケーションの実行時間が長くなると、メモリリークが増える可能性があります。多すぎると、最終的にメモリが不足し、プログラムが正常に実行できなくなります。
もう1つは、オペレーティングシステムにこのメモリを再利用するように指示し、このメモリは実際に再利用されますが、このアドレスを格納し、空にされていないポインタ変数がプログラムに残っていることです。ポインタがポインタになります。ポインタが指すメモリが再利用されているため、このメモリが再び使用されたのか、元のデータがまだ保存されているのかはわかりません。その後、このポインタを介してこのメモリのデータを誤って使用すると、読み書きに関係なく、さまざまな奇妙な問題が発生し、私たちが見つけるのが困難になります。この記事では、主にこのようなワイルドポインタ問題の原因と配置方法について説明します。
2.ワイルドポインタはどのような問題を引き起こす可能性がありますか?
開発中に発生したEXC_BAD_ACCESSの問題のほとんどは、ワイルドポインターが原因で発生し、SIGSEGVとSIGBUSの2つの主要なシグナルがあります。その中で、SIGSEGVは、操作のアドレスが不正であるか、未割り当てのメモリにアクセスされているか、書き込み許可のないメモリに書き込まれていることを示します。SIGBUSは、間違ったメモリタイプのアクセスを示します。
ワイルドポインタによって引き起こされる問題は、あらゆる種類の奇妙で見つけるのが難しいものです。プログラムでワイルドポインタを使用する場合、次の2つのシナリオが考えられます。
1>アクセスされたメモリは上書きされません
元のオブジェクトが依存している他のオブジェクトを削除しないと、プログラムの動作に問題があるように見えるかもしれませんが、実際には非常に危険であり、プログラムロジックのパフォーマンスが制御できなくなります。
元のオブジェクトが依存している他のオブジェクトを削除すると、内部に他のワイルドポインターが生成される可能性があり、さまざまな複雑な異常なシナリオが引き続き発生します。
2>アクセスしたメモリが再度上書きされます
このスレーブシナリオはさらに厄介です。現在のメモリ領域のアクセス可能性が変更されると、objc_msgSendの失敗、SIGBUSアドレスタイプの例外、SIGFPE演算子の例外、SIGILL命令の例外など、多くの種類の例外が生成されます。
現在のメモリにアクセスできる場合、他の場所に不良メモリを書き込むことは意図に反している可能性があり、他の場所で使用されている場合は例外が発生します。使用するデータ型が元のオブジェクトと一致しない可能性もあります。その結果、実装されていないセレクタークラスのエラー、メソッドクラスの検索エラー、さまざまな基になるロジックエラー、mallocエラーが発生します。この時点で、問題のトラブルシューティングは非常に困難です。
要約すると、ワイルドポインタの害は非常に大きく、異常なクラッシュを引き起こすだけでなく、他の通常使用されるコードの例外を引き起こす可能性があり、再現性とランダム性があります。たとえば、クラッシュのスタックは次のとおりです。オブジェクトのメソッドが呼び出されること。見つけるのは簡単ではありませんが、コードを検索するときに同様のメソッド呼び出しを見つけることはできません。実際、他の場所にワイルドポインターの問題があり、正しいオブジェクトはワイルドポインタに割り当てられます。ポイントされたメモリで、ワイルドポインタはこのメモリ内のデータを破棄します。このクラッシュの問題については、ほとんど無力です。
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クラスのオブジェクトプロパティはunsafe_unretainedとして宣言されています。この修飾子は、現在のプロパティがARCによって管理されていないことを意味します。参照するオブジェクトが解放された後、このポインタも空白のままになりますか。上記のコードでは、viewDidLoadメソッドでMyObjectオブジェクトを作成し、それを現在のコントローラーのobjectプロパティにコピーします。スタック内のオブジェクトのライフサイクルは現在のコードブロックで有効であるため、viewDidLoadメソッドが終了すると、このメモリはリサイクルされます。このとき、オブジェクトポインタはワイルドポインタになります。
次のように、viewDidLoadメソッドの最後にブレークポイントを設定して、現在のMyOjectオブジェクトのメモリ割り当てアドレスを監視できます。
次の実行では、オブジェクトオブジェクトによって割り当てられたメモリアドレスは0x600001e542d0(実行ごとに異なります)であり、後でオブジェクトのプロパティにアクセスすることは、実際にはこのメモリ内のデータへのアクセスであることがわかります。メモリアドレスがわかっているので、アクセスにアドレスを直接使用することも必ずしも変数を必要としません。たとえば、上の図のLLDBのpo命令は、メモリアドレスに直接メッセージを送信できます。効果は、変数を介したオブジェクトメソッド。
その後、実行後に現在のページをクリックできます。ほとんどの場合、アドレス例外Crashが発生します。次のように、LLDBを介してスレッドのスタック情報を出力できます。
次のように、プログラムがmainメソッドに直接クラッシュし、さらに奇妙なスタック情報を入力する場合があります。
上の図に示すように、スタック情報により、配列のnameメソッドを呼び出すように求められます。これは、実際には、このメモリブロックが再割り当てされているためです。
ロジックを使わずにデモプロジェクトを作成しただけで、ワイルドポインターの問題は非常に多様です。実際のプロジェクトの場合、ワイルドポインターに問題があると、問題の原因を見つけるのが難しくなります。また、ARC環境では、上記のシナリオ例は実際には非常に簡単に確認でき、ワイルドポインターの理由は、マルチスレッドの安全でないデータの読み取りと書き込みが原因で発生します。マルチスレッドの使用と組み合わせると、ワイルドポインターの問題はより困難になります。チェックする。
第二に、原則からのワイルドポインタの監視
ワイルドポインタによって引き起こされる問題を解決するには、プログラミングに加えて、危険な書き込み方法を避けるように注意を払うようにしてください。さらに重要なことに、一連のソリューションを要約して、プロセス内のこのような問題を監視できます。ワイルドポインタ問題の特性により、メモリが解放されたときにワイルドポインタ問題が発生するかどうかは実際にはわかりません。また、ワイルドポインタ問題が発生した後にバックトラックすることはできません。したがって、このタイプの問題の監視ソリューションを見つけるには、事前設定されたアイデアを使用する必要があります。つまり、現在のメモリが解放された後もアクセスするためのワイルドポインターがまだあると仮定すると、設計上、この部分を実際に解放することはできません。このメモリは問題があるとマークされており、問題のあるメモリへのアクセスがあることが判明した場合は、ワイルドポインタの問題を示しています。メモリをマークするときに、クラス名などの元のオブジェクトの情報を記録することもできるため、ワイルドポインターの問題が発生した場合、特定のクラッシュスタックが何であっても、どのクラスのオブジェクト解放の問題が発生したかを知ることができます。 。ワイルドポインタは、トラブルシューティングの範囲を大幅に狭める可能性があります。
したがって、ワイルドポインタの問題に対処するための核となるポイントは、次の2つのポイントにあります。
1.マークされたメモリをプリセットし、ワイルドポインタの問題がトリガーされるのを受動的に待ちます。
2.ワイルドポインタの問題の原因となるクラスを記録し、クラッシュが発生したときにスタックの代わりにクラスオブジェクトを使用して調査を開始します。
上記の2つのポイントについて、それを達成する方法を見てみましょう。
1.ゾンビオブジェクト
解放されるオブジェクトのメモリは実際にはリサイクルされず、マークされるだけであり、この時点でオブジェクトを「ゾンビオブジェクト」として視覚化します。Xcodeはデフォルトでゾンビオブジェクトを開くことをサポートしています。ゾンビオブジェクトにアクセスすると、必然的にクラッシュが発生し、コンソールは関連するプロンプト情報を出力します。Xcodeで実行するスキームを編集し、次のようにゾンビオブジェクト機能をオンにします。
プロジェクトを再度実行すると、プログラムがクラッシュした後、次の情報が出力されます。
*** -[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メソッドを処理しますが、NSObjectのdeallocメソッドは実際にはRuntimeに置き換えられていると推測されます。CoreFoundationのソースコードにはゾンビに関するコンテンツも含まれています。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;
}
上記の分析により、システムによって実装されたゾンビオブジェクトは実際には非常に安全であり、通常のコードの動作に悪影響を及ぼさないことがわかりました。唯一の影響は、メモリがリサイクルされないことです。これにより、メモリ使用量ですが、特定の戦略によって解放できます。
3.オンラインワイルドポインタ問題収集を手動で実装する
システムゾンビオブジェクトの実装原理を理解した後、つまり、デバッグ環境に依存せずに、このアイデアに従ってゾンビオブジェクト監視機能を実装することもできます。
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
次のように、deallocメソッドを置き換える新しいNSObjectカテゴリを作成します。
// 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
上記のコードは、いくつかのフォールトトレラントな判断を除いて、システムのゾンビオブジェクトと同じ考え方を持っています。
テストコードを再度実行すると、ワイルドポインターにアクセスすると、例外が100%生成され、出力は次のようになります。
0x600003a8c2e0-[MyObject name]:向已经dealloc的对象发送了消息
これで、原則としてXcodeに依存しないワイルドポインタ監視ツールを実装しただけです。
2.監視をCポインタに拡張します
オブジェクトのゾンビ化により、OCレイヤーのワイルドポインター問題を十分に監視できますが、この方法はCレイヤーのポインターには実用的ではありません。プロジェクトでC関連のポインターを使用する場合、メモリが原因です。管理方法参照カウントがないと、フック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.メモリが再利用された後、現在のメモリ内のデータは制御不能になり、他のユーザーまたは以前にクリアされていないポインタによって使用される可能性があります。
上記のシナリオに関係なく、このワイルドポインターの問題は非常にランダムであり、デバッグが困難です。したがって、コアで対処する必要があるのは、ランダム性を必然性に変更することです。つまり、クラッシュする可能性があるのではなく、これらの問題のあるメモリを使用するときに直接クラッシュする方法を見つけることです。シナリオ1の処理は簡単です。Cでmallocメソッドをフックし、メモリを割り当てた後、事前に定義された例外データをメモリに直接書き込むことができるため、初期化前にこのデータを使用するとクラッシュが発生します。シナリオ2では、Cでfreeメソッドをフックし、メモリを再利用した後、合意された例外データをこのメモリに直接書き込むことができます。次回メモリが再割り当てされない場合、使用後に必然的にクラッシュが発生します。Xcodeが提供するMallocScribbleデバッグ機能は、このように実装されています。
XcodeのMallocScribbleオプションをオンにして、上記のコードを実行すると、効果は次のようになります。
mallocがメモリを割り当てた後、すべてのバイトが0xAAで満たされ、初期化の前に使用するとクラッシュが必然的に発生することがわかります。これはAppleの公式ドキュメントの説明と一致していますが、解放後、このメモリは他のコンテンツによって上書きされる可能性があるため、ドキュメントに記載されているように、取得されるメモリデータは0x55ではない場合があります。公式ウェブサイトのドキュメントは次のとおりです。
また、Malloc Scribbleのアイデアに従って、ワイルドポインターの問題をランダムから不可避に変更するツールを手動で実装することもできます。システムのmalloc関連の関数と無料の関数を書き直すだけです。C言語関数のフックには、フィッシュフックライブラリを直接使用できます。
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ポインターを普遍的に使用でき、一時的にメモリを使用せずにオブジェクトメモリの回復を真に実現します。ただし、大きな欠点もあります。たとえば、解放後に書き込まれた0x55は、他の場所でこのメモリが書き換えられてランダムにクラッシュする可能性があるため、多くの場合無効です。もちろん、カスタムfreeメソッドで元のシステムのfreeメソッドを呼び出すこともできないため、実際にはゾンビオブジェクトスキームと同様に、メモリを強制的に割り当てることはできません。また、ゾンビオブジェクトスキームと比較すると、Malloc Scribbleはある程度のランダム性しか変更できないため、問題を明らかにするのに便利ですが、開発者にとっては、問題のデータの種類を示す情報があまりありません。それでも難しい。
第四に、いくつかの拡張機能
上記は、ワイルドポインタ問題を監視するいくつかの方法と原則の簡単な紹介です。ゾンビオブジェクトとMallocScribbleに加えて、Xcodeはメモリの問題を監視するためのアドレスサニタイザーツールも提供します。原則はmallocとfree関数を処理することですが、プログラムが問題のあるメモリにアクセスすると、時間内にクラッシュする可能性があります。同時に、このツールはmalloc中にオブジェクトのスタック情報を保存できるため、問題を特定できます。どの方法を採用しても、本当にオンラインで実行したい場合は、データ収集戦略、ゾンビオブジェクトメモリのクリーニングタイミング、問題を判断してスタックを取得するタイミングなど、まだやるべきことがたくさんあります。 。
最後に、この記事が、開発中のワイルドポインターの問題に対処するためのいくつかのアイデアをもたらすことを願っています。この記事に書かれているサンプルコードは、次のアドレスからダウンロードできます。
https://github.com/ZYHshao/ZombiesDemo
テクノロジーに焦点を合わせ、愛を理解し、喜んで共有し、友達になりましょう
QQ:316045346