Tại sao số nguyên tràn trên x86 với GCC gây ra một vòng lặp vô hạn?


129

Đoạn mã sau đi vào một vòng lặp vô hạn trên 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;
}

Vì vậy, đây là thỏa thuận: tràn số nguyên đã ký là hành vi không xác định về mặt kỹ thuật. Nhưng GCC trên x86 thực hiện số học số nguyên bằng cách sử dụng các hướng dẫn số nguyên x86 - bao trùm trên tràn.

Do đó, tôi đã mong đợi nó sẽ tràn vào - mặc dù thực tế đó là hành vi không xác định. Nhưng đó rõ ràng không phải là trường hợp. Vậy ... Tôi đã bỏ lỡ gì?

Tôi đã biên dịch cái này bằng cách sử dụng:

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

Đầu ra GCC:

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

... (infinite loop)

Với tối ưu hóa bị vô hiệu hóa, không có vòng lặp vô hạn và đầu ra là chính xác. Visual Studio cũng biên dịch chính xác điều này và cho kết quả như sau:

Đầu ra đúng:

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

Dưới đây là một số biến thể khác:

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

Đây là tất cả các thông tin phiên bản có liên quan:

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

Vì vậy, câu hỏi là: Đây có phải là một lỗi trong GCC? Hay tôi đã hiểu nhầm điều gì đó về cách GCC xử lý số học số nguyên?

* Tôi cũng gắn thẻ C này, vì tôi cho rằng lỗi này sẽ sinh sản ở C. (Tôi chưa xác minh nó.)

BIÊN TẬP:

Đây là tập hợp của vòng lặp: (nếu tôi nhận ra nó đúng)

.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
Điều này sẽ dễ trả lời hơn nhiều nếu bạn bao gồm mã lắp ráp được tạo từ đó gcc -S.
Greg Hewgill

Việc lắp ráp dài đáng ngạc nhiên. Tôi vẫn nên chỉnh sửa nó trong?
Bí ẩn

Chỉ cần các phần có liên quan đến vòng lặp của bạn, xin vui lòng.
Greg Hewgill

12
-1. bạn nói rằng đây là nói đúng hành vi không xác định và hỏi liệu đây có phải là hành vi không xác định. Vì vậy, đây không phải là một câu hỏi thực sự cho tôi.
Julian Schaub - litb

8
@ JohannesSchaub-litb Cảm ơn bạn đã bình luận. Có lẽ từ ngữ xấu về phía tôi. Tôi sẽ cố gắng hết sức để làm rõ cách kiếm tiền của bạn (và tôi sẽ chỉnh sửa câu hỏi cho phù hợp). Về cơ bản, tôi biết đó là UB. Nhưng tôi cũng biết rằng GCC trên x86 sử dụng các hướng dẫn số nguyên x86 - bao trùm trên tràn. Vì vậy, tôi mong đợi nó sẽ được bọc mặc dù nó là UB. Tuy nhiên, điều đó đã không làm tôi bối rối. Do đó câu hỏi.
Bí ẩn

Câu trả lời:


178

Khi tiêu chuẩn nói rằng đó là hành vi không xác định, điều đó có nghĩa là nó . Chuyện gì cũng có thể xảy ra. "Bất cứ điều gì" bao gồm "thường là số nguyên bao quanh, nhưng đôi khi những điều kỳ lạ xảy ra".

Có, trên CPU x86, số nguyên thường bao bọc theo cách bạn mong đợi. Đây là một trong những trường hợp ngoại lệ. Trình biên dịch giả định rằng bạn sẽ không gây ra hành vi không xác định và tối ưu hóa kiểm tra vòng lặp. Nếu bạn thực sự muốn quay vòng, chuyển -fwrapvđến g++hoặc gcckhi biên dịch; điều này cung cấp cho bạn ngữ nghĩa tràn (được bổ sung twos) được xác định rõ, nhưng có thể ảnh hưởng đến hiệu suất.


24
Tuyệt vời. Tôi đã không nhận thức được -fwrapv. Cảm ơn đã chỉ ra điều này.
Bí ẩn

1
Có một tùy chọn cảnh báo nào cố gắng để ý các vòng lặp vô hạn vô tình?
Jeff Burdges

5
Tôi đã tìm thấy -Wunafe-loop-tối ưu hóa được đề cập ở đây: stackoverflow.com/questions/2982507/iêu
Jeff Burdges

