ทำไมจำนวนเต็มล้นใน x86 กับ GCC ทำให้เกิดการวนซ้ำไม่สิ้นสุด?


129

รหัสต่อไปนี้จะเข้าสู่วงวนไม่สิ้นสุดบน 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;
}

ดังนั้นนี่คือข้อตกลง: การล้นจำนวนเต็มที่ลงนามคือพฤติกรรมที่ไม่ได้กำหนดทางเทคนิค แต่ GCC บน x86 ใช้เลขคณิตจำนวนเต็มโดยใช้คำแนะนำจำนวนเต็ม x86 - ซึ่งตัดกับโอเวอร์โฟลว์

ดังนั้นฉันคาดหวังว่ามันจะห่อหุ้มด้วยน้ำล้น - แม้ว่าจะเป็นพฤติกรรมที่ไม่ได้กำหนด แต่นั่นไม่ใช่กรณีที่ชัดเจน แล้วฉันจะพลาดอะไร

ฉันรวบรวมสิ่งนี้โดยใช้:

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

เอาท์พุท GCC:

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

... (infinite loop)

เมื่อปิดใช้งานการปรับให้เหมาะสมจะไม่มีลูปไม่สิ้นสุดและเอาต์พุตถูกต้อง Visual Studio ยังรวบรวมสิ่งนี้อย่างถูกต้องและให้ผลลัพธ์ต่อไปนี้:

เอาท์พุทที่ถูกต้อง:

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

นี่คือรูปแบบอื่น ๆ :

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

นี่คือข้อมูลรุ่นที่เกี่ยวข้องทั้งหมด:

~/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$ 

ดังนั้นคำถามคือ:นี่เป็นข้อบกพร่องใน GCC หรือไม่? หรือว่าฉันเข้าใจผิดเกี่ยวกับวิธีที่ GCC จัดการเลขคณิตจำนวนเต็ม?

* ฉันกำลังติดแท็ก C เช่นนี้เพราะฉันคิดว่าข้อผิดพลาดนี้จะสร้างซ้ำใน C. (ฉันยังไม่ได้ยืนยัน)

แก้ไข:

นี่คือการประกอบของวง: (ถ้าฉันจำได้อย่างถูกต้อง)

.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

10
gcc -Sนี้จะเป็นจำนวนมากตอบได้มากขึ้นถ้าคุณรวมถึงรหัสที่สร้างขึ้นจากการชุมนุม
Greg Hewgill

การชุมนุมยาวอย่างน่าประหลาดใจ ฉันควรแก้ไขมันด้วยหรือไม่
Mysticial

ได้โปรดเป็นส่วนที่เกี่ยวข้องกับวงของคุณ
Greg Hewgill

12
-1 คุณบอกว่านี่เป็นการพูดที่ไม่ได้กำหนดพฤติกรรมอย่างเข้มงวดและถามว่านี่เป็นพฤติกรรมที่ไม่ได้กำหนดหรือไม่ ดังนั้นนี่ไม่ใช่คำถามจริงสำหรับฉัน
Johannes Schaub - litb

8
@ JohannesSchaub-litb ขอบคุณสำหรับความคิดเห็น อาจใช้ถ้อยคำที่ไม่ดีในส่วนของฉัน ฉันจะพยายามทำให้ดีที่สุดเพื่อให้ชัดเจนในการรับการยกเลิกการลงคะแนนของคุณ (และฉันจะแก้ไขคำถามตามลำดับ) โดยทั่วไปฉันรู้ว่ามันเป็น UB แต่ฉันก็รู้ว่า GCC บน x86 ใช้คำแนะนำจำนวนเต็ม x86 - ซึ่งตัดกับล้น ดังนั้นฉันคาดว่ามันจะห่อแม้จะเป็น UB อย่างไรก็ตามมันไม่ได้และนั่นทำให้ฉันสับสน ดังนั้นคำถาม
ลึกลับ

คำตอบ:


178

เมื่อมาตรฐานกล่าวว่าไม่ได้กำหนดพฤติกรรมของมัน, มันหมายความว่า สิ่งที่สามารถเกิดขึ้นได้ "อะไร" รวมถึง "มักจะเต็มไปด้วยจำนวนเต็ม แต่ในบางครั้งสิ่งแปลกเกิดขึ้น"

