Hành vi không xác định, về nguyên tắc


8

Cho dù trong C hay C ++, tôi nghĩ rằng chương trình bất hợp pháp này, có hành vi theo tiêu chuẩn C hoặc C ++ là không xác định, rất thú vị:

#include <stdio.h>

int foo() {
    int a;
    const int b = a;
    a = 555;
    return b;
}

void bar() {
    int x = 123;
    int y = 456;
}

int main() {
    bar();
    const int n1 = foo();
    const int n2 = foo();
    const int n3 = foo();
    printf("%d %d %d\n", n1, n2, n3);
    return 0;
}

Đầu ra trên máy của tôi (sau khi biên dịch mà không tối ưu hóa):

123 555 555

Tôi nghĩ rằng chương trình bất hợp pháp này rất thú vị vì nó minh họa cơ chế ngăn xếp, bởi vì lý do chính là người ta sử dụng C hoặc C ++ (thay vì nói, Java) là để lập trình gần với phần cứng, gần với cơ chế ngăn xếp và tương tự.

Tuy nhiên, trên StackOverflow, khi mã của người hỏi vô tình đọc từ bộ lưu trữ chưa được khởi tạo, các câu trả lời được nâng cấp mạnh mẽ nhất luôn trích dẫn tiêu chuẩn C hoặc C ++ (đặc biệt là C ++) cho hiệu ứng mà hành vi không được xác định. Tất nhiên, điều này đúng, theo như tiêu chuẩn, thì hành vi thực sự không xác định được, nhưng điều gây tò mò là các câu trả lời thay thế, từ góc độ phần cứng hoặc cơ học, để điều tra lý do tại sao một hành vi không xác định cụ thể (chẳng hạn như đầu ra ở trên) có thể đã xảy ra, rất hiếm và có xu hướng bị bỏ qua.

Tôi thậm chí còn nhớ một câu trả lời cho rằng hành vi không xác định có thể bao gồm việc định dạng lại ổ cứng của tôi. Tôi đã không lo lắng quá nhiều về điều đó, mặc dù, trước khi chạy chương trình trên.

Câu hỏi của tôi là: Tại sao việc dạy độc giả chỉ quan trọng hơn là hành vi không được xác định trong C hoặc C ++, hơn là hiểu hành vi không xác định? Ý tôi là, nếu người đọc hiểu hành vi không xác định, thì anh ta sẽ không có khả năng tránh nó chứ?

Giáo dục của tôi xảy ra là trong kỹ thuật điện, và tôi làm việc như một kỹ sư xây dựng-xây dựng, và lần cuối cùng tôi đã có một công việc như một lập trình viên cho mỗi gia nhập là 1994, do đó, tôi tò mò muốn hiểu quan điểm của người sử dụng với truyền thống hơn, nền tảng phát triển phần mềm gần đây.


3
Đôi khi thật khó để hiểu chương trình của bạn thực sự làm gì cho đến khi bạn nhìn vào hội đồng được sản xuất và thấy rằng trình biên dịch đột nhiên tối ưu hóa một đoạn mã tốt do một đoạn hành vi không xác định.
chris

7
Hành vi không xác định có nghĩa là bất cứ điều gì có thể xảy ra. Cho dù đầu ra có ý nghĩa hay không, điều đó không thành vấn đề ... Đó chỉ là may mắn ngẫu nhiên mà trình biên dịch được triển khai như bạn mong đợi ....
Jaa-c

5
Cách trình biên dịch chọn biên dịch UB quá cụ thể là một câu hỏi SO hữu ích: nó phụ thuộc vào trình biên dịch cụ thể, HĐH, kiến ​​trúc máy, mức tối ưu hóa và phiên bản chính xác của trình biên dịch bạn đang sử dụng. Một loạt các bài viết tại blog.llvm.org/2011/05/what-every-c-programmer-should-ledge.html là một tổng quan tốt về lý do tại sao bạn nên tránh UB và một số điều có thể sai.
Paul Hankin

4
Một trình biên dịch khác nhau, hoặc cùng một trình biên dịch theo các cài đặt khác nhau, các mức tối ưu hóa khác nhau hoặc thậm chí trên một hệ thống khác nhau, có thể biên dịch mã khác nhau. Bạn không thể biết chắc chắn kết quả sẽ như thế nào. Vì nó phụ thuộc vào "ma thuật đen" bên trong của trình biên dịch, và nó có thể bị ảnh hưởng bởi các tùy chọn và các tham số bên ngoài khác, khiến nó không thể tái tạo được, và ngay cả khi nó không được khuyến khích. Nếu bạn muốn tìm hiểu về ngăn xếp, có nhiều cách tốt hơn để làm như vậy, có lẽ tôi sẽ đề nghị xem xét một đầu ra lắp ráp mã hợp lệ.
Tommy Andersen

