なぜprintf(“%f”、0); 未定義の動作を与える?


87

ステートメント

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

0を出力します。

ただし、ステートメント

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

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

ある種の未定義の動作を示していることに気づきましたが、具体的にその理由を理解できません。

全てのビットが0である、浮動小数点の値がまだ有効であるfloat0の値を持つ
floatint(つまりも関連している場合)、私のマシン上で同じサイズです。

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

PS私が使用した場合と同じ動作が見られます

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

37
printfはを期待していdoubleますが、あなたはそれにを与えていintます。floatそしてint、あなたのマシン上で同じ大きさかもしれないが、0.0f実際に変換され、double可変長引数リスト(とに押し込まれたときprintfを期待)。要するに、あなたはprintfあなたが使用している指定子とあなたが提供している引数に基づいて交渉の終わりを達成していません。
WhozCraig

22
Varargs関数は、関数の引数を対応するパラメーターの型に自動的に変換しません。変換できないためです。プロトタイプを持つ非可変引数関数とは異なり、コンパイラーは必要な情報を利用できません。
EOF 2016

3
うーん...「変種」。私は新しい単語を学びました...
マイクロビンソン


3
次に試すのは、の(uint64_t)0代わりにを渡して、0まだランダムな動作が得られるかどうかを確認することです(同じサイズと配置であると想定doubleuint64_tています)。異なるタイプのレジスタが渡されるため、一部のプラットフォーム(x86_64など)では出力がランダムになる可能性があります。
Ian Abbott

回答:


121

"%f"フォーマットは、型の引数が必要ですdouble。あなたはそれにタイプの引数を与えていますint。そのため、動作は未定義です。

標準では、すべてのビット0が0.0(多くの場合そうですが)、または任意のdouble値の有効な表現であることint、およびそれdoubleが同じサイズであること(それがであることを覚えてdoubleいないことfloat)であること、またはそれらが同じであっても保証しませんサイズ、それらは可変引数関数への引数として同じ方法で渡されます。

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

N1570 7.21.6.1パラグラフ9:

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

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

