Tại thời điểm nào trong vòng lặp, tràn số nguyên trở thành hành vi không xác định?


86

Đây là một ví dụ để minh họa câu hỏi của tôi liên quan đến một số mã phức tạp hơn nhiều mà tôi không thể đăng ở đây.

#include <stdio.h>
int main()
{
    int a = 0;
    for (int i = 0; i < 3; i++)
    {
        printf("Hello\n");
        a = a + 1000000000;
    }
}

Chương trình này chứa hành vi không xác định trên nền tảng của tôi vì asẽ tràn trên vòng lặp thứ 3.

Điều đó làm cho toàn bộ chương trình có hành vi không xác định, hoặc chỉ sau khi tràn thực sự xảy ra ? Liệu trình biên dịch có thể giải quyết vấn đề a sẽ bị tràn để nó có thể khai báo toàn bộ vòng lặp là không xác định và không bận tâm chạy các printfs mặc dù tất cả chúng đều xảy ra trước khi tràn?

(Được gắn thẻ C và C ++ mặc dù khác nhau vì tôi muốn biết câu trả lời cho cả hai ngôn ngữ nếu chúng khác nhau.)


7
Wonder nếu trình biên dịch có thể làm việc ra rằng akhông được sử dụng (ngoại trừ để tính chính nó) và chỉ cần loại bỏa
4.386.427

12
Bạn có thể thưởng thức My Little Optimizer: Undefined Behavior is Magic từ CppCon năm nay. Đó là tất cả về những gì trình biên dịch tối ưu hóa có thể thực hiện dựa trên hành vi không xác định.
TartanLlama



Câu trả lời:


108

Nếu bạn quan tâm đến một câu trả lời thuần túy lý thuyết, tiêu chuẩn C ++ cho phép hành vi không xác định để "du hành thời gian":

[intro.execution]/5: Một triển khai tuân thủ thực hiện một chương trình được định dạng tốt sẽ tạo ra cùng một hành vi có thể quan sát được như một trong những thực thi có thể có của phiên bản tương ứng của máy trừu tượng với cùng một chương trình và cùng một đầu vào. Tuy nhiên, nếu bất kỳ quá trình thực hiện nào như vậy chứa một thao tác không xác định, thì tiêu chuẩn này không đặt ra yêu cầu đối với việc triển khai thực hiện chương trình đó với đầu vào đó (ngay cả đối với các hoạt động trước thao tác không xác định đầu tiên)

Như vậy, nếu chương trình của bạn chứa hành vi không xác định, thì hành vi của toàn bộ chương trình của bạn là không xác định.


4
@KeithThompson: Nhưng sau đó, sneeze()bản thân hàm không được xác định trên bất kỳ thứ gì thuộc lớp Demon(trong đó giống mũi là một lớp con), làm cho toàn bộ mọi thứ trở nên tròn trịa.
Sebastian Lenartowicz

1
Nhưng printf có thể không trả về, do đó, hai vòng đầu tiên được xác định bởi vì cho đến khi hoàn tất, vẫn chưa rõ sẽ có UB. Xem stackoverflow.com/questions/23153445/…
usr

1
Đây là lý do tại sao một trình biên dịch là về mặt kỹ thuật trong phạm vi quyền của nó phát ra "nop" cho hạt nhân Linux (vì mã bootstrap dựa vào hành vi undefined): blog.regehr.org/archives/761
Crashworks

3
@Crashworks Và đó là lý do tại sao Linux được viết bằng, và biên soạn như, unportable C. (tức là một superset của C đòi hỏi phải có một trình biên dịch đặc biệt với opitions đặc biệt, chẳng hạn như -fno-nghiêm ngặt-aliasing)
user253751

3
@usr Tôi mong đợi nó được xác định nếu printfkhông trả về, nhưng nếu printfsẽ trả về, thì hành vi không xác định có thể gây ra sự cố trước đó printfđược gọi. Do đó, du hành thời gian. printf("Hello\n");và sau đó dòng biên dịch tiếp theo nhưundoPrintf(); launchNuclearMissiles();
user253751

31

Đầu tiên, hãy để tôi sửa tiêu đề của câu hỏi này:

Hành vi không xác định không (cụ thể) thuộc lĩnh vực thực thi.

Hành vi không xác định ảnh hưởng đến tất cả các bước: biên dịch, liên kết, tải và thực thi.

Một số ví dụ để củng cố điều này, hãy nhớ rằng không có phần nào là đầy đủ:

  • trình biên dịch có thể giả định rằng các phần mã chứa Hành vi không xác định không bao giờ được thực thi, và do đó giả sử các đường dẫn thực thi dẫn đến chúng là mã chết. Xem Những gì mọi lập trình viên C nên biết về hành vi không xác định của không ai khác ngoài Chris Lattner.
  • trình liên kết có thể giả định rằng khi có nhiều định nghĩa về một ký hiệu yếu (được nhận dạng bằng tên), tất cả các định nghĩa đều giống hệt nhau nhờ Quy tắc một định nghĩa
  • trình nạp (trong trường hợp bạn sử dụng thư viện động) cũng có thể giả định như vậy, do đó chọn ký hiệu đầu tiên mà nó tìm thấy; cái này thường (ab) dùng để chặn cuộc gọi bằng LD_PRELOADthủ thuật trên Unix
  • việc thực thi có thể không thành công (SIGSEV) nếu bạn sử dụng con trỏ treo

