Saya melacak serangga yang sangat jahat yang bersembunyi di balik permata kecil ini. Saya sadar bahwa menurut spesifikasi C ++, luapan bertanda tangan merupakan perilaku yang tidak terdefinisi, tetapi hanya jika luapan terjadi saat nilai diperluas ke lebar-bit sizeof(int)
. Seperti yang saya pahami, meningkatkan char
perilaku seharusnya tidak pernah menjadi tidak terdefinisi selama sizeof(char) < sizeof(int)
. Tapi itu tidak menjelaskan bagaimana c
mendapatkan nilai yang tidak mungkin . Sebagai integer 8-bit, bagaimana bisa c
menyimpan nilai yang lebih besar dari lebar bitnya?
Kode
// Compiled with gcc-4.7.2
#include <cstdio>
#include <stdint.h>
#include <climits>
int main()
{
int8_t c = 0;
printf("SCHAR_MIN: %i\n", SCHAR_MIN);
printf("SCHAR_MAX: %i\n", SCHAR_MAX);
for (int32_t i = 0; i <= 300; i++)
printf("c: %i\n", c--);
printf("c: %i\n", c);
return 0;
}
Keluaran
SCHAR_MIN: -128
SCHAR_MAX: 127
c: 0
c: -1
c: -2
c: -3
...
c: -127
c: -128 // <= The next value should still be an 8-bit value.
c: -129 // <= What? That's more than 8 bits!
c: -130 // <= Uh...
c: -131
...
c: -297
c: -298 // <= Getting ridiculous now.
c: -299
c: -300
c: -45 // <= ..........
Lihat di ideone.
c++
gcc
undefined-behavior
Tidak ditandatangani
sumber
sumber
printf()
konversi?Jawaban:
Ini adalah bug kompilator.
Meskipun mendapatkan hasil yang tidak mungkin untuk perilaku yang tidak ditentukan adalah konsekuensi yang valid, sebenarnya tidak ada perilaku yang tidak ditentukan dalam kode Anda. Apa yang terjadi adalah kompilator menganggap perilakunya tidak terdefinisi, dan mengoptimalkannya.
Jika
c
didefinisikan sebagaiint8_t
, danint8_t
dipromosikan menjadiint
, makac--
seharusnya melakukan penguranganc - 1
dalamint
aritmatika dan mengubah hasilnya kembali menjadiint8_t
. Pengurangan dalamint
tidak meluap, dan mengonversi nilai integral di luar rentang ke tipe integral lain adalah valid. Jika tipe tujuan ditandatangani, hasilnya ditentukan oleh implementasi, tetapi harus berupa nilai yang valid untuk tipe tujuan. (Dan jika tipe tujuan tidak bertanda tangan, hasilnya didefinisikan dengan baik, tetapi itu tidak berlaku di sini.)sumber
c
dalam tipe yang lebih luas. Agaknya, itulah yang terjadi di sini.Kompiler dapat memiliki bug selain ketidaksesuaian dengan standar, karena ada persyaratan lain. Kompiler harus kompatibel dengan versi lain itu sendiri. Mungkin juga diharapkan kompatibel dalam beberapa hal dengan kompiler lain, dan juga untuk menyesuaikan dengan beberapa keyakinan tentang perilaku yang dianut oleh mayoritas basis penggunanya.
Dalam kasus ini, tampaknya bug kepatuhan. Ekspresi
c--
harus dimanipulasic
dengan cara yang mirip denganc = c - 1
. Di sini, nilaic
di sebelah kanan dipromosikan menjadi tipeint
, dan kemudian pengurangan terjadi. Karenac
berada dalam kisaranint8_t
, pengurangan ini tidak akan meluap, tetapi dapat menghasilkan nilai yang berada di luar kisaranint8_t
. Saat nilai ini ditetapkan, konversi terjadi kembali ke jenisint8_t
sehingga hasilnya cocok kembalic
. Dalam kasus di luar rentang, konversi memiliki nilai yang ditentukan penerapan. Namun nilai di luar rentangint8_t
bukanlah nilai yang ditentukan implementasi yang valid. Implementasi tidak dapat "mendefinisikan" bahwa tipe 8 bit tiba-tiba menampung 9 bit atau lebih. Untuk nilai yang akan ditentukan implementasi berarti bahwa sesuatu dalam kisaranint8_t
itu diproduksi, dan program berlanjut. Oleh karena itu, standar C memungkinkan perilaku seperti aritmatika saturasi (umum pada DSP) atau pembungkus (arsitektur arus utama).Kompiler menggunakan tipe mesin dasar yang lebih luas saat memanipulasi nilai tipe integer kecil seperti
int8_t
atauchar
. Ketika aritmatika dilakukan, hasil yang berada di luar jangkauan tipe integer kecil dapat ditangkap dengan andal dalam tipe yang lebih luas ini. Untuk mempertahankan perilaku yang terlihat secara eksternal bahwa variabel tersebut berjenis 8 bit, hasil yang lebih luas harus dipotong menjadi kisaran 8 bit. Kode eksplisit diperlukan untuk melakukan itu karena lokasi penyimpanan mesin (register) lebih lebar dari 8 bit dan senang dengan nilai yang lebih besar. Di sini, kompilator mengabaikan untuk menormalkan nilai dan hanya meneruskannya apaprintf
adanya. Penentu konversi%i
dalamprintf
tidak mengetahui bahwa argumen aslinya berasal dariint8_t
perhitungan; itu hanya bekerja dengan fileint
argumen.sumber
Saya tidak bisa memasukkan ini dalam komentar, jadi saya mempostingnya sebagai jawaban.
Untuk beberapa alasan yang sangat aneh,
--
kebetulan operatornya adalah pelakunya.Saya menguji kode yang diposting di Ideone dan menggantinya
c--
denganc = c - 1
dan nilainya tetap dalam kisaran [-128 ... 127]:Mata aneh? Saya tidak tahu banyak tentang apa yang dilakukan kompilator terhadap ekspresi seperti
i++
ataui--
. Ini mungkin mempromosikan nilai kembali keint
dan meneruskannya. Itulah satu-satunya kesimpulan logis yang dapat saya berikan karena Anda sebenarnya SUDAH mendapatkan nilai yang tidak dapat dimasukkan ke dalam 8-bit.sumber
c = c - 1
berartic = (int8_t) ((int)c - 1
. Mengonversi out-of-rangeint
menjadiint8_t
memiliki perilaku yang ditentukan tetapi hasil yang ditentukan implementasi. Sebenarnya, bukankahc--
seharusnya melakukan konversi yang sama juga?Saya kira perangkat keras yang mendasarinya masih menggunakan register 32-bit untuk menampung int8_t itu. Karena spesifikasi tidak memaksakan perilaku overflow, implementasi tidak memeriksa overflow dan memungkinkan nilai yang lebih besar untuk disimpan juga.
Jika Anda menandai variabel lokal saat
volatile
Anda memaksa untuk menggunakan memori untuk itu dan akibatnya mendapatkan nilai yang diharapkan dalam rentang tersebut.sumber
printf
tidak peduli tentangsizeof
nilai format.Kode assembler mengungkapkan masalahnya:
EBX harus diganti dengan penurunan setelah FF, atau hanya BL yang harus digunakan dengan sisa EBX yang jelas. Penasaran bahwa itu menggunakan sub, bukan Desember. -45 benar-benar misterius. Ini adalah inversi bitwise 300 & 255 = 44. -45 = ~ 44. Ada hubungan di suatu tempat.
Ini melalui lebih banyak pekerjaan menggunakan c = c - 1:
Ia kemudian menggunakan hanya bagian rendah dari RAX, jadi dibatasi ke -128 hingga 127. Opsi kompiler "-g -O2".
Tanpa pengoptimalan, ini menghasilkan kode yang benar:
Jadi itu bug di pengoptimal.
sumber
Menggunakan
%hhd
sebagai ganti%i
! Harus menyelesaikan masalah Anda.Apa yang Anda lihat di sana adalah hasil dari pengoptimalan kompilator yang dikombinasikan dengan Anda memberi tahu printf untuk mencetak angka 32bit dan kemudian mendorong nomor (seharusnya 8bit) ke tumpukan, yang sebenarnya berukuran penunjuk, karena begitulah cara kerja opcode push di x86.
sumber
g++ -O3
. Mengubah%i
ke%hhd
tidak mengubah apa pun.Saya pikir ini dilakukan dengan pengoptimalan kode:
Kompilator menggunakan
int32_t i
variabel fori
danc
. Nonaktifkan pengoptimalan atau lakukan cast langsungprintf("c: %i\n", (int8_t)c--);
sumber
(int8_t)(c & 0x0000ffff)--
c
didefinisikan sebagaiint8_t
, tetapi ketika beroperasi++
atau--
diint8_t
atasnya secara implisit dikonversi terlebih dahulu keint
dan hasil operasi sebagai gantinya nilai internal c dicetak dengan printf yang kebetulanint
.Melihat nilai sebenarnya dari
c
setelah seluruh loop, terutama setelah penurunan laluitu nilai yang benar yang menyerupai perilaku
-128 + 1 = 127
c
mulai menggunakanint
memori ukuran tetapi dicetak sepertiint8_t
saat dicetak hanya dengan menggunakan8 bits
. Memanfaatkan semua32 bits
saat digunakan sebagaiint
[Bug Penyusun]
sumber
Saya pikir itu terjadi karena loop Anda akan pergi sampai int i akan menjadi 300 dan c menjadi -300. Dan nilai terakhir adalah karena
sumber