Mengapa integer overflow pada x86 dengan GCC menyebabkan loop tak terbatas?

129

Kode berikut masuk ke loop tak terbatas pada GCC:

#include <iostream>
using namespace std;

int main(){
    int i = 0x10000000;

    int c = 0;
    do{
        c++;
        i += i;
        cout << i << endl;
    }while (i > 0);

    cout << c << endl;
    return 0;
}

Jadi, inilah masalahnya: Signed integer overflow adalah perilaku yang secara teknis tidak terdefinisi. Tetapi GCC pada x86 mengimplementasikan bilangan bulat aritmatika menggunakan instruksi integer x86 - yang membungkus overflow.

Oleh karena itu, saya akan berharap untuk membungkus overflow - meskipun fakta bahwa itu adalah perilaku yang tidak terdefinisi. Tapi jelas bukan itu masalahnya. Jadi apa yang saya lewatkan?

Saya menyusun ini menggunakan:

~/Desktop$ g++ main.cpp -O2

Output GCC:

~/Desktop$ ./a.out
536870912
1073741824
-2147483648
0
0
0

... (infinite loop)

Dengan optimisasi dinonaktifkan, tidak ada loop tak terbatas dan output benar. Visual Studio juga mengkompilasi dengan benar dan memberikan hasil sebagai berikut:

Output yang Benar:

~/Desktop$ g++ main.cpp
~/Desktop$ ./a.out
536870912
1073741824
-2147483648
3

Berikut beberapa variasi lain:

i *= 2;   //  Also fails and goes into infinite loop.
i <<= 1;  //  This seems okay. It does not enter infinite loop.

Inilah semua informasi versi yang relevan:

~/Desktop$ g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/x86_64-linux-gnu/gcc/x86_64-linux-gnu/4.5.2/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ..

...

Thread model: posix
gcc version 4.5.2 (Ubuntu/Linaro 4.5.2-8ubuntu4) 
~/Desktop$ 

Jadi pertanyaannya adalah: Apakah ini bug di GCC? Atau apakah saya salah paham tentang bagaimana GCC menangani bilangan bulat aritmatika?

* Saya juga menandai C ini, karena saya menganggap bug ini akan mereproduksi dalam C. (Saya belum memverifikasi itu.)

EDIT:

Inilah perakitan loop: (jika saya mengenalinya dengan benar)

.L5:
addl    %ebp, %ebp
movl    $_ZSt4cout, %edi
movl    %ebp, %esi
.cfi_offset 3, -40
call    _ZNSolsEi
movq    %rax, %rbx
movq    (%rax), %rax
movq    -24(%rax), %rax
movq    240(%rbx,%rax), %r13
testq   %r13, %r13
je  .L10
cmpb    $0, 56(%r13)
je  .L3
movzbl  67(%r13), %eax
.L4:
movsbl  %al, %esi
movq    %rbx, %rdi
addl    $1, %r12d
call    _ZNSo3putEc
movq    %rax, %rdi
call    _ZNSo5flushEv
cmpl    $3, %r12d
jne .L5
Mistikal
sumber
10
Ini akan jauh lebih dapat dijawab jika Anda menyertakan kode perakitan yang dihasilkan gcc -S.
Greg Hewgill
Perakitan itu panjang sekali. Haruskah saya mengeditnya?
Mysticial
Tolong, hanya bagian-bagian yang relevan dengan loop Anda.
Greg Hewgill
12
-1. Anda mengatakan bahwa ini adalah perilaku tegas yang tidak jelas dan tanyakan apakah ini perilaku yang tidak jelas. jadi ini bukan pertanyaan nyata bagi saya.
Johannes Schaub - litb
8
@ JohannesSchaub-litb Terima kasih telah memberikan komentar. Mungkin kata-kata yang buruk di pihak saya. Saya akan mencoba yang terbaik untuk mengklarifikasi cara untuk mendapatkan undownvote Anda (dan saya akan mengedit pertanyaan yang sesuai). Pada dasarnya, saya tahu itu UB. Tapi saya juga tahu bahwa GCC pada x86 menggunakan instruksi integer x86 - yang membungkus overflow. Oleh karena itu, saya berharap untuk membungkusnya meskipun itu adalah UB. Namun, itu tidak dan itu membingungkan saya. Karena itu pertanyaannya.
Mysticial

Jawaban:

178

Ketika standar mengatakan itu perilaku yang tidak terdefinisi, itu artinya . Segalanya bisa terjadi. "Apa pun" termasuk "biasanya bilangan bulat membungkus, tetapi terkadang hal aneh terjadi".

