【C言語のChar型で終端用の記号\0が抜けているコーディングについて】


以下のシチュエーションで問題を放置したときに発生すると思われる危険と現象を
説明してください(質問者は中途半端な理解です。)。

<シチュエーション>
C言語のChar型で終端用の記号\0が抜けているコーディングを発見しました。

char 変数a[2]
char 変数b[2]

a[0]="a"
a[1]="b"

b[0]="c"
b[1]="d"

となっており、a配列をアウトプットするとabcdとなってしまいます。
これはメモリの位置が連続しているため、連続してb配列の中身も読み込んでしまったものと思いますが
a配列は"ユニークな値20桁以下"という規定なのでabcdの値でも問題ありません。

デバッグモードで無事に動作しているし、そのまま放置することにしました。

End.

ご回答よろしくお願いします。

回答の条件
  • URL必須
  • 1人2回まで
  • 13歳以上
  • 登録:2010/03/06 00:39:32
  • 終了:2010/03/08 17:41:48

ベストアンサー

id:uehaj No.5

uehaj回答回数158ベストアンサー獲得回数152010/03/06 07:38:38

ポイント150pt

これらの変数を利用する側が、aやbが¥0で終端された文字列(ASCIIZ文字列)であることを期待するなら、これはかなり深刻なバグです。しかし、¥0で終端されている事を期待していないなら,何も問題ありません。「¥0で終端されている事を期待しない」とは


strncpy(buf, a, 2);


のように、aの長さ(文字数)を指定して処理すれば良いという事です。ただし、aの長さを指定する方法が別途必要ですね。(別の変数を用意する、決め打ちにするなど)


ただ,ご質問のケースでは、aの長さを別に指定してたりしない、¥0終端を期待していると思うので、一般には修正した方がいい部類でしょう。


さて、bの次の値が何か、が問題です。「たまたま¥0」なのか、必然なのか、「たまたまといいつつ、(あるOS、あるコンパイラ・・など)条件が定まれば必ずそうなる」のか?です。


その観点からは、これらの変数の記憶クラスが何か、が問題になります。記憶クラスとは、staticとかautoとかで指定されるものです。


C言語では、明示的に初期化していないauto変数の初期値は不定です。auto変数とは,関数内で定義したローカル変数のことで、スタック上に配置されるものです。スタックというのは、関数呼び出し毎に割り当て/解放処理が行われて再利用されるので、前回呼ばれた関数が記録した変数や、あるいは同様にスタック上に配置される関数引数や関数の戻り値アドレスの値が得られ、どんな値が得られるか分からないし、実際毎回異なる値が得られることもあるのです。こういう値を「ゴミの値」という呼ぶ事もあります。


しかしながら、逆に言うと静的記憶クラスに属する

  • 静的変数
  • 大域変数

については、明示的に初期値を指定していない場合でも、特定の値に初期化されている可能性があります。特に

  • これらの変数の暗黙の初期値は、整数型は0、ポインタ型はNULL、浮動小数点数型は0.0と仕様で定められている
  • 整数型の間では、0の表現が「全てのビットが0」という場合、別の整数型の変数として見ても0は0と表現されるCPUが多い。たとえばint変数0として初期化された領域は、char変数の¥0としても解釈されるCPUが圧倒的に多い。例えばx86 CPUはそうである。
  • 変数間のパディング領域があったとして、それも静的領域である場合、0パディングとするOS/コンパイラが多い。

これらにより、bの次が¥0であることを、限定条件下で「あえて」「良くない事と知りつつ」期待してよい場合があるかもしれません。お薦めはしませんが。

// 「ゴミの値」は\0であることは確率的に低いので、質問者の方はおそらく静

// 的変数もしくは大域変数を使われているのでしょうね。