Đây là điều rất đáng sợ về Hành vi không xác định: không thể dự đoán trước, hành vi chính xác nào sẽ xảy ra và dự đoán này phải được xem lại ở mỗi lần cập nhật chuỗi công cụ, hệ điều hành cơ bản, ...


Tôi khuyên bạn nên xem video này của Michael Spencer (Nhà phát triển LLVM): CppCon 2016: My Little Optimizer: Undefined Behavior is Magic .


3
Đây là điều khiến tôi lo lắng. Trong mã thực của tôi, nó phức tạp nhưng tôi có thể gặp trường hợp nó sẽ luôn tràn. Và tôi không thực sự quan tâm đến điều đó, nhưng tôi lo rằng mã "đúng" cũng sẽ bị ảnh hưởng bởi điều này. Rõ ràng là tôi cần phải sửa chữa nó, nhưng sửa chữa đòi hỏi sự hiểu biết :)
jcoder

8
@jcoder: Có một lối thoát quan trọng ở đây. Trình biên dịch không được phép đoán ở dữ liệu đầu vào. Miễn là có ít nhất một đầu vào mà Hành vi không xác định không xảy ra, trình biên dịch phải đảm bảo rằng đầu vào cụ thể này vẫn tạo ra đầu ra phù hợp. Tất cả những lời bàn tán đáng sợ về tối ưu hóa nguy hiểm chỉ áp dụng cho UB không thể tránh khỏi . Thực tế mà nói, nếu bạn đã sử dụng argclàm số vòng lặp, trường hợp argc=1không tạo ra UB và trình biên dịch sẽ buộc phải xử lý điều đó.
MSalters

@jcoder: Trong trường hợp này, đây không phải là mã chết. Tuy nhiên, trình biên dịch có thể đủ thông minh để suy ra rằng ikhông thể tăng nhiều hơn Nlần và do đó giá trị của nó bị giới hạn.
Matthieu M.

4
@jcoder: Nếu f(good);hiện một số điều X và f(bad);gọi hành vi không xác định, sau đó một chương trình mà chỉ gọi f(good);là bảo đảm để làm X, nhưng f(good); f(bad);không được bảo đảm để làm X.

4
@Hurkyl thú vị hơn, nếu mã của bạn là như vậy if(foo) f(good); else f(bad);, một trình biên dịch thông minh sẽ loại bỏ sự so sánh và sản xuất và vô điều kiện foo(good).
John Dvorak

28

Một trình biên dịch C hoặc C ++ tối ưu hóa tích cực nhắm mục tiêu 16 bit intsẽ biết rằng hành vi khi thêm 1000000000vào một intkiểu là không xác định .

Nó được phép theo một trong hai tiêu chuẩn để làm bất cứ điều gì nó muốn, có thể bao gồm việc xóa toàn bộ chương trình, rời đi int main(){}.

Nhưng những gì về ints lớn hơn ? Tôi không biết có trình biên dịch nào làm được điều này (và tôi không phải là chuyên gia về thiết kế trình biên dịch C và C ++), nhưng tôi tưởng tượng rằng đôi khi một trình biên dịch nhắm mục tiêu 32 bit inttrở lên sẽ tìm ra rằng vòng lặp là vô hạn ( ikhông thay đổi) vì thế acuối cùng sẽ tràn. Vì vậy, một lần nữa, nó có thể tối ưu hóa kết quả đầu ra int main(){}. Điểm tôi đang cố gắng đưa ra ở đây là khi việc tối ưu hóa trình biên dịch ngày càng trở nên tích cực hơn, ngày càng có nhiều cấu trúc hành vi không xác định đang tự biểu hiện theo những cách không mong muốn.

Thực tế là vòng lặp của bạn là vô hạn bản thân nó không phải là không xác định vì bạn đang ghi vào đầu ra chuẩn trong thân vòng lặp.


3
Nó có được tiêu chuẩn cho phép làm bất cứ điều gì nó muốn ngay cả trước khi biểu hiện hành vi không xác định không? Điều này được nêu ở đâu?
jimifiki

4
tại sao 16 bit? Tôi đoán OP đang tìm kiếm lỗi tràn 32 bit đã ký.
4386427

8
@jimifiki Trong tiêu chuẩn. C ++ 14 (N4140) 1.3.24 "hành vi chưa được xác định = hành vi mà tiêu chuẩn này không áp đặt yêu cầu." Thêm vào đó là một ghi chú dài dòng chi tiết. Nhưng vấn đề ở đây không phải là hành vi của một "câu lệnh" không được xác định, đó là hành vi của chương trình. Điều đó có nghĩa là miễn là UB được kích hoạt bởi một quy tắc trong tiêu chuẩn (hoặc do không có quy tắc), thì tiêu chuẩn đó sẽ ngừng áp dụng cho toàn bộ chương trình . Vì vậy, bất kỳ phần nào của chương trình có thể hoạt động theo cách mà nó muốn.
Angew không còn tự hào về SO

5
Tuyên bố đầu tiên là sai. Nếu intlà 16 bit, việc bổ sung sẽ diễn ra long(vì toán hạng theo nghĩa đen có kiểu long) nơi nó được xác định rõ ràng, sau đó được chuyển đổi bằng một chuyển đổi do triển khai xác định trở lại int.
R .. GitHub DỪNG TRỢ GIÚP LÚC NỮA,

2
@usr hành vi của printfđược xác định theo tiêu chuẩn để luôn trả về
MM

11

Về mặt kỹ thuật, theo tiêu chuẩn C ++, nếu một chương trình chứa hành vi không xác định, hành vi của toàn bộ chương trình, ngay cả tại thời điểm biên dịch (trước khi chương trình được thực thi), là không xác định.

