Mengapa printf ("% f", 0); berikan perilaku yang tidak terdefinisi?

87

Pernyataan

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

mencetak 0.

Namun demikian, pernyataan tersebut

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

mencetak nilai acak.

Saya menyadari bahwa saya menunjukkan semacam perilaku yang tidak jelas, tetapi saya tidak tahu mengapa secara spesifik.

Nilai floating point di mana semua bitnya 0 masih valid floatdengan nilai 0.
floatdan intberukuran sama pada mesin saya (bahkan jika itu relevan).

Mengapa menggunakan literal integer alih-alih literal floating point printfmenyebabkan perilaku ini?

PS perilaku yang sama dapat dilihat jika saya menggunakan

int i = 0;
printf("%f\n", i);
Trevor Hickey
sumber
37
printfmengharapkan double, dan Anda memberikannya int. floatdan intmungkin berukuran sama di komputer Anda, tetapi 0.0fsebenarnya diubah menjadi doubleketika didorong ke daftar argumen variadic (dan printfmengharapkan itu). Singkatnya, Anda tidak memenuhi akhir dari penawaran printfberdasarkan spesifikasi yang Anda gunakan dan argumen yang Anda berikan.
WhozCraig
22
Varargs-functions tidak secara otomatis mengonversi argumen fungsi ke tipe parameter yang sesuai, karena mereka tidak bisa. Informasi yang diperlukan tidak tersedia untuk compiler, tidak seperti fungsi non-varargs dengan prototipe.
EOF
3
Oooh ... "variadics". Saya baru saja belajar kata baru ...
Mike Robinson
3
Hal berikutnya untuk mencoba adalah untuk lulus (uint64_t)0bukan 0dan melihat apakah Anda masih mendapatkan perilaku acak (dengan asumsi doubledan uint64_tmemiliki ukuran dan keselarasan yang sama). Kemungkinan keluarannya akan tetap acak pada beberapa platform (misalnya x86_64) karena jenis yang berbeda diteruskan di register yang berbeda.
Ian Abbott

Jawaban:

121

The "%f"Format membutuhkan sebuah argumen tipe double. Anda memberikan argumen tipe int. Itulah mengapa perilakunya tidak terdefinisi.

Standar tidak menjamin bahwa semua-bit-nol adalah representasi yang valid 0.0(meskipun sering), atau dari doublenilai apa pun , atau itu intdan doubleberukuran sama (ingat itu double, bukanfloat ), atau, bahkan jika mereka sama size, yang diteruskan sebagai argumen ke fungsi variadic dengan cara yang sama.

Ini mungkin terjadi untuk "bekerja" di sistem Anda. Itu kemungkinan gejala terburuk dari perilaku tidak terdefinisi, karena itu menyulitkan untuk mendiagnosis kesalahan.

N1570 7.21.6.1 paragraf 9:

... Jika ada argumen yang bukan tipe yang benar untuk spesifikasi konversi yang sesuai, perilaku tidak ditentukan.

Argumen tipe floatdipromosikan double, itulah sebabnya printf("%f\n",0.0f)berhasil. Argumen jenis integer lebih sempit daripada intyang dipromosikan ke intatau ke unsigned int. Aturan promosi ini (ditentukan oleh N1570 6.5.2.2 paragraf 6) tidak membantu dalam kasus printf("%f\n", 0).