Ya, pada CPU x86, integer biasanya membungkus seperti yang Anda harapkan. Ini adalah salah satu pengecualian itu. Compiler menganggap Anda tidak akan menyebabkan perilaku tidak terdefinisi, dan mengoptimalkan tes loop. Jika Anda benar-benar ingin sampul, sampaikan -fwrapvke g++atau gccsaat kompilasi; ini memberi Anda semantik overflow yang didefinisikan dengan baik (dua-komplemen), tetapi dapat merusak kinerja.

omong kosong
sumber
24
Oh wow. Saya tidak menyadarinya -fwrapv. Terima kasih telah menunjukkan ini.
Mysticial
1
Apakah ada opsi peringatan yang mencoba memperhatikan loop tak terbatas yang tidak disengaja?
Jeff Burdges 6/11
5
Saya menemukan -Wunsafe-loop-optimization yang disebutkan di sini: stackoverflow.com/questions/2982507/…
Jeff Burdges
1
-1 "Ya, pada CPU x86, integer biasanya membungkus seperti yang Anda harapkan." itu salah. tapi halus. seingat saya adalah mungkin untuk membuat mereka terjebak pada overflow, tapi bukan itu yang sedang kita bicarakan di sini , dan saya belum pernah melihatnya selesai. selain itu, dan mengabaikan operasi b86 x86 (tidak diizinkan representasi dalam C ++) operasi integer x86 selalu membungkus, karena mereka adalah pelengkap dua. Anda salah mengoptimasi g ++ yang salah (atau sangat tidak praktis dan tidak masuk akal) untuk properti operasi integer x86.
Ceria dan hth. - Alf
5
@ Cheersandhth.-Alf, dengan 'pada CPU x86' Maksudku 'ketika Anda sedang mengembangkan untuk CPU x86 menggunakan kompiler C'. Apakah saya benar-benar perlu mengejanya? Jelas semua pembicaraan saya tentang kompiler dan GCC tidak relevan jika Anda mengembangkan assembler, dalam hal ini semantik untuk integer overflow memang sangat jelas.
bdonlan
18

Sederhana: Perilaku tidak terdefinisi - terutama dengan pengoptimalan ( -O2) dihidupkan - artinya segala sesuatu dapat terjadi.

Kode Anda berperilaku seperti yang Anda harapkan tanpa -O2saklar.

Ini berfungsi cukup baik dengan icl dan tcc, tetapi Anda tidak dapat mengandalkan hal-hal seperti itu ...

Menurut ini , optimasi gcc sebenarnya mengeksploitasi integer overflow yang ditandatangani. Ini berarti bahwa "bug" adalah dengan desain.

Dennis
sumber
Agak menyebalkan bahwa kompiler akan memilih untuk loop tak terbatas dari semua hal untuk perilaku yang tidak ditentukan.
Balikkan
27
@Inverse: Saya tidak setuju. Jika Anda telah membuat kode sesuatu dengan perilaku tidak terdefinisi, berdoalah untuk loop yang tak terbatas. Membuatnya lebih mudah untuk dideteksi ...
Dennis
Maksud saya jika kompiler secara aktif mencari UB, mengapa tidak memasukkan pengecualian alih-alih mencoba hyper-mengoptimalkan kode yang rusak?
Balikkan
15
@Inverse: Compiler tidak aktif mencari perilaku yang tidak terdefinisi , ia menganggap itu tidak terjadi. Ini memungkinkan kompiler untuk mengoptimalkan kode. Misalnya, alih-alih menghitung for (j = i; j < i + 10; ++j) ++k;, itu hanya akan ditetapkan k = 10, karena ini akan selalu benar jika tidak ada luapan yang masuk.
Dennis
@Inverse Kompiler tidak "memilih" untuk apa pun. Anda menulis loop dalam kode Anda. Kompiler tidak menemukannya.
Lightness Races in Orbit
13

Yang penting untuk dicatat di sini adalah bahwa program C ++ ditulis untuk mesin abstrak C ++ (yang biasanya ditiru melalui instruksi perangkat keras). Fakta bahwa Anda mengkompilasi untuk x86 sama sekali tidak relevan dengan fakta bahwa ini memiliki perilaku yang tidak terdefinisi.

Kompiler bebas menggunakan keberadaan perilaku tidak terdefinisi untuk meningkatkan optimisasinya, (dengan menghapus conditional dari loop, seperti dalam contoh ini). Tidak ada pemetaan yang dijamin, atau bahkan bermanfaat, antara konstruksi level C ++ dan konstruksi kode level x86 selain dari persyaratan bahwa kode mesin akan, ketika dijalankan, menghasilkan hasil yang diminta oleh mesin abstrak C ++.

Mankarse
sumber
5
i += i;

// overflow tidak ditentukan.

Dengan -fwrapv sudah benar. -fwrapv

lostyzd
sumber
3

