Null ポインターの逆参照により未定義の動作が発生する

私は最近、C/C++ で &P->m_foo 式を使用して P をヌル ポインターにすることが合法かどうかという問題に関して、意図せずに大きな議論を引き起こしました。プログラマーのコミュニティは 2 つの陣営に分かれました。最初の人は自信を持って合法ではないと主張しましたが、他の人は合法であると確信していました.両当事者はさまざまな議論とリンクを提供し、ある時点で物事を明確にする必要があることに気づきました。そのために、私は Microsoft MVP の専門家と連絡を取り、Visual C++ の Microsoft 開発チームと非公開のメーリング リストを通じて連絡を取りました。彼らは私がこの記事を準備するのを手伝ってくれました。答えが待ちきれない方へ:そのコードは正しくありません。

討論履歴

すべては、PVS-Studio アナライザーを使用した Linux カーネル チェックに関する記事から始まりました。しかし、問題はチェック自体とは何の関係もありません。ポイントは、その記事で Linux のコードから次の断片を引用したことです:

static int podhd_try_init(struct usb_interface *interface,
        struct usb_line6_podhd *podhd)
{
  int err;
  struct usb_line6 *line6 = &podhd->line6;

  if ((interface == NULL) || (podhd == NULL))
    return -ENODEV;
  ....
}

未定義の動作を引き起こすと考えたため、このコードを危険と呼びました。

その後、私は大量の電子メールとコメントを受け取り、読者は私のその考えに反対し、彼らの説得力のある議論に屈する寸前でさえありました.たとえば、そのコードが正しいことの証明として、通常は次のような offsetof マクロの実装が指摘されています。

#define offsetof(st, m) ((size_t)(&((st *)0)->m))

ここではヌル ポインターの逆参照を扱いますが、コードは問題なく動作します。また、ヌルポインタによるアクセスはなかったので問題ないというメールもありました。

私はだまされやすい傾向がありますが、疑わしい情報については再確認するようにしています。私はこの件について調査を開始し、最終的に「Null ポインター逆参照の問題に関する考察」という小さな記事を書きました。

すべてが私の正しさを示唆していました。そのようなコードを書くことはできません。しかし、私の結論に説得力のある証拠を提供し、標準からの関連する抜粋を引用することはできませんでした.

その記事を公開した後、私は再び抗議の電子メールの攻撃を受けました。私は言語の専門家に質問をして、彼らの意見を聞きました。この記事は彼らの回答をまとめたものです。

Cについて

'podhd' がヌル ポインターの場合、'&podhd->line6' 式は C 言語で未定義の動作です。

C99 標準では、「&」アドレス演算子について次のように述べています (6.5.3.2「アドレスと間接演算子」):

単項 &演算子のオペランドは、関数指定子、[] または単項 * 演算子の結果、またはビットフィールドではなく、レジスタ ストレージで宣言されていないオブジェクトを指定する左辺値のいずれかでなければなりません-クラス指定子。

式 'podhd->line6' は明らかに関数指定子ではなく、[] または * 演算子の結果です。それはです 左辺値式。ただし、「podhd」ポインターが NULL の場合、6.3.2.3「ポインター」に次のように記載されているため、式はオブジェクトを指定しません。

null ポインター定数がポインター型に変換された場合、null ポインターと呼ばれる結果のポインターは、オブジェクトまたは関数へのポインターと等しくないことが保証されます。

「左辺値が評価時にオブジェクトを指定しない場合、動作は未定義です」(C99 6.3.2.1「左辺値、配列、および関数指示子」):

左辺値は、オブジェクト型または void 以外の不完全な型を持つ式です。左辺値が評価時にオブジェクトを指定しない場合、動作は未定義です。

つまり、同じアイデアを簡単に説明すると:

-> がポインターで実行されると、オブジェクトが存在しない左辺値に評価され、結果として動作が未定義になります。

C++ について

C++ 言語でも、まったく同じです。 'podhd' が null ポインターの場合、'&podhd->line6' 式は未定義の動作です。

前回の記事で言及した WG21 での議論 (232. Is indirection through a null pointer undefined behavior?) は、いくつかの混乱をもたらしました。それに参加しているプログラマーは、この式は未定義の動作ではないと主張しています。ただし、「podhd」がヌル ポインターである「podhd->line6」の使用を許可する句を C++ 標準で見つけた人は誰もいません。

「podhd」ポインターは、オブジェクトを指定する必要があるという基本的な制約 (5.2.5/4、2 番目の箇条書き) に違反しています。アドレスとして nullptr を持つ C++ オブジェクトはありません。

まとめ

struct usb_line6 *line6 = &podhd->line6;