Trong thực tế, vì trình biên dịch có thể giả định (như một phần của tối ưu hóa) rằng tràn sẽ không xảy ra, ít nhất hành vi của chương trình ở lần lặp thứ ba của vòng lặp (giả sử là máy 32 bit) sẽ không được xác định, mặc dù có khả năng bạn sẽ nhận được kết quả chính xác trước lần lặp thứ ba. Tuy nhiên, vì hành vi của toàn bộ chương trình là không xác định về mặt kỹ thuật, nên không có gì ngăn chương trình tạo ra đầu ra hoàn toàn không chính xác (bao gồm cả không có đầu ra), gặp sự cố trong thời gian chạy tại bất kỳ thời điểm nào trong quá trình thực thi hoặc thậm chí không thể biên dịch hoàn toàn (vì hành vi không xác định kéo dài đến thời gian biên dịch).

Hành vi không xác định cung cấp cho trình biên dịch nhiều chỗ hơn để tối ưu hóa bởi vì chúng loại bỏ các giả định nhất định về những gì mã phải làm. Khi làm như vậy, các chương trình dựa trên các giả định liên quan đến hành vi không xác định sẽ không được đảm bảo hoạt động như mong đợi. Do đó, bạn không nên dựa vào bất kỳ hành vi cụ thể nào được coi là không xác định theo tiêu chuẩn C ++.


Điều gì sẽ xảy ra nếu phần UB nằm trong một if(false) {}phạm vi? Điều đó có gây độc cho toàn bộ chương trình, do trình biên dịch giả định rằng tất cả các nhánh đều chứa ~ phần logic được xác định rõ ràng, và do đó hoạt động trên các giả định sai?
mlvljr

1
Tiêu chuẩn không áp đặt bất kỳ yêu cầu nào đối với hành vi không xác định, vì vậy về lý thuyết , có, nó gây độc cho toàn bộ chương trình. Tuy nhiên, trong thực tế , bất kỳ trình biên dịch tối ưu hóa nào cũng có thể chỉ xóa mã chết, vì vậy nó có thể sẽ không ảnh hưởng đến việc thực thi. Tuy nhiên, bạn vẫn không nên dựa vào hành vi này.
bwDraco

Thông tin cần biết, thanx :)
mlvljr

9

Để hiểu tại sao hành vi không xác định lại có thể 'du hành thời gian' như @TartanLlama đã nói một cách đầy đủ về nó , chúng ta hãy xem quy tắc 'as-if':

1.9 Thực hiện chương trình

1 Các mô tả ngữ nghĩa trong tiêu chuẩn này xác định một máy trừu tượng không xác định được tham số hóa. Tiêu chuẩn này không yêu cầu về cấu trúc của việc triển khai phù hợp. Đặc biệt, họ không cần sao chép hoặc mô phỏng cấu trúc của máy trừu tượng. Thay vào đó, các triển khai tuân thủ được yêu cầu để mô phỏng (chỉ) hành vi có thể quan sát được của máy trừu tượng như được giải thích bên dưới.

Với điều này, chúng ta có thể xem chương trình như một 'hộp đen' với một đầu vào và một đầu ra. Đầu vào có thể là đầu vào của người dùng, tệp và nhiều thứ khác. Đầu ra là 'hành vi quan sát được' được đề cập trong tiêu chuẩn.

Tiêu chuẩn chỉ định nghĩa một ánh xạ giữa đầu vào và đầu ra, không có gì khác. Nó thực hiện điều này bằng cách mô tả một 'hộp đen ví dụ', nhưng nói rõ ràng rằng bất kỳ hộp đen nào khác có cùng một ánh xạ đều có giá trị như nhau. Điều này có nghĩa là nội dung của hộp đen không liên quan.

Với suy nghĩ này, sẽ không hợp lý nếu nói rằng hành vi không xác định xảy ra tại một thời điểm nhất định. Trong phần triển khai mẫu của hộp đen, chúng ta có thể nói nó xảy ra ở đâu và khi nào, nhưng hộp đen thực tế có thể là một cái gì đó hoàn toàn khác, vì vậy chúng ta không thể nói nó xảy ra ở đâu và khi nào nữa. Về mặt lý thuyết, một trình biên dịch chẳng hạn có thể quyết định liệt kê tất cả các đầu vào có thể có và tính toán trước các kết quả đầu ra. Sau đó, hành vi không xác định sẽ xảy ra trong quá trình biên dịch.

Hành vi không xác định là sự không tồn tại của một ánh xạ giữa đầu vào và đầu ra. Một chương trình có thể có hành vi không xác định đối với một số đầu vào, nhưng hành vi được xác định đối với hành vi khác. Khi đó, ánh xạ giữa đầu vào và đầu ra chỉ đơn giản là không đầy đủ; có đầu vào mà không tồn tại ánh xạ tới đầu ra.
Chương trình trong câu hỏi có hành vi không xác định cho bất kỳ đầu vào nào, vì vậy ánh xạ trống.


6

Giả sử intlà 32-bit, hành vi không xác định xảy ra ở lần lặp thứ ba. Vì vậy, nếu, ví dụ, nếu vòng lặp chỉ có thể truy cập có điều kiện hoặc có thể kết thúc có điều kiện trước lần lặp thứ ba, sẽ không có hành vi không xác định trừ khi thực sự đạt đến lần lặp thứ ba. Tuy nhiên, trong trường hợp có hành vi không xác định, tất cả đầu ra của chương trình là không xác định, bao gồm đầu ra là "trong quá khứ" liên quan đến việc gọi hành vi không xác định. Ví dụ: trong trường hợp của bạn, điều này có nghĩa là không có gì đảm bảo sẽ thấy 3 thông báo "Xin chào" trong đầu ra.