ใช่บน x86 CPU จำนวนเต็มมักห่อหุ้มตามที่คุณคาดหวัง นี่คือหนึ่งในข้อยกเว้นเหล่านั้น คอมไพเลอร์ถือว่าคุณจะไม่ทำให้เกิดพฤติกรรมที่ไม่ได้กำหนดและปรับการทดสอบลูปให้เหมาะสม หากคุณต้องการให้ล้อมรอบจริงๆส่งผ่าน-fwrapvไปยังg++หรือgccเมื่อรวบรวม; สิ่งนี้จะให้ความหมายของโอเวอร์โฟลว์ที่มีความหมายชัดเจนมาก


24
โอ้ว้าว. -fwrapvผมก็ไม่ทราบ ขอบคุณที่ชี้นำสิ่งนี้
Mysticial

1
มีตัวเลือกคำเตือนที่พยายามสังเกตลูปไม่สิ้นสุดโดยไม่ตั้งใจหรือไม่?
Jeff Burdges

5
ฉันพบ -Wunsafe-loop-optimization ที่กล่าวถึงที่นี่: stackoverflow.com/questions/2982507/…
Jeff Burdges

1
-1 "ใช่แล้วบนซีพียู x86 จำนวนเต็มมักห่อหุ้มตามที่คุณคาดหวัง" มันผิด แต่มันบอบบาง ในขณะที่ฉันจำได้ว่ามันเป็นไปได้ที่จะทำให้พวกเขาติดกับล้น แต่นั่นไม่ใช่สิ่งที่เรากำลังพูดถึงที่นี่และฉันไม่เคยเห็นมันทำ นอกเหนือจากนั้นและไม่สนใจการดำเนินการ x86 bcd (ไม่อนุญาตให้ใช้การแทนใน C ++) จำนวนเต็ม x86 x ห่อเสมอเพราะพวกเขาทั้งสองเข้าด้วยกัน คุณเข้าใจผิดว่าการปรับแต่ง g ++ เกิดความผิดพลาด (หรือไม่เหมาะสมอย่างยิ่งและไร้สาระ) สำหรับคุณสมบัติของจำนวนเต็ม x86
ไชโยและ hth - Alf

5
@ Cheersandhth. -Alf โดย 'on x86 CPU' ฉันหมายถึง 'เมื่อคุณพัฒนาสำหรับ x86 CPUs โดยใช้คอมไพเลอร์ C' ฉันต้องสะกดมันออกมาจริงๆเหรอ? เห็นได้ชัดว่าการพูดคุยของฉันทั้งหมดเกี่ยวกับคอมไพเลอร์และ GCC นั้นไม่เกี่ยวข้องหากคุณกำลังพัฒนาในแอสเซมเบลอร์
bdonlan

18

มันง่าย: พฤติกรรมที่ไม่ได้กำหนด - โดยเฉพาะอย่างยิ่ง-O2เมื่อเปิดใช้การเพิ่มประสิทธิภาพ ( ) หมายความว่าอะไรจะเกิดขึ้น

โค้ดของคุณมีพฤติกรรมตามที่คุณคาดหวังหากไม่มี-O2สวิตช์

มันใช้งานได้ดีกับ icl และ tcc แต่คุณไม่สามารถพึ่งพาสิ่งเหล่านี้ได้ ...

ตามนี้การเพิ่มประสิทธิภาพ gcc ใช้ประโยชน์จากจำนวนเต็มล้นล้นจริงลงนาม นี่ก็หมายความว่า "บั๊ก" เกิดจากการออกแบบ


มันแย่มากที่คอมไพเลอร์จะเลือกวงที่ไม่สิ้นสุดของทุกสิ่งสำหรับพฤติกรรมที่ไม่ได้กำหนด
ผกผัน

27
@ อินเวอร์ส: ฉันไม่เห็นด้วย หากคุณเขียนโค้ดบางอย่างที่มีพฤติกรรมที่ไม่ได้กำหนดไว้ให้อธิษฐานเพื่อการวนซ้ำไม่สิ้นสุด ทำให้ง่ายต่อการตรวจสอบ ...
เดนนิส

ฉันหมายถึงว่าคอมไพเลอร์กำลังมองหา UB ทำไมไม่แทรกข้อยกเว้นแทนที่จะพยายามเพิ่มประสิทธิภาพรหัสที่ใช้งานไม่ได้?
Inverse

