Home > ラーニング > セキュアコーディング > C セキュアコーディングスタンダード > 02. 宣言と初期化 (DCL)
C言語には様々な種類の定数がある: 10 や 0x1C のような整数定数や、1.0 や 6.022e+23 といった浮動小数点定数、'a' や '\x10' のような文字定数など。Cにはたとえば"hello, world" や "\n" のような文字列リテラルもある。これらはリテラルと呼ばれることもある。
プログラムロジックでリテラルを使うと、ソースコードの可読性が低下することがある。そのため、リテラル一般、特に整数定数はマジックナンバーと呼ばれることが多い。その利用目的が不明瞭であるからだ。マジックナンバーは、任意の値を表す定数値のこともあれば(たとえば適切だと判断されたバッファサイズ)、可塑性の高い概念を表すこともある(たとえば、大人とみなされる年齢は地政学的な境界によって変わりうる)。プログラムロジックにリテラルを埋め込むのではなく、適切な名前をつけたシンボル定数を使い、コードの意図を明解にすること。また、特定の値を変更しなくてはならない場合、シンボル定数を一度再アサインする方が、その値のインスタンスをすべて修正するよりも効率的かつエラーになりにくい。
C言語には、名前付きのシンボル定数を作成する様々な方法がある。const修飾オブジェクト、列挙定数、オブジェクト形式マクロなどがそれである。これらのメカニズムにはそれぞれ利点と欠点がある。
const 修飾されたオブジェクトには有効範囲(scope)があり、コンパイラによる型チェックが可能である。これらは(マクロ定義ではなく)名前付きオブジェクトであるため、デバッグツールを使ってオブジェクト名を表示することができる。オブジェクトはメモリを消費する。
const修飾オブジェクトを使うことで定数の厳密な型を指定することができる。たとえば
const unsigned int buffer_size = 256;
は、buffer_size をunsigned int 型の定数として定義する。
残念ながら、以下に示すようなコンパイル時に整数定数が必要とされるような場合には、const修飾オブジェクトを使うことはできない。
これらの場合には、整数定数(右辺値)を使わなくてはならない。
const 修飾オブジェクトを用いることで、プログラマはオブジェクトのアドレスを取得することができる。
const int max = 15; int a[max]; /* 関数外の無効な宣言 */ const int *p; /* const 修飾オブジェクトはアドレスを取得できる */ p = &max;
const 修飾オブジェクトは往々にして実行時オーバーヘッドを招く。[Saks 01b] たとえば、たいていのCコンパイラはconst修飾オブジェクトにメモリを割り当てる。関数本体の中で宣言されたconst修飾オブジェクトは自動記憶域期間を持つだろう。その場合、コンパイラはオブジェクトに対して、スタック上に記憶域を割り当てる。それゆえ、この記憶域は、それを含む関数が呼び出されるたびに、割り当てと初期化が行われなくてはならない。
列挙定数を使うことで、int として表現可能な値を持つ整数定数式を表現することができる。const修飾オブジェクトとはことなり、列挙定数はメモリを消費しない。値に対して記憶域が割り当てられることはないため、列挙定数のアドレスを取得することはできない。
enum { max = 15 };
int a[max]; /* 関数外でOK */
int *p;
p = &max; /* エラー: 列挙定数に対する '&' */
列挙定数は値の型を指定することを許さない。値が int 型で表現できる列挙定数は必ず int である。
以下に示す前処理指令の形式:
# define 識別子 置換要素並び
は オブジェクト形式マクロを定義する。それ以降、そのマクロ名の出現を、この指令の残りを構成する置換要素並びの前処理字句列で置き換える。[ISO/IEC 9899:1999]
Cプログラマはやたらとシンボル定数をオブジェクト形式マクロとして定義する。たとえば次のコード
#define buffer_size 256
は、buffer_size を256という値を持つマクロとして定義している。プリプロセッサは、コンパイラがシンボルの処理を行う前にマクロを置換する。以降のコンパイルフェーズではbuffer_sizeのようなマクロシンボルは現れない。コンパイラはマクロ置換後のソーステキストのみを扱うのである。その結果、コンパイラの多くはデバッガにわたすシンボル名を保持しない。
マクロ名は、他の名前には適用される有効範囲の規則に従わない。それゆえ、マクロが想定外の場所で置換され、予期せぬ結果をもたらすおそれがある。
オブジェクト形式マクロはメモリを消費しない。したがって、これを参照するポインタを作成することはできない。また、マクロは型チェックされない。これはマクロがプリプロセッサによってテキスト置換されるからである。
マクロはコンパイル時引数として渡されることがある。
以下の表に const修飾オブジェクト、列挙定数、オブジェクト形式マクロ定義の違いについてまとめる。
| 手法 | いつ評価されるか | メモリの消費 | デバッガから見えるか | 型チェック | コンパイル時の定数式 |
|---|---|---|---|---|---|
| 列挙定数 | コンパイル時 | no | yes | yes | yes |
| const修飾 | 実行時 | yes | yes | yes | no |
| マクロ | プリプロセッサ | no | no | no | yes |
このコード例では整数リテラル18が何を意味するのか明確ではない。
/* ... */
if (age >= 18) {
/* なにかアクションをとる */
}
else {
/* 別のアクションをとる */
}
/* ... */
以下の解決法では、整数リテラル18をシンボル定数 ADULT_AGE に置き換え、コードの意味を明快にしている。
enum { ADULT_AGE=18 };
/* ... */
if (age >= ADULT_AGE) {
/* なにかアクションをとる */
}
else {
/* 別のアクションをとる */
}
/* ... */
以下のコード例のように、整数リテラルを使い配列の範囲を指定することは多々ある。
char buffer[256]; /* ... */ fgets(buffer, 256, stdin);
整数リテラルをこのように使うとバッファオーバーフローに容易につながりうる。たとえば、バッファサイズは小さく変更されているのにfgets()呼び出しで使用する整数リテラルが小さく変更されていない場合など。
以下の解決法では、整数リテラルを列挙定数に置き換えている。(「DCL00-C. イミュータブルなオブジェクトは const 修飾する」を参照のこと。)
enum { BUFFER_SIZE=256 };
char buffer[BUFFER_SIZE];
/* ... */
fgets(buffer, BUFFER_SIZE, stdin);
列挙定数であれば定数式が必要などんな場所でも安全に使うことができる。
多くの場合、新たなシンボルを定義するのではなく、既存のシンボルからなるシンボル式を使うことで、望ましいコード可読性を手に入れることができる。たとえば、sizeof 式は列挙定数と同じように使うことができる。(「EXP09-C. 型や変数のサイズは sizeof を使って求める」を参照。)
char buffer[256]; /* ... */ fgets(buffer, sizeof(buffer), stdin);
上記の例のように sizeof 式を使うことでプログラム中で宣言される名前の数は減る。これは一般によいアイデアである。[Saks 02] sizeof 演算子はほとんど常にコンパイル時に評価される(可変長配列の場合は除く)。
sizeof() を使う場合は、「ARR01-C. 配列のサイズを求める際に sizeof 演算子をポインタに適用しない」に留意すること。
以下のコードでは、文字列リテラル "localhost" と整数定数 1234 がプログラムロジックに直接埋め込まれており、変更するのが難しい
LDAP *ld = ldap_init("localhost", 1234);
if (ld == NULL) {
perror("ldap_init");
return(1);
}
以下の解決法では、ホスト名とポート番号がどちらもオブジェクト形式マクロとして定義されており、コンパイル時に引数として渡される。
#ifndef PORTNUMBER /* might be passed on compile line */
# define PORTNUMBER 1234
#endif
#ifndef HOSTNAME /* might be passed on compile line */
# define HOSTNAME "localhost"
#endif
/* ... */
LDAP *ld = ldap_init(HOSTNAME, PORTNUMBER);
if (ld == NULL) {
perror("ldap_init");
return(1);
}
DCL06-EX1: 数値定数をシンボル定数に置き換えることは多くの場合よいプラクティスであるが、やりすぎになる場合もある。目的は可読性の改善であることに留意しよう。以下のコード例のように、定数自身が実現したい何かの抽象である場合は例外にできる。
x = (-b + sqrt(b*b - 4*a*c)) / (2*a);
以下の例では、数値定数をシンボル定数に置き換えているがコードの可読性は一向に改善しておらず、かえってコードが読みにくくなっている恐れがある。
enum { TWO = 2 }; /* a scalar */
enum { FOUR = 4 }; /* a scalar */
enum { SQUARE = 2 }; /* an exponent */
x = (-b + sqrt(pow(b, SQUARE) - FOUR*a*c))/ (TWO * a);
レコメンデーションを実装する場合は常に健全な判断が必要となる。
この例は不適切な演算(負の数の sqrt() を計算する)についてチェックしていないことに注意。数学関数の定義域エラーと値域エラーの検出方法の詳細については「FLP32-C. 数学関数における定義域エラーおよび値域エラーを防止または検出する」を参照のこと。
数値リテラルを使うとコードが読みにくく、理解しにくくなる。バッファオーバーフローの原因の多くは、ある場所でマジックナンバーを変更して(配列宣言など)、他の場所(配列を使ったループ)で変更しないことに起因する。
| レコメンデーション | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
|---|---|---|---|---|---|
| DCL06-C | 低 | 低 | 中 | P2 | L3 |
LDRA tool suite V 7.6.0 はこのレコメンデーションの違反を検出することができる。
Compass/ROSE は、単純に、コード中に使われているマジックナンバーやマジックストリングを探すことで、このレコメンデーションの違反を検出できる。つまり、いかなる数字(-1, 0, 1, 2 など正規化された数字は除く)であっても、それがコード中で変数に代入されずに出現する場合はマジックナンバーであり、これらはconst整数やenum, マクロに割り当てられなくてはならない。同様に、文字列リテラル ("" や個々の文字は除く)であっても、それがコード中でchar*やchar[]に割り当てられていないものはマジックストリングである。
DCL06-C. Use meaningful symbolic constants to represent literal values in program logic