2015/06/11
[C言語] 配列の添字に負の値は使えるか
今は終わりではない。これは終わりの始まりですらない。しかしあるいは、始まりの終わりかもしれない。
C言語で配列の添字に負の値を使うのは有りでしょうか。つまり以下のようなコードです。
a[-1] = 0;C言語では、配列int a[n]は、*((int*)(((int*)a + n))と等しいということなので、aが配列名、つまり、
int a[10]のように定義されたものなら、それは範囲外アクセスになってしまいます。
しかし、
int b[10] int* a = &b[1];のようなものであれば、*(a-1) = b[0]なので、問題なく動作します。
ということで、一応、言語的には使えなくもない負の添字ですが、不用意に使わない方が良いです。
例えば、以下のように有効な配列の範囲外を指すような使い方は、たとえ値参照しなくても使うべきではありません。
int b[10] int* c = &b[-1];C言語的には、ポインター 演算は、一度に割り振られた領域と、仮想的な"終端"を越えた1つめ の要素にだけ定義されていて、それ以外では未定義になります。よって、aが有効なアドレス空間の先頭に位置していた場合などに、a[-1]が有効なアドレスになるとは限らないということがありえます。
また、負の添字以前に、配列の添字に符号付き(signed)の型を使うのは、気づきにくい不具合を生む可能性があるという話もあります。例えば、次のようなコード。
#define ARRAY_SIZE 200 int my_array[ARRAY_SIZE]; int array(int n) { if (n >= ARRAY_SIZE) { return -1; } return my_array[n]; }
単純なエラーチェック付きアクセサですが、これまた単純な配慮漏れがあります。
そう、nが負(マイナス値)の時の配慮が入っていないのです。
そのため、nがマイナスで渡されると、エラーチェックをすり抜けて、範囲外アクセスが発生します。この例では参照のみですが、値更新するような関数なら、任意の場所が書き換えられてしまうようなバグです。
さらに、あぶないのは以下のようなコード。
#define ARRAY_SIZE 200 int my_array[ARRAY_SIZE]; int array(int n) { if (n >= ARRAY_SIZE) { return -1; } return my_array[n]; } int func() { char n = 150; printf("%d\n", array(n)); }今後は、nがchar型になっています。char型は処理系によって、符号付きの場合と、符号なしの場合がありますが、符号ありの場合、150という値は保持されず、負の値になってしまいます。よって、負数を意図して使おうとしているわけではないものの、結果的に負数を使ってしまっています。
gccでは配列添字にcharを使っていると警告されるオプション(-Wchar-subscripts)があります。
- (参考) gccで警告を要求/抑止するオプション
charの暗黙の型変換は、いろんなバグの温床になるので、そもそもサイズのような変数の型にcharを使うのは、配列の添字に限らずやめるべきです。この手の変数については、符号なし型、できればsize_t(C言語ならstdlib.hで定義)を使うのが適切です。最新のC言語仕様であるC11の処理系であれば、オブジェクトの最大値を表す型としてrsize_tも使えます。
- (参考) Build Insider : C11の仕様-脆弱性対応に関連する機能強化点
- (参考) JPCERT C コーディングスタンダード INT01-C. オブジェクトのサイズを表現するすべての整数値に rsize_t もしくは size_t を使用する
以上、C言語における配列の添字に負の値、あるいは、符号型を使った時の動作とリスクのお話でした。
この手の話は制約を理解して使えばいいという見方もあるかもしれませんが、ソフトウェア開発の現場等では、あとで別の人がメンテナンスする時の罠になりかねません。実際の現場では、あまりトリッキーなコードを書くのは控えておくのが望ましいですね。

【関連書籍】
-
C/C++セキュアコーディング (SEI SERIES・A CERT BOOK) Robert C. Seacord JPCERT
-
CプログラミングFAQ―Cプログラミングのよく尋ねられる質問 Steve Summit 北野欽一
-
Cプログラミングの落とし穴 Andrew Koenig 中村明
コメント