Bagaimana saya mendapatkan nilai yang lebih besar dari 8 bit dari integer 8-bit?

118

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 charperilaku seharusnya tidak pernah menjadi tidak terdefinisi selama sizeof(char) < sizeof(int). Tapi itu tidak menjelaskan bagaimana cmendapatkan nilai yang tidak mungkin . Sebagai integer 8-bit, bagaimana bisa cmenyimpan 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.

Tidak ditandatangani
sumber
61
"Saya sadar bahwa sesuai dengan spesifikasi C ++, overflow yang ditandatangani tidak ditentukan." -- Baik. Tepatnya, bukan hanya nilainya yang tidak ditentukan, perilakunya juga. Muncul untuk mendapatkan hasil yang tidak mungkin secara fisik adalah konsekuensi yang valid.
@hvd Saya yakin seseorang memiliki penjelasan tentang seberapa umum implementasi C ++ menyebabkan perilaku ini. Mungkin ada hubungannya dengan keselarasan atau bagaimana printf()konversi?
rliu
Yang lain telah membahas masalah utama. Komentar saya lebih umum dan berhubungan dengan pendekatan diagnostik. Saya percaya bagian dari mengapa Anda menemukan teka-teki semacam itu adalah keyakinan yang tidak mungkin salah. Jelas, ini bukan tidak mungkin, jadi terima itu dan lihat lagi
Tim X
@TimX - Saya mengamati perilakunya dan jelas menarik kesimpulan bahwa itu bukan tidak mungkin dalam pengertian itu. Saya menggunakan kata tersebut untuk merujuk pada integer 8-bit yang memiliki nilai 9-bit, yang menurut definisi tidak mungkin dilakukan. Fakta bahwa ini terjadi menunjukkan bahwa itu tidak diperlakukan sebagai nilai 8-bit. Seperti yang telah diatasi oleh orang lain, hal ini disebabkan oleh bug compiler. Satu-satunya kemustahilan yang tampak di sini adalah nilai 9-bit dalam ruang 8-bit, dan kemustahilan yang tampak ini dijelaskan oleh ruang yang sebenarnya "lebih besar" dari yang dilaporkan.
Unsigned
Saya baru saja mengujinya di mesin saya, dan hasilnya sesuai dengan yang seharusnya. c: -120 c: -121 c: -122 c: -123 c: -124 c: -125 c: -126 c: -127 c: -128 c: 127 c: 126 c: 125 c: 124 c: 123 c: 122 c: 121 c: 120 c: 119 c: 118 c: 117 Dan lingkungan saya adalah: Ubuntu-12.10 gcc-4.7.2
VELVETDETH

Jawaban:

111

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 cdidefinisikan sebagai int8_t, dan int8_tdipromosikan menjadi int, maka c--seharusnya melakukan pengurangan c - 1dalam intaritmatika dan mengubah hasilnya kembali menjadi int8_t. Pengurangan dalam inttidak 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
Saya tidak akan menggambarkannya sebagai "bug". Karena luapan bertanda tangan menyebabkan perilaku tak terdefinisi, kompilator berhak menganggapnya tidak akan terjadi, dan mengoptimalkan perulangan untuk mempertahankan nilai perantara cdalam tipe yang lebih luas. Agaknya, itulah yang terjadi di sini.
Mike Seymour
4
@MikeSeymour: Satu-satunya kelebihan di sini adalah pada konversi (implisit). Luapan pada konversi yang ditandatangani tidak memiliki perilaku yang tidak ditentukan; itu hanya menghasilkan hasil yang ditentukan implementasi (atau memunculkan sinyal yang ditentukan implementasi, tetapi tampaknya itu tidak terjadi di sini). Perbedaan dalam definisi antara operasi aritmatika dan konversi ganjil, tapi begitulah standar bahasa mendefinisikannya.
Keith Thompson
2
@KeithThompson Itu adalah sesuatu yang berbeda antara C dan C ++: C memungkinkan sinyal yang ditentukan implementasi, sedangkan C ++ tidak. C ++ hanya mengatakan "Jika jenis tujuan ditandatangani, nilainya tidak berubah jika dapat direpresentasikan dalam jenis tujuan (dan lebar bidang bit); jika tidak, nilainya ditentukan oleh implementasi".
Saat ini terjadi, saya tidak dapat mereproduksi perilaku aneh di g ++ 4.8.0.
Daniel Landau
2
@DanielLandau Lihat komentar 38 dalam bug itu: "Diperbaiki untuk 4.8.0." :)
15

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 dimanipulasi cdengan cara yang mirip dengan c = c - 1. Di sini, nilai cdi sebelah kanan dipromosikan menjadi tipe int, dan kemudian pengurangan terjadi. Karena cberada dalam kisaran int8_t, pengurangan ini tidak akan meluap, tetapi dapat menghasilkan nilai yang berada di luar kisaran int8_t. Saat nilai ini ditetapkan, konversi terjadi kembali ke jenis int8_tsehingga hasilnya cocok kembali c. Dalam kasus di luar rentang, konversi memiliki nilai yang ditentukan penerapan. Namun nilai di luar rentang int8_tbukanlah 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 kisaran int8_titu 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_tatau char. 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 apa printfadanya. Penentu konversi %idalam printftidak mengetahui bahwa argumen aslinya berasal dari int8_tperhitungan; itu hanya bekerja dengan fileint argumen.