2
Vấn đề với câu hỏi này là cách bạn định nghĩa "không xác định" (ha!). Nếu bạn biết trình biên dịch sẽ làm gì, thì nó không được xác định : nó được xác định theo triển khai (nếu tiêu chuẩn ISO C không cho phép triển khai rõ ràng để xác định nó, thì bây giờ nó được xác định theo triển khai sử dụng GNU C hoặc bất cứ điều gì thay vì ISO C). Thật không có ý nghĩa khi nói về "hiểu" UB thực sự ; nếu nó có thể được hiểu một cách nhất quán, thì không.
Leushenko

Câu trả lời:


5

Phân tích giá trị của Frama-C, một bộ phân tích tĩnh mục tiêu có mục đích là tìm ra tất cả các hành vi không xác định trong chương trình C, coi việc chuyển nhượng const int b = a;là ổn. Đây là một quyết định thiết kế có chủ ý nhằm cho phép memcpy()(thường được triển khai như một vòng lặp trên unsigned charcác phần tử của mảng ảo và tiêu chuẩn C được cho là có thể thực hiện lại như vậy) để sao chép struct(có thể có các thành viên đệm và chưa được khởi tạo) khác.

Ngoại lệ, chỉ dành cho các lvalue = lvalue;bài tập mà không có chuyển đổi can thiệp, nghĩa là, đối với một bài tập tương đương với một bản sao của một lát bộ nhớ cho vị trí bộ nhớ sang vị trí bộ nhớ khác.

Tôi (là một trong những tác giả của phân tích giá trị của Frama-C) đã thảo luận điều này với Xavier Leroy tại thời điểm anh ta tự hỏi về định nghĩa để chọn trong trình biên dịch C đã được xác minh CompCert, vì vậy anh ta có thể đã sử dụng định nghĩa tương tự. Theo ý kiến ​​của tôi, sạch hơn so với những gì mà tiêu chuẩn C cố gắng thực hiện với các giá trị không xác định có thể là biểu diễn bẫy và loại unsigned charđược đảm bảo không có bất kỳ biểu diễn bẫy nào, nhưng cả CompCert và Frama-C đều cho rằng các mục tiêu tương đối không kỳ lạ, và có lẽ ủy ban tiêu chuẩn hóa đã cố gắng đáp ứng các nền tảng trong đó việc đọc một bản chưa được khởi tạo intthực sự có thể hủy bỏ chương trình.

Quay trở lại b, hoặc đi n1, n2hoặc n3để printfở cuối ít nhất có thể được coi hành vi không xác định, vì sao chép một lát chưa được khởi tạo bộ nhớ không làm cho nó khởi tạo. Với phiên bản Frama-C cũ:

$ frama-c -val t.c

t.c:19:… accessing uninitialized left-value: assert \initialized(&n1);

Và trong một phiên bản cũ của CompCert, sau những sửa đổi nhỏ để làm cho chương trình được chấp nhận:

$ ccomp -interp t.c
Time 33: in function foo, expression <loc> = <undef>
ERROR: Undefined behavior

8

Hành vi không xác định cuối cùng có nghĩa là hành vi là không xác định. Các lập trình viên không biết rằng họ đang viết mã không xác định chỉ là những lập trình viên không biết gì. Trang web này nhằm mục đích làm cho các lập trình viên tốt hơn (và ít hiểu biết hơn).

Viết một chương trình chính xác khi đối mặt với hành vi không xác định là không thể. Tuy nhiên, nó là một môi trường lập trình chuyên biệt, và đòi hỏi một loại kỷ luật lập trình khác.

Ngay cả trong ví dụ của bạn, nếu chương trình nhận được tín hiệu bên ngoài, các giá trị trên "ngăn xếp" có thể thay đổi theo cách bạn không nhận được các giá trị mong đợi. Hơn nữa, nếu máy có các giá trị bẫy, việc đọc các giá trị ngẫu nhiên rất có thể gây ra điều gì đó kỳ lạ xảy ra.


4
@jxh Tôi không chắc là không xác định là đúng. Một chương trình có thể không được xác định nhưng hoàn toàn có thể lặp lại trên một nền tảng nhất định, phải không?
lượng

3
@Arman: Nó có thể hoặc không thể lặp lại trên một nền tảng nhất định, đó là điểm chính.
jxh

