NULLの再定義


118

アドレス0x0000が有効で、ポートI / Oを含むシステム用のCコードを書いています。したがって、NULLポインタにアクセスする可能性のあるバグは検出されないままであり、同時に危険な動作を引き起こします。

このため、NULLを別のアドレス、たとえば無効なアドレスに再定義したいと思います。誤ってそのようなアドレスにアクセスすると、エラーを処理できるハードウェア割り込みが発生します。このコンパイラのstddef.hにアクセスしたため、実際に標準ヘッダーを変更してNULLを再定義できます。

私の質問は、これはC標準と競合しますか?標準の7.17からわかる限り、マクロは実装定義です。NULL が0でなければならないことを述べている標準の他の場所はありますか?

もう1つの問題は、多くのコンパイラーが、データ型に関係なく、すべてをゼロに設定して静的初期化を実行することです。標準では、コンパイラーは整数をゼロに、ポインターをNULLに設定する必要があるとしていますが。コンパイラーに対してNULLを再定義すると、このような静的な初期化が失敗することがわかります。手動でコンパイラヘッダーを大胆に変更した場合でも、コンパイラの動作が間違っていると見なすことはできますか?なぜなら、この特定のコンパイラーは、静的な初期化を行うときにNULLマクロにアクセスしないことは確かです。


3
これは本当に良い質問です。私には答えはありませんが、私は尋ねなければなりません:有効なものを0x00に移動して、「通常の」システムのようにNULLを無効なアドレスにすることは不可能ですか?できない場合は、安全に無効なアドレスを使用して、確実に割り当ててから保護することができますmprotect。または、プラットフォームにASLRなどがない場合、プラットフォームの物理メモリを超えてアドレス指定します。幸運を。
Borealid、2011

8
コードが使用している場合、どのように機能しif(ptr) { /* do something on ptr*/ }ますか?NULLが0x0と異なるように定義されている場合は機能しますか?
Xavier T.11年

3
Cポインタは、メモリアドレスとの強制的な関係はありません。ポインタ演算のルールが守られている限り、ポインタ値は何でもかまいません。ほとんどの実装では、ポインタ値としてメモリアドレスを使用することを選択していますが、同型であればどのようなものでも使用できます。
datenwolf

2
@bdonlanこれはMISRA-Cの(助言)規則にも違反します。
ランディン、2011

2
@Andreasうん、それも私の考えです。ソフトウェアを実行するハードウェアをハードウェア設計者が設計することを許可してはなりません。:)
ランディン

回答:


84

C標準では、マシンのアドレス0にnullポインターを置く必要はありません。ただし、0定数をポインター値にキャストすると、NULLポインター(§6.3.2.3/ 3)が生成され、nullポインターをブール値として評価するとfalseになる必要があります。本当にゼロアドレス必要で、ゼロアドレスでNULLはない場合、これは少し扱いに​​くい場合があります。

それでも、コンパイラと標準ライブラリに(大きな)変更を加えたため、標準ライブラリにNULL厳密に準拠したまま、代替ビットパターンで表現することは不可能ではありません。しかし、真と評価されるように、それ自体の定義を単に変更するだけで十分ではありませんNULLNULL

具体的には、次のことを行う必要があります。

  • ポインタへの代入(またはポインタへのキャスト)でリテラルのゼロを、などの他の魔法の値に変換するように調整します-1
  • 0代わりに、マジック値をチェックするために、ポインターと定数整数の間の等価テストを調整します(§6.5.9/ 6)
  • ポインター型がブール値として評価されるすべてのコンテキストについて、ゼロをチェックするのではなく、マジック値と等しいかどうかをチェックするようにします。これは同等性テストのセマンティクスに基づいていますが、コンパイラーは内部的に異なる方法で実装する場合があります。§6.5.13/ 3、§6.5.14/ 3、§6.5.15/ 4、§6.5.3.3/ 5、§6.8.4.1/ 2、§6.8.5/ 4を参照
  • cafが指摘したように、静的オブジェクトの初期化(§6.7.8/ 10)および部分複合初期化子(§6.7.8/ 21)のセマンティクスを更新して、新しいnullポインター表現を反映させます。
  • 真のアドレス0にアクセスする別の方法を作成します。

処理する必要のないものがいくつかあります。例えば:

int x = 0;
void *p = (void*)x;

この後p、nullポインタであることが保証されません。定数の割り当てのみを処理する必要があります(これは、真のアドレス0にアクセスするための優れたアプローチです)。同様に:

int x = 0;
assert(x == (void*)0); // CAN BE FALSE

また:

void *p = NULL;
int x = (int)p;

xであるとは限りません0

要するに、この状態はC言語委員会によって明らかに検討され、NULLの代替表現を選択する人のために考慮が払われました。あなたが今しなければならないすべてはあなたのコンパイラーに大きな変更を加えることです、そしてちょっとちょっとあなたはやっつけられました:)

補足として、コンパイラーが適切になる前に、ソースコード変換ステージでこれらの変更を実装できる場合があります。つまり、プリプロセッサ->コンパイラ->アセンブラ->リンカの通常のフローの代わりに、プリプロセッサ-> NULL変換->コンパイラ->アセンブラ->リンカを追加します。次に、次のような変換を行うことができます。

p = 0;
if (p) { ... }
/* becomes */
p = (void*)-1;
if ((void*)(p) != (void*)(-1)) { ... }

これには、完全なCパーサーと、タイプパーサー、typedefと変数宣言の分析が必要であり、どの識別子がポインターに対応するかを決定します。ただし、これにより、コンパイラのコード生成部分を適切に変更する必要がなくなります。clangはこれを実装するのに役立つかもしれません-私はそれがこのような変換を考慮して設計されたことを理解しています。もちろん、標準ライブラリにも変更を加える必要があります。