でもそうだとしても、以下は注意した方がいいでしょう。

  • bの次の変数を、プログラムの実行を通じて変更しないか?する可能性は無いか?
  • aとbの間に隙間が無いか?コンパイルオプション(特に最適化オプションを使用し、変数のメモリ配置を8バイト境界に配置するなどの最適化オプションを指定してコンパイルしてちゃんと動作するか)
  • このプログラムを他のOSで動作させる可能性がある場合、他のOSで動作させてみる
  • スタックチェックオプションをつけてコンパイルしてみる、StackGuardやproprisなどでチェックしてみる。メモリバグ解析ツール(puryfyなど)をかけてみる。

場合によっては動作しないケースが実際にあるかもしれないという事です。

まー繰り返しになりますが、直すのが無難だとは思います。

URLは関係ありそうなキーワードです。

http://www.google.co.jp/search?hl=ja&client=firefox-a&hs=OVg&rls...

id:harunoharuno

非常に丁寧な回答どうもありがとうございました。

また、

独学であまり理解できていないメモリ管理についても説明いただきとても参考になりました。

ご推察の通り私が質問に使用したCharの配列は、実際のコーディングではstaticで宣言しているものです。

静的領域のパディング領域は0パディングであることが多いんですね。

今回の対応と致しましては、

できるだけ多くの環境で動作できるよう問題のコードを修正いたします。

参考情報もありがとうございました。リンクの先にメモリの扱い方を

4つに分けて説明しているHPがあり、参考になりました。

2010/03/08 15:33:55

その他の回答(4件)

id:Km1967 No.1

Km1967回答回数224ベストアンサー獲得回数352010/03/06 01:07:22

ポイント20pt

後続の処理がabcdという値を受け取った場合にどうなるかによる

たとえばメモリリークやデータ破壊に繋がるかもしれない

素直に書き直すべき

char 変数a[2] 
char 変数b[2]

a[0]='a'
a[1]='b'

b[0]='c'
b[1]='d'

http://www.grapecity.com/japan/powernews/column/clang/023/page01...

id:harunoharuno

a配列以降のアドレスに

配置されるデータがb配列の場合はさして問題ではありません。


たとえば、

デバッグモードを解除してビルドしたら

a配列の後ろにC配列(データ内容="abcdefghijklmnopqrstuvwxyz")が続くように

なったりということはあり得るかな?と思っています(この場合ユニークな値20桁以下の条件から外れてしまいます)。

メモリ上の配置ルールしらないのでなんとなく

怖いです。ここら辺についてもう少し詳しくご回答

いただきたくよろしくお願いします。

2010/03/06 01:22:10
id:gemwell6809 No.2

gemwell6809回答回数3ベストアンサー獲得回数02010/03/06 02:05:34

ポイント100pt

変数領域のメモリ上の配置は環境依存なので、たまたまそう言う配置になっている…と言う事ができるかと思います。

ですので、デバッグモードを解除したビルドで配置が変るか?もコンパイル環境によって変ります。

別の環境でコンパイルした場合、a配列とb配列の並びが逆になることもあり得ます。


この問題を放置した場合の危険性ですが、

a配列とb配列の並びが維持されていたとしても、b配列が終端されていないので、更に先のメモリをアクセスすることになります。

意図しない文字コードが表示されることが考えられますし、制御コードに相当する値があった場合には表示が崩れることが考えられます。

また、配列のメモリ配置がプログラム・コードのアクセスが許されている領域の末端に配置された場合には許可されない領域にまでアクセスすることになり、メモリ保護機構のある実行環境であれば、例外を出してプログラムが異常終了することも考えられます。

(a配列とb配列の並びが維持されなかった場合も、やはり、意図しない領域をアクセスすることになります。)


ともかく、たまたま動いただけでC言語として保証される書き方ではない…と言えます。


http://www1.cts.ne.jp/~clab/hsample/Point/Point19.html

id:harunoharuno

そうですよね。

終端を設定していないとどんなデータが読みだされるかわかったもんじゃないですよね。

環境によってもメモリ上での配置はかわるとのことですし対策をとる必要性を感じます。

