Home > ラーニング > セキュアコーディング > C セキュアコーディングスタンダード > 01. プリプロセッサ (PRE)
マクロは危険である。本物の関数と同じように使えるが、セマンティクスが異なるからである。C99 では、C 言語仕様にインライン関数がつけ加えられた。インライン関数とマクロが交換可能な場合は、インライン関数の方を使うようにすべきである。関数をインライン関数として定義することは、例えば、通常の関数機構の代替としてインライン置換を使うことにより、関数呼出しを可能なかぎり速くすることを示唆する。(「PRE31-C. 代入、インクリメント、デクリメント、volatile アクセス、または関数呼び出しを含む引数を使って安全でないマクロを呼び出さない」、「PRE01-C. マクロ内の引数名は括弧で囲む」、および「PRE02-C. マクロ置換リストは括弧で囲む」を参照のこと。)
インライン置換は、テキスト上の置き換えではないし、新しい関数を作成することもない。例えば、その関数本体内で使われるマクロの展開は、関数本体が出現する時点で行ない、関数が呼び出される時点では行わない。そして関数本体の位置で有効範囲にある宣言を識別子が参照する。
関数をインライン展開するかどうかはローレベルな最適化の話であり、プログラマからの入力に依らずコンパイラが決めるべきことである。以下の観点に基いてインライン関数の使用を見極めるべきである。
スタティック関数はインライン関数と同じくらい有用だ。(インライン関数と違って)C90でサポート済みである。
以下のコード例では、マクロ CUBE() は、副作用のある式を引数として渡されるとき、未定義の動作を持つ。
#define CUBE(X) ((X) * (X) * (X)) /* ... */ int i = 2; int a = 81 / CUBE(++i);
この例では、a の初期化は次のように展開される。
int a = 81 / ((++i) * (++i) * (++i));
これは未定義の動作を持つ。(「EXP30-C. 副作用完了点間の評価の順序に依存しない」を参照。)
マクロ定義をインライン関数に置き換えると、副作用は関数の呼出しの直前に一回だけ実行される。
inline int cube(int i) {
return i * i * i;
}
/* ... */
int i = 2;
int a = 81 / cube(++i);
以下のコード例では、プログラマは EXEC_BUMP() というマクロを定義している。このマクロは、指定した関数を呼び出し、グローバル変数をインクリメントしている。[Dewhurst 02] この例のように、ある関数の定義のなかでマクロが展開されると、マクロの定義本体で使われている識別子は、その関数本体のスコープにある宣言を参照することになる。その結果、aFunc 関数のなかでマクロが呼び出されたとき、うかつにもグローバル変数と同じ名称のローカル変数をインクリメントしてしまう。このコード例は、「DCL01-C. サブスコープで変数名を再利用しない」にも違反している。
size_t count = 0;
#define EXEC_BUMP(func) (func(), ++count)
void g(void) {
printf("Called g, count = %zu.\n", count);
}
void aFunc(void) {
size_t count = 0;
while (count++ < 10) {
EXEC_BUMP(g);
}
}
aFunc() を実行したときに、以下の行が (あやまって) 5回出力される結果となる。
Called g, count = 0.
以下の解決法では、EXEC_BUMP() マクロをインライン関数 exec_bump() に置き換えている。aFunc 呼出しは今度は count の値を 0 から 9 まで (正しく) 出力する。
size_t count = 0;
void g(void) {
printf("Called g, count = %zu.\n", count);
}
typedef void (*exec_func)(void);
inline void exec_bump(exec_func f) {
f();
++count;
}
void aFunc(void) {
size_t count = 0;
while (count++ < 10) {
exec_bump(g);
}
}
インライン関数を使うことで、識別子 count は関数本体がコンパイルされるときに参照できるグローバル変数にバインドされる。関数呼出し時に、別の変数で同じ名前を持つものにバインドされることはない。
関数と違ってマクロの実行はインターリーブすることがある。そのため、個々には問題ないふたつのマクロが、ひとつの式のなかで組み合わされると未定義の動作を起こすことがある。
以下の例では、F() と G() の両方がグローバル変数 operations をインクリメントしている。これは、ふたつのマクロが一緒に使われるとき問題をひきおこす。
#define F(x) (++operations, ++calls_to_F, 2*x) #define G(x) (++operations, ++calls_to_G, x + 1) /* ... */ y = F(x) + G(x);
変数 operations は同じ式のなかで2回、値の読み込みと変更がなされる。例えば以下のような実行順序になった場合に、間違った値になるかもしれない。
operations を レジスタ0 に読み込む operations を レジスタ1 に読み込む レジスタ0 をインクリメント レジスタ1 をインクリメント レジスタ0 を operations にストア レジスタ1 を operations にストア
このコード例は「EXP30-C. 副作用完了点間の評価の順序に依存しない」にも違反している。
インライン関数を含む関数の実行は、インターリーブされない。そのため、問題となる実行順序になることはない。
inline int f(int x) {
++operations;
++calls_to_f;
return 2*x;
}
inline int g(int x) {
++operations;
++calls_to_g;
return x + 1;
}
/* ... */
y = f(x) + g(x);
GNU C (とその他いくつかのコンパイラ)は、C99 に追加される前からインライン関数をサポートしており、C99 と大きく異なるセマンティクスを持つ。C99 と GNU C におけるセマンティクスの違いについて、Richard Kettlewell による優れた解説がある。[Kettlewell 03]
PRE00-EX1: マクロを使うと、ローカル関数(そのスコープ内の自動変数にアクセスできるコード)を実装できるが、インライン関数では実現できない。
PRE00-EX2: マクロを使うと、ある種のlazy calculationをサポートすることもできる。これもインライン関数では実現することができない。例えば、
#define SELECT(s, v1, v2) ((s) ? (v1) : (v2))
このコードは、セレクタ s の値に応じて、ふたつの式のうちのひとつだけを計算する。
PRE00-EX3: マクロを使うと、コンパイル時定数を得ることができる。これは、次の例のようにインライン関数では必ずしも実現できるとは限らない。
#define ADD_M(a, b) ((a) + (b))
static inline add_f(int a, int b) {
return a + b;
}
この例では、ADD_M(3,4) マクロ呼出しは定数式を生成するが、add_f(3,4) 関数呼出しはそうならない。
PRE00-EX4: マクロを使えば、C 言語では実装できない型総称関数の機能を実装できる。これは、C++ ではテンプレートの機能を使って実現されている。
「MEM02-C. メモリ割り当て関数の結果は、割り当てた型へのポインタに即座にキャストする」に、関数形式マクロを使って型総称関数を作る例がある。
型総称マクロを使うと例えば、ふたつの同じ型の変数の値を入れ換える操作を行なうことができる。
PRE00-EX5: マクロの引数は名前呼出し(call-by-name)で評価されるが、関数は値呼出し(call-by-value)で評価される。名前呼出し(call-by-name)のセマンティクスが要求されるときはマクロを使わねばならない。
マクロの不適切な使用は未定義の動作を引き起こすかもしれない。
| レコメンデーション | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
|---|---|---|---|---|---|
| PRE00-C | 中 | 低 | 中 | P4 | L3 |
LDRA V 7.6.0 はこのレコメンデーションへの違反を検出できる。
PRE00-C. Prefer inline or static functions to function-like macros