Tolong orang, perilaku tidak terdefinisi persis seperti itu, tidak terdefinisi . Itu berarti apa pun bisa terjadi. Dalam prakteknya (seperti dalam kasus ini), kompiler bebas untuk menganggapnya tidakdipanggil, dan lakukan apa pun yang diinginkan jika itu bisa membuat kode lebih cepat / lebih kecil. Apa yang terjadi dengan kode yang tidak boleh dijalankan adalah dugaan siapa pun. Itu akan tergantung pada kode di sekitarnya (tergantung pada itu, kompiler juga bisa menghasilkan kode yang berbeda), variabel / konstanta yang digunakan, bendera kompiler, ... Oh, dan kompiler dapat diperbarui dan menulis kode yang sama secara berbeda, atau Anda bisa dapatkan kompiler lain dengan tampilan berbeda pada pembuatan kode. Atau hanya mendapatkan mesin yang berbeda, bahkan model lain dalam garis arsitektur yang sama bisa sangat baik memiliki perilaku yang tidak terdefinisi itu sendiri (mencari opcode yang tidak terdefinisi, beberapa programmer yang giat menemukan bahwa pada beberapa mesin awal itu kadang-kadang melakukan hal-hal yang bermanfaat ...) . Tidak ada+ msgstr "kompiler memberikan perilaku pasti pada perilaku tidak terdefinisi". Ada area yang didefinisikan implementasi, dan di sana Anda harus dapat mengandalkan kompilator yang berperilaku konsisten.

vonbrand
sumber
1
Ya, saya tahu betul apa perilaku tidak terdefinisi itu. Tetapi ketika Anda tahu bagaimana aspek-aspek tertentu dari bahasa diimplementasikan untuk lingkungan tertentu, Anda dapat mengharapkan untuk melihat jenis UB tertentu dan bukan yang lain. Saya tahu bahwa GCC mengimplementasikan bilangan bulat aritmatika sebagai x86 bilangan bulat aritmatika - yang membungkus overflow. Jadi saya menganggap perilaku itu seperti itu. Apa yang tidak saya harapkan adalah GCC untuk melakukan sesuatu yang lain seperti yang dijawab bdonlan.
Mysticial
7
Salah. Apa yang terjadi adalah bahwa GCC diperbolehkan untuk menganggap Anda tidak akan meminta perilaku tidak terdefinisi, sehingga hanya memancarkan kode seolah-olah itu tidak bisa terjadi. Jika hal itu terjadi, instruksi untuk melakukan apa yang Anda minta tanpa perilaku yang tidak ditentukan dijalankan, dan hasilnya adalah apa pun yang dilakukan CPU. Yaitu, pada x86 adalah melakukan hal-hal x86. Jika itu adalah prosesor lain, itu bisa melakukan sesuatu yang sama sekali berbeda. Atau kompiler bisa menjadi cukup pintar untuk mengetahui bahwa Anda memanggil perilaku yang tidak terdefinisi dan mulai nethack (ya, beberapa versi kuno gcc melakukan hal itu).
vonbrand
4
Saya yakin Anda salah membaca komentar saya. Saya berkata: "Apa yang tidak saya harapkan" - itulah sebabnya saya mengajukan pertanyaan itu sejak awal. Saya tidak berharap GCC melakukan trik.
Mysticial
1

Sekalipun kompiler harus menetapkan bahwa bilangan bulat bilangan bulat harus dianggap sebagai bentuk "tidak kritis" dari Perilaku Tidak Terdefinisi (sebagaimana didefinisikan dalam Lampiran L), hasil dari bilangan bulat bilangan bulat harus, tidak ada janji platform spesifik dari perilaku yang lebih spesifik, menjadi setidaknya dianggap sebagai "nilai sebagian tidak tentu". Berdasarkan aturan tersebut, menambahkan 1073741824 + 1073741824 dapat secara sewenang-wenang dianggap menghasilkan 2147483648 atau -2147483648 atau nilai lain yang kongruen dengan 2147483648 mod 4294967296, dan nilai yang diperoleh dengan penambahan dapat secara sewenang-wenang dianggap sebagai nilai apa pun yang sesuai dengan nilai 0 mod 42949676.

Aturan yang memungkinkan overflow untuk menghasilkan "nilai-nilai yang sebagian tak tentu" akan didefinisikan dengan cukup baik untuk mematuhi surat dan semangat Lampiran L, tetapi tidak akan mencegah kompiler membuat kesimpulan umum yang sama-sama berguna seperti yang akan dibenarkan jika luapan tidak dibatasi Perilaku tidak terdefinisi. Ini akan mencegah kompiler membuat beberapa "optimisasi" palsu yang efek utamanya dalam banyak kasus adalah mengharuskan pemrogram menambahkan kekacauan tambahan pada kode yang tujuan utamanya adalah untuk mencegah "optimasi" tersebut; apakah itu akan menjadi hal yang baik atau tidak tergantung pada sudut pandang seseorang.

supercat
sumber