0引数を必要とする非可変関数に定数を渡す場合double、関数のプロトタイプが可視であると想定して、動作が適切に定義されていることに注意してください。たとえば、sqrt(0)(後#include <math.h>)は引数0を暗黙的にintからdouble-に変換します。これは、コンパイラが宣言をsqrt見て、double引数が必要であることを確認できるためです。そのような情報はありませんprintf。のようなVariadic関数printfは特別であり、それらの呼び出しを記述する際により注意が必要です。


13
ここにいくつかの優れたコアポイントがあります。まず、そうではdoubleないfloatので、OPの幅の仮定が成り立たない可能性があります(おそらく成り立たない)。第2に、整数ゼロと浮動小数点ゼロが同じビットパターンを持っているという仮定も成立しません。よくできました
オービットのライトネスレース

2
@LucasTrzesniewski:わかりましたが、私の回答がどのように疑問を投げかけるかはわかりません。私は状態やったfloatに昇格さをdouble理由を説明せずに、それは主なポイントではありませんでした。
キース・トンプソン

2
robertbristow・ジョンソン@:コンパイラはのための特別なフックを持っている必要はありませんprintfgccは、例えば、(それがエラーを診断することができますので、いくつかを持っているけれども、場合フォーマット文字列リテラルです)。コンパイラはprintffrom の宣言を見ることができます<stdio.h>。これは、最初のパラメータがaでconst char*あり、残りがで示されることを通知し, ...ます。いいえ、%fのためであるdouble(とfloatに昇格さdouble)、および%lfためのものですlong double。C標準では、スタックについては何も述べられていません。printf正しく呼び出された場合のみの動作を指定します。
キース・トンプソン

2
@ robertbristow-johnson:昔の夢では、「lint」はgccが現在実行している追加のチェックの一部を実行することがよくありました。にfloat渡されたprintfはに昇格しdoubleます。それについて不思議なことは何もありません。それは可変関数を呼び出すための言語規則にすぎません。printfそれ自体は、フォーマット文字列を介して、呼び出し元がそれに渡すと主張したものを知っています。その主張が正しくない場合、動作は未定義です。
キース・トンプソン

2
小さな補正:l長さ修飾子は、「次に影響を及ぼさないaAeEfFg、またはG変換指定」は、長さのための改質剤long doubleの変換がありますL。(@ robertbristow-johnsonも興味があるかもしれません)
Daniel Fischer

58

最初に、他のいくつかの回答で触れましたが、私の頭では十分に明確に述べいません。ライブラリ関数がor 引数をとるほとんどのコンテキストで整数を提供するのに役立ちます。コンパイラは自動的に変換を挿入します。たとえば、は明確に定義されており、とまったく同じように動作し、そこで使用される他の整数型式についても同様です。doublefloatsqrt(0)sqrt((double)0)

printf違います。引数の数が可変であるため異なります。その関数プロトタイプは

extern int printf(const char *fmt, ...);

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

printf(message, 0);

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

(最新のコンパイラー、フォーマット文字列を調べて、型の不一致があることを通知できますが、意図したとおりに変換を挿入し始めることはありません。 、数年後、あまり役に立たないコンパイラで再構築されたときよりも)

(int)0と(float)0.0がほとんどの最近のシステムでは、両方とも32ビットとして表され、そのすべてがゼロであるとすると、なぜそれが偶然に動作しないのでしょうか。C規格では「これは機能する必要はなく、ご自身で行う」と述べていますが、機能しない最も一般的な2つの理由を説明します。これはおそらくそれが必要でない理由を理解するのに役立つでしょう。

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

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


1
通常の関数にレジスターを使用するアーキテクチャーの中でも、可変個関数にレジスターを使用するアーキテクチャーはありますか?他の関数(float / short / char引数を持つ関数を除く)をで宣言できるとしても、可変個の関数を適切に宣言する必要があるのはそのためだと思いました()
Random832 2016

3
@ Random832今日では、可変個引数と通常の関数の呼び出し規則の唯一の違いは、指定された引数の実際の数のカウントなど、いくつかの追加データが可変個引数に提供される可能性があることです。それ以外の場合は、すべてが通常の機能の場合とまったく同じ場所に配置されます。たとえば、x86-64.org / documentation / abi.pdfのセクション3.2を参照してください。ここで、多様性に対する唯一の特別な扱いは、渡されたヒントALです。(はい、これはの実装va_argが以前よりはるかに複雑であることを意味します。)
zwol

@ Random832:一部のアーキテクチャでは、引数の数とタイプがわかっている関数は、特別な命令を使用することでより効率的に実装できるためだといつも思っていました。
celtschk

@celtschkあなたは、SPARCとIA64の「レジスタウィンドウ」を考えているかもしれません。これらは、少数の引数を使用する関数呼び出しの一般的なケースを加速するはずでした(実際には、実際には、それらは正反対です)。1つの呼び出しサイトの引数の数は、呼び出し先が可変長であるかどうかに関係なく、常にコンパイル時定数であるため、可変長関数の呼び出しを特別に処理する必要はありません。
zwol

@zwol:いいえ、ret n8086 の命令を考えていnました。ハードコードされた整数で、可変関数には適用されませんでした。ただし、Cコンパイラが実際にそれを利用したかどうかはわかりません(非Cコンパイラは確かに利用しました)。
celtschk

13

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


4
また、printf() 特に、その引数が任意の型になるように設計されています。format-stringの各要素がどのタイプを予期しているのかを把握し、正しく提供する必要があります。
マイクロビンソン

@MikeRobinson:まあ、あらゆるプリミティブCタイプ。これは、考えられるすべてのタイプの非常に小さなサブセットです。
MSalters 2016

13

なぜフロートリテラルの代わりに整数リテラルを使用すると、この動作が発生するのですか?

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

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

あなたがしようとしたときと同じ種類の未定義の動作をするでしょう

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

3
のいくつかの特定の実装はprintfそのように機能する可能性があります(渡された項目がアドレスではなく値であることを除いて)。C標準では、他の可変関数がどのように printf機能するを指定せず、その動作を指定するだけです。特に、スタックフレームについては触れられていません。
キース・トンプソン

小さな難しさ:型付きの1つの型付きパラメーターであるフォーマット文字列はありprintfません。ところで、質問にはCとC ++の両方のタグが付けられており、Cの方が本当に関連があります。私はおそらく例として使用しなかったでしょう。const char*reinterpret_cast
キース・トンプソン

ただ興味深い観察:未定義の動作は同じで、おそらく同じメカニズムによるものですが、詳細に小さな違いがあります:問題のようにintを渡すと、intをdoubleとして解釈しようとすると、UBがprintf 内で発生します-あなたの例では、pfを逆参照するとき、それはすでにで起こります...
アコンカグア

@Aconcaguaの説明を追加。
ῥεῖπάντα

このコードサンプルは、厳密なエイリアシング違反のUBです。これは、質問の対象とはまったく異なる問題です。たとえば、浮動小数点数が異なるレジスタで整数に渡される可能性を完全に無視します。
MM

12

一致しないprintf()指定子"%f"とタイプを使用すると(int) 0、未定義の動作が発生します。

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

UBの原因の候補。

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

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

  3. doubleそして、int異なるスタック用いてそれらの値を渡すことができる(一般的な対のFPUのスタック。)

  4. A double 0.0 、すべて0のビットパターンでは定義されない場合あります。(まれ)


10

これは、コンパイラの警告から学ぶ絶好の機会の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互換性のないタイプの引数を渡しているため、は未定義の動作を生成しています。


9

何が混乱しているのかわかりません。

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

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


3
@Voo:そのフォーマット文字列修飾子残念ながら名前が付けられていますが、intここで受け入れられると思われる理由はまだわかりません。
オービットのライトネスレース2016年

1
@Voo:「(これは有効なフロートパターンとしても修飾されます)」なぜint有効なフロートパターンとして修飾されますか?2の補数とさまざまな浮動小数点エンコーディングには、ほとんど共通点がありません。
オービットのライトネスレース

2
ほとんどのライブラリ関数では、0型指定doubleされた引数に整数リテラルを提供すると正しいことが実行されるため、混乱を招きます。コンパイラーがprintfによってアドレス指定された引数スロットに対して同じ変換を行わないことは、初心者には明らかではありません%[efg]
zwol

1
@Voo:これがどれほどひどく間違っているかに興味がある場合は、x86-64 SysV ABIでは、浮動小数点引数が整数引数とは異なるレジスタセットで渡されることを考慮してください。
EOF 2016

1
@LightnessRacesinOrbit 何かがUBである理由を議論することは常に適切だと思います。これには通常、許可される実装の許容範囲と、一般的なケースで実際に何が起こるかについて話すことが含まれます。
zwol 2016

4

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

それを締めくくる:標準では、2番目の引数をor floatまたはorのみにしdouble、それ以外のものは許可しません。


4

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

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

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

0

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

これにより、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実装を考慮):

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);



参照

  1. gnuプロジェクトglibcの "printf"(vprintf)の実装)
  2. printfの簡略化コードの例
  3. va_argの簡略化コードの例
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.