6

私たちは、コーディング/開発の日々の中で、この特定の最も一般的な例外の 1 つに出くわします。私の質問は理由についてではありません(実際にnullを指す参照変数のプロパティにアクセスしようとすると発生することは承知しています) が、CLR によって NULL REFERENCE EXCEPTION がどのように生成されるかについてです。

ときどき、null への参照を識別し (おそらく null はメモリ内の予約済み領域です)、CLR によって例外を発生させるメカニズムを考えざるを得なくなります。CLR がこの特定の例外を識別して発生させる方法。OSはその中で何らかの役割を果たしていますか?

それに関する最も興味深い主張の1つを共有したいと思います。

nullは、実際には CLR に認識されている常に予約されているメモリ空間であり、あらゆる種類のアクセスが禁止されています。したがって、そのスペースの参照が見つかった場合、デフォルトでは、CLR によって NULL 参照例外として解釈される、OS を介したアクセス拒否の種類の例外が生成されます。

上記の声明を裏付ける記事や投稿が見つからなかったので、信じがたい. 詳細やその他の理由を掘り下げていない可能性がありますが、Stackoverflow は、最良の応答が得られる最も適切なプラットフォームの 1 つであることを期待しています。

4

3 に答える 3

11

そうである必要はありません (明示的なチェックがある場合もあります) が、アクセス違反の例外をトラップすることで機能します。

.NET オブジェクトはネイティブ オブジェクトに変換されます。そのフィールドは特定の方法で配置されたメモリ ブロックになり、そのメソッドはネイティブ マシン コード メソッドに組み込まれ、v テーブルまたはその他の仮想メソッド オーバーロード メカニズムが作成されます。

  1. フィールドへのアクセスとは、オブジェクトのアドレスを見つけ、メンバーのオフセットを追加し、参照されているメモリの一部を読み書きすることを意味します。

  2. 仮想メソッドの呼び出しとは、オブジェクトのアドレスを見つけ、そのメソッド テーブルを見つけ (オブジェクト内でオフセットを設定)、メソッドのアドレスを見つけて (テーブル内でオフセットを設定)、渡されたオブジェクトのアドレスを指定してそのアドレスでメソッドを呼び出すことを意味します。 (thisポインター)。

  3. 非仮想メソッドの呼び出しとは、渡されたオブジェクトのアドレス (thisポインター) を使用してメソッドを呼び出すことを意味します。

明らかに、問題のアドレスに実際のオブジェクトがない場合、ケース 1 と 2 は何らかの形で失敗しますが、ケース 3 は機能します (ただし、ケース 1 または 2 につながる可能性があります)。これがうまくいかない主な原因は 2 つあります。

  1. 実際にはこのタイプのオブジェクトではないメモリの任意のビットにアクセスする可能性があり、あらゆる種類の刺激的で追跡が非常に困難なバグにつながります (.NET コードは通常、このシナリオを引き起こすものにはなりません)。

  2. 保護されているメモリの任意のビットにアクセスして、アクセス違反を引き起こす可能性があります。

2 番目のケースについては、C、C++、または ASM コーディングで知っているかもしれません。そうでない場合でも、おそらくプログラムがクラッシュし、息を切らしながらあるアドレスでのアクセス違反について話しているのを見たことがあるでしょう。その場合、指定されたアドレスはほぼ何でもかまいませんが、ほとんどの場合、0x00000000 または 0x00000020 のような非常に低いアドレスであることに気付いたかもしれません。これらは、コードがフィールドにアクセスするか、仮想メソッドを呼び出すかにかかわらず、null ポインターを逆参照しようとすることが原因でした (これは、基本的にフィールドにアクセスしてから、取得した内容に応じて呼び出します)。

現在、最初の 64k またはメモリは常に保護されているため、null ポインターを逆参照すると、最初のケース (任意のメモリが誤用され、奇妙な「コアのファンダンゴ」バグが発生する) ではなく、常に 2 番目のケース (アクセス違反) が発生します。 )。

