1. ホーム
  2. objective-c

nil / NULLブロックを実行すると、なぜバスエラーが発生するのですか?

2023-09-16 17:28:34

質問

私はブロックをよく使うようになり、すぐにnilブロックがバスエラーを引き起こすことに気づきました。

typedef void (^SimpleBlock)(void);
SimpleBlock aBlock = nil;
aBlock(); // bus error

これは、nilオブジェクトへのメッセージを無視するObjective-Cの通常の動作に反しているように思えます。

NSArray *foo = nil;
NSLog(@"%i", [foo count]); // runs fine

したがって、私はブロックを使う前に通常のnilチェックに頼らざるを得ません。

if (aBlock != nil)
    aBlock();

またはダミーブロックを使用する。

aBlock = ^{};
aBlock(); // runs fine

他の選択肢はないのでしょうか?nilブロックは単にnopではだめな理由があるのでしょうか?

どのように解決するのですか?

もう少し、完全な回答で説明したいと思います。まず、このコードを考えてみましょう。

#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 この構造体の4番目のメンバーであるが興味深いものです。これは関数ポインターで、ブロックの実装が保持されているコードを指しています。そのため、プロセッサはブロックが呼び出されたときにそのコードにジャンプしようとします。の前の構造体のバイト数を数えてみてください。 invoke メンバの前にある構造体のバイト数を数えると、10進数で12、16進数でCであることがわかります。

つまり、ブロックが呼び出されると、プロセッサはブロックのアドレスを取り、12を加えて、そのメモリアドレスに保持されている値をロードしようとするのです。そして、そのアドレスにジャンプしようとします。しかし、ブロックがnilであれば、アドレス 0xc . これは明らかに不正なアドレスなので、セグメンテーションフォールトが発生します。

Objective-C のメッセージコールのように黙って失敗するのではなく、このようにクラッシュしなければならない理由は、本当に設計上の選択です。コンパイラーは、ブロックをどのように呼び出すかを決定する作業を行っているため、ブロックが呼び出されるたびに nil チェック コードを注入する必要があります。これでは、コードサイズが大きくなり、パフォーマンスも悪くなります。もう一つの方法は、nilチェックを行うトランポリンを使用することです。しかし、これもパフォーマンスが低下します。Objective-Cのメッセージは、実際に呼び出されるメソッドを調べる必要があるため、すでにトランポリンを経由しています。ランタイムはメソッドの遅延注入やメソッド実装の変更を許可しているので、どのみちすでにトランポリンを通過しているのです。この場合、nilチェックを行うことによる余分なペナルティは重要ではありません。

根拠を説明するのに少しは役に立ったでしょうか。

より詳しい情報は、私の ブログ 投稿 .