podhd ポインターが 0 の場合、このコードは C と C++ の両方で正しくありません。ポインターが 0 の場合、未定義の動作が発生します。

プログラムがうまく動くかどうかは、まったくの運です。未定義の動作は、プログラマーが期待したとおりにプログラムを実行するなど、さまざまな形をとる場合があります。これは、未定義の動作の特殊なケースの 1 つに過ぎず、それだけです。

そのようなコードを書くことはできません。逆参照する前にポインタをチェックする必要があります。

その他のアイデアとリンク

  • 「offsetof()」演算子の慣用的な実装を検討する場合、コンパイラの実装では、移植性のない手法を使用してその機能を実装することが許可されていることを考慮する必要があります。コンパイラのライブラリ実装が 'offsetof()' の実装でヌル ポインター定数を使用するという事実は、'podhd' がヌル ポインターである場合にユーザー コードが '&podhd->line6' を使用することを許可しません。
  • 未定義の動作が発生しないことを前提として、GCC は最適化できます/実行し、ここでヌル チェックを削除します。カーネルは、これを行わないようにコンパイラに指示するために、一連のスイッチを使用してコンパイルします。例として、専門家は記事「すべての C プログラマーが未定義の動作について知っておくべきこと #2/3」を参照しています。
  • また、TUN/TAP ドライバーを使用したカーネル エクスプロイトにヌル ポインターの同様の使用法が関与していたことも興味深いかもしれません。 「NULL ポインターの楽しみ」を参照してください。一部の人々に類似性が当てはまらないと思わせる主な違いは、TUN/TAP ドライバーのバグでは、null ポインターがアクセスした構造体フィールドが、単に値を持つのではなく、変数を初期化するための値として明示的に取得されたことです。取得したフィールドのアドレス。ただし、標準 C に関する限り、null ポインターを介してフィールドのアドレスを取得することは、まだ未定義の動作です。
  • &P->m_foo を書くときに P ==nullptr が OK であるケースはありますか?はい、たとえば sizeof 演算子の引数の場合:sizeof(&P->m_foo).

謝辞

この記事は、私が疑いの余地のない能力を持っている専門家のおかげで可能になりました.執筆を手伝ってくれた次の方々に感謝します:

  • マイケル バー Windows サービス、ネットワーク、デバイス ドライバーなどのシステム レベルおよび組み込みソフトウェアを専門とする C/C++ 愛好家です。彼は、スタック オーバーフロー コミュニティで、C および C++ に関する質問に答えていることがよくあります (また、より簡単な C# の質問に答えることもあります)。彼は、Visual C++ で 6 つの Microsoft MVP 賞を受賞しています。
  • ビリー・オニール (主に) C++ 開発者であり、スタック オーバーフローへの貢献者です。彼は、Trustworthy Computing チームの Microsoft ソフトウェア開発エンジニアです。彼は以前、Malware Bytes や PreEmptive Solutions など、セキュリティ関連の複数の場所で働いていました。
  • ジョバンニ ディカーニオ は、Windows オペレーティング システムの開発を専門とするコンピューター プログラマーです。 Giovanni は、イタリアのコンピューター雑誌で、C++、OpenGL、およびその他のプログラミングに関するコンピューター プログラミングの記事を書いています。彼はいくつかのオープンソース プロジェクトにもコードを提供しました。 Giovanni は、Microsoft MSDN フォーラムや最近では Stack Overflow で C および C++ プログラミングの問題を解決する人々を支援するのが好きです。彼は、Visual C++ で 8 つの Microsoft MVP 賞を受賞しています。
  • ガブリエル ドス レイス マイクロソフトのプリンシパル ソフトウェア開発エンジニアです。彼は研究者でもあり、C++ コミュニティの長年のメンバーでもあります。彼の研究対象には、信頼できるソフトウェアのプログラミング ツールが含まれます。マイクロソフトに入社する前は、テキサス A&M 大学で助教授を務めていました。 Dos Reis 博士は、信頼できる計算数学と教育活動のためのコンパイラに関する研究により、2012 年国立科学財団キャリア賞を受賞しました。 C++ 標準化委員会のメンバーです。

参考文献

  • ウィキペディア。未定義の動作。
  • C および C++ の未定義の動作に関するガイド。パート 1、2、3.
  • ウィキペディア。オフセット
  • LLVM ブログ。すべての C プログラマーが未定義の動作について知っておくべきこと #2/3.
  • LWN。 NULL ポインターをお楽しみください。パート 1、2。
  • スタック オーバーフロー。 nullptr と等しいポインターの逆参照は、標準で定義されていない動作ですか?