(本質問は興味による質問なのでしばらく回答受付いたします。追加情報などございましたらまたよろしくお願いします)

2010/03/06 02:23:32
id:T_SKG No.3

T_SKG回答回数206ベストアンサー獲得回数182010/03/06 02:12:22

ポイント100pt

変数aと変数bが、連続したメモリ上にとられ、さらにその先のメモリが

たまたま、ゼロクリアされたままの未使用だったのでしょうか、\0 に

なっていたのは、運が良かっただけです。


もし、変数b に続くメモリの17byteが使われて中に \0が入ってなければ、

変数aは、20桁を超えます。


それに、今後もコンパイラが、変数aと変数bと運良く\0が入っている領域

をメモリ上に並べて割り当ててくれる保証はありません。


メモリを極力節約するため、union にしてあるとか、余程トリッキーな

コーディングでないかぎり、こんなものは、修正するべきでしょう。


http://www.mapee.jp/cpp/post_31.html

id:harunoharuno

どうもありがとうございます。

「配列Bの終端でなんでうまく止まるのかな?」と少々疑問に感じていたのですが、

たまたま\0値ということだったのか・・・。

回答していただいている通り修正すべきですね。

(本質問は本日午前8: 00を持って締め切りとします。)

2010/03/06 02:33:34
id:garyo No.4

garyo回答回数1782ベストアンサー獲得回数962010/03/06 04:03:10

ポイント50pt

たまたま運良く止まったと考えた方が良いでしょう。

終端を忘れたプログラムの場合、文字列のコピーなどで、終端が見つからないため、ひたすら長い文字列と解釈して、暴走したりすることがあります。

http://q.hatena.ne.jp/answer

id:uehaj No.5

uehaj回答回数158ベストアンサー獲得回数152010/03/06 07:38:38ここでベストアンサー

ポイント150pt

これらの変数を利用する側が、aやbが¥0で終端された文字列(ASCIIZ文字列)であることを期待するなら、これはかなり深刻なバグです。しかし、¥0で終端されている事を期待していないなら,何も問題ありません。「¥0で終端されている事を期待しない」とは


strncpy(buf, a, 2);


のように、aの長さ(文字数)を指定して処理すれば良いという事です。ただし、aの長さを指定する方法が別途必要ですね。(別の変数を用意する、決め打ちにするなど)


ただ,ご質問のケースでは、aの長さを別に指定してたりしない、¥0終端を期待していると思うので、一般には修正した方がいい部類でしょう。


さて、bの次の値が何か、が問題です。「たまたま¥0」なのか、必然なのか、「たまたまといいつつ、(あるOS、あるコンパイラ・・など)条件が定まれば必ずそうなる」のか?です。


その観点からは、これらの変数の記憶クラスが何か、が問題になります。記憶クラスとは、staticとかautoとかで指定されるものです。


C言語では、明示的に初期化していないauto変数の初期値は不定です。auto変数とは,関数内で定義したローカル変数のことで、スタック上に配置されるものです。スタックというのは、関数呼び出し毎に割り当て/解放処理が行われて再利用されるので、前回呼ばれた関数が記録した変数や、あるいは同様にスタック上に配置される関数引数や関数の戻り値アドレスの値が得られ、どんな値が得られるか分からないし、実際毎回異なる値が得られることもあるのです。こういう値を「ゴミの値」という呼ぶ事もあります。


しかしながら、逆に言うと静的記憶クラスに属する

  • 静的変数
  • 大域変数

については、明示的に初期値を指定していない場合でも、特定の値に初期化されている可能性があります。特に

  • これらの変数の暗黙の初期値は、整数型は0、ポインタ型はNULL、浮動小数点数型は0.0と仕様で定められている
  • 整数型の間では、0の表現が「全てのビットが0」という場合、別の整数型の変数として見ても0は0と表現されるCPUが多い。たとえばint変数0として初期化された領域は、char変数の¥0としても解釈されるCPUが圧倒的に多い。例えばx86 CPUはそうである。
  • 変数間のパディング領域があったとして、それも静的領域である場合、0パディングとするOS/コンパイラが多い。