6

Câu trả lời của TartanLlama là đúng. Hành vi không xác định có thể xảy ra bất cứ lúc nào, ngay cả trong thời gian biên dịch. Điều này có vẻ vô lý, nhưng đó là một tính năng chính để cho phép trình biên dịch làm những gì họ cần làm. Không phải lúc nào cũng dễ dàng trở thành một trình biên dịch. Bạn phải làm chính xác những gì thông số cho biết, mọi lúc. Tuy nhiên, đôi khi rất khó để chứng minh rằng một hành vi cụ thể đang xảy ra. Nếu bạn còn nhớ vấn đề tạm dừng, thì việc phát triển phần mềm mà bạn không thể chứng minh liệu nó có hoàn thành hay đi vào một vòng lặp vô hạn khi được cung cấp một đầu vào cụ thể là điều khá nhỏ.

Chúng tôi có thể làm cho các trình biên dịch trở nên bi quan và liên tục biên dịch vì sợ rằng hướng dẫn tiếp theo có thể là một trong những vấn đề tạm dừng như các vấn đề, nhưng điều đó không hợp lý. Thay vào đó, chúng tôi cung cấp cho trình biên dịch một quyền: đối với các chủ đề "hành vi không xác định" này, chúng được giải phóng khỏi mọi trách nhiệm. Hành vi không xác định bao gồm tất cả các hành vi bất chính tinh vi đến mức chúng ta gặp khó khăn khi tách chúng ra khỏi các vấn đề ngăn chặn thực sự khó chịu và bất chính và không thể.

Có một ví dụ mà tôi thích đăng, mặc dù tôi thừa nhận rằng tôi đã mất nguồn, vì vậy tôi phải diễn giải lại. Đó là từ một phiên bản MySQL cụ thể. Trong MySQL, chúng có một bộ đệm tròn chứa đầy dữ liệu do người dùng cung cấp. Tất nhiên, họ muốn đảm bảo rằng dữ liệu không tràn bộ đệm, vì vậy họ đã kiểm tra:

if (currentPtr + numberOfNewChars > endOfBufferPtr) { doOverflowLogic(); }

Nó trông đủ lành mạnh. Tuy nhiên, nếu numberOfNewChars thực sự lớn và tràn thì sao? Sau đó, nó bao bọc xung quanh và trở thành một con trỏ nhỏ hơn endOfBufferPtr, vì vậy logic tràn sẽ không bao giờ được gọi. Vì vậy, họ đã thêm một kiểm tra thứ hai, trước đó:

if (currentPtr + numberOfNewChars < currentPtr) { detectWrapAround(); }

Có vẻ như bạn đã quan tâm đến lỗi tràn bộ đệm phải không? Tuy nhiên, một lỗi đã được gửi cho biết rằng bộ đệm này đã tràn trên một phiên bản Debian cụ thể! Điều tra cẩn thận cho thấy rằng phiên bản Debian này là phiên bản đầu tiên sử dụng phiên bản gcc đặc biệt xuất sắc. Trên phiên bản gcc này, trình biên dịch nhận ra rằng currentPtr + numberOfNewChars không bao giờ có thể là một con trỏ nhỏ hơn currentPtr bởi vì tràn cho con trỏ là hành vi không xác định! Điều đó là đủ để gcc tối ưu hóa toàn bộ quá trình kiểm tra, và đột nhiên bạn không được bảo vệ khỏi sự cố tràn bộ đệm mặc dù bạn đã viết mã để kiểm tra nó!

Đây là hành vi đặc biệt. Mọi thứ đều hợp pháp (mặc dù từ những gì tôi nghe được, gcc đã lùi lại sự thay đổi này trong phiên bản tiếp theo). Đó không phải là những gì tôi sẽ coi là hành vi trực quan, nhưng nếu bạn mở rộng trí tưởng tượng của mình một chút, thật dễ dàng để thấy một biến thể nhỏ của tình huống này có thể trở thành một vấn đề tạm dừng đối với trình biên dịch. Bởi vì điều này, những người viết thông số kỹ thuật đã đặt nó là "Hành vi không xác định" và tuyên bố rằng trình biên dịch hoàn toàn có thể làm bất cứ điều gì nó hài lòng.


Tôi không coi các trình biên dịch đặc biệt đáng kinh ngạc mà đôi khi hoạt động như thể số học có dấu được thực hiện trên các loại có phạm vi mở rộng ra ngoài "int", đặc biệt khi xem xét rằng ngay cả khi thực hiện tạo mã đơn giản trên x86, đôi khi làm như vậy hiệu quả hơn việc cắt ngắn trung gian các kết quả. Điều đáng kinh ngạc hơn là khi tràn ảnh hưởng đến các phép tính khác , điều này có thể xảy ra trong gcc ngay cả khi mã lưu trữ tích của hai giá trị uint16_t vào một uint32_t - một hoạt động không có lý do chính đáng để hành động đáng ngạc nhiên trong một bản dựng không làm sạch.
supercat