Perhatikan bahwa jika Anda meneruskan konstanta 0ke fungsi non-variadic yang mengharapkan doubleargumen, perilakunya didefinisikan dengan baik, dengan asumsi prototipe fungsi terlihat. Misalnya, sqrt(0)(setelah #include <math.h>) secara implisit mengonversi argumen 0dari intmenjadi double- karena kompilator dapat melihat dari deklarasi sqrtbahwa ia mengharapkan doubleargumen. Tidak ada informasi seperti itu untuk printf. Fungsi variadik seperti printfitu istimewa, dan membutuhkan lebih banyak perhatian dalam menulis panggilan kepada mereka.

Keith Thompson
sumber
13
Beberapa poin inti yang bagus di sini. Pertama, bahwa itu doubletidak floatbegitu mungkin tidak (mungkin tidak) terus asumsi lebar OP. Kedua, asumsi bahwa integer zero dan floating-point zero memiliki pola bit yang sama juga tidak berlaku. Kerja bagus
Lightness Races in Orbit
2
@LucasTrzesniewski: Oke, tapi saya tidak mengerti bagaimana jawaban saya menimbulkan pertanyaan. Saya menyatakan bahwa floatdipromosikan menjadi doubletanpa menjelaskan mengapa, tetapi itu bukan poin utamanya.
Keith Thompson
2
@ robertbristow-johnson: Kompiler tidak perlu memiliki kait khusus untuk printf, meskipun gcc, misalnya, memiliki beberapa sehingga dapat mendiagnosis kesalahan ( jika format string adalah literal). Kompilator dapat melihat deklarasi printffrom <stdio.h>, yang memberitahukannya bahwa parameter pertama adalah a const char*dan sisanya ditandai dengan , .... Tidak, %funtuk double(dan floatdipromosikan menjadi double), dan %lfuntuk long double. Standar C tidak mengatakan apa-apa tentang tumpukan. Ini menentukan perilaku printfhanya jika dipanggil dengan benar.
Keith Thompson
2
@ robertbristow-johnson: Dalam keadaan linglung, "lint" sering melakukan beberapa pemeriksaan tambahan yang sekarang dilakukan oleh gcc. A floatditeruskan ke printfdipromosikan menjadi double; tidak ada yang ajaib tentang itu, itu hanya aturan bahasa untuk memanggil fungsi variadic. printfsendiri tahu melalui string format apa yang diklaim pemanggil untuk diteruskan kepadanya; jika klaim itu salah, perilakunya tidak ditentukan.
Keith Thompson
2
Koreksi kecil: lpanjang modifier "tidak berpengaruh pada pengikut a, A, e, E, f, F, g, atau Gspecifier konversi", panjang pengubah untuk long doublekonversi L. (@ robertbristow-johnson mungkin juga tertarik)
Daniel Fischer
58

Off pertama, seperti yang disinggung di beberapa jawaban yang lain tetapi tidak, pikiran saya, terbilang cukup jelas: Ini tidak bekerja untuk memberikan sebuah integer dalam kebanyakan konteks di mana fungsi perpustakaan membutuhkan doubleatau floatargumen. Kompilator secara otomatis akan memasukkan konversi. Misalnya, sqrt(0)terdefinisi dengan baik dan akan berperilaku persis seperti sqrt((double)0), dan hal yang sama berlaku untuk ekspresi tipe bilangan bulat lainnya yang digunakan di sana.

printfberbeda. Ini berbeda karena membutuhkan sejumlah variabel argumen. Prototipe fungsinya adalah

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

Karena itu, saat Anda menulis

printf(message, 0);

kompilator tidak mempunyai informasi apapun tentang tipe apa yang printf mengharapkan argumen kedua itu. Ini hanya memiliki tipe ekspresi argumen, yaitu int, untuk dilalui. Oleh karena itu, tidak seperti kebanyakan fungsi perpustakaan, Anda, programmer, untuk memastikan daftar argumen sesuai dengan harapan format string.

(Kompiler modern dapat melihat ke dalam string format dan memberi tahu Anda bahwa Anda memiliki jenis ketidakcocokan, tetapi mereka tidak akan mulai memasukkan konversi untuk mencapai apa yang Anda maksud, karena lebih baik kode Anda harus rusak sekarang, ketika Anda akan melihatnya , dibandingkan beberapa tahun kemudian ketika dibangun kembali dengan kompilator yang kurang membantu.)

Sekarang, separuh pertanyaan lainnya adalah: Mengingat bahwa (int) 0 dan (float) 0.0, pada kebanyakan sistem modern, keduanya direpresentasikan sebagai 32 bit yang semuanya nol, mengapa tetap tidak berfungsi, secara tidak sengaja? Standar C hanya mengatakan "ini tidak diharuskan untuk bekerja, Anda sendiri", tapi izinkan saya menjelaskan dua alasan paling umum mengapa ini tidak akan berhasil; yang mungkin akan membantu Anda memahami mengapa hal itu tidak diwajibkan.

Pertama, karena alasan historis, ketika Anda melewati floatmelalui daftar argumen variabel itu akan dipromosikan untuk double, yang, pada kebanyakan sistem modern, adalah 64 bit lebar. Jadi printf("%f", 0)hanya mengirimkan 32 bit nol ke callee mengharapkan 64 dari mereka.

Kedua, alasan yang sama penting adalah bahwa argumen fungsi floating-point dapat dilewatkan di berbagai tempat dari argumen integer. Misalnya, sebagian besar CPU memiliki file register terpisah untuk bilangan bulat dan nilai floating-point, jadi mungkin ada aturan bahwa argumen 0 hingga 4 masuk dalam register r0 hingga r4 jika bilangan bulat, tetapi f0 hingga f4 jika berupa floating-point. Jadi printf("%f", 0)lihat di register f1 untuk nol itu, tapi tidak ada sama sekali.

zwol
sumber
1
Apakah ada arsitektur yang menggunakan register untuk fungsi variadic, bahkan di antara yang menggunakannya untuk fungsi normal? Saya pikir itulah alasan mengapa fungsi variadic diperlukan untuk dideklarasikan dengan benar meskipun fungsi lain [kecuali yang memiliki argumen float / short / char] dapat dideklarasikan dengan ().
Acak832
3
@ Random832 Saat ini, satu-satunya perbedaan antara variadic dan konvensi pemanggilan fungsi normal adalah bahwa mungkin ada beberapa data tambahan yang dipasok ke variadic, seperti jumlah argumen yang diberikan. Jika tidak, semuanya akan berjalan di tempat yang sama persis dengan fungsi normal. Lihat misalnya bagian 3.2 dari x86-64.org/documentation/abi.pdf , di mana satu-satunya perlakuan khusus untuk variadics adalah petunjuk yang diteruskan AL. (Ya, ini berarti penerapannya va_argjauh lebih rumit daripada sebelumnya.)
zwol
@ Random832: Saya selalu berpikir alasannya adalah bahwa pada beberapa fungsi arsitektur dengan jumlah dan tipe argumen yang diketahui dapat diimplementasikan secara lebih efisien, dengan menggunakan instruksi khusus.
celtschk
@celtschk Anda mungkin berpikir tentang "jendela register" pada SPARC dan IA64, yang seharusnya mempercepat kasus umum pemanggilan fungsi dengan sejumlah kecil argumen (sayangnya, dalam praktiknya, mereka melakukan hal yang sebaliknya). Mereka tidak memerlukan compiler untuk menangani panggilan fungsi variadic secara khusus, karena jumlah argumen di salah satu situs panggilan selalu merupakan konstanta waktu kompilasi, terlepas dari apakah callee-nya variadic.
zwol
@zwol: Tidak, saya sedang memikirkan ret ninstruksi 8086, di mana nadalah bilangan bulat hard-code, yang karenanya tidak berlaku untuk fungsi variadic. Namun saya tidak tahu apakah ada kompilator C yang benar-benar memanfaatkannya (tentu saja kompiler non-C melakukannya).
celtschk
13

Biasanya ketika Anda memanggil fungsi yang mengharapkan a double, tetapi Anda menyediakannya int, kompilator akan secara otomatis mengonversi menjadi doubleuntuk Anda. Itu tidak terjadi dengan printf, karena jenis argumen tidak ditentukan dalam prototipe fungsi - kompilator tidak tahu bahwa konversi harus diterapkan.

Mark Ransom
sumber
4
Juga, printf() secara khusus dirancang sedemikian rupa sehingga argumennya dapat berupa jenis apa pun. Anda harus mengetahui tipe mana yang diharapkan oleh setiap elemen dalam format-string, dan Anda harus menyediakannya dengan benar.
Mike Robinson
@ MikeRobinson: Ya, semua tipe C primitif. Yang merupakan bagian yang sangat, sangat kecil dari semua jenis yang mungkin.
MSalters
13

Mengapa menggunakan literal integer alih-alih literal float menyebabkan perilaku ini?

Karena printf()tidak ada parameter yang diketik selain const char* formatstringsebagai yang pertama. Ini menggunakan elipsis gaya-c (... ) untuk yang lainnya.

Ini hanya memutuskan bagaimana menafsirkan nilai yang diteruskan di sana sesuai dengan jenis pemformatan yang diberikan dalam format string.

Anda akan memiliki jenis perilaku tidak terdefinisi yang sama seperti saat mencoba

 int i = 0;
 const double* pf = (const double*)(&i);
 printf("%f\n",*pf); // dereferencing the pointer is UB
πάντα ῥεῖ
sumber
3
Beberapa implementasi tertentu printfmungkin berfungsi seperti itu (kecuali bahwa item yang diteruskan adalah nilai, bukan alamat). Standar C tidak menentukan bagaimana printf dan fungsi variadic lainnya bekerja, itu hanya menentukan perilakunya. Secara khusus, frame stack tidak disebutkan.
Keith Thompson
Sebuah quibble kecil: printfmemang memiliki satu parameter yang diketik, format string, yang bertipe const char*. BTW, pertanyaannya diberi tag C dan C ++, dan C benar-benar lebih relevan; Saya mungkin tidak akan menggunakan reinterpret_castsebagai contoh.
Keith Thompson
Hanya pengamatan yang menarik: Perilaku tidak terdefinisi yang sama, dan kemungkinan besar karena mekanisme yang identik, tetapi dengan perbedaan kecil dalam detail: Meneruskan int seperti dalam pertanyaan, UB terjadi dalam printf ketika mencoba menafsirkan int sebagai double - dalam contoh Anda , itu sudah terjadi di luar saat dereferensi pf ...
Aconcagua
@Aconcagua Menambahkan klarifikasi.
πάντα ῥεῖ
Contoh kode ini adalah UB untuk pelanggaran aliasing ketat, masalah yang sama sekali berbeda dengan pertanyaan yang diajukan. Misalnya Anda sepenuhnya mengabaikan kemungkinan bahwa float diteruskan dalam register berbeda ke integer.
MM
12

Menggunakan mis-matched printf()specifier "%f"dan ketik (int) 0mengarah ke perilaku undefined.

Jika spesifikasi konversi tidak valid, perilakunya tidak ditentukan. C11dr §7.21.6.1 9

Kandidat penyebab UB.

  1. Ini adalah UB per spesifikasi dan kompilasi rumit - 'kata nuf.

  2. doubledan intmemiliki ukuran yang berbeda.

  3. doubledan intdapat meneruskan nilainya menggunakan tumpukan yang berbeda (tumpukan umum vs. FPU .)

  4. A double 0.0 mungkin tidak ditentukan oleh semua pola bit nol. (langka)

chux - Kembalikan Monica
sumber
10

Ini adalah salah satu peluang bagus untuk belajar dari peringatan kompiler Anda.

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

atau

$ 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.

Jadi, printfmenghasilkan perilaku tidak terdefinisi karena Anda meneruskannya dengan jenis argumen yang tidak kompatibel.

wyrm
sumber
9

Saya tidak yakin apa yang membingungkan.

String format Anda mengharapkan a double; Anda memberikan sebagai gantinyaint .

Apakah kedua jenis memiliki lebar bit yang sama sama sekali tidak relevan, kecuali hal itu dapat membantu Anda menghindari pengecualian pelanggaran memori keras dari kode rusak seperti ini.

Balapan Ringan dalam Orbit
sumber
3
@Voo: Sayangnya, pengubah format string itu dinamai, tetapi saya masih tidak mengerti mengapa Anda berpikir bahwa an intakan diterima di sini.
Balapan Ringan di Orbit
1
@Voo: "(yang juga akan memenuhi syarat sebagai pola pelampung yang valid)" Mengapa sebuah intmemenuhi syarat sebagai pola pelampung yang valid? Dua komplemen dan berbagai pengkodean floating-point hampir tidak memiliki kesamaan.
Balapan Ringan di Orbit
2
Ini membingungkan karena, untuk sebagian besar fungsi perpustakaan, memberikan literal integer 0ke argumen yang diketik doubleakan melakukan Hal yang Benar. Tidak jelas bagi pemula bahwa kompilator tidak melakukan konversi yang sama untuk printfslot argumen yang dialamatkan oleh %[efg].
zwol
1
@Voo: Jika Anda tertarik dengan betapa salahnya hal ini, pertimbangkan bahwa pada x86-64 SysV ABI, argumen floating-point diteruskan dalam set register yang berbeda dengan argumen integer.
EOF
1
@LightnessRacesinOrbit Saya pikir itu selalu tepat untuk membahas mengapa sesuatu itu UB, yang biasanya melibatkan pembicaraan tentang penerapan apa yang diperbolehkan dan apa yang sebenarnya terjadi dalam kasus umum.
zwol
4

"%f\n"menjamin hasil yang dapat diprediksi hanya jika printf()parameter kedua memiliki tipe double. Selanjutnya, argumen tambahan dari fungsi variadic adalah subjek dari promosi argumen default. Argumen bilangan bulat termasuk dalam promosi bilangan bulat, yang tidak pernah menghasilkan nilai tipe floating-point. Dan floatparameter dipromosikan menjadidouble .

Untuk melengkapi: standar memungkinkan argumen kedua menjadi atau floatatau doubledan tidak ada yang lain.

Sergio
sumber
4

Mengapa secara formal UB kini telah dibahas dalam beberapa jawaban.

Alasan mengapa Anda secara khusus mendapatkan perilaku ini bergantung pada platform, tetapi mungkin adalah sebagai berikut:

  • printfmengharapkan argumennya sesuai dengan propagasi vararg standar. Itu berarti floatwasiat menjadi a doubledan apa pun yang lebih kecil dari intwasiat menjadi int.
  • Anda melewati intdimana fungsi mengharapkan a double. Anda intmungkin 32 bit, double64 bit Anda. Itu berarti bahwa empat byte tumpukan dimulai dari tempat argumen seharusnya berada 0, tetapi empat byte berikut memiliki konten yang berubah-ubah. Itulah yang digunakan untuk membangun nilai yang ditampilkan.
glglgl
sumber
0

Penyebab utama dari masalah "nilai tidak ditentukan" ini berdiri di cast penunjuk pada intnilai yang diteruskan ke bagian printfparameter variabel ke penunjuk pada doublejenis yang va_argdijalankan makro.

Hal ini menyebabkan referensi ke area memori yang tidak sepenuhnya diinisialisasi dengan nilai yang diteruskan sebagai parameter ke printf, karena doubleukuran area buffer memori lebih besar dari intukuran.

Oleh karena itu, ketika penunjuk ini didereferensi, ia mengembalikan nilai yang belum ditentukan, atau lebih baik "nilai" yang berisi sebagian nilai yang diteruskan sebagai parameter ke printf, dan untuk bagian yang tersisa dapat berasal dari area buffer tumpukan lain atau bahkan area kode ( meningkatkan pengecualian kesalahan memori), buffer overflow nyata .


Itu dapat mempertimbangkan bagian spesifik dari implementasi kode semplificated dari "printf" dan "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..
.... 


implementasi nyata di vprintf (mempertimbangkan gnu impl.) dari manajemen kasus kode parameter nilai ganda adalah:

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



referensi

  1. implementasi gnu project glibc dari "printf" (vprintf))
  2. contoh kode semplifikasi printf
  3. contoh kode semplifikasi va_arg
Ciro Corvino
sumber