これらにより、bの次が¥0であることを、限定条件下で「あえて」「良くない事と知りつつ」期待してよい場合があるかもしれません。お薦めはしませんが。

// 「ゴミの値」は\0であることは確率的に低いので、質問者の方はおそらく静

// 的変数もしくは大域変数を使われているのでしょうね。

でもそうだとしても、以下は注意した方がいいでしょう。

  • bの次の変数を、プログラムの実行を通じて変更しないか?する可能性は無いか?
  • aとbの間に隙間が無いか?コンパイルオプション(特に最適化オプションを使用し、変数のメモリ配置を8バイト境界に配置するなどの最適化オプションを指定してコンパイルしてちゃんと動作するか)
  • このプログラムを他のOSで動作させる可能性がある場合、他のOSで動作させてみる
  • スタックチェックオプションをつけてコンパイルしてみる、StackGuardやproprisなどでチェックしてみる。メモリバグ解析ツール(puryfyなど)をかけてみる。

場合によっては動作しないケースが実際にあるかもしれないという事です。

まー繰り返しになりますが、直すのが無難だとは思います。

URLは関係ありそうなキーワードです。

http://www.google.co.jp/search?hl=ja&client=firefox-a&hs=OVg&rls...

id:harunoharuno

非常に丁寧な回答どうもありがとうございました。

また、

独学であまり理解できていないメモリ管理についても説明いただきとても参考になりました。

ご推察の通り私が質問に使用したCharの配列は、実際のコーディングではstaticで宣言しているものです。

静的領域のパディング領域は0パディングであることが多いんですね。

今回の対応と致しましては、

できるだけ多くの環境で動作できるよう問題のコードを修正いたします。

参考情報もありがとうございました。リンクの先にメモリの扱い方を

4つに分けて説明しているHPがあり、参考になりました。

2010/03/08 15:33:55
  • id:uehaj
    >静的領域のパディング領域は0パディングであることが多いんですね。

    すみません、補足ですが、「多い」とまで言えるかはわからないです。
    MacOSのGCCでは以下の通りでした。

    #include <stdio.h>

    int main() {
    static int a=0x77777777;
    static char b=0x44;
    static int c=0x55555555;
    int i;

    for (i=0; i<100; i++) {
    printf("%2x ", *((char*)&c + i));
    }

    }

    出力結果

    55 55 55 55 44 0 0 0 77 77 77 77 0 0 0 0 0 0 .....

    少なくとも、この環境で、charに関しては、0パディングのようです。
    ただ、他の環境を調べた上で、確かに比率として「多い」とまで
    いえるかは保留させてください(gccの戦略がそうそうは
    かわらないとはおもいますが、gcc以外の他のコンパイラを調べて
    確認したわけではないので・・)。

    加えて、パディングが\0にならないかもしれないと思っているケースが
    あって、それは「メモリの書き壊しを検出する」という目的で、
    変数境界にあえて事前にユニークな値を書き込んでおく、という場合です。
    ちょっと記憶が定かではないのですが、mallocで得られた領域とかに
    はあえて前後に0x98とかあまり使われないような値を初期値にしている
    ときがあったような。ただそれが静的領域での書き壊し検出でも採用さ
    れていたという記憶があるわけではないのですが・・。

    あと、いくつか綴り間違ってますね。
    Puryfy->Purify
    propris->ProPolice
    大変失礼致しました。






この質問への反応(ブックマークコメント)

「あの人に答えてほしい」「この質問はあの人が答えられそう」というときに、回答リクエストを送ってみてましょう。

これ以上回答リクエストを送信することはできません。制限について

絞り込み :
はてなココの「ともだち」を表示します。
回答リクエストを送信したユーザーはいません