Kaz
sumber
Ini adalah penjelasan yang jelas.
David Healy
Kompilator menghasilkan kode yang baik dengan pengoptimal dimatikan. Oleh karena itu, penjelasan yang menggunakan "aturan" dan "definisi" tidak dapat diterapkan. Ini bug di pengoptimal.
14

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--dengan c = c - 1dan nilainya tetap dalam kisaran [-128 ... 127]:

c: -123
c: -124
c: -125
c: -126
c: -127
c: -128 // about to overflow
c: 127  // woop
c: 126
c: 125
c: 124
c: 123
c: 122

Mata aneh? Saya tidak tahu banyak tentang apa yang dilakukan kompilator terhadap ekspresi seperti i++atau i--. Ini mungkin mempromosikan nilai kembali ke intdan meneruskannya. Itulah satu-satunya kesimpulan logis yang dapat saya berikan karena Anda sebenarnya SUDAH mendapatkan nilai yang tidak dapat dimasukkan ke dalam 8-bit.

pengguna123
sumber
4
Karena promosi yang tidak terpisahkan, c = c - 1berarti c = (int8_t) ((int)c - 1. Mengonversi out-of-range intmenjadi int8_tmemiliki perilaku yang ditentukan tetapi hasil yang ditentukan implementasi. Sebenarnya, bukankah c--seharusnya melakukan konversi yang sama juga?
12

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 volatileAnda memaksa untuk menggunakan memori untuk itu dan akibatnya mendapatkan nilai yang diharapkan dalam rentang tersebut.

Zoltán
sumber
1
Oh wow. Saya lupa bahwa assembly yang dikompilasi akan menyimpan variabel lokal di register jika bisa. Ini sepertinya jawaban yang paling mungkin dengan printftidak peduli tentang sizeofnilai format.
rliu
3
@roliu Jalankan g ++ -O2 -S code.cpp, dan Anda akan melihat rakitannya. Selain itu, printf () adalah fungsi argumen variabel, jadi argumen yang peringkatnya kurang dari int akan dipromosikan menjadi int.
no
@nos saya ingin. Saya belum dapat menginstal boot loader UEFI (khususnya refind) untuk menjalankan archlinux di mesin saya, jadi saya belum benar-benar membuat kode dengan alat GNU dalam waktu yang lama. Aku akan melakukannya ... akhirnya. Untuk saat ini hanya C # dalam VS dan mencoba mengingat C / mempelajari beberapa C ++ :)
rliu
@rollu Jalankan di mesin virtual, misalnya VirtualBox
nos
@nos Tidak ingin menggagalkan topik, tapi ya, saya bisa. Saya juga bisa menginstal linux dengan bootloader BIOS. Saya hanya keras kepala dan jika saya tidak bisa membuatnya bekerja dengan bootloader UEFI maka saya mungkin tidak akan membuatnya bekerja sama sekali: P.
rliu
11

Kode assembler mengungkapkan masalahnya:

:loop
mov esi, ebx
xor eax, eax
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
sub ebx, 1
call    printf
cmp ebx, -301
jne loop

mov esi, -45
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
xor eax, eax
call    printf

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:

mov eax, ebx
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
add ebx, 1
not eax
movsx   ebp, al                 ;uses only the lower 8 bits
xor eax, eax
mov esi, ebp

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:

movzx   eax, BYTE PTR [rbp-1]
sub eax, 1
mov BYTE PTR [rbp-1], al
movsx   edx, BYTE PTR [rbp-1]
mov eax, OFFSET FLAT:.LC2   ;"c: %i\n"
mov esi, edx

Jadi itu bug di pengoptimal.


sumber
4

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.

Zotta
sumber
1
Saya dapat mereproduksi perilaku asli di sistem saya menggunakan g++ -O3. Mengubah %ike %hhdtidak mengubah apa pun.
Keith Thompson
3

Saya pikir ini dilakukan dengan pengoptimalan kode:

for (int32_t i = 0; i <= 300; i++)
      printf("c: %i\n", c--);

Kompilator menggunakan int32_t ivariabel for idan c. Nonaktifkan pengoptimalan atau lakukan cast langsung printf("c: %i\n", (int8_t)c--);

Vsevolod.dll
sumber
Kemudian matikan pengoptimalan. atau lakukan sesuatu seperti ini:(int8_t)(c & 0x0000ffff)--
Vsevolod
1

cdidefinisikan sebagai int8_t, tetapi ketika beroperasi ++atau --di int8_tatasnya secara implisit dikonversi terlebih dahulu ke intdan hasil operasi sebagai gantinya nilai internal c dicetak dengan printf yang kebetulan int.

Melihat nilai sebenarnya dari csetelah seluruh loop, terutama setelah penurunan lalu

-301 + 256 = -45 (since it revolved entire 8 bit range once)

itu nilai yang benar yang menyerupai perilaku -128 + 1 = 127

cmulai menggunakan intmemori ukuran tetapi dicetak seperti int8_tsaat dicetak hanya dengan menggunakan 8 bits. Memanfaatkan semua 32 bitssaat digunakan sebagaiint

[Bug Penyusun]

Izhar Aazmi
sumber
0

Saya pikir itu terjadi karena loop Anda akan pergi sampai int i akan menjadi 300 dan c menjadi -300. Dan nilai terakhir adalah karena

printf("c: %i\n", c);
r.mirzojonov
sumber
'c' adalah nilai 8 bit, oleh karena itu tidak mungkin untuk menyimpan angka sebesar -300.