ゼロオブジェクト

次の長さとポインタのペアの違いは何ですか?



size_t len1 = 0; char *ptr1 = NULL; size_t len2 = 0; char *ptr2 = malloc(0); size_t len3 = 0; char *ptr3 = (char *)malloc(4096) + 4096; size_t len4 = 0; char ptr4[0]; size_t len5 = 0; char ptr5[];
      
      







多くの場合、5つの式はすべて同じ結果を生成します。 他では、彼らの行動は根本的に異なる場合があります。 明らかな違いの1つは、ポインタを渡して解放する機能ですが、それについては考慮しません。



最初のケースは興味深いですが、他のケースとはあまりにも異なるため、今のところ延期します。



malloc(0)



malloc(0)の動作は標準で定義されています。 nullまたは一意のポインターを返すことができます。 多くの実装での2番目のオプションは、内部で長さを1つ増やすことで実行されます(通常は16に丸められます)。 規則によると、このようなポインターを逆参照することはできませんが、通常は数バイトが割り当てられているため、このようなプログラムはクラッシュしません。



NULLを返すと、興味深いバグが発生する可能性があります。 多くの場合、mallocからNULLを返すことはエラーと見なされます。



 if ((ptr = malloc(len)) == NULL) err(1, "out of memory");
      
      







lenがゼロの場合、追加のcheck && len!= 0を追加しないと、違法なエラーメッセージが表示されます。「malloc uncheck」の支持者のセクトに入ることもできます。



OpenBSDでは、mallocはゼロの扱いが異なります。 サイズがゼロのデータを配置すると、キーPROT_NONEを使用してmprotect()で保護されたページの一部が返されます。 そのようなポインターを逆参照しようとすると、フォールにつながります。



一意の記号の要件は、それらを使用する際の「不正行為」を禁止していることに注意してください。



 int thezero; void * malloc(size_t len) { if (len == 0) return &thezero; } void free(void *ptr) { if (ptr == &thezero) return; }
      
      







このような実装は、連続した呼び出しが同じ値を返すため、ルールに準拠していません。 したがって、2番目のケースは、実装に応じて、1番目と3番目の両方に似ています。



その他の場合



mallocがエラーをスローしない場合、ほとんどの場合、オプション3、4、および5は同じように機能します。 主な違いは、sizeof(ptr)/ sizeof(ptr [0])の使用にあります(ループなど)。 これは、誤った答え、正しい答えにつながるか、コンパイル段階で途切れてまったく何にもつながりません。 オプション4は標準では許可されていませんが、コンパイラーはそれを飲み込む可能性があります。



これらのオプションの最初のオプションとの最大の違いは、これらがヌルチェックに合格することです。 空の配列と欠落した配列の違いのようになります。 同様に、メモリ内で1バイトを占有しますが、空の文字列はnull文字列と等しくありません。



ヌルオブジェクト



最初のオプションに戻り、オブジェクトをゼロにしましょう。 次の呼び出しを検討してください。



 memset(ptr, 0, 0);
      
      







0バイトのptrを0に割り当てます。リストされている5つのポインターのどれがそのような呼び出しを行いますか? 3、4、および5。2番目—一意のポインタの場合。 しかし、ptrがNULLの場合はどうでしょうか?



「ライブラリ関数の使用」セクションの標準Cは次のように述べています。

関数の引数に無効な値がある場合(値が関数のドメイン外にある、ポインターがプログラムにアクセスできる領域外のメモリーを指している、またはポインターがヌルの場合)、動作は決定されません。


「文字列関数に関する規約」セクションでは、以下を指定しています。

size_t nとして宣言された引数が関数内の配列の長さを決定する場合、この関数が呼び出されたときにnの値はゼロになることがあります。 特定の関数の説明で特に指定されていない限り、引数引数の値は有効でなければなりません。


明らかに、NULLの0バイトのmemset結果は未定義になります。 memset、memcpy、およびmemmoveのドキュメントは、nullポインターを受け入れることができることを示していません。 反例として、snprintfを説明できます。「nがゼロの場合、何も書き込まれず、sがNULLポインターになる可能性があります。」 同様に、POSIXからの読み取り関数のドキュメントでは、長さゼロの読み取りはエラーとは見なされませんが、実装では、エラーについて他のパラメーター(無効なバッファーポインターなど)をチェックできます。



そして実際には何ですか? memsetやmemcpyなどの関数で長さゼロを処理する最も簡単な方法は、ループに入って何もしないことです。 通常Cでは、未定義の動作が何らかの反応を引き起こしますが、この場合、通常のポインターでは何も起こらないことがすでに決定されています。 ポインターの異常をチェックすることは不要です。



ヌルではないが無効なポインターをチェックすることは非常に複雑です。 memcpyはこれをまったく行わず、プログラムをクラッシュさせます。 読み取り呼び出しも何もチェックしません。 エラーを検出するためのハンドルを設定するコピーアウト関数にチェックを委任します。 nullチェックを追加できますが、このようなポインターはこれらの関数に対して0x1または0xffffffffよりも無効ではありません。特別な処理はありません。



残念



実際には、これは多くのコードを持つことを意味し、(意図的にまたは偶然に)NULLポインターとゼロ長が有効であることを意味します。 ポインターがNULLであることが判明した場合、エラー出力とmemcpyに出力を追加して実験を行うことにし、新しいlibcをインストールしました。



 Feb 11 01:52:47 carbolite xsetroot: memcpy with NULL Feb 11 01:53:18 carbolite last message repeated 15 times
      
      







ええ、それは長くかかりませんでした。 彼はそこで何をしているのだろうか:



 Feb 11 01:53:18 carbolite gdb: memcpy with NULL Feb 11 01:53:19 carbolite gdb: memcpy with NULL
      
      







なるほど、なるほど。 これらのメッセージはすぐに退屈するようです。 そのまますべてを返します。



結果



私はこの問題を取り上げました。なぜなら、「定義されていないが動作するはず」の領域とCコンパイラを最適化する領域の交差点では、何も良いことが起こらないからです。 スマートコンパイラはmemcpy呼び出しを確認し、両方のポインターを有効としてマークし、ヌルチェックを削除できます。



 int backup; void copyint(int *ptr) { size_t len = sizeof(int); if (!ptr) len = 0; memcpy(&backup, ptr, len); }
      
      







ただし、コンパイラがゼロのチェックを削除し、nullポインタが渡されると、明らかに上記のコードは正常に機能しません。



過去に、このような逆参照とチェックの最適化がセキュリティギャップにつながった場合に出くわしたことがあるため、この質問に興奮しています。 このような高水準の標準への準拠に対応していないソフトウェアの場合、これはかなり悲しいニュースです。



最初は、memcpyの「参照解除」の後にゼロのチェックを削除するようコンパイラーに納得させることはできませんでしたが、これはこれが起こらないという意味ではありません。 gcc 4.9は、このチェックは最適化によって削除されると述べています。 OpenBSDでは、gcc 4.9パッケージ(多くのパッチを含む)は、-O3を使用してもデフォルトでチェックを削除しませんが、「-fdelete-null-pointer-checks」を有効にすると、チェックが削除されます。 clangについてはわかりません。最初のテストでは、clangが削除されないことが示されていますが、保証はありません。 理論的には、彼はそのような最適化を実行することもできます。



All Articles