これは .NET でもまったく同じです (というか、それによって生成された jit されたコード)。 jit されたコードは に変換されNullReferenceException、それ以外の場合は に変換されますAccessViolationException

逆参照はしないが、保護されたメモリにアクセスするコードでこれをシミュレートできます (読み取りのみを行うため、誤って保護されていないメモリにヒットした場合でも、結果はそれほど奇妙ではありません!) :

次のコードは、AccessViolationException を発生させます。

unsafe
{
  int read = *((int*)long.MaxValue - 8);
}

次のコードは NullReferenceException を発生させます。

unsafe
{
  int read = *((int*)8);
}

どちらのコードも、実際には何も逆参照していません。どちらもアクセス違反を引き起こしますが、CLR は、後者はおそらく null 参照が原因であると想定し (公平に言えば、最も可能性の高いシナリオ)、それを発生させます。

したがって、どのようにフィールドにアクセスし、callvirtこれを引き起こす可能性があるかを確認できます。

C# が安全に実行できる場合でも null 参照でメソッドを呼び出すことを許可しないという決定により、C#callvirtの大部分のケースで IL として使用され、唯一の例外は静的メソッドまたはコンパイル時にnull参照にならないように示すことができます。callvirt(編集:メソッドが実際に仮想である場合でも、コンパイラがa を a に置き換えることができることをコンパイラが確認できる他のいくつかのケースがありcallます[コンパイラがどのオーバーロードがヒットするかを知ることができる場合]、後のコンパイラはこれをもう少し行いますただし、callvirt想像以上に頻繁に使用されます)。

興味深いケースは、最適化により、 with で呼び出されたメソッドcallvirtがインライン化される可能性があることを意味しますが、コンパイル時に非 null であることが保証されていない場合です。このような場合、「呼び出し」(実際には呼び出しではない) が発生する場所の前にフィールド アクセスを追加してNullReferenceException、メソッドの途中ではなく開始時に正確にトリガーすることができます。これは、最適化によって観測された動作が変更されないことを意味します。

于 2012-06-28T09:32:30.723 に答える
4

MS の実装である IIRC は、アクセス違反を介してこれを行います。Null は基本的にゼロ参照であり、基本的には、そのアドレス空間を意図的に予約し、このページをマップしないままにします。メモリ アクセス違反は CPU/OS レベルで自動的に発生し (つまり、null チェックを行うための追加コードは必要ありません)、CLI はこれを null 参照例外として報告します。

興味深いことに、メモリはページ単位で処理されるため、(十分に努力すれば) 同じ理由でゼロ以外の低い値で null 参照例外を実際にシミュレートできます。

編集: Eric Lippert は、この関連する質問/回答でこれについて説明しています: https://stackoverflow.com/a/8681563

于 2012-06-28T08:52:06.777 に答える
1

CLI仕様-ECMA-335を読んだことがありますか?あなたはそこにいくつかの答えを見つけるでしょう。

11クラスのセマンティクス...タイプとしてクラスを持つ変数またはフィールドが作成されると(たとえば、クラスタイプのローカル変数を持つメソッドを呼び出すことによって)、値は最初はnull、つまり特別な値になります。 that:=は、特定のクラスのインスタンスではありませんが、すべてのクラスタイプで使用できます。

そして、ldnull命令の説明:

ldnullは、null参照(タイプO)をスタックにプッシュします。これは、ロケーションがライブになる前またはデッドになる前にロケーションを初期化するために使用されます。[理論的根拠:ldnullは冗長であると思われるかもしれません:代わりにldc.i4.0またはldc.i8.0を使用しないのはなぜですか?答えは、ldnullはサイズに依存しないnullを提供するということです。これは、存在しないldc.i命令に類似しています。ただし、CILにldc.i命令が含まれている場合でも、型の追跡が容易になるため、検証アルゴリズムがldnull命令を保持することにはメリットがあります。理論的根拠] 検証可能性:ldnull命令は常に検証可能であり、他の参照型(§I.8.7.3)に割り当て可能なnull型(§1.8.1.2)の値を生成します。

于 2012-06-28T08:22:39.243 に答える