13

コードのクラッシュを分析するには、実際のCの第一人者の助けが必要です。クラッシュを修正するためではありません。簡単に修正できますが、完全に不可能に思えるので、修正する前に、このクラッシュがどのように発生する可能性があるかを理解したいと思います。

このクラッシュはお客様のマシンでのみ発生し、このユーザーのデータベースのコピーを取得できないため、ローカルで再現することはできません(したがって、デバッガーを使用してコードをステップ実行することはできません)。私の会社では、コードの数行を変更してこの顧客のカスタムビルドを作成することもできません(したがって、printf行を追加してコードを再度実行させることはできません)。もちろん、顧客はデバッグシンボル。言い換えれば、私のデバグ能力は非常に限られています。それでも、クラッシュを突き止めて、デバッグ情報を取得することができました。しかし、その情報を見てからコードを見ると、プログラムフローが問題の行に到達する方法を理解できません。その行に到達するずっと前に、コードがクラッシュしているはずです。私はここで完全に迷子になっています。

関連するコードから始めましょう。それは非常に小さなコードです:

// ... code above skipped, not relevant ...

if (data == NULL) return -1;

information = parseData(data);

if (information == NULL) return -1;

/* Check if name has been correctly \0 terminated */
if (information->kind.name->data[information->kind.name->length] != '\0') {
    freeParsedData(information);
    return -1;
}

/* Copy the name */
realLength = information->kind.name->length + 1;
*result = malloc(realLength);
if (*result == NULL) {
    freeParsedData(information);
    return -1;
}
strlcpy(*result, (char *)information->kind.name->data, realLength);

// ... code below skipped, not relevant ...

もう終わりです。strlcpyでクラッシュします。実行時にstrlcpyが実際にどのように呼び出されるかさえわかります。strlcpyは、実際には次のパラメーターで呼び出されます。

strlcpy ( 0x341000, 0x0, 0x1 );

これを知っていると、strlcpyがクラッシュする理由はかなり明白です。NULLポインタから1文字を読み取ろうとしますが、もちろんクラッシュします。また、最後のパラメーターの値は1であるため、元の長さは0である必要があります。私のコードには明らかにバグがあり、名前データがNULLであるかどうかをチェックできません。私はこれを修正できます、問題ありません。

私の質問は
、そもそもこのコードがどのようにしてstrlcpyに到達できるのかということです。
このコードがifステートメントでクラッシュしないのはなぜですか?

私は自分のマシンでローカルにそれを試しました:

int main (
    int argc,
    char ** argv
) {
    char * nullString = malloc(10);
    free(nullString);
    nullString = NULL;

    if (nullString[0] != '\0') {
        printf("Not terminated\n");
        exit(1);
    }
    printf("Can get past the if-clause\n");

    char xxx[10];
    strlcpy(xxx, nullString, 1);
    return 0;   
}

このコードにifステートメントが渡されることはありません。ifステートメントでクラッシュしますが、これは間違いなく予想されます。

それで、name-> dataが本当にNULLの場合、最初のコードがクラッシュせずにそのifステートメントを渡すことができる理由を誰かが考えることができますか?これは私には全く不思議です。決定論的ではないようです。

重要な追加情報:
2つのコメントの間のコードは本当に完全であり、何も残されていません。さらに、アプリケーションはシングルスレッドであるため、バックグラウンドでメモリを予期せず変更する可能性のあるスレッドは他にありません。これが発生するプラットフォームは、PPC CPU(G4、何らかの役割を果たす可能性がある場合)です。そして、誰かが「種類」について疑問に思う場合、これは「情報」に「種類」という名前の「ユニオン」が含まれ、名前が再び構造体であるためです(種類はユニオンであり、可能なすべてのユニオン値は異なるタイプの構造体です)。しかし、これはすべてここでは実際には重要ではありません。