Tất nhiên, kiểm tra chính xác sẽ là if(numberOfNewChars > endOfBufferPtr - currentPtr), với điều kiện numberOfNewChars không bao giờ có thể là số âm và currentPtr luôn trỏ đến một nơi nào đó trong bộ đệm mà bạn thậm chí không cần kiểm tra "bao bọc" vô lý. (Tôi không nghĩ rằng đoạn code mà bạn cung cấp có bất kỳ hy vọng làm việc trong một bộ đệm tròn - bạn đã bỏ qua bất cứ điều gì là cần thiết cho rằng trong diễn giải, vì vậy tôi bỏ qua trường hợp đó cũng)
Random832

@ Random832 Tôi đã bỏ ra một tấn. Tôi đã cố gắng trích dẫn ngữ cảnh lớn hơn, nhưng vì tôi bị mất nguồn, tôi nhận thấy việc diễn giải ngữ cảnh khiến tôi gặp nhiều rắc rối hơn nên tôi bỏ qua. Tôi thực sự cần tìm báo cáo lỗi bị thổi phồng đó để có thể trích dẫn nó một cách chính xác. Nó thực sự là một ví dụ mạnh mẽ về cách bạn có thể nghĩ rằng bạn đã viết mã theo một cách nào đó và biên dịch nó hoàn toàn khác.
Cort Ammon

Đây là vấn đề lớn nhất của tôi với hành vi không xác định. Nó khiến đôi khi không thể viết đúng mã và khi trình biên dịch phát hiện ra nó, theo mặc định, nó sẽ không cho bạn biết nó được kích hoạt hành vi không xác định. Trong trường hợp này, người dùng chỉ muốn thực hiện số học - con trỏ hoặc không - và tất cả công việc khó khăn của họ để viết mã an toàn đã được hoàn tác. Ít nhất phải có một cách để chú thích một phần mã để nói - không có tối ưu hóa cầu kỳ ở đây. C / C ++ được sử dụng trong quá nhiều lĩnh vực quan trọng để cho phép tình huống nguy hiểm này để tiếp tục ủng hộ Optimzation
John McGrath

4

Ngoài các câu trả lời lý thuyết, một quan sát thực tế cho thấy rằng trong một thời gian dài, các trình biên dịch đã áp dụng các phép biến đổi khác nhau trên các vòng lặp để giảm lượng công việc được thực hiện bên trong chúng. Ví dụ, đã cho:

for (int i=0; i<n; i++)
  foo[i] = i*scale;

một trình biên dịch có thể chuyển đổi nó thành:

int temp = 0;
for (int i=0; i<n; i++)
{
  foo[i] = temp;
  temp+=scale;
}

Do đó tiết kiệm một phép nhân với mỗi lần lặp vòng lặp. Một hình thức tối ưu hóa bổ sung, mà các trình biên dịch thích ứng với các mức độ tích cực khác nhau, sẽ biến điều đó thành:

if (n > 0)
{
  int temp1 = n*scale;
  int *temp2 = foo;
  do
  {
    temp1 -= scale;
    *temp2++ = temp1;
  } while(temp1);
}

Ngay cả trên các máy có vòng lặp im lặng khi tràn, điều đó có thể hoạt động sai nếu có một số nào đó nhỏ hơn n, khi nhân với tỷ lệ, sẽ cho kết quả là 0. Nó cũng có thể biến thành một vòng lặp vô tận nếu tỷ lệ được đọc từ bộ nhớ nhiều hơn một lần. đã thay đổi giá trị của nó một cách bất ngờ (trong bất kỳ trường hợp nào mà "scale" có thể thay đổi giữa vòng lặp mà không cần gọi UB, trình biên dịch sẽ không được phép thực hiện tối ưu hóa).

Mặc dù hầu hết các tối ưu hóa như vậy sẽ không gặp bất kỳ sự cố nào trong trường hợp nhân hai loại không dấu ngắn để mang lại giá trị nằm giữa INT_MAX + 1 và UINT_MAX, nhưng gcc có một số trường hợp mà phép nhân như vậy trong một vòng lặp có thể khiến vòng lặp thoát sớm . Tôi đã không nhận thấy các hành vi như vậy xuất phát từ các hướng dẫn so sánh trong mã được tạo, nhưng có thể quan sát được trong trường hợp trình biên dịch sử dụng tràn để suy ra rằng một vòng lặp có thể thực thi nhiều nhất 4 lần trở xuống; theo mặc định, nó không tạo ra cảnh báo trong trường hợp một số đầu vào sẽ gây ra UB và những đầu vào khác thì không, ngay cả khi các suy luận của nó khiến giới hạn trên của vòng lặp bị bỏ qua.


4

Hành vi không xác định, theo định nghĩa, là một vùng màu xám. Bạn chỉ đơn giản là không thể dự đoán nó sẽ làm gì hoặc sẽ không làm gì - đó là "hành vi không xác định" nghĩa là gì .

Từ thời xa xưa, các lập trình viên đã luôn cố gắng cứu vãn tàn dư của tính xác định từ một tình huống không xác định. Họ có một số mã mà họ thực sự muốn sử dụng, nhưng hóa ra là không xác định, vì vậy họ cố gắng tranh luận: "Tôi biết nó không xác định, nhưng chắc chắn rằng, tệ nhất là nó sẽ làm điều này hoặc điều này; nó sẽ không bao giờ làm điều đó . " Và đôi khi những lập luận này ít nhiều đúng - nhưng thường thì chúng sai. Và khi các trình biên dịch ngày càng thông minh hơn (hoặc, một số người có thể nói, lén lút và lén lút hơn), ranh giới của câu hỏi tiếp tục thay đổi.

