web-dev-qa-db-ja.com

なぜprintf( "%f"、0);未定義の動作を与えますか?

声明

printf("%f\n",0.0f);

0を出力します。

ただし、ステートメント

printf("%f\n",0);

ランダムな値を出力します。

私はある種の未定義の振る舞いを示していることを理解していますが、具体的な理由を理解することはできません。

すべてのビットが0である浮動小数点値は、値が0の有効なfloatのままです。
floatintは、マシン上で同じサイズです(関連がある場合)。

printfで浮動小数点リテラルの代わりに整数リテラルを使用すると、この動作が発生するのはなぜですか?

追伸私が使用すると同じ動作が見られます

int i = 0;
printf("%f\n", i);
86
Trevor Hickey

_"%f"_形式には、double型の引数が必要です。タイプintの引数を指定しています。そのため、動作は未定義です。

この規格は、すべてのビットがゼロであることを_0.0_(多くの場合はそうですが)、またはdouble値、またはintおよびdoubleは同じサイズです(doubleではなくfloatであることに注意してください)、または同じサイズであっても、引数として可変引数関数に渡される同じ方法。

システムで「動作する」ことがあります。これは、エラーの診断が困難になるため、未定義の動作の最悪の症状です。

N157 7.21.6.1パラグラフ9:

...いずれかの引数が対応する変換仕様の正しいタイプでない場合、動作は未定義です。

タイプfloatの引数はdoubleに昇格されるため、printf("%f\n",0.0f)が機能します。 intよりも狭い整数型の引数は、intまたは_unsigned int_に昇格されます。これらのプロモーションルール(N1570 6.5.2.2パラグラフ6で指定)は、printf("%f\n", 0)の場合には役立ちません。

119
Keith Thompson

最初に、いくつかの他の答えで触れましたが、私の心には十分に明確に綴っていませんでした:doesmostライブラリ関数がdoubleまたはfloat引数を取るコンテキスト。コンパイラは自動的に変換を挿入します。たとえば、sqrt(0)は明確に定義されており、sqrt((double)0)とまったく同じように動作し、そこで使用される他の整数型の式についても同じことが言えます。

printfは異なります。異なる数の引数を取るため、異なります。その関数プロトタイプは

_extern int printf(const char *fmt, ...);
_

したがって、あなたが書くとき

_printf(message, 0);
_

コンパイラーは、2番目の引数がどのタイプprintfexpectsであるかについての情報を持っていません。渡される引数式の型(int)のみがあります。したがって、ほとんどのライブラリ関数とは異なり、引数リストがフォーマット文字列の期待に一致することを確認するのはプログラマーです。