15
@ อินเวิร์ส: คอมไพเลอร์ไม่ได้กำลังมองหาพฤติกรรมที่ไม่ได้กำหนดมันจะถือว่ามันไม่เกิดขึ้น สิ่งนี้อนุญาตให้คอมไพเลอร์ปรับโค้ดให้เหมาะสม ตัวอย่างเช่นแทนที่จะคำนวณfor (j = i; j < i + 10; ++j) ++k;มันเพิ่งจะถูกตั้งค่าk = 10เนื่องจากสิ่งนี้จะเป็นจริงเสมอหากไม่มีการโอเวอร์โฟลว์ที่ลงนามแล้ว
Dennis

@Inverse ผู้รวบรวมไม่ได้ "เลือก" เพื่ออะไร คุณเขียนลูปในรหัสของคุณ คอมไพเลอร์ไม่ได้ประดิษฐ์มัน
การแข่งขัน Lightness ใน Orbit

13

สิ่งสำคัญที่ควรทราบที่นี่คือโปรแกรม C ++ เขียนขึ้นสำหรับเครื่องนามธรรม C ++ (ซึ่งมักจะจำลองผ่านคำแนะนำฮาร์ดแวร์) ความจริงที่ว่าคุณกำลังรวบรวมสำหรับ x86 คือทั้งหมดที่ไม่เกี่ยวข้องกับความจริงที่ว่านี้มีพฤติกรรมที่ไม่ได้กำหนด

คอมไพเลอร์มีอิสระในการใช้การมีอยู่ของพฤติกรรมที่ไม่ได้กำหนดเพื่อปรับปรุงการเพิ่มประสิทธิภาพของมัน (โดยการลบเงื่อนไขออกจากลูปดังที่แสดงในตัวอย่างนี้) ไม่มีการรับประกันหรือมีประโยชน์แม้กระทั่งการทำแผนที่ระหว่างการสร้างระดับ C ++ และการสร้างรหัสเครื่องระดับ x86 นอกเหนือจากข้อกำหนดที่รหัสเครื่องจักรจะดำเนินการเมื่อสร้างผลลัพธ์ที่ต้องการโดยเครื่องนามธรรม C ++



3

โปรดคนไม่ได้กำหนดพฤติกรรมเป็นสิ่งที่ไม่ได้กำหนด หมายความว่าอะไรจะเกิดขึ้น ในทางปฏิบัติ (เช่นในกรณีนี้) คอมไพเลอร์มีอิสระที่จะคิดได้ว่ามันจะไม่ถูกเรียกใช้และทำทุกสิ่งที่มันพอใจหากสามารถทำให้โค้ดเร็วขึ้น / เล็กลงได้ เกิดอะไรขึ้นกับรหัสที่ไม่ควรเรียกใช้คือใคร ๆ ก็เดาได้ มันจะขึ้นอยู่กับโค้ดที่อยู่รอบ ๆ (ขึ้นอยู่กับว่าคอมไพเลอร์สามารถสร้างโค้ดที่แตกต่างกันได้ดี) ตัวแปร / ค่าคงที่ที่ใช้แฟล็กคอมไพเลอร์ ... โอ้และคอมไพเลอร์อาจได้รับการอัปเดตและเขียนรหัสเดียวกันแตกต่างกัน รับคอมไพเลอร์อื่นด้วยมุมมองที่ต่างกันในการสร้างรหัส หรือเพียงแค่ได้รับเครื่องที่แตกต่างกันแม้แต่รุ่นอื่นในสายสถาปัตยกรรมเดียวกันก็อาจมีพฤติกรรมที่ไม่ได้กำหนดของมันเอง (ดู opcodes ที่ไม่ได้กำหนดโปรแกรมเมอร์บางคนที่กล้าได้กล้าเสียพบว่าในเครื่องแรก ๆ บางครั้งก็ทำสิ่งที่มีประโยชน์ ... ) . มีไม่มี"คอมไพเลอร์ให้พฤติกรรมที่แน่นอนกับพฤติกรรมที่ไม่ได้กำหนด" มีพื้นที่ที่กำหนดการใช้งานและคุณควรจะสามารถนับรวมกับพฤติกรรมของคอมไพเลอร์ได้อย่างสม่ำเสมอ