Vì vậy, thực sự, nếu bạn muốn viết mã được đảm bảo hoạt động và sẽ tiếp tục hoạt động trong thời gian dài, chỉ có một lựa chọn: tránh hành vi không xác định bằng mọi giá. Quả thật, nếu bạn đâm vào nó, nó sẽ quay lại ám ảnh bạn.


tuy nhiên, đây là điều ... trình biên dịch có thể sử dụng hành vi không xác định để tối ưu hóa nhưng HỌ CHUNG KHÔNG NÓI VỚI BẠN. Vì vậy, nếu chúng tôi có công cụ tuyệt vời này mà bạn phải tránh làm X bằng mọi giá, tại sao trình biên dịch không thể đưa ra cảnh báo để bạn có thể sửa chữa nó?
Jason S

1

Một điều mà ví dụ của bạn không xem xét là tối ưu hóa. ađược đặt trong vòng lặp nhưng không bao giờ được sử dụng và một trình tối ưu hóa có thể giải quyết vấn đề này. Do đó, việc loại bỏ ahoàn toàn hoàn toàn là hợp pháp đối với trình tối ưu hóa , và trong trường hợp đó, tất cả các hành vi không xác định sẽ biến mất như nạn nhân của boojum.

Tuy nhiên, tất nhiên bản thân điều này là không xác định, vì tối ưu hóa là không xác định. :)


1
Không có lý do gì để xem xét việc tối ưu hóa khi xác định xem hành vi đó có phải là không xác định hay không.
Keith Thompson

2
Thực tế là chương trình hoạt động như người ta có thể giả định không có nghĩa là hành vi không xác định "biến mất". Hành vi vẫn chưa được xác định và bạn chỉ đơn giản là dựa vào may mắn. Thực tế là hành vi của chương trình có thể thay đổi dựa trên các tùy chọn trình biên dịch là một chỉ báo mạnh mẽ rằng hành vi đó là không xác định.
Jordan Melo

@JordanMelo Vì nhiều câu trả lời trước đây đã thảo luận về tối ưu hóa (và OP đã hỏi cụ thể về điều đó), tôi đã đề cập đến một tính năng của tối ưu hóa mà chưa có câu trả lời trước nào đề cập đến. Tôi cũng chỉ ra rằng mặc dù tối ưu hóa có thể loại bỏ nó, nhưng việc phụ thuộc vào tối ưu hóa để hoạt động theo bất kỳ cách cụ thể nào lại không được xác định. Tôi chắc chắn không giới thiệu nó! :)
Graham

@KeithThompson Chắc chắn rồi, nhưng OP đã hỏi cụ thể về tối ưu hóa và ảnh hưởng của nó đối với hành vi không xác định mà anh ấy sẽ thấy trên nền tảng của mình. Hành vi cụ thể đó có thể biến mất, tùy thuộc vào tối ưu hóa. Như tôi đã nói trong câu trả lời của mình, sự không xác định sẽ không.
Graham

0

Vì câu hỏi này được gắn thẻ kép C và C ++ nên tôi sẽ thử và giải quyết cả hai. C và C ++ có các cách tiếp cận khác nhau ở đây.

Trong C, việc triển khai phải có khả năng chứng minh hành vi không xác định sẽ được gọi để xử lý toàn bộ chương trình như thể nó có hành vi không xác định. Trong ví dụ OPs, việc trình biên dịch chứng minh điều đó có vẻ tầm thường và do đó nó giống như thể toàn bộ chương trình là không xác định.

Chúng ta có thể thấy điều này từ Báo cáo Lỗi 109 mà điểm mấu chốt của nó yêu cầu:

Tuy nhiên, nếu Tiêu chuẩn C nhận ra sự tồn tại riêng biệt của "giá trị không xác định" (mà việc tạo đơn thuần không liên quan đến "hành vi không xác định" hoàn toàn) thì một người thực hiện thử nghiệm trình biên dịch có thể viết một trường hợp thử nghiệm như sau và anh ta / cô ta cũng có thể mong đợi (hoặc có thể yêu cầu) rằng một triển khai tuân thủ, ít nhất phải biên dịch mã này (và có thể cũng cho phép nó thực thi) mà không "thất bại".

int array1[5];
int array2[5];
int *p1 = &array1[0];
int *p2 = &array2[0];

int foo()
{
int i;
i = (p1 > p2); /* Must this be "successfully translated"? */
1/0; /* Must this be "successfully translated"? */
return 0;
}

Vì vậy, câu hỏi mấu chốt là: Đoạn mã trên phải được "dịch thành công" (bất kể điều đó có nghĩa là gì)? (Xem chú thích kèm theo điều khoản phụ 5.1.1.3.)

và câu trả lời là:

Tiêu chuẩn C sử dụng thuật ngữ "giá trị không xác định" chứ không phải "giá trị không xác định". Việc sử dụng một đối tượng có giá trị không xác định dẫn đến hành vi không xác định. Chú thích của điều khoản phụ 5.1.1.3 chỉ ra rằng việc triển khai có thể tự do tạo ra bất kỳ số lượng chẩn đoán nào miễn là chương trình hợp lệ vẫn được dịch chính xác. Nếu một biểu thức mà việc loại bỏ sẽ dẫn đến hành vi không xác định xuất hiện trong ngữ cảnh yêu cầu biểu thức hằng, thì chương trình chứa không tuân thủ nghiêm ngặt. Hơn nữa, nếu mọi thực thi có thể có của một chương trình nhất định sẽ dẫn đến hành vi không xác định, thì chương trình đã cho không tuân thủ nghiêm ngặt. Việc triển khai tuân thủ không được không dịch một chương trình tuân thủ nghiêm ngặt đơn giản vì một số khả năng thực thi chương trình đó sẽ dẫn đến hành vi không xác định. Bởi vì foo có thể không bao giờ được gọi, ví dụ được đưa ra phải được dịch thành công bằng một triển khai tuân thủ.

