これについてもう少し詳しく説明し、より完全な答えを示したいと思います。まず、このコードについて考えてみましょう。
#import <Foundation/Foundation.h>
int main(int argc, char *argv[]) {
void (^block)() = nil;
block();
}
これを実行すると、次のblock()
ような行でクラッシュが発生します(32ビットアーキテクチャで実行した場合、これは重要です)。
EXC_BAD_ACCESS(コード= 2、アドレス= 0xc)
それで、それはなぜですか?さて、これ0xc
が最も重要なビットです。クラッシュは、プロセッサがメモリアドレスの情報を読み取ろうとしたことを意味します0xc
。これはほぼ間違いなく完全に間違ったことです。そこに何かがある可能性は低いです。しかし、なぜこのメモリ位置を読み取ろうとしたのでしょうか。まあ、それはブロックが実際にボンネットの下に構築される方法によるものです。
ブロックが定義されると、コンパイラーは実際にスタック上に次の形式の構造体を作成します。
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
この場合、ブロックはこの構造体へのポインターになります。この構造の4番目のメンバーであるinvoke
、は興味深いものです。これは関数ポインタであり、ブロックの実装が保持されているコードを指します。したがって、ブロックが呼び出されると、プロセッサはそのコードにジャンプしようとします。メンバーの前の構造体のバイト数を数えるとinvoke
、10進数で12、または16進数でCであることがわかります。
したがって、ブロックが呼び出されると、プロセッサはブロックのアドレスを取得し、12を加算して、そのメモリアドレスに保持されている値をロードしようとします。次に、そのアドレスにジャンプしようとします。ただし、ブロックがnilの場合は、アドレスを読み取ろうとします0xc
。これは明らかにダフアドレスであるため、セグメンテーション違反が発生します。
さて、Objective-Cメッセージ呼び出しのように黙って失敗するのではなく、このようなクラッシュでなければならない理由は、実際には設計上の選択です。コンパイラはブロックの呼び出し方法を決定する作業を行っているため、ブロックが呼び出されるすべての場所にnilチェックコードを挿入する必要があります。これにより、コードサイズが大きくなり、パフォーマンスが低下します。別のオプションは、nilチェックを行うトランポリンを使用することです。ただし、これにはパフォーマンスの低下も発生します。Objective-Cメッセージは、実際に呼び出されるメソッドを検索する必要があるため、すでにトランポリンを通過しています。ランタイムはメソッドのレイジーインジェクションとメソッド実装の変更を可能にするので、とにかくすでにトランポリンを通過しています。この場合、nilチェックを実行することによる追加のペナルティは重要ではありません。
それが理論的根拠を説明するのに少し役立つことを願っています。
詳細については、私のブログ 投稿を参照してください。