1
ใช่ฉันรู้ดีว่าพฤติกรรมที่ไม่ได้กำหนดคืออะไร แต่เมื่อคุณทราบว่ามีการใช้งานแง่มุมบางอย่างของภาษาอย่างไรสำหรับสภาพแวดล้อมที่เฉพาะเจาะจงคุณสามารถคาดหวังที่จะเห็น UB บางประเภทและไม่ใช่คนอื่น ๆ ฉันรู้ว่า GCC ใช้เลขคณิตจำนวนเต็มเป็นเลขคณิตเลขจำนวนเต็ม x86 ซึ่งครอบคลุมมากเกินไป ดังนั้นฉันจึงสันนิษฐานว่าพฤติกรรมเช่นนี้ สิ่งที่ฉันไม่ได้คาดหวังก็คือ GCC จะทำอย่างอื่นตามที่ bdonlan ตอบ
Mysticial

7
ไม่ถูกต้อง. สิ่งที่เกิดขึ้นคือ GCC ได้รับอนุญาตให้สมมติว่าคุณจะไม่เรียกใช้พฤติกรรมที่ไม่ได้กำหนดดังนั้นมันจึงปล่อยโค้ดราวกับว่ามันไม่สามารถเกิดขึ้นได้ ถ้ามันไม่เกิดขึ้นคำแนะนำในการทำสิ่งที่คุณขอกับไม่มีพฤติกรรมที่ไม่ได้กำหนดจะได้รับการดำเนินการและผลที่ได้คือสิ่งที่ CPU ไม่ คือบน x86 นั้นเป็นสิ่งที่ x86 หากเป็นโปรเซสเซอร์อื่นก็สามารถทำสิ่งที่แตกต่างอย่างสิ้นเชิง หรือคอมไพเลอร์อาจฉลาดพอที่จะเข้าใจได้ว่าคุณกำลังเรียกพฤติกรรมที่ไม่ได้กำหนดไว้และเริ่มทำสิ่งที่ไม่เหมาะสม
vonbrand

4
ฉันเชื่อว่าคุณอ่านความคิดเห็นของฉันผิด ฉันพูดว่า: "สิ่งที่ฉันไม่ได้คาดหวัง" - นี่คือเหตุผลที่ฉันถามคำถามตั้งแต่แรก ฉันไม่ได้คาดหวังว่า GCC จะใช้กลอุบายใด ๆ
ลึกลับ

1

แม้ว่าคอมไพเลอร์จะต้องระบุว่าจำนวนเต็มมากเกินไปจะต้องได้รับการพิจารณาในรูปแบบ "ที่ไม่สำคัญ" ของพฤติกรรมที่ไม่ได้กำหนด (ตามที่กำหนดในภาคผนวก L) ผลของการล้นจำนวนเต็มควรขาดสัญญาแพลตฟอร์มเฉพาะของพฤติกรรมที่เฉพาะเจาะจงมากขึ้น อย่างน้อยที่สุดถือว่าเป็น "มูลค่าไม่แน่นอนบางส่วน" ภายใต้กฎดังกล่าวการเพิ่ม 1073741824 + 1073741824 อาจถือว่าเป็นการยอมให้ตามอำเภอใจ 2147483648 หรือ -2147483648 หรือค่าอื่นใดที่สอดคล้องกับ 2147483648 mod 4294967296 และค่าที่ได้จากการเพิ่มเติมอาจถือว่าเป็นค่าใดก็ได้ที่สอดคล้องกันโดยพลการ

กฎที่อนุญาตให้มีการล้นได้ "ค่าที่ไม่แน่นอนบางส่วน" จะถูกกำหนดไว้อย่างดีพอที่จะปฏิบัติตามจดหมายและวิญญาณของภาคผนวก L แต่จะไม่ป้องกันผู้รวบรวมจากการทำการอนุมานที่มีประโยชน์โดยทั่วไปเช่นเดียวกับที่จะพิสูจน์ว่า พฤติกรรมที่ไม่ได้กำหนด มันจะป้องกันคอมไพเลอร์จากการทำให้ "การเพิ่มประสิทธิภาพ" ของปลอมบางอย่างซึ่งผลหลักในหลาย ๆ กรณีคือต้องการให้โปรแกรมเมอร์เพิ่มความยุ่งเหยิงเพิ่มเติมให้กับรหัสที่มีวัตถุประสงค์เพียงอย่างเดียวคือการป้องกัน ไม่ว่าจะเป็นสิ่งที่ดีหรือไม่ก็ขึ้นอยู่กับมุมมองของคน ๆ หนึ่ง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.