Trong C ++, cách tiếp cận có vẻ thoải mái hơn và sẽ đề xuất một chương trình có hành vi không xác định bất kể việc triển khai có thể chứng minh nó tĩnh hay không.

Chúng tôi có [intro.abstrac] p5 cho biết:

Một triển khai tuân thủ thực hiện một chương trình được định dạng tốt sẽ tạo ra cùng một hành vi có thể quan sát được như một trong những thực thi có thể có của phiên bản tương ứng của máy trừu tượng với cùng một chương trình và cùng một đầu vào. Tuy nhiên, nếu bất kỳ thực thi nào như vậy chứa một hoạt động không xác định, thì tài liệu này không yêu cầu việc triển khai thực hiện chương trình đó với đầu vào đó (thậm chí không liên quan đến các hoạt động trước hoạt động không xác định đầu tiên).


Thực tế là việc thực thi một hàm sẽ gọi UB chỉ có thể ảnh hưởng đến cách chương trình hoạt động khi được cung cấp một số đầu vào cụ thể nếu ít nhất một lần thực thi chương trình có thể xảy ra khi đầu vào đó sẽ gọi UB. Thực tế là việc gọi một hàm sẽ gọi UB không ngăn cản một chương trình có hành vi được xác định khi nó được cung cấp đầu vào mà sẽ không cho phép hàm được gọi.
supercat

@supercat Tôi tin rằng đó là câu trả lời của tôi mà chúng tôi nói cho C ít nhất.
Shafik Yaghmour

Tôi nghĩ rằng điều tương tự cũng áp dụng cho văn bản được trích dẫn lại C ++, vì cụm từ "Bất kỳ thực thi nào như vậy" đề cập đến các cách chương trình có thể thực thi với một đầu vào cụ thể. Nếu một đầu vào cụ thể không thể dẫn đến một hàm thực thi, tôi không thấy gì trong văn bản được trích dẫn để nói rằng bất kỳ thứ gì trong một hàm như vậy sẽ dẫn đến UB.
supercat

-2

Câu trả lời hàng đầu là một quan niệm sai lầm (nhưng phổ biến):

Hành vi không xác định là thuộc tính thời gian chạy *. Nó KHÔNG THỂ "thời gian du lịch"!

Một số hoạt động nhất định được xác định (theo tiêu chuẩn) để có tác dụng phụ và không thể tối ưu hóa được. Các thao tác thực hiện I / O hoặc volatilecác biến truy cập thuộc loại này.

Tuy nhiên , có một lưu ý: UB có thể là bất kỳ hành vi nào , bao gồm cả hành vi hoàn tác các hoạt động trước đó. Điều này có thể gây ra hậu quả tương tự, trong một số trường hợp, đối với việc tối ưu hóa mã trước đó.

Trên thực tế, điều này phù hợp với câu trích dẫn trong câu trả lời trên cùng (phần nhấn mạnh của tôi):

Một triển khai tuân thủ thực hiện một chương trình được định dạng tốt sẽ tạo ra cùng một hành vi có thể quan sát được như một trong những thực thi có thể có của phiên bản tương ứng của máy trừu tượng có cùng chương trình và cùng một đầu vào.
Tuy nhiên, nếu bất kỳ quá trình thực hiện nào như vậy chứa một thao tác không xác định, thì tiêu chuẩn này không yêu cầu việc triển khai thực hiện chương trình đó với đầu vào đó (thậm chí không liên quan đến các thao tác trước thao tác không xác định đầu tiên).

Có, trích dẫn này nói "không liên quan đến các hoạt động trước hoạt động không xác định đầu tiên" , nhưng lưu ý rằng đây là đặc biệt về mã đang được thực thi , không chỉ được biên dịch.
Rốt cuộc, hành vi không xác định không thực sự được tiếp cận sẽ không làm gì cả và để dòng chứa UB thực sự được tiếp cận, mã đứng trước nó phải thực thi trước!

Vì vậy, có, một khi UB được thực thi , bất kỳ tác động nào của các hoạt động trước đó sẽ trở thành không xác định. Nhưng cho đến khi điều đó xảy ra, việc thực thi chương trình đã được xác định rõ.

Tuy nhiên, lưu ý rằng tất cả các lần thực thi chương trình dẫn đến việc này xảy ra có thể được tối ưu hóa cho các chương trình tương đương , bao gồm bất kỳ chương trình nào thực hiện các hoạt động trước đó nhưng sau đó bỏ tác dụng của chúng. Do đó, mã trước đó có thể được tối ưu hóa bất cứ khi nào làm như vậy sẽ tương đương với việc các hiệu ứng của chúng được hoàn tác ; nếu không, nó không thể. Xem ví dụ bên dưới.

* Lưu ý: Điều này không mâu thuẫn với UB xảy ra tại thời điểm biên dịch . Nếu trình biên dịch thực sự có thể chứng minh rằng mã UB sẽ luôn được thực thi cho tất cả các đầu vào, thì UB có thể kéo dài thời gian biên dịch. Tuy nhiên, điều này đòi hỏi phải biết rằng tất cả các mã trước đó cuối cùng sẽ trả về , đây là một yêu cầu mạnh mẽ. Một lần nữa, hãy xem ví dụ / giải thích bên dưới.


