DCL11-C. 可変引数関数に関連する型問題について理解する
可変引数関数の可変引数(関数宣言における ...
の部分に相当する)は、va_arg()
マクロによって解釈される。va_arg()
マクロは、実装された可変引数関数本体のなかで、初期化された引数リストから次の引数を抽出する。抽出されるオブジェクトのサイズは指定された型により決定される。型とそれに対応する実引数が一致していない場合、動作は未定義となり、データの誤解釈やアラインメントエラーを引き起こす恐れがある(「EXP36-C. ポインタをより厳密にアラインされるポインタ型に変換しない」を参照)。
コンパイラは可変引数関数に渡される可変引数の型をチェックしない。そのため、プログラマは、実引数に既定の実引数拡張を行った後の型が、対応する仮引数の型と適合することを保証する義務がある。
int
よりも順位の低い型の整数引数は、その型の値をすべてint
で保持できるならば、int
に格上げされる。そうでなければ、unsigned int
に格上げされる(「整数の格上げ」)。float
型の引数はdouble
に格上げされる。
違反コード (型解釈エラー)
C の printf()
関数は可変引数関数として実装されている。以下の違反コード例では、null終端バイト文字列と整数引数を、書式指定文字列の指定に反して入れ換えてしまっている。従って、整数はnull終端バイト文字列へのポインタとして解釈され、参照される。これは恐らくプログラムの異常終了を引き起こすだろう。同様に、ポインタ error_message
は整数型として解釈されることに注意。
const char *error_msg = "Error occurred";
/* ... */
printf("%s:%d", 15, error_msg);
適合コード (型解釈エラー)
以下の適合コードでは、書式指定文字列を変更し、適切な変換指定子が引数に対応している。
const char *error_msg = "Error occurred";
/* ... */
printf("%d:%s", 15, error_msg);
このように、書式指定関数に渡される引数が与えられた書式指定文字列と適切に対応するよう注意しなくてはならない。
違反コード (型アラインメントエラー)
以下のコード例では、printf()
関数は間違って %d
指定子を使って long long
型整数を処理する。これにより、値が引数リストから抽出される際にデータの欠損や間違った表現につながるかもしれない。
long long a = 1;
const char msg[] = "Default message";
/* ... */
printf("%d %s", a, msg);
long long
は解釈されなかったため、long long
がより多くの記憶域を使用する場合、その次の書式指定子 %s
が指す位置が予期せずずれてしまい、メッセージへのポインタの代わりに未知のデータが使用されることになるかもしれない。
適合コード (型アラインメントエラー)
以下の適合コードは、長さ修飾子ll
を %d
書式指定子に追加しており、その結果 printf()
の可変引数パーサーが、可変引数リストから、long long
引数に対応する正しいバイト数を抽出する。
long long a = 1;
const char msg[] = "Default message";
/* ... */
printf("%lld %s", a, msg);
違反コード (NULL
)
C 標準では、NULL
を整数定数またはポインタ定数のいずれか(実装定義)と定めている。NULL
が可変引数を持たない関数にポインタ型実引数として渡される場合には適切な型変換が行われる。しかし、可変引数を持つ関数に可変引数として渡される場合には、例え sizeof(NULL) != sizeof(void *)
だったとしても型変換は行われない。これが問題になるのは次のような状況だ:
- ポインタ型と整数型のサイズが異なっており、
NULL
が整数定数として定義されている。あるいは、 - サイズの異なる複数のポインタ型が提供されており、
NULL
がvoid
へのポインタと定義されている。この場合NULL
はchar
型へのポインタと同じサイズを持つとされており(C11 セクション6.2.5、パラグラフ28)、仮引数で指定されているポインタ型と異なるサイズとなる可能性がある
このような状況では、次のコードは未定義の動作となる。
char* string = NULL;
printf("%s %d\n", string, 1);
int
型が 32 ビット、ポインタ型が 64 ビットのシステムでは、printf()
が、2 番目の引数 NULL
をポインタの上位ビット、3 番目の引数 1
をポインタの下位ビットと解釈してしまうかもしれない。この場合、printf()
は、値 0x00000001
をポインタ値として使用して出力を行い、さらに %d
変換指定子に対応する値を読み取ろうとする。しかし、そこに呼出し元から提供されているものはなにもない。
適合コード (NULL
)
この適合コードでは、printf()
に NULL
を渡さないようにしている。
char* string = NULL;
printf("%s %d\n", (string ? string : "null"), 1);
リスク評価
可変引数関数に渡される引数の型と関数本体が想定する型が食い違うと、プログラムが異常終了したり、意図せぬ情報漏えいにつながる可能性がある。
レコメンデーション |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
---|---|---|---|---|---|
DCL11-C |
高 |
中 |
高 |
P6 |
L2 |
自動検出(最新の情報はこちら)
ツール |
バージョン |
チェッカー |
説明 |
---|---|---|---|
Axivion Bauhaus Suite |
6.9.0 |
CertC-DCL11 |
|
Compass/ROSE |
|
|
現在、このレコメンデーションの違反は検出されない。可変引数関数の作成者とその呼び出し側の取り決めを強制することが困難なため、このレコメンデーションの違反を自動検出することは一般に不可能であるが、 |
1.2
|
CC2.DCL11 |
部分的に実装済み |
|
GCC |
4.3.5 |
|
|
Klocwork |
2018
|
MISRA.FUNC.VARARG |
|
LDRA tool suite |
9.7.1
|
41 S, 589 S |
部分的に実装済み |
Parasoft C/C++test |
10.4.1 |
CERT_C-DCL11-a |
There should be no mismatch between the '%s' or '%c' tag from format string and its corresponding argument in 'printf' function invocation |
Parasoft Insure++ |
|
|
Runtime analysis |
Polyspace Bug Finder |
R2018a |
Format string specifiers and arguments mismatch
|
String specifiers do not match corresponding arguments The features of <stdarg.h> shall not be used |
PRQA QA-C |
9.5
|
0179 (U), 0184 (U), 0185 (U), 0186 (U), 0190 (U), 0191 (U), 0192 (U), 0193 (U), 0194 (U), 0195 (U), 0196 (U), 0197 (U), 0198 (U), 0199 (U), 0200 (U), 0201 (U), 0206 (U), 0207, 0208 |
部分的に実装済み |
PVS-Studio |
6.23 |
V576 |
|
関連するガイドライン
ISO/IEC TR 24772:2013 | Type System [IHN] Subprogram Signature Mismatch [OTR] |
MISRA C:2012 | Rule 17.1 (required) |
翻訳元
これは以下のページを翻訳したものです。
DCL11-C. Understand the type issues associated with variadic functions (revision 131)