1
-1 "Có, trên CPU x86, số nguyên thường bao bọc theo cách bạn mong đợi." Sai rồi. nhưng nó tinh tế. khi tôi nhớ lại có thể khiến họ mắc bẫy tràn, nhưng đó không phải là những gì chúng ta đang nói ở đây , và tôi chưa bao giờ thấy nó được thực hiện. ngoài ra, và bỏ qua các hoạt động b86 xcd (không được phép biểu diễn trong C ++) các số nguyên x86 luôn luôn bao bọc, bởi vì chúng là hai phần bù. bạn đang nhầm lẫn tối ưu hóa g ++ bị lỗi (hoặc cực kỳ không thực tế và vô nghĩa) cho một thuộc tính của các số nguyên x86.
Chúc mừng và hth. - Alf

5
@ Cheersandhth.-Alf, bởi 'trên CPU x86' Ý tôi là 'khi bạn đang phát triển cho CPU x86 bằng trình biên dịch C'. Tôi có thực sự cần phải đánh vần nó ra? Rõ ràng tất cả các cuộc nói chuyện của tôi về trình biên dịch và GCC là không liên quan nếu bạn đang phát triển trình biên dịch chương trình, trong trường hợp đó, ngữ nghĩa cho tràn số nguyên thực sự được xác định rõ.
bdonlan

18

Thật đơn giản: Hành vi không xác định - đặc biệt là bật tối ưu hóa ( -O2) - có nghĩa là mọi thứ đều có thể xảy ra.

Mã của bạn hoạt động như (bạn) mong đợi mà không cần -O2chuyển đổi.

Nhân tiện, nó hoạt động khá tốt với icl và tcc, nhưng bạn không thể dựa vào những thứ như thế ...

Theo đó , tối ưu hóa gcc thực sự khai thác tràn số nguyên đã ký. Điều này có nghĩa là "lỗi" là do thiết kế.


Thật tệ khi một trình biên dịch sẽ chọn một vòng lặp vô hạn của tất cả mọi thứ cho hành vi không xác định.
Nghịch đảo

27
@ Nội dung: Tôi không đồng ý. Nếu bạn đã mã hóa một cái gì đó với hành vi không xác định, hãy cầu nguyện cho một vòng lặp vô hạn. Làm cho nó dễ dàng hơn để phát hiện ...
Dennis

Ý tôi là nếu trình biên dịch đang tích cực tìm kiếm UB, tại sao không chèn một ngoại lệ thay vì cố gắng tối ưu hóa siêu mã bị hỏng?
Nghịch đảo

15
@Inverse: Trình biên dịch không chủ động tìm kiếm hành vi không xác định , nó giả định rằng nó không xảy ra. Điều này cho phép trình biên dịch tối ưu hóa mã. Ví dụ, thay vì tính toán for (j = i; j < i + 10; ++j) ++k;, nó sẽ chỉ được đặt k = 10, vì điều này sẽ luôn đúng nếu không xảy ra tràn ký kết.
Dennis

@Inverse Trình biên dịch không "chọn" cho bất cứ điều gì. Bạn đã viết vòng lặp trong mã của bạn. Trình biên dịch đã không phát minh ra nó.
Các cuộc đua nhẹ nhàng trong quỹ đạo

13

Điều quan trọng cần lưu ý ở đây là các chương trình C ++ được viết cho máy trừu tượng C ++ (thường được mô phỏng thông qua các hướng dẫn phần cứng). Việc bạn đang biên dịch cho x86 hoàn toàn không liên quan đến thực tế rằng điều này có hành vi không xác định.

Trình biên dịch có thể tự do sử dụng sự tồn tại của hành vi không xác định để cải thiện tối ưu hóa của nó, (bằng cách loại bỏ một điều kiện khỏi một vòng lặp, như trong ví dụ này). Không có sự bảo đảm, hoặc thậm chí hữu ích, ánh xạ giữa các cấu trúc mức C ++ và cấu trúc mã máy cấp x86 ngoài yêu cầu mà mã máy sẽ, khi được thực thi, tạo ra kết quả theo yêu cầu của máy trừu tượng C ++.



3