Để làm cho điều này cụ thể, hãy lưu ý rằng mã sau phải in foovà chờ đầu vào của bạn bất kể hành vi không xác định nào theo sau nó:

printf("foo");
getchar();
*(char*)1 = 1;

Tuy nhiên, cũng lưu ý rằng không có gì đảm bảo foosẽ vẫn còn trên màn hình sau khi UB xảy ra, hoặc ký tự bạn đã nhập sẽ không còn trong bộ đệm nhập nữa; cả hai thao tác này đều có thể được "hoàn tác", có tác dụng tương tự như "du hành thời gian" UB.

Nếu getchar()dòng không có ở đó, sẽ hợp pháp nếu các dòng được tối ưu hóa nếu và chỉ khi điều đó không thể phân biệt được với việc xuất ra foovà sau đó "hủy bỏ" nó.

Việc có thể phân biệt được hay không sẽ phụ thuộc hoàn toàn vào việc triển khai (tức là vào trình biên dịch và thư viện chuẩn của bạn). Ví dụ, bạn có thể printf chặn luồng của bạn ở đây trong khi chờ chương trình khác đọc đầu ra không? Hay nó sẽ trở lại ngay lập tức?

  • Nếu nó có thể chặn ở đây, thì một chương trình khác có thể từ chối đọc đầu ra đầy đủ của nó, và nó có thể không bao giờ quay trở lại, và do đó UB có thể không bao giờ thực sự xảy ra.

  • Nếu nó có thể trở lại ngay lập tức ở đây, thì chúng ta biết nó phải trở lại, và do đó tối ưu hóa nó hoàn toàn không thể phân biệt được với việc thực thi nó và sau đó bỏ tác dụng của nó.

Tất nhiên, vì trình biên dịch biết hành vi nào được phép đối với phiên bản cụ thể của printfnó, nó có thể tối ưu hóa tương ứng và do đó printfcó thể được tối ưu hóa trong một số trường hợp chứ không phải những trường hợp khác. Nhưng, một lần nữa, lời biện minh là điều này sẽ không thể phân biệt được với UB chưa thực hiện các hoạt động trước đó, chứ không phải mã trước đó bị "đầu độc" vì UB.


1
Bạn đang hoàn toàn đọc sai tiêu chuẩn. Nó cho biết hành vi khi thực thi chương trình là không xác định. Giai đoạn = Stage. Câu trả lời này sai 100%. Tiêu chuẩn rất rõ ràng - chạy một chương trình với đầu vào tạo ra UB tại bất kỳ điểm nào trong luồng thực thi ngây thơ là không xác định.
David Schwartz

@DavidSchwartz: Nếu bạn làm theo cách diễn giải của mình để đưa ra kết luận hợp lý, bạn sẽ nhận ra rằng nó không có ý nghĩa logic. Đầu vào không phải là thứ được xác định đầy đủ khi chương trình bắt đầu. Đầu vào của chương trình (ngay cả sự hiện diện của nó ) tại bất kỳ dòng nào cho trước được phép phụ thuộc vào tất cả các tác dụng phụ của chương trình cho đến dòng đó. Do đó, chương trình không thể tránh tạo ra các tác dụng phụ xảy ra trước vạch UB, vì điều đó đòi hỏi sự tương tác với môi trường của nó và do đó ảnh hưởng đến việc liệu vạch UB có đạt được hay không ngay từ đầu.
user541686

3
Điều đó không quan trọng. Có thật không. Một lần nữa, bạn chỉ thiếu trí tưởng tượng. Ví dụ: nếu trình biên dịch có thể nói rằng không có mã tuân thủ nào có thể phân biệt được sự khác biệt, thì nó có thể di chuyển mã UB sao cho phần UB thực thi trước các đầu ra mà bạn ngây thơ mong đợi là "trước".
David Schwartz

2
@Mehrdad: Có lẽ cách nói tốt hơn sẽ là nói rằng UB không thể du hành ngược thời gian qua điểm cuối cùng, nơi mà điều gì đó có thể đã xảy ra trong thế giới thực đã khiến hành vi được xác định. Nếu một triển khai có thể xác định bằng cách kiểm tra các bộ đệm đầu vào rằng không có cách nào trong số 1000 lệnh gọi tiếp theo tới getchar () có thể chặn và nó cũng có thể xác định rằng UB sẽ xảy ra sau lệnh gọi thứ 1000, nó sẽ không bắt buộc phải thực hiện bất kỳ lệnh nào trong số các cuộc gọi. Tuy nhiên, nếu một thực hiện là để xác định rằng thực hiện sẽ không vượt qua một getchar () cho đến khi tất cả các đầu ra trước đã ...
supercat

2
... được gửi đến một thiết bị đầu cuối 300 baud và bất kỳ điều khiển-C nào xảy ra trước đó sẽ khiến getchar () tăng tín hiệu ngay cả khi có các ký tự khác trong bộ đệm trước nó, khi đó việc triển khai như vậy không thể di chuyển bất kỳ UB nào qua đầu ra cuối cùng trước getchar (). Điều khó khăn là biết trong trường hợp nào thì một trình biên dịch nên được mong đợi để chuyển qua lập trình viên bất kỳ hành vi nào đảm bảo việc triển khai thư viện có thể cung cấp vượt quá những yêu cầu của Tiêu chuẩn.
supercat
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.