2
さて、§6.3.2.3のテキストは見つかりませんでしたが、そのようなステートメントがどこかにあるのではないと思いました:)。私はこれが私の質問に答えると思います。標準では、新しいCコンパイラを書いて私をバックアップしない限り、NULLを再定義することはできません:)
Lundin

2
良いトリックは、コンパイラーをハックして、ポインター<->整数変換が無効なポインターである特定の値をXORし、ターゲットアーキテクチャがそれを安価に実行できるほど簡単なことです(通常、単一ビットセットの値になります) 、0x20000000など)。
Simon Richter

2
コンパイラで変更する必要があるもう1つのことは、複合型のオブジェクトの初期化です。オブジェクトが部分的に初期化されている場合は、明示的な初期化子が存在しないすべてのポインタをに初期化する必要がありますNULL
カフェ、2011

20

標準では、値が0の整数定数式、またはvoid *型に変換された式はnullポインター定数であると規定されています。これは、(void *)0常にnullポインタであることを意味しますが、与えられたint i = 0;場合、そうである(void *)i必要はありません。

Cの実装は、ヘッダーとコンパイラーで構成されます。ヘッダーを変更してredefineしNULL、コンパイラーを変更して静的な初期化を修正しない場合は、非準拠の実装が作成されています。正しくない振る舞いをするのは、一緒に取られた実装全体であり、それを壊した場合、あなたは本当に他に誰も非難することがありません;)

上記のルールにより、静的な初期化だけでなく、もちろん、ポインタが与えられたp場合if (p)はと同等if (p != NULL)です。


8

C stdライブラリを使用すると、NULLを返す可能性のある関数で問題が発生します。たとえば、mallocのドキュメントには次のように記載されています。

関数が要求されたメモリブロックの割り当てに失敗した場合、nullポインタが返されます。

mallocと関連する関数は、特定のNULL値を持つバイナリに既にコンパイルされているため、NULLを再定義した場合、C stdライブラリを含むツールチェーン全体を再構築できない限り、C stdライブラリを直接使用することはできません。

また、stdライブラリではNULLが使用されているため、stdヘッダーを含める前にNULLを再定義すると、ヘッダーにリストされているNULL定義が上書きされる可能性があります。インライン化されたものは、コンパイルされたオブジェクトと矛盾します。

代わりに、独自のNULL「MYPRODUCT_NULL」を独自の使用のために定義し、C stdライブラリとの間の変換を回避または変換します。


6

NULLのままにして、ポート0x0000へのIOを特別なケースとして扱います。おそらくアセンブラーで作成されたルーチンを使用するため、標準のCセマンティクスの影響を受けません。IOW、NULLを再定義せず、ポート0x00000を再定義します。

Cコンパイラーを作成または変更している場合、NULLの逆参照を回避するために必要な作業(CPUが役に立たないと仮定した場合)は、NULLがどのように定義されていても同じであるため、NULLを定義したままにする方が簡単です。ゼロとして、ゼロがCから逆参照されないことを確認してください。


この問題は、NULLが誤ってアクセスされた場合にのみ発生し、意図的にポートにアクセスされた場合には発生しません。なぜポートI / Oを再定義するのですか?すでに正常に機能しています。
ランディン

2
@Lundin誤って、またはそうでなくても、NULLは、またはを使用するCプログラムでのみ逆参照できるため、コンパイラーはIOポート0x0000を保護するためにそれらのみに注意する必要があります。*pp[]p()
アパララ2011

@Lundin質問の2番目の部分:C内からアドレス0へのアクセスを制限すると、ポート0x0000に到達する別の方法が必要になります。アセンブラーで書かれた関数はそれを行うことができます。C内から、ポートは0xFFFFなどにマップできますが、関数を使用してポート番号を忘れておくのが最善です。
アパララ2011

3

他の人が言及しているようにNULLを再定義することの極端な困難を考えると、よく知られているハードウェアアドレスの逆参照再定義する方が簡単かもしれません。アドレスを作成するときは、すべての既知のアドレスに1を追加して、既知のIOポートが次のようになるようにします。

  #define CREATE_HW_ADDR(x)(x+1)
  #define DEREFERENCE_HW_ADDR(x)(*(x-1))

  int* wellKnownIoPort = CREATE_HW_ADDR(0x00000000);

  printf("IoPortIs" DEREFERENCE_HW_ADDR(wellKnownIoPort));

関心のあるアドレスがグループ化されていて、アドレスに1を追加しても何も競合しないと安心できる場合(ほとんどの場合、競合しないはずです)、これを安全に実行できる可能性があります。そして、次の形式でツールチェーン/標準ライブラリと式を再構築することについて心配する必要はありません。

  if (pointer)
  {
     ...
  }

まだ働く

クレイジーなことは知っていますが、アイデアをそこに捨てると思いました:)。


この問題は、NULLが誤ってアクセスされた場合にのみ発生し、意図的にポートにアクセスされた場合には発生しません。なぜポートI / Oを再定義するのですか?すでに正常に機能しています。
ランディン

@LundInでは、ツールチェーン全体の再構築を微調整するか、コードのこの一部を変更するか、どちらがより困難かを選択する必要があると思います。
ダグT.

2

nullポインターのビットパターンは、整数0のビットパターンと同じではない場合があります。ただし、NULLマクロの展開は、nullポインター定数である必要があります。 *)。

適合性を維持しながら目的の結果を得るには、ツールチェーンを変更(またはおそらく構成)する必要がありますが、それは実現可能です。


1

あなたはトラブルを求めています。NULLnull以外の値に再定義すると、このコードは無効になります。

   if(myPointer)
   {
      // myPointerはnullではありません
      ...
   }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.