DCL06-C. リテラル値の表現には意味のあるシンボル定数を使う
C 言語には様々な種類の定数がある: 10
や 0x1C
のような整数定数や、1.0
や 0x1C
といった浮動小数点定数、'a'
や '\x10'
のような文字定数など。C にはたとえば "hello, world"
や "\n"
のような文字列リテラルもある。これらはリテラルと呼ぶことができる。
プログラムロジックでリテラルを使うと、ソースコードの可読性が低下することがある。そのため、リテラル一般、特に整数定数はマジックナンバーと呼ばれることが多い。その利用目的が不明瞭であるからだ。マジックナンバーは、任意の値を表す定数値のこともあれば(たとえば適切だと判断されたバッファサイズ)、可塑性の高い概念を表すこともある(たとえば、大人とみなされる年齢は地政学的な境界によって変わりうる)。プログラムロジックにリテラルを埋め込むのではなく、適切な名前をつけたシンボル定数を使い、コードの意図を明解にすること。また、特定の値を変更しなくてはならない場合、シンボル定数を一度再アサインする方が、その値のインスタンスをすべて修正するよりも効率的かつエラーになりにくい。[Saks 2002]
C言語には、名前付きのシンボル定数を作成する様々な方法がある。const
修飾オブジェクト、列挙定数、オブジェクト形式マクロなどがそれである。これらのメカニズムにはそれぞれ利点と欠点がある。
const
修飾オブジェクト
const
修飾されたオブジェクトには有効範囲(scope)があり、コンパイラによる型チェックが可能である。これらは(マクロ定義ではなく)名前付きオブジェクトであるため、デバッグツールを使ってオブジェクト名を表示することができる。オブジェクトはメモリを消費する。
const
修飾オブジェクトを使うことで定数の厳密な型を指定することができる。たとえば、次のような例が考えられる。
const unsigned int buffer_size = 256;
は、buffer_size
をunsigned int
型の定数として定義する。
残念ながら、以下に示すようなコンパイル時に整数定数が必要とされるような場合には、const
修飾オブジェクトを使うことはできない。
- 構造体のビットフィールドメンバのサイズ
- 配列のサイズ (可変長配列の場合は除く)
- 列挙定数の値
case
定数の値
これらの場合には、整数定数(右辺値)を使わなくてはならない。
const
修飾オブジェクトを用いることで、プログラマはオブジェクトのアドレスを取得することができる。
const int max = 15; int a[max]; /* 関数外の無効な宣言 */ const int *p; /* const 修飾オブジェクトはアドレスを取得できる */ p = &max;
const
修飾オブジェクトは往々にして実行時オーバーヘッドを招く。[Saks 2001b]たとえば、たいていの C コンパイラは const
修飾オブジェクトにメモリを割り当てる。関数本体の中で宣言された const
修飾オブジェクトは自動記憶域期間を持つだろう。その場合、コンパイラはオブジェクトに対して、スタック上に記憶域を割り当てる。それゆえ、この記憶域は、それを含む関数が呼び出されるたびに、割り当てと初期化が行われなくてはならない。
列挙定数
列挙定数を使うことで、int
として表現可能な値を持つ整数定数式を表現することができる。const
修飾オブジェクトとはことなり、列挙定数はメモリを消費しない。値に対して記憶域が割り当てられることはないため、列挙定数のアドレスを取得することはできない。
enum { max = 15 }; int a[max]; /* 関数外でOK */ const int *p; p = &max; /* エラー: 列挙定数に対する '&' */
列挙定数は値の型を指定することを許さない。値が int
型で表現できる列挙定数は必ず int
である。
オブジェクト形式マクロ
以下に示す前処理指令の形式
#
define
識別子 置換要素並び
は オブジェクト形式マクロを定義する。それ以降、そのマクロ名の出現を、この指令の残りを構成する置換要素並びの前処理字句列で置き換える。
Cプログラマはやたらとシンボル定数をオブジェクト形式マクロとして定義する。たとえば次のコード
#define buffer_size 256
は、buffer_size
を 256 という値を持つマクロとして定義している。プリプロセッサは、コンパイラがシンボルの処理を行う前にマクロを置換する。以降のコンパイルフェーズでは buffer_size
のようなマクロシンボルは現れない。コンパイラはマクロ置換後のソーステキストのみを扱うのである。その結果、コンパイラの多くはデバッガにわたすシンボル名を保持しない。
マクロ名は、他の名前には適用される有効範囲の規則に従わない。それゆえ、マクロが想定外の場所で置換され、予期せぬ結果をもたらすおそれがある。
オブジェクト形式マクロはメモリを消費しない。したがって、これを参照するポインタを作成することはできない。また、マクロは型チェックされない。これはマクロがプリプロセッサによってテキスト置換されるからである。
マクロはコンパイル時引数として渡されることがある。
まとめ
以下の表に const
修飾オブジェクト、列挙定数、オブジェクト形式マクロ定義の違いについてまとめる。
方法 |
いつ評価されるか |
メモリの消費 |
デバッガから見えるか |
型チェック |
コンパイル時の定数式 |
---|---|---|---|---|---|
列挙定数 |
コンパイル時 |
無 |
有 |
有 |
有 |
|
実行時 |
有 |
有 |
有 |
無 |
マクロ |
プリプロセッサ |
無 |
無 |
無 |
有 |
違反コード
このコード例では整数リテラル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()
呼び出しで使用する整数リテラルが小さく変更されていない場合など。
適合コード (enum
)
以下の適合コードでは、整数リテラルを列挙定数に置き換えている(「DCL00-C. 不変(immutable)オブジェクトは const 修飾する」を参照のこと)。
enum { BUFFER_SIZE=256 }; char buffer[BUFFER_SIZE]; /* ... */ fgets(buffer, BUFFER_SIZE, stdin);
列挙定数であれば定数式が必要などんな場所でも安全に使うことができる。
適合コード (sizeof
)
多くの場合、新たなシンボルを定義するのではなく、既存のシンボルからなるシンボル式を使うことで、望ましいコード可読性を手に入れることができる。たとえば、sizeof
式は列挙定数と同じように使うことができる(「EXP09-C. 型や変数のサイズは sizeof を使って求める」を参照のこと)。
char buffer[256]; /* ... */ fgets(buffer, sizeof(buffer), stdin);
上記の例のように sizeof
式を使うことでプログラム中で宣言される名前の数は減る。これは一般によいアイデアである。[Saks 2002]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 |
自動検出(最新の情報はこちら)
ツール |
バージョン |
チェッカー |
説明 |
---|---|---|---|
Compass/ROSE |
|
|
単純に、コード中に使われているマジックナンバーやマジックストリングを探すことで、このレコメンデーションの違反を検出できる。つまり、いかなる数字(−1、0、1、2 など正規化された数字は除く)であっても、それがコード中で変数に代入されずに出現する場合はマジックナンバーであり、これらは |
ECLAIR |
1.1 |
nomagicc |
実装済み |
LDRA tool suite |
V. 8.5.4 |
201 S |
実装済み |
PRQA QA-C | 8.1 |
3120 |
部分的に実装済み |
関連するガイドライン
CERT C++ Secure Coding Standard | DCL06-CPP. Use meaningful symbolic constants to represent literal values in program logic |
MITRE CWE | CWE-547, Use of hard-coded, security-relevant constants |
参考資料
[Henricson 1992] | Chapter 10, "Constants" |
[Saks 2001a] | |
[Saks 2001b] | |
[Saks 2002] | |
[Summit 2005] | Question 10.5b |
翻訳元
これは以下のページを翻訳したものです。
DCL06-C. Use meaningful symbolic constants to represent literal values (revision 132)