1
@Giorgio: Điểm khác là hành vi không xác định không cần phải xác định, ngay cả đối với cùng một nền tảng và việc thực hiện.
jxh

1
C và C ++ sử dụng hai thuật ngữ khác nhau: hành vi không xác định và hành vi không xác định. Cũng có trình tự không xác định. Và sự phân biệt là quan trọng. Có thể, mặc dù khó khăn, để viết một chương trình chính xác với sự hiện diện của hành vi không xác định. Nhưng không có số lượng mã hóa cẩn thận nào có thể đảm bảo tính chính xác khi có hành vi không xác định. Hành vi không xác định sẽ loại bỏ ý nghĩa ngữ nghĩa của toàn bộ chương trình của bạn. Mặt khác, hành vi không được xác định bởi ngôn ngữ có thể được xác định bởi nền tảng.
Ben Voigt

1
@jxh: Các hệ thống chống lỗi thực sự khá thú vị. Nhưng họ không xác định hành vi khoan dung. Các bản sao đang chạy trong bước khóa gặp phải hành vi không xác định đều có thể đưa ra lựa chọn sai và bỏ phiếu sẽ không giúp ích gì sau đó.
Ben Voigt

6

Tại sao điều quan trọng hơn là dạy cho người đọc chỉ đơn thuần rằng hành vi không được xác định trong C hoặc C ++, hơn là để hiểu hành vi không xác định?

Bởi vì hành vi cụ thể có thể không lặp lại, thậm chí từ chạy sang chạy mà không xây dựng lại.

Theo đuổi chính xác những gì đã xảy ra có thể là một bài tập học thuật hữu ích để hiểu rõ hơn về những điều kỳ quặc của nền tảng cụ thể của bạn, nhưng từ góc độ mã hóa , bài học duy nhất có liên quan là "đừng làm vậy". Một biểu thức như a++ * a++là một lỗi mã hóa, dừng hoàn toàn. Đó thực sự là tất cả mọi người cần biết.


5

"Hành vi không xác định" là viết tắt của "Hành vi này không mang tính quyết định, nó không chỉ có thể hoạt động khác nhau trong các trình biên dịch hoặc nền tảng phần cứng khác nhau, thậm chí nó có thể hoạt động khác nhau trong các phiên bản khác nhau của cùng một trình biên dịch."

Hầu hết các lập trình viên sẽ coi đây là một đặc điểm không mong muốn, đặc biệt là vì C và C ++ là các ngôn ngữ dựa trên tiêu chuẩn ; nghĩa là, bạn sử dụng chúng, một phần, bởi vì đặc tả ngôn ngữ đảm bảo chắc chắn về cách ngôn ngữ sẽ hoạt động, nếu bạn đang sử dụng trình biên dịch tuân thủ tiêu chuẩn.

Như với hầu hết mọi thứ trong lập trình, bạn phải cân nhắc những lợi thế và bất lợi. Nếu lợi ích của một số hoạt động là UB vượt quá khó khăn để khiến nó hoạt động theo kiểu ổn định, không dựa trên nền tảng, thì bằng mọi cách, hãy sử dụng hành vi không xác định. Hầu hết các lập trình viên sẽ nghĩ rằng nó không đáng, hầu hết thời gian.

Biện pháp khắc phục cho mọi hành vi không xác định là kiểm tra hành vi mà bạn thực sự có được, được cung cấp một nền tảng và trình biên dịch cụ thể. Loại kiểm tra đó không phải là một bài kiểm tra mà một lập trình viên chuyên gia có thể khám phá cho bạn trong một thiết lập Hỏi & Đáp.


+1 Như @aschepler đã giải thích rõ hơn tôi, các chi tiết cụ thể về hành vi không xác định có xu hướng được quan tâm trong quá trình gỡ lỗi. Nếu đơn vị của tôi kiểm tra segfaults và tôi hiểu cơ chế quản lý bộ nhớ tạo ra segfaults, thì tôi có thể gỡ lỗi chương trình của mình nhanh hơn. Tất nhiên bạn đã đúng: thật khó để nghĩ về một trường hợp trong đó người ta cố tình gọi UB trong mã hoàn thành!
THB

1
Bạn bỏ lỡ "với các tùy chọn biên dịch khác nhau". Luôn vui vẻ khi các phiên bản Phát triển / Thử nghiệm / Phát hành hoạt động khác nhau.
Henk Holterman

1
Hoặc thậm chí "có thể tạo ra các kết quả khác nhau trong các lần chạy liên tiếp của cùng một nhị phân, kết quả từ một quá trình biên dịch duy nhất".
Vatine