ここでのアイデアに感謝します。単なる理論ではなく、この理論がお客様に本当に当てはまることを確認できる方法があれば、さらに感謝しています。

解決

私はすでに正しい答えを受け入れましたが、誰かがGoogleでこの質問を見つけた場合に備えて、実際に起こったことは次のとおりです。

ポインタは、すでに解放されているメモリを指しています。メモリを解放しても、すべてがゼロになったり、プロセスがメモリを一度にシステムに戻したりすることはありません。そのため、メモリが誤って解放されたとしても、正しい値が含まれていました。「 ifcheck 」を実行した時点では、問題のポインタはNULLではありません。

そのチェックの後、mallocを呼び出して新しいメモリを割り当てます。ここでmallocが正確に何をするかはわかりませんが、mallocまたはfreeを呼び出すたびに、プロセスの仮想アドレス空間のすべての動的メモリに広範囲にわたる結果が生じる可能性があります。malloc呼び出しの後、ポインターは実際にはNULLです。どういうわけか、malloc(またはmallocが使用するシステムコール)は、ポインタ自体が配置されている(ポインタが指すデータではなく、ポインタ自体がダイナミックメモリにある)すでに解放されているメモリをゼロにします。そのメモリをゼロにすると、ポインタの値は0x0になります。これは、私のシステムではNULLに等しく、strlcpyが呼び出されると、もちろんクラッシュします。

したがって、この奇妙な動作を引き起こす本当のバグは、私のコードのまったく別の場所にありました。決して忘れないでください:解放されたメモリはその値を保持しますが、それはどのくらいの間あなたのコントロールを超えています。アプリにすでに解放されたメモリにアクセスするというメモリのバグがあるかどうかを確認するには、解放される前に解放されたメモリが常にゼロになっていることを確認してください。OS Xでは、実行時に環境変数を設定することでこれを行うことができます(何も再コンパイルする必要はありません)。もちろん、これはプログラムをかなり遅くしますが、あなたはそれらのバグをずっと早く捕まえるでしょう。

4

17 に答える 17

13

まず、nullポインターの逆参照は、未定義の動作です。クラッシュすることも、クラッシュしないことも、壁紙をスポンジボブスクエアパンツの写真に設定することもできます。

とはいえ、nullポインターを逆参照すると、通常はクラッシュします。したがって、問題はおそらくメモリの破損に関連しています。たとえば、文字列の1つの終わりを超えて書き込むことによるものです。これにより、遅延効果のクラッシュが発生する可能性があります。プログラムが利用可能な仮想メモリの終わりに突き当たらない限り、失敗する可能性は非常に低いので、私は特に疑っていmalloc(1)ます。その場合はおそらく気付くでしょう。

編集:OPは、nullではなく結果であると指摘しましたinformation->kind.name->data。次に、潜在的な問題があります。

information->kind.name->datanullかどうかのチェックはありません。それに関する唯一のチェックは