Xin mọi người, hành vi không xác định là chính xác, không xác định . Nó có nghĩa là bất cứ điều gì có thể xảy ra. Trong thực tế (như trong trường hợp này), trình biên dịch có thể tự do giả định nó sẽ khôngđược gọi và làm bất cứ điều gì nó muốn nếu điều đó có thể làm cho mã nhanh hơn / nhỏ hơn. Điều gì xảy ra với mã không nên chạy là phỏng đoán của bất kỳ ai. Nó sẽ phụ thuộc vào mã xung quanh (tùy theo đó, trình biên dịch có thể tạo mã khác nhau), các biến / hằng được sử dụng, cờ trình biên dịch, ... Ồ, và trình biên dịch có thể được cập nhật và viết cùng một mã khác nhau, hoặc bạn có thể có được một trình biên dịch khác với một cái nhìn khác về việc tạo mã. Hoặc chỉ cần lấy một máy khác, ngay cả một mô hình khác trong cùng một dòng kiến ​​trúc cũng có thể có hành vi không xác định của riêng nó (tìm kiếm các mã không xác định, một số lập trình viên dám nghĩ rằng trên một số máy đầu tiên đôi khi đã làm những việc hữu ích ...) . Có"Trình biên dịch đưa ra một hành vi xác định về hành vi không xác định". Có những khu vực được xác định theo triển khai và ở đó bạn sẽ có thể tin tưởng vào trình biên dịch hoạt động nhất quán.


1
Vâng, tôi biết rất rõ hành vi không xác định là gì. Nhưng khi bạn biết cách các khía cạnh nhất định của ngôn ngữ được triển khai cho một môi trường cụ thể, bạn có thể mong đợi thấy một số loại UB nhất định chứ không phải các loại khác. Tôi biết rằng GCC thực hiện số học số nguyên dưới dạng số học x86 số nguyên - bao bọc trên tràn. Vì vậy, tôi giả định hành vi như vậy. Điều tôi không mong đợi là GCC sẽ làm một cái gì đó khác như bdonlan đã trả lời.
Bí ẩn

7
Sai lầm. Điều gì xảy ra là GCC được phép cho rằng bạn sẽ không gọi hành vi không xác định, do đó, nó chỉ phát ra mã như thể không thể xảy ra. Nếu nó không xảy ra, các hướng dẫn để làm những gì bạn yêu cầu với không hành vi undefined được thực hiện, và kết quả là bất cứ điều gì CPU không. Tức là, trên x86 là công cụ x86. Nếu nó là một bộ xử lý khác, nó có thể làm một cái gì đó hoàn toàn khác. Hoặc trình biên dịch có thể đủ thông minh để nhận ra rằng bạn đang kêu gọi hành vi không xác định và bắt đầu nethack (vâng, một số phiên bản cổ của gcc đã làm chính xác điều đó).
vonbrand

4
Tôi tin rằng bạn đọc sai nhận xét của tôi. Tôi nói: "Điều tôi không mong đợi" - đó là lý do tại sao tôi đặt câu hỏi ngay từ đầu. Tôi không mong đợi GCC sẽ có bất kỳ thủ đoạn nào.
Bí ẩn

1

Ngay cả khi một trình biên dịch chỉ định rằng tràn số nguyên phải được coi là một dạng "Hành vi không quan trọng" của Hành vi không xác định (như được định nghĩa trong Phụ lục L), thì kết quả của một tràn số nguyên sẽ không có một lời hứa nền tảng cụ thể nào về hành vi cụ thể hơn, ở mức tối thiểu được coi là "giá trị không xác định một phần". Theo các quy tắc như vậy, việc thêm 1073741824 + 1073741824 có thể được coi tùy ý là mang lại 2147483648 hoặc -2147483648 hoặc bất kỳ giá trị nào khác phù hợp với 2147483648 mod 4294967296 và các giá trị thu được từ bổ sung có thể được coi là bất kỳ giá trị nào.

Các quy tắc cho phép tràn để mang lại "các giá trị không xác định một phần" sẽ được xác định đầy đủ để tuân theo thư và tinh thần của Phụ lục L, nhưng sẽ không ngăn cản trình biên dịch đưa ra các suy luận chung hữu ích như sẽ được chứng minh nếu tràn không bị ràng buộc Hành vi không xác định. Nó sẽ ngăn trình biên dịch thực hiện một số "tối ưu hóa" giả mạo mà tác dụng chính của nó trong nhiều trường hợp là yêu cầu các lập trình viên thêm lộn xộn vào mã có mục đích duy nhất là ngăn chặn các "tối ưu hóa" đó; điều đó có tốt hay không phụ thuộc vào quan điểm của một người.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.