Hành vi không xác định đôi khi có ý nghĩa, và đôi khi có nghĩa là "Hành vi hành động này nên hoạt động giống hệt nhau trên tất cả các triển khai cho các nền tảng mà chúng tôi biết, nhưng sẽ được phép hành xử khác nhau trên các nền tảng có vấn đề; hành vi bình thường trên các nền tảng phổ biến vì các nhà văn trình biên dịch không cố tình che giấu sẽ xử lý mọi thứ theo cách mà Tiêu chuẩn có yêu cầu họ ". Một ví dụ về cái sau sẽ là (-1)<<1C89 được định nghĩa là -2 trên các nền tảng sử dụng phần bổ sung hai phần không đệm ...
supercat

... các loại số nguyên, nhưng C99 coi là Hành vi không xác định mà không đưa ra bất kỳ lý do nào cho sự thay đổi. Nếu một người diễn giải ý nghĩa dự định như trên, thì đó sẽ không phải là một thay đổi đột phá ngoại trừ trên các nền tảng mà hành vi C89 là không thực tế nhưng dù sao một số mã vẫn dựa vào nó.
supercat

1

Nếu tài liệu cho một trình biên dịch cụ thể cho biết nó sẽ làm gì khi mã thực hiện một thứ được coi là "Hành vi không xác định" theo tiêu chuẩn, thì mã dựa trên hành vi đó sẽ hoạt động chính xác khi được biên dịch với trình biên dịch đó , nhưng có thể hành xử theo cách tùy ý khi được biên dịch bằng một số trình biên dịch khác có tài liệu không chỉ định hành vi.

Nếu tài liệu cho trình biên dịch không chỉ định cách nó sẽ xử lý một số "hành vi không xác định" cụ thể, thì thực tế là hành vi của chương trình dường như tuân theo các quy tắc nhất định không nói về cách mọi chương trình tương tự sẽ hoạt động. Bất kỳ yếu tố nào cũng có thể khiến trình biên dịch phát ra mã xử lý các tình huống bất ngờ khác nhau - đôi khi theo cách có vẻ kỳ quái.

Ví dụ, hãy xem xét trên một máy có intsố nguyên 32 bit:

int undef_behavior_example(uint16_t size1, uint16_t size2)
{
  int flag = 0;
  if ((uint32_t)size1 * size2 > 2147483647u)
    flag += 1;
  if (((size1*size2) & 127) != 0) // Test whether product is a multiple of 128
    flag += 2;
  return flag;
}

Nếu size1size2cả hai đều bằng 46341 (sản phẩm của họ là 2147488281) người ta có thể mong đợi rằng hàm sẽ trả về 3, nhưng một trình biên dịch có thể bỏ qua hoàn toàn thử nghiệm đầu tiên; hoặc sản phẩm sẽ đủ nhỏ để điều kiện sai, hoặc phép nhân sắp tới sẽ tràn và giải phóng trình biên dịch của bất kỳ yêu cầu nào để làm, hoặc đã làm, bất cứ điều gì. Trong khi hành vi như vậy có vẻ kỳ quái, một số tác giả trình biên dịch dường như rất tự hào về khả năng của trình biên dịch của họ để loại bỏ các bài kiểm tra "không cần thiết" như vậy. Một số người có thể hy vọng rằng việc tràn vào bội số thứ hai, tệ nhất là, sẽ khiến tất cả các bit của sản phẩm cụ thể đó bị hỏng tùy ý; trên thực tế, tuy nhiên,


Phép nhân sẽ không được thực hiện modulo UINT16_MAX?
tò mò

@cperedguy: Nếu intlà số nguyên 32 bit, thì các giá trị loại uint16_tsẽ được thăng cấp inttrước bất kỳ tính toán nào liên quan đến chúng. Một quy tắc thường sẽ ổn nếu việc triển khai chỉ xử lý số học đã ký khác với không dấu trong trường hợp chúng có các hành vi được xác định khác nhau.
supercat

Tôi tin rằng bất kỳ toán hạng loại không dấu nào khiến thao tác không được ký.
tò mò

@cquilguy: Một số trình biên dịch đã hoạt động theo cách đó trong những ngày trước Tiêu chuẩn, nhưng Tiêu chuẩn chỉ định rằng các loại không dấu xếp hạng bên dưới unsignedvà có phạm vi giá trị sẽ hoàn toàn phù hợp với điều đó int, được thăng cấp lên một chữ ký int.
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.