if (information->kind.name->data[information->kind.name->length] != '\0') {

これがnullであると仮定しinformation->kind.name->dataますが、information-> kind.name-> lengthはたとえば100です。この場合、このステートメントは次のようになります。

if (*(information->kind.name->data + 100) != '\0') {

これはNULLを逆参照せず、アドレス100を逆参照します。これがクラッシュせず、アドレス100に0が含まれている場合、このテストは合格します。

于 2009-08-26T14:14:47.767 に答える
11

free()構造が'dされたメモリにあるか、ヒープが破損している可能性があります。その場合、malloc()それが自由であると考えて、メモリを変更している可能性があります。

あなたはメモリチェッカーの下であなたのプログラムを実行しようとするかもしれません。Mac OS Xをサポートするメモリチェッカーの1つはvalgrindですが、Mac OS XはIntelでのみサポートされ、PowerPCではサポートされません。

于 2009-08-26T14:37:41.300 に答える
5

nullポインターを逆参照する効果は、私が知る限り、標準では定義されていません。

C標準6.5.3.2/4によると:

ポインターに無効な値が割り当てられている場合、単項*演算子の動作は定義されていません。

したがって、クラッシュする場合としない場合があります。

于 2009-08-26T14:10:30.597 に答える
3

スタックが破損している可能性があります。参照しているコード行がまったく実行されていない可能性があります。

于 2009-08-26T14:06:32.550 に答える
2

私の理論では、これinformation->kind.name->lengthは非常に大きな値であるため、information->kind.name->data[information->kind.name->length]実際には有効なメモリアドレスを参照しています。

于 2009-08-26T14:20:23.280 に答える
1

最後のifステートメントの後に「{」がないということは、「// ...上記のコードがスキップされ、関連性がない...」セクションの何かがコードのフラグメント全体へのアクセスを制御していることを意味します。貼り付けられたすべてのコードのうち、strlcpyのみが実行されます。解決策:制御を明確にするために中括弧のないifステートメントは使用しないでください。

このことを考慮...

if(false)
{
    if(something == stuff)
    {
        doStuff();

    .. snip ..

    if(monkey == blah)
        some->garbage= nothing;
        return -1;
    }
}
crash();

「crash();」のみ 実行されます。

于 2009-08-26T16:11:44.057 に答える
1

私はあなたのプログラムをvalgrindの下で実行します。NULLポインターに問題があることはすでに知っているので、そのコードのプロファイルを作成します。

ここでのvalgrindの利点は、すべてのポインター参照をチェックし、そのメモリー位置が以前に宣言されているかどうかをチェックし、行番号、構造、およびメモリーについて知りたいその他の情報を通知することです。

他のみんなが言ったように、0のメモリ位置を参照することは「que sera、sera」のようなものです。

私のCの色合いのスパイダーマンは、あなたがそれらの構造物を壊すべきだと私に言っています

if (information->kind.name->data[information->kind.name->length] != '\0') {

のような行

    if (information == NULL) {
      return -1; 
    }
    if (information->kind == NULL) {
      return -1; 
    }

等々。

于 2009-08-26T17:47:20.590 に答える
1

NULLポインターを逆参照する動作は、標準では定義されていません。クラッシュすることは保証されておらず、実際にメモリに書き込もうとしない限り、クラッシュしないことがよくあります。

于 2009-08-26T14:12:10.843 に答える
1

参考までに、この行を見ると、次のようになります。

if (information->kind.name->data[information->kind.name->length] != '\0') {

最大3つの異なるポインター逆参照が表示されます。

  1. 情報
  2. 名前
  3. データ(固定配列ではなくポインタの場合)

null以外の情報をチェックしますが、名前やデータはチェックしません。それらが正しいことをあなたが確信する理由は何ですか?

また、以前にヒープに損傷を与えた可能性のある他の何かについて、他の感情をここにエコーします。Windowsで実行している場合は、gflagsを使用してページ割り当てなどを行うことを検討してください。これは、自分または他の誰かがバッファの終わりを超えて書き込み、ヒープを踏んでいるかどうかを検出するために使用できます。

あなたがMacを使用していることを確認しました-gflagsのコメントは無視してください-これを読んでいる他の誰かを助けるかもしれません。OS Xより前のバージョンで実行している場合は、ヒープにストレスをかけるための便利なMacsbugsツールがいくつかあります(ヒープスクランブルコマンド「hs」など)。

于 2009-08-26T14:36:52.893 に答える
1

strlcpyの呼び出しでキャストされるchar*に興味があります。

タイプデータ*のサイズがシステムの文字*と異なる可能性がありますか?charポインターが小さい場合、NULLになる可能性のあるデータポインターのサブセットを取得できます。

例:

int a = 0xffff0000;
short b = (short) a; //b could be 0 if lower bits are used

編集:スペルミスが修正されました。

于 2009-08-26T15:06:55.560 に答える
1

これは、「データ」ポインタがNULLであるのを乗り越えることができる1つの特定の方法です。

if (information->kind.name->data[information->kind.name->length] != '\0') {

情報->kind.name->長さが大きいと言います。特定のコンパイラを使用する特定のプラットフォーム(たとえば、ストックgccコンパイラを使用するほとんどの* nix)で、少なくとも4096より大きい場合、コードは「kind.name-> data +information->kind.nameのアドレス」のメモリ読み取りになります。 ->長さ]。

下位レベルでは、その読み取りは「アドレス(0 + 8653)のメモリの読み取り」(または任意の長さ)です。* nixesでは、アドレス空間の最初のページを「アクセス不可」としてマークするのが一般的です。つまり、メモリアドレス0〜4096を読み取るNULLポインタを逆参照すると、ハードウェアトラップがアプリケーションに伝播され、クラッシュします。

その最初のページを過ぎて読むと、有効なマップされたメモリ、たとえば共有ライブラリまたはそこにマップされた他の何かに突っ込む可能性があります-そしてメモリアクセスは失敗しません。そして、それは大丈夫です。NULLポインターの逆参照は未定義の動作であり、失敗する必要はありません。

于 2009-08-26T15:24:00.770 に答える
0

うわー、それは奇妙です。貢献しないかもしれませんが、私には少し疑わしいことが1つあります。

情報とデータが適切なポインター(null以外)であるが、information.kind.nameがnullである場合はどうなりますか。このポインタはstrlcpy行まで逆参照されないため、nullの場合は、それまでクラッシュしない可能性があります。もちろん、それよりも前に、data[1]を逆参照して\0に設定します。これもクラッシュするはずですが、なんらかの問題が原因で、プログラムが0x01への書き込みアクセス権を持っているが0x00への書き込みアクセス権を持っていない可能性があります。

また、ある場所ではinformation-> name.lengthを使用していますが、別の場所ではinformation-> kind.name.lengthを使用しているようですが、それがタイプミスなのか、それとも望ましいのかはわかりません。

于 2009-08-26T14:13:48.283 に答える
0

nullポインターを逆参照すると、未定義の動作が発生し、必ずしもクラッシュするわけではinformation->kind.name->dataありませんが、の内容ではなく、の値を確認する必要がありinformation->kind.name->data[1]ます。

于 2009-08-26T14:25:57.047 に答える
0
char * p = NULL;

p[i]は

p += i;

これは、nullpointerでも有効な操作です。次に、メモリ位置0x0000[...]iをポイントします

于 2009-08-26T14:42:16.863 に答える
0

とにかくinformation->kind.name->dataがnullかどうかを常に確認する必要がありますが、この場合は

if (*result == NULL) 
    freeParsedData(information);
    return -1;
}

あなたは{を逃しました

そのはず

if (*result == NULL)
{ 
     freeParsedData(information);
     return -1;
}

これは、代わりにこのコーディングスタイルの正当な理由です

if (*result == NULL) { 
    freeParsedData(information);
    return -1;
}

中括弧がif句から分離されていないコードブロックの形状に慣れているため、欠落している中括弧を見つけられない可能性があります。

于 2009-08-26T15:58:10.870 に答える
0

* result = malloc(realLength); // ???

新しく割り当てられたメモリセグメントのアドレスは、変数「result」に含まれるアドレスによって参照される場所に格納されます。

これは意図ですか?その場合、strlcpyを変更する必要があるかもしれません。

于 2009-08-26T16:53:33.863 に答える
-1

私の理解によると、この問題の特殊なケースは、Nullポインターを使用して読み取りまたは書き込みを試みた結果、無効なアクセスが発生することです。ここで、問題の検出はハードウェアに大きく依存します。一部のプラットフォームでは、NULLポインタを使用して読み取りまたは書き込みのためにメモリにアクセスすると、例外が発生します。

于 2012-07-16T08:53:44.313 に答える