(現代のコンパイラcanは、フォーマット文字列を調べて、型の不一致があることを伝えますが、変換を挿入し始めません何年も後に、あまり役に立たないコンパイラーで再構築されたときよりも、あなたのコードがすぐに壊れるはずです。

さて、質問の残りの半分は:(int)0と(float)0.0がほとんどの最新システムで、両方とも32ビットとして表され、すべてゼロであると仮定すると、どうして偶然に動作しないのでしょうか? C規格では、「これは動作する必要はありません。自分で実行してください」と書かれていますが、動作しない最も一般的な2つの理由を説明します。それはおそらくあなたが理解するのに役立つでしょうなぜそれは必須ではありません。

まず、歴史的な理由により、floatを可変引数リストに渡すと、doublepromotedが取得されます。ほとんどの最新システムでは、64ビット幅です。したがって、printf("%f", 0)は、32個のゼロビットのみを64個の期待される呼び出し先に渡します。

同様に重要な2番目の理由は、浮動小数点関数の引数が整数の引数とは異なるplaceで渡される可能性があることです。たとえば、ほとんどのCPUには整数と浮動小数点値用に別々のレジスタファイルがあるため、引数0から4は、整数の場合はレジスタr0からr4に、浮動小数点の場合はf0からf4に入れられるという規則になります。したがって、printf("%f", 0)はレジスターf1でそのゼロを探しますが、まったくありません。

58
zwol

浮動小数点数リテラルの代わりに整数リテラルを使用すると、この動作が発生するのはなぜですか?

printf()には、最初のパラメーターとしてconst char* formatstring以外に型付きパラメーターがないためです。残りのすべてにcスタイルの省略記号(...)を使用します。

書式文字列で指定された書式タイプに従って、そこに渡された値を解釈する方法を決定するだけです。

あなたはしようとするときと同じ種類の未定義の振る舞いをするでしょう

 int i = 0;
 const double* pf = (const double*)(&i);
 printf("%f\n",*pf); // dereferencing the pointer is UB
13

通常、doubleを期待する関数を呼び出すが、intを指定すると、コンパイラーは自動的にdoubleに変換します。引数の型が関数プロトタイプで指定されていないため、printfでは発生しません。コンパイラーは変換を適用する必要があることを知りません。

13
Mark Ransom

一致しないprintf()指定子"%f"と入力(int) 0は未定義の動作につながります。

変換仕様が無効な場合、動作は未定義です。 C11dr§7.21.6.19

UBの原因の候補。

  1. それは仕様ごとのUBであり、コンパイルは普通ではありません-'nufは言いました。

  2. doubleintはサイズが異なります。

  3. doubleintは、異なるスタックを使用して値を渡すことができます(一般的なvs. [〜#〜] fpu [〜#〜] スタック。)

  4. double 0.0mightは、すべてゼロのビットパターンでは定義されません。 (まれ)

12
chux

これは、コンパイラの警告から学ぶ絶好の機会の1つです。

$ gcc -Wall -Wextra -pedantic fnord.c 
fnord.c: In function ‘main’:
fnord.c:8:2: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=]
  printf("%f\n",0);
  ^

または

$ clang -Weverything -pedantic fnord.c 
fnord.c:8:16: warning: format specifies type 'double' but the argument has type 'int' [-Wformat]
        printf("%f\n",0);
                ~~    ^
                %d
1 warning generated.

したがって、printfは、互換性のないタイプの引数を渡すため、未定義の動作を引き起こします。

10
wyrm

何が混乱するのか分かりません。

フォーマット文字列にはdoubleが必要です。代わりにintを提供します。

2つの型のビット幅が同じであるかどうかはまったく関係ありませんが、このような壊れたコードからハードメモリ違反の例外が発生するのを防ぐのに役立つ場合があります。

なぜ正式にUBであるかは、いくつかの回答で議論されています。

この動作を具体的に取得する理由はプラットフォームに依存しますが、おそらく次のとおりです。

  • printfは、標準の可変引数伝播に従って引数を期待します。つまり、floatdoubleになり、intより小さいものはintになります。
  • 関数がintを予期しているdoubleを渡している。 intはおそらく32ビット、doubleは64ビットです。つまり、引数が置かれるはずの場所から始まる4つのスタックバイトは0が、次の4バイトには任意のコンテンツがあります。それは、表示される値を構築するために使用されるものです。
4
glglgl

_"%f\n"_は、2番目のprintf()パラメーターのタイプがdoubleである場合にのみ、予測可能な結果を​​保証します。次に、可変個引数関数の追加の引数は、デフォルトの引数昇格の対象です。整数の引数は整数の昇格の対象になり、浮動小数点型の値にはなりません。また、floatパラメーターはdoubleに昇格されます。

さらに、標準では、2番目の引数がfloatまたはdoubleになり、それ以外は何も許可されません。

4
Sergio

この「未定値」問題の主な原因は、int変数パラメーターセクションに渡されるprintf値のポインターのキャストが、_va_arg_マクロが実行するdoubleタイプのポインターへのキャストにあります。

doubleサイズのメモリバッファ領域がintサイズより大きいため、printfにパラメータとして渡された値で完全に初期化されなかったメモリ領域への参照が発生します。

したがって、このポインターが逆参照されると、未定義の値、またはprintfにパラメーターとして渡された値の一部を含む「値」が返され、残りの部分は別のスタックバッファー領域またはコード領域からも取得できます(メモリフォールト例外を発生させる)、実際のバッファオーバーフロー


「printf」および「va_arg」のコード化実装のこれらの特定の部分を考慮することができます...

printf

_va_list arg;
....
case('%f')
      va_arg ( arg, double ); //va_arg is a macro, and so you can pass it the "type" that will be used for casting the int pointer argument of printf..
.... 
_


ダブル値パラメータのコードケース管理のvprintfでの実際の実装(gnu impl。を考慮):

_if (__ldbl_is_dbl)
{
   args_value[cnt].pa_double = va_arg (ap_save, double);
   ...
}
_



va_arg

_char *p = (double *) &arg + sizeof arg;  //printf parameters area pointer

double i2 = *((double *)p); //casting to double because va_arg(arg, double)
   p += sizeof (double);
_



references

  1. "printf"(vprintf)のgnuプロジェクトglibc実装)
  2. printfの暗号化コードの例
  3. va_argの暗号化コードの例
0
Ciro Corvino