私のプラットフォームsizeof(int)==sizeof(void*)
で、次のコードがあるとします。
printf( "%p", rand() );
の代わりに有効なポインターではない値を渡すため、これは未定義の動作になり%p
ますか?
私のプラットフォームsizeof(int)==sizeof(void*)
で、次のコードがあるとします。
printf( "%p", rand() );
の代わりに有効なポインターではない値を渡すため、これは未定義の動作になり%p
ますか?
@larsmanの回答(制約に違反したため、動作は未定義であると述べています)を拡張するために、実際のC実装をsizeof(int) == sizeof(void*)
次に示しますが、コードは同等ではありませんprintf( "%p", (void*)rand() );
Motorola 68000 プロセッサには、一般的な計算に使用される 16 個のレジスタがありますが、同等ではありません。a0
それらのうちの 8 つ ( ~という名前a7
) はメモリへのアクセス (アドレス レジスタ) に使用され、残りの 8 つ ( d0
~d7
) は演算 (データ レジスタ) に使用されます。このアーキテクチャの有効な呼び出し規約は次のとおりです。
d0
andに渡しますd1
。残りをスタックに渡します。a0
およびに渡しますa1
。残りをスタックに渡します。これは、最新の多くのプロセッサで使用されている呼び出し規則と同様に、完全に正当な呼び出し規則です。
たとえば、関数 を呼び出すには、inとinvoid foo(int i, void *p)
を渡します。i
d0
p
a0
関数を呼び出すには、inとinvoid bar(void *p, int i)
も渡すことに注意してください。i
d0
p
a0
これらのルールの下で、printf("%p", rand())
はフォーマット文字列を に渡し、a0
乱数パラメータを に渡しますd0
。一方、 ではprintf("%p", (void*)rand())
フォーマット文字列を渡し、a0
ではランダム ポインタ パラメータを渡しますa1
。
構造は次のva_list
ようになります。
struct va_list {
int d0;
int d1;
int a0;
int a1;
char *stackParameters;
int intsUsed;
int pointersUsed;
};
最初の 4 つのメンバーは、レジスタの対応するエントリ値で初期化されます。、 、およびをstackParameters
介して渡される最初のスタックベースのパラメーターへのポイントは、それぞれ整数とポインターである名前付きパラメーターの数に初期化されます。...
intsUsed
pointersUsed
このva_arg
マクロは、予想されるパラメーターの型に基づいて異なるコードを生成するコンパイラ組み込み関数です。
va_arg(ap, T)
展開され(T*)get_pointer_arg(&ap)
ます。va_arg(ap, T)
展開され(T)get_integer_arg(&ap)
ます。va_arg(ap, T)
展開され*(T*)get_other_arg(&ap, sizeof(T))
ます。get_pointer_arg
関数は次のようになります。
void *get_pointer_arg(va_list *ap)
{
void *p;
switch (ap->pointersUsed++) {
case 0: p = ap->a0; break;
case 1: p = ap->a1; break;
case 2: p = *(void**)get_other_arg(ap, sizeof(p)); break;
}
return p;
}
get_integer_arg
関数は次のようになります。
int get_integer_arg(va_list *ap)
{
int i;
switch (ap->intsUsed++) {
case 0: i = ap->d0; break;
case 1: i = ap->d1; break;
case 2: i = *(int*)get_other_arg(ap, sizeof(i)); break;
}
return i;
}
get_other_arg
関数は次のようになります。
void *get_other_arg(va_list *ap, size_t size)
{
void *p = ap->stackParameters;
ap->stackParameters += ((size + 3) & ~3);
return p;
}
前述のように、 を呼び出すprintf("%p", rand())
と、フォーマット文字列が に渡されa0
、ランダムな整数が に渡されd0
ます。しかし、printf
関数が実行されると、%p
形式を確認して を実行します。これは、 の代わりにからパラメータva_arg(ap, void*)
を使用get_pointer_arg
して読み取ります。初期化していないのでゴミが入っています。生成した乱数は無視されます。a1
d0
a1
例をさらに進めると、printf("%p %i %s", rand(), 0, "hello");
これがあった場合、次のように呼び出されます。
a0
= フォーマット文字列のアドレス (最初のポインタ パラメータ)a1
= 文字列のアドレス"hello"
(2 番目のポインター パラメーター)d0
= 乱数 (最初の整数パラメーター)d1
= 0 (2 番目の整数パラメーター)関数が実行されると、期待どおりprintf
にフォーマット文字列が読み取らa0
れます。からポインタ%p
を取得して出力するa1
ので、文字列のアドレスを取得します"hello"
。次に、 を見て%i
からパラメータを取得するd0
ので、乱数を出力します。最後に、を確認%s
し、スタックからパラメーターを取得します。しかし、スタックにパラメーターを渡していません! これにより、未定義のスタック ガベージが読み取られ、文字列ポインターであるかのように出力しようとすると、プログラムがクラッシュする可能性が高くなります。
C 標準、7.21.6.1、fprintf
関数は、次のように述べています
p
引数は へのポインタでなければなりませんvoid
。
付録 J.2 により、これは制約であり、制約に違反すると UB が発生します。
(以下は、なぜこれが UB であるべきかという私の以前の理由です。これは複雑すぎました。)
void*
そのパラグラフはから を取得する方法を説明していません...
が、C 標準自体がこの目的のために提供している唯一の方法は 7.16.1.1 ですva_arg
。
型が実際の次の引数の型と互換性がない場合(デフォルトの引数の昇格に従って昇格されるため)、動作は未定義です
6.2.7 互換型と複合型を読んだ場合、サイズに関係なく、互換性があり、互換性があるべきであるというヒントはvoid*
ありません。int
したがって、標準 Cva_arg
で実装する唯一の方法であるため、動作は未定義です。printf
はい、未定義です。C++11、3.7.4.2/4 から:
無効なポインター値を使用した場合 (解放関数に渡すことを含む) の影響は未定義です。
脚注付き:
一部の実装では、システム生成のランタイム エラーが発生します。
%p は、printf の単なる出力形式指定です。型がポインターでない場合、一部のコンパイラーは警告を発行しますが、ポインターを逆参照または検証する必要はありません。
int main(void)
{
int t = 5;
printf("%p\n", t);
}
コンパイル警告:
warning: format ‘%p’ expects argument of type ‘void*’, but argument 2 has type ‘int’ [-Wformat]
出力:
0x5