Có phải tiêu chuẩn C ++ cho phép một bool chưa được khởi tạo để làm hỏng chương trình không?


500

Tôi biết rằng một "hành vi không xác định" trong C ++ có thể cho phép trình biên dịch thực hiện bất cứ điều gì nó muốn. Tuy nhiên, tôi đã gặp sự cố khiến tôi ngạc nhiên, vì tôi cho rằng mã này đủ an toàn.

Trong trường hợp này, sự cố thực sự chỉ xảy ra trên một nền tảng cụ thể bằng trình biên dịch cụ thể và chỉ khi bật tối ưu hóa.

Tôi đã thử một vài thứ để tái tạo vấn đề và đơn giản hóa nó đến mức tối đa. Đây là một trích xuất của một hàm được gọi Serialize, sẽ lấy tham số bool và sao chép chuỗi truehoặc falsevào bộ đệm đích hiện có.

Hàm này có được xem xét mã không, có cách nào để nói rằng trên thực tế, nó có thể bị sập nếu tham số bool là một giá trị chưa được khởi tạo không?

// Zero-filled global buffer of 16 characters
char destBuffer[16];

void Serialize(bool boolValue) {
    // Determine which string to print based on boolValue
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    const size_t len = strlen(whichString);

    // Copy string into destination buffer, which is zero-filled (thus already null-terminated)
    memcpy(destBuffer, whichString, len);
}

Nếu mã này được thực thi với tối ưu hóa 5.0.0 clang, nó sẽ / có thể bị sập.

Nhà điều hành ternary dự kiến ​​có boolValue ? "true" : "false"vẻ đủ an toàn đối với tôi, tôi đã giả sử, "Dù giá trị rác là boolValuegì không quan trọng, vì dù sao nó cũng sẽ đánh giá là đúng hay sai."

Tôi đã thiết lập một ví dụ Compiler Explorer cho thấy vấn đề trong quá trình tháo gỡ, đây là ví dụ hoàn chỉnh. Lưu ý: để khắc phục sự cố, sự kết hợp mà tôi thấy có hiệu quả là sử dụng Clang 5.0.0 với tối ưu hóa -O2.

#include <iostream>
#include <cstring>

// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
    bool uninitializedBool;

   __attribute__ ((noinline))  // Note: the constructor must be declared noinline to trigger the problem
   FStruct() {};
};

char destBuffer[16];

// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
    // Determine which string to print depending if 'boolValue' is evaluated as true or false
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    size_t len = strlen(whichString);

    memcpy(destBuffer, whichString, len);
}

int main()
{
    // Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
    FStruct structInstance;

    // Output "true" or "false" to stdout
    Serialize(structInstance.uninitializedBool);
    return 0;
}

Vấn đề phát sinh là do trình tối ưu hóa: đủ thông minh để suy ra rằng các chuỗi "đúng" và "sai" chỉ khác nhau về độ dài bằng 1. Vì vậy, thay vì thực sự tính toán độ dài, nó sử dụng giá trị của chính bool, nên về mặt kỹ thuật là 0 hoặc 1 và đi như thế này:

const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue;       // clang clever optimization

Mặc dù điều này là "thông minh", nhưng có thể nói, câu hỏi của tôi là: Liệu tiêu chuẩn C ++ có cho phép trình biên dịch giả sử một bool chỉ có thể có biểu diễn số bên trong là '0' hoặc '1' và sử dụng nó theo cách như vậy không?

Hoặc đây có phải là một trường hợp được xác định theo thực thi, trong trường hợp đó, việc triển khai giả định rằng tất cả các bool của nó sẽ chỉ chứa 0 hoặc 1, và bất kỳ giá trị nào khác là lãnh thổ hành vi không xác định?


200
Đó là một câu hỏi tuyệt vời. Đó là một minh họa chắc chắn về cách hành vi không xác định không chỉ là mối quan tâm về mặt lý thuyết. Khi mọi người nói bất cứ điều gì có thể xảy ra là kết quả của UB, rằng "bất cứ điều gì" thực sự có thể khá đáng ngạc nhiên. Người ta có thể cho rằng hành vi không xác định vẫn biểu hiện theo những cách có thể dự đoán được, nhưng ngày nay với các trình tối ưu hóa hiện đại không hoàn toàn đúng. OP đã dành thời gian để tạo ra một MCVE, điều tra vấn đề kỹ lưỡng, kiểm tra việc tháo gỡ và hỏi một câu hỏi rõ ràng, đơn giản về nó. Không thể yêu cầu thêm.
John Kugelman

7
Quan sát rằng yêu cầu mà không phải là 0 đánh giá đối với điều đó truelà một quy tắc về các hoạt động của Boolean, bao gồm cả việc gán cho một bool Cảnh (có thể ngầm gọi một static_cast<bool>()tùy thuộc vào chi tiết cụ thể). Tuy nhiên, đây không phải là một yêu cầu về biểu diễn bên booltrong của trình biên dịch được chọn.
Euro Micelli

2
Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Samuel Liew

3
Trên một lưu ý rất liên quan, đây là một nguồn "không tương thích" vui nhộn. Nếu bạn có ABI A có giá trị pad 0 trước khi gọi hàm, nhưng biên dịch các hàm sao cho giả sử tham số là không đệm và ABI B thì ngược lại (không phải là zero-pad, nhưng không giả sử zero tham số được đệm), phần lớn sẽ hoạt động, nhưng một chức năng sử dụng B ABI sẽ gây ra vấn đề nếu nó gọi một chức năng sử dụng A ABI có tham số 'nhỏ'. IIRC bạn có cái này trên x86 với tiếng kêu và ICC.
TLW

1
@TLW: Mặc dù Tiêu chuẩn không yêu cầu việc triển khai cung cấp bất kỳ phương tiện gọi hoặc được gọi bởi mã bên ngoài, nhưng sẽ rất hữu ích khi có một phương tiện chỉ định những điều đó cho việc triển khai khi chúng có liên quan (việc triển khai không có chi tiết như vậy có liên quan có thể bỏ qua các thuộc tính như vậy).
supercat

Câu trả lời:


285

Có, ISO C ++ cho phép (nhưng không yêu cầu) triển khai để đưa ra lựa chọn này.

Nhưng cũng lưu ý rằng ISO C ++ cho phép trình biên dịch phát ra mã bị lỗi nhằm mục đích (ví dụ: với một lệnh bất hợp pháp) nếu chương trình gặp UB, ví dụ như một cách giúp bạn tìm lỗi. (Hoặc bởi vì đó là DeathStation 9000. Việc tuân thủ nghiêm ngặt là không đủ để triển khai C ++ có ích cho bất kỳ mục đích thực tế nào). Vì vậy, ISO C ++ sẽ cho phép một trình biên dịch tạo ra asm bị lỗi (vì những lý do hoàn toàn khác nhau) ngay cả trên mã tương tự đọc chưa được khởi tạo uint32_t. Mặc dù đó là loại bố cục cố định không có biểu diễn bẫy.

Đó là một câu hỏi thú vị về cách thức triển khai thực tế hoạt động, nhưng hãy nhớ rằng ngay cả khi câu trả lời khác nhau, mã của bạn vẫn không an toàn vì C ++ hiện đại không phải là phiên bản di động của ngôn ngữ lắp ráp.


Bạn đang biên dịch cho Hệ thống V86I x86-64 , chỉ định rằng một hàm boolnhư một hàm trong một thanh ghi được biểu diễn bằng các mẫu bit false=0true=1 trong 8 bit thấp của thanh ghi 1 . Trong bộ nhớ, boollà loại 1 byte một lần nữa phải có giá trị nguyên là 0 hoặc 1.

(ABI là một tập hợp các lựa chọn triển khai mà trình biên dịch cho cùng một nền tảng đồng ý để chúng có thể tạo mã gọi các hàm của nhau, bao gồm kích thước loại, quy tắc bố cục cấu trúc và quy ước gọi.)

ISO C ++ không chỉ định nó, nhưng quyết định ABI này được phổ biến rộng rãi vì nó làm cho chuyển đổi bool-> int trở nên rẻ (chỉ là phần mở rộng bằng không) . Tôi không biết bất kỳ ABI nào không cho phép trình biên dịch giả sử 0 hoặc 1 boolcho bất kỳ kiến ​​trúc nào (không chỉ x86). Nó cho phép tối ưu hóa như !myboolvới xor eax,1việc lật bit thấp: Bất kỳ mã nào có thể có thể lật một bit / số nguyên / bool trong khoảng từ 0 đến 1 trong một lệnh CPU . Hoặc biên dịch a&&bthành bitwise AND cho boolcác loại. Một số trình biên dịch thực sự tận dụng các giá trị Boolean là 8 bit trong trình biên dịch. Là hoạt động trên chúng không hiệu quả? .

Nói chung, quy tắc as-if cho phép trình biên dịch tận dụng những điều có thật trên nền tảng đích đang được biên dịch , vì kết quả cuối cùng sẽ là mã thực thi thực hiện cùng một hành vi có thể nhìn thấy bên ngoài như nguồn C ++. (Với tất cả các hạn chế mà Hành vi không xác định đặt vào thứ thực sự "hiển thị bên ngoài": không phải với trình gỡ lỗi, mà từ một luồng khác trong chương trình C ++ được hình thành / hợp pháp.)

Trình biên dịch chắc chắn được phép tận dụng tối đa một bảo lãnh ABI trong nó mã gen, và làm cho mã như bạn thấy đó tối ưu hóa strlen(whichString)để
5U - boolValue.
(BTW, tối ưu hóa này là loại thông minh, nhưng có thể thiển cận so với phân nhánh và nội tuyến memcpylà kho lưu trữ dữ liệu ngay lập tức 2. )

Hoặc trình biên dịch có thể đã tạo một bảng các con trỏ và lập chỉ mục nó với giá trị nguyên của boolmột lần nữa, giả sử nó là 0 hoặc 1. ( Khả năng này là câu trả lời của @ Barmar .)


Trình __attribute((noinline))xây dựng của bạn với tối ưu hóa được kích hoạt dẫn đến tiếng kêu chỉ cần tải một byte từ ngăn xếp để sử dụng như uninitializedBool. Nó tạo không gian cho đối tượng mainvới push rax(nhỏ hơn và vì nhiều lý do khác nhau về hiệu quả như sub rsp, 8), do đó, bất kỳ rác nào trong AL khi nhập vào mainđều là giá trị mà nó sử dụng uninitializedBool. Đây là lý do tại sao bạn thực sự có các giá trị không chỉ 0.

5U - random garbagecó thể dễ dàng bọc đến một giá trị không dấu lớn, dẫn memcpy đi vào bộ nhớ chưa được ánh xạ. Đích nằm trong bộ nhớ tĩnh, không phải ngăn xếp, vì vậy bạn không ghi đè địa chỉ trả lại hoặc thứ gì đó.


Các triển khai khác có thể đưa ra các lựa chọn khác nhau, ví dụ false=0true=any non-zero value. Sau đó, clang có thể sẽ không tạo ra mã bị lỗi cho trường hợp cụ thể này của UB. (Nhưng nó vẫn được phép nếu nó muốn.) Tôi không biết bất kỳ triển khai nào chọn bất cứ thứ gì khác mà x86-64 làm bool, nhưng tiêu chuẩn C ++ cho phép nhiều thứ mà không ai muốn hoặc thậm chí không muốn làm phần cứng đó là bất cứ thứ gì như CPU ​​hiện tại.

ISO C ++ khiến nó không xác định những gì bạn sẽ tìm thấy khi kiểm tra hoặc sửa đổi biểu diễn đối tượng của abool . (ví dụ bởi memcpying boolvào unsigned char, mà bạn được phép làm vì char*lon bí danh bất cứ điều gì. Và unsigned charđược đảm bảo để không có bit đệm, vì vậy chuẩn C ++ không chính thức cho phép bạn hexdump đại diện đối tượng mà không cần bất kỳ UB. Pointer-casting để sao chép các đối tượng đại diện khác với việc gán char foo = my_bool, tất nhiên, vì vậy booleanization thành 0 hoặc 1 sẽ không xảy ra và bạn sẽ có được đại diện đối tượng thô.)

Bạn đã một phần "ẩn" các UB trên con đường thực hiện điều này từ trình biên dịch vớinoinline . Tuy nhiên, ngay cả khi nó không nội tuyến, tối ưu hóa liên vùng vẫn có thể tạo ra một phiên bản của hàm phụ thuộc vào định nghĩa của hàm khác. (Đầu tiên, clang đang thực hiện một thư viện thực thi, không phải là thư viện chia sẻ Unix nơi có thể xảy ra sự xen kẽ biểu tượng. Thứ hai, định nghĩa bên trong class{}định nghĩa để tất cả các đơn vị dịch phải có cùng định nghĩa. Giống như với inlinetừ khóa.)

Vì vậy, một trình biên dịch có thể chỉ phát ra một rethoặc ud2(hướng dẫn bất hợp pháp) như định nghĩa cho main, bởi vì đường dẫn thực hiện bắt đầu ở đầu các maincuộc gặp gỡ không thể tránh khỏi Hành vi không xác định. (Trình biên dịch có thể thấy tại thời điểm biên dịch nếu nó quyết định đi theo đường dẫn thông qua hàm tạo không nội tuyến.)

Bất kỳ chương trình nào gặp UB đều hoàn toàn không xác định cho toàn bộ sự tồn tại của nó. Nhưng UB bên trong một chức năng hoặc if()nhánh không bao giờ thực sự chạy không làm hỏng phần còn lại của chương trình. Trong thực tế điều đó có nghĩa là trình biên dịch có thể quyết định phát ra một lệnh bất hợp pháp, hoặc a ret, hoặc không phát ra bất cứ thứ gì và rơi vào khối / hàm tiếp theo, cho toàn bộ khối cơ bản có thể được chứng minh tại thời điểm biên dịch để chứa hoặc dẫn đến UB.

GCC và Clang trong thực tế làm thực sự đôi khi phát ra ud2trên UB, thay vì thậm chí cố gắng để tạo mã cho đường dẫn thực hiện mà làm cho không có ý nghĩa. Hoặc đối với các trường hợp như rơi ra khỏi phần cuối của voidhàm không hoạt động, gcc đôi khi sẽ bỏ qua một retlệnh. Nếu bạn đã nghĩ rằng "chức năng của tôi sẽ trở lại với bất kỳ rác nào trong RAX", thì bạn đã nhầm. Trình biên dịch C ++ hiện đại không coi ngôn ngữ như ngôn ngữ lắp ráp di động nữa. Chương trình của bạn thực sự phải là C ++ hợp lệ, mà không đưa ra các giả định về cách một phiên bản độc lập không nội tuyến của chức năng của bạn có thể trông như thế nào.

Một ví dụ thú vị khác là tại sao đôi khi truy cập không được phân bổ vào bộ nhớ mmap'ed đôi khi lại bị lỗi trên AMD64? . x86 không có lỗi trên các số nguyên không được phân bổ, phải không? Vì vậy, tại sao một sai lệch uint16_t*sẽ là một vấn đề? Bởi vì alignof(uint16_t) == 2, và vi phạm giả định đó đã dẫn đến một segfault khi tự động vector hóa với SSE2.

Xem thêm 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 # 1/3 , một bài viết của nhà phát triển clang.

Điểm mấu chốt: nếu trình biên dịch nhận thấy UB vào thời gian biên dịch, nó có thể "phá vỡ" (phát ra asm đáng ngạc nhiên) đường dẫn qua mã của bạn gây ra UB ngay cả khi nhắm mục tiêu ABI trong đó bất kỳ mẫu bit nào là đại diện cho đối tượng hợp lệ bool.

Mong đợi sự thù địch hoàn toàn đối với nhiều sai lầm của lập trình viên, đặc biệt là những điều mà trình biên dịch hiện đại cảnh báo. Đây là lý do tại sao bạn nên sử dụng -Wallvà sửa chữa các cảnh báo. C ++ không phải là ngôn ngữ thân thiện với người dùng và một cái gì đó trong C ++ có thể không an toàn ngay cả khi nó sẽ an toàn trong asm trên mục tiêu bạn đang biên dịch. (ví dụ: tràn tràn đã ký là UB trong C ++ và trình biên dịch sẽ cho rằng điều đó không xảy ra, ngay cả khi biên dịch cho phần bù x86 của 2, trừ khi bạn sử dụng clang/gcc -fwrapv.)

UB biên dịch theo thời gian biên dịch luôn nguy hiểm và thật khó để chắc chắn (với tối ưu hóa thời gian liên kết) rằng bạn đã thực sự ẩn UB khỏi trình biên dịch và do đó có thể suy luận về loại asm nào sẽ tạo ra.

Không được quá kịch tính; thường các trình biên dịch sẽ cho phép bạn thoát khỏi một số thứ và phát ra mã như bạn mong đợi ngay cả khi có thứ gì đó là UB. Nhưng có thể nó sẽ là một vấn đề trong tương lai nếu các nhà biên dịch triển khai một số tối ưu hóa để có thêm thông tin về phạm vi giá trị (ví dụ: một biến không âm, có thể cho phép nó tối ưu hóa tiện ích mở rộng ký hiệu thành tiện ích mở rộng miễn phí trên x86- 64). Ví dụ, trong gcc và clang hiện tại, việc tmp = a+INT_MINkhông tối ưu hóa a<0luôn luôn là sai, chỉ có điều đó tmpluôn luôn âm. (Vì INT_MIN+ a=INT_MAXlà âm trên mục tiêu bổ sung của 2 này và akhông thể cao hơn mục tiêu đó.)

Vì vậy, gcc / clang hiện không quay lại để lấy thông tin phạm vi cho các đầu vào của phép tính, chỉ dựa trên kết quả dựa trên giả định không có tràn tràn đã ký: ví dụ trên Godbolt . Tôi không biết nếu đây là tối ưu hóa có chủ ý "bỏ qua" trong tên thân thiện với người dùng hay không.

Cũng lưu ý rằng việc triển khai (còn gọi là trình biên dịch) được phép xác định hành vi mà ISO C ++ không xác định . Ví dụ: tất cả các trình biên dịch hỗ trợ nội tại của Intel (như _mm_add_ps(__m128, __m128)đối với vectơ SIMD thủ công) phải cho phép hình thành các con trỏ căn chỉnh sai, đó là UB trong C ++ ngay cả khi bạn không tham gia chúng. __m128i _mm_loadu_si128(const __m128i *)không tải không được phân bổ bằng cách lấy một đối số không đúng __m128i*, không phải là một void*hoặc char*. Là `reinterpret_cast`ing giữa con trỏ vectơ phần cứng và loại tương ứng là một hành vi không xác định?

GNU C / C ++ cũng định nghĩa hành vi dịch chuyển trái của một số đã ký âm (thậm chí không có -fwrapv), tách biệt với các quy tắc UB tràn ký thông thường. ( Đây là UB trong ISO C ++ , trong khi các thay đổi bên phải của các số đã ký được xác định theo triển khai (logic so với số học); triển khai chất lượng tốt chọn số học trên CTNH có dịch chuyển đúng số học, nhưng ISO C ++ không chỉ định). Điều này được ghi lại trong phần Integer của hướng dẫn sử dụng GCC , cùng với việc xác định hành vi được xác định thực hiện mà các tiêu chuẩn C yêu cầu triển khai để xác định cách này hay cách khác.

Chắc chắn có các vấn đề về chất lượng thực hiện mà các nhà phát triển trình biên dịch quan tâm; họ thường không cố gắng tạo ra các trình biên dịch có chủ ý thù địch, nhưng tận dụng tất cả các ổ gà UB trong C ++ (ngoại trừ các trình biên dịch mà họ chọn để xác định) để tối ưu hóa đôi khi gần như không thể phân biệt được.


Chú thích 1 : 56 bit trên có thể là rác mà callee phải bỏ qua, như thường lệ đối với các loại hẹp hơn so với thanh ghi.

( Các ABI khác thực hiện các lựa chọn khác nhau ở đây . Một số yêu cầu các loại số nguyên hẹp phải bằng 0 hoặc mở rộng đăng nhập để điền vào một thanh ghi khi được chuyển đến hoặc trả về từ các hàm, như MIPS64 và PowerPC64. Xem phần cuối của câu trả lời x86-64 này so sánh với các ISA trước đó .)

Ví dụ, một người gọi có thể đã tính toán a & 0x01010101trong RDI và sử dụng nó cho việc khác trước khi gọi bool_func(a&1). Người gọi có thể tối ưu hóa đi &1vì nó đã làm điều đó với byte thấp như một phần của and edi, 0x01010101nó và nó biết rằng cần có callee để bỏ qua các byte cao.

Hoặc nếu một bool được truyền dưới dạng đối số thứ 3, có thể một người gọi tối ưu hóa cho kích thước mã tải nó mov dl, [mem]thay vì movzx edx, [mem], tiết kiệm 1 byte với chi phí phụ thuộc sai vào giá trị cũ của RDX (hoặc hiệu ứng đăng ký một phần khác, tùy thuộc vào trên mô hình CPU). Hoặc cho đối số đầu tiên, mov dil, byte [r10]thay vì movzx edi, byte [r10], vì cả hai đều yêu cầu tiền tố REX.

Đây là lý do tại sao phát ra vang movzx eax, diltrong Serialize, thay vì sub eax, edi. (Đối với các số nguyên, clang vi phạm quy tắc ABI này, thay vào đó tùy thuộc vào hành vi không có giấy tờ của gcc và clang thành số nguyên hẹp 0 hoặc ký hiệu mở rộng thành 32 bit. Là một phần mở rộng dấu hoặc không cần thiết khi thêm phần bù 32 bit vào con trỏ cho x86-64 ABI? Vì vậy, tôi rất thích thú khi thấy nó không làm điều tương tự bool.)


Chú thích 2: Sau khi phân nhánh, bạn chỉ cần có 4 byte movngay lập tức hoặc lưu trữ 4 byte + 1 byte. Độ dài được ẩn trong chiều rộng cửa hàng + độ lệch.

OTOH, glcc memcpy sẽ thực hiện hai lần tải / lưu trữ 4 byte với sự chồng chéo phụ thuộc vào độ dài, do đó, điều này thực sự sẽ làm cho toàn bộ mọi thứ không có các nhánh có điều kiện trên boolean. Xem L(between_4_7):khối trong memcpy / memmove của glibc. Hoặc ít nhất, đi theo cùng một cách cho boolean trong phân nhánh của memcpy để chọn kích thước khối.

Nếu nội tuyến, bạn có thể sử dụng movgấp 2 lần + cmovvà bù có điều kiện hoặc bạn có thể để dữ liệu chuỗi trong bộ nhớ.

Hoặc nếu điều chỉnh cho Intel Ice Lake ( với tính năng Fast Short REP MOV ), thực tế rep movsbcó thể là tối ưu. glibc memcpycó thể bắt đầu sử dụng rep movsb cho các kích thước nhỏ trên CPU có tính năng đó, tiết kiệm rất nhiều phân nhánh.


Công cụ phát hiện UB và sử dụng các giá trị chưa được khởi tạo

Trong gcc và clang, bạn có thể biên dịch -fsanitize=undefinedđể thêm công cụ thời gian chạy sẽ cảnh báo hoặc lỗi trên UB xảy ra trong thời gian chạy. Điều đó sẽ không bắt các biến đơn vị, mặc dù. (Bởi vì nó không tăng kích thước loại để nhường chỗ cho bit "chưa được khởi tạo").

Xem https://developers.redhat.com/blog/2014/10/16/gcc-undDef-behavior-sanitizer-ubsan/

Để tìm cách sử dụng dữ liệu chưa được khởi tạo, có Bộ khử trùng địa chỉ và Bộ khử trùng bộ nhớ trong clang / LLVM. https://github.com/google/sanitulators/wiki/MemorySanitizer hiển thị các ví dụ về clang -fsanitize=memory -fPIE -pieviệc phát hiện các lần đọc bộ nhớ chưa được khởi tạo. Nó có thể hoạt động tốt nhất nếu bạn biên dịch mà không tối ưu hóa, vì vậy tất cả các lần đọc các biến cuối cùng thực sự tải từ bộ nhớ trong asm. Chúng cho thấy nó đang được sử dụng -O2trong trường hợp tải không tối ưu hóa. Tôi đã không thử bản thân mình. (Trong một số trường hợp, vd . Nhưng-fsanitize=memory thay đổi mã asm được tạo và có thể dẫn đến kiểm tra cho việc này.)

Nó sẽ chấp nhận sao chép bộ nhớ chưa được khởi tạo, và các thao tác logic và số học đơn giản với nó. Nói chung, MemorySanitizer âm thầm theo dõi sự lây lan của dữ liệu chưa được khởi tạo trong bộ nhớ và báo cáo cảnh báo khi một nhánh mã được lấy (hoặc không lấy) tùy thuộc vào giá trị chưa được khởi tạo.

MemorySanitizer thực hiện một tập hợp con các chức năng được tìm thấy trong Valgrind (công cụ Memcheck).

Nó sẽ hoạt động trong trường hợp này bởi vì lệnh gọi glibc memcpyvới lengthtính toán từ bộ nhớ chưa được khởi tạo sẽ (bên trong thư viện) dẫn đến một nhánh dựa trên length. Nếu nó đã nội tuyến một phiên bản hoàn toàn không phân nhánh chỉ sử dụng cmov, lập chỉ mục và hai cửa hàng, thì nó có thể không hoạt động.

Valgrindmemcheck cũng sẽ tìm kiếm loại vấn đề này, một lần nữa không phàn nàn nếu chương trình chỉ đơn giản là sao chép xung quanh dữ liệu chưa được khởi tạo. Nhưng nó nói rằng nó sẽ phát hiện khi "Nhảy hoặc di chuyển có điều kiện phụ thuộc vào (các) giá trị chưa được khởi tạo", để cố gắng bắt bất kỳ hành vi có thể nhìn thấy bên ngoài nào phụ thuộc vào dữ liệu chưa được khởi tạo.

Có lẽ ý tưởng đằng sau việc không gắn cờ chỉ là một tải là các cấu trúc có thể có phần đệm và sao chép toàn bộ cấu trúc (bao gồm cả phần đệm) với tải / vectơ rộng không phải là lỗi ngay cả khi các thành viên riêng lẻ chỉ được viết một lần. Ở cấp độ asm, thông tin về phần đệm và phần thực sự của giá trị đã bị mất.


2
Tôi đã thấy một trường hợp tồi tệ hơn khi biến đó lấy một giá trị không nằm trong phạm vi số nguyên 8 bit, mà chỉ trong toàn bộ thanh ghi CPU. Và Itanium có một điều tồi tệ hơn nữa, việc sử dụng một biến chưa được khởi tạo có thể sụp đổ hoàn toàn.
Joshua

2
@Joshua: oh đúng rồi, điểm tốt, suy đoán rõ ràng của Itanium sẽ gắn thẻ các giá trị đăng ký với giá trị tương đương "không phải là số", chẳng hạn như sử dụng các lỗi giá trị.
Peter Cordes

11
Hơn nữa, điều này cũng minh họa tại sao UB featurebug được giới thiệu trong thiết kế ngôn ngữ C và C ++ ngay từ đầu: bởi vì nó mang lại cho trình biên dịch chính xác loại tự do này, hiện đã cho phép các trình biên dịch hiện đại nhất thực hiện các trình biên dịch hiện đại nhất này. tối ưu hóa làm cho C / C ++ trở thành ngôn ngữ trung cấp hiệu suất cao như vậy.
The_Sympathizer

2
Và thế là cuộc chiến giữa các nhà văn biên dịch C ++ và các lập trình viên C ++ cố gắng viết các chương trình hữu ích vẫn tiếp tục. Câu trả lời này, hoàn toàn toàn diện trong việc trả lời câu hỏi này, cũng có thể được sử dụng như là bản sao quảng cáo thuyết phục cho các nhà cung cấp công cụ phân tích tĩnh ...
davidbak

4
@The_Sympathizer: Đã bao gồm UB để cho phép các triển khai thực hiện theo bất kỳ cách nào có ích nhất cho khách hàng của họ . Nó không có ý định cho rằng tất cả các hành vi nên được coi là hữu ích như nhau.
supercat

56

Trình biên dịch được phép giả định rằng giá trị boolean được truyền dưới dạng đối số là giá trị boolean hợp lệ (nghĩa là giá trị đã được khởi tạo hoặc chuyển đổi thành truehoặc false). Các truegiá trị không phải là giống như các số nguyên 1 - thực sự, có thể có cơ quan đại diện khác nhau của truefalse- nhưng các tham số phải có một số đại diện hợp lệ của một trong hai giá trị, trong đó "đại diện hợp lệ" là implementation- xác định.

Vì vậy, nếu bạn không khởi tạo a boolhoặc nếu bạn thành công trong việc ghi đè nó thông qua một số con trỏ thuộc loại khác, thì các giả định của trình biên dịch sẽ sai và Hành vi không xác định sẽ xảy ra. Bạn đã được cảnh báo:

50) Sử dụng giá trị bool theo các cách được mô tả theo Tiêu chuẩn quốc tế này như là không xác định được, như bằng cách kiểm tra giá trị của một đối tượng tự động chưa được khởi tạo, có thể khiến nó hoạt động như thể nó không đúng cũng không sai. (Chú thích cho đoạn 6 của §6.9.1, Các loại cơ bản)


11
" trueGiá trị không nhất thiết phải giống với số nguyên 1" là loại sai lệch. Chắc chắn, mẫu bit thực tế có thể là một cái gì đó khác, nhưng khi được chuyển đổi / quảng bá ngầm (cách duy nhất bạn thấy một giá trị khác true/ false), trueluôn luôn 1falseluôn luôn0 . Tất nhiên, trình biên dịch như vậy cũng sẽ không thể sử dụng thủ thuật mà trình biên dịch này đang cố sử dụng (sử dụng thực tế boollà mẫu bit thực tế chỉ có thể 0hoặc 1), do đó, nó không liên quan đến vấn đề của OP.
ShadowRanger

4
@ShadowRanger Bạn luôn có thể kiểm tra trực tiếp đối tượng.
TC

7
@shadowranger: quan điểm của tôi là việc thực hiện có trách nhiệm. Nếu nó giới hạn các biểu diễn hợp lệ của truemẫu bit 1, thì đó là đặc quyền của nó. Nếu nó chọn một số bộ đại diện khác, thì nó thực sự không thể sử dụng tối ưu hóa được ghi chú ở đây. Nếu nó chọn đại diện cụ thể, thì nó có thể. Nó chỉ cần được nhất quán trong nội bộ. Bạn có thể kiểm tra biểu diễn của a boolbằng cách sao chép nó vào một mảng byte; đó không phải là UB (nhưng nó là thực hiện xác định)
rici

3
Có, tối ưu hóa trình biên dịch (tức là triển khai C ++ trong thế giới thực) đôi khi sẽ phát ra mã phụ thuộc vào boolviệc có một mẫu bit 0hoặc 1. Họ không tái phân tích lại boolmỗi lần họ đọc nó từ bộ nhớ (hoặc một thanh ghi giữ một hàm arg). Đó là những gì câu trả lời này đang nói. ví dụ : gcc4.7 + có thể tối ưu hóa return a||bđể or eax, editrong một hàm trả về bool, hoặc MSVC thể tối ưu hóa a&bđể test cl, dl. x86 testlà một bitwise and , do đó, nếu cl=1dl=2kiểm tra đặt cờ theo cl&dl = 0.
Peter Cordes

5
Điểm quan trọng về hành vi không xác định là trình biên dịch được phép đưa ra nhiều kết luận hơn về nó, ví dụ như giả sử rằng một đường dẫn mã sẽ dẫn đến việc truy cập một giá trị chưa được khởi tạo hoàn toàn, vì đảm bảo rằng đó chính xác là trách nhiệm của lập trình viên . Vì vậy, không chỉ về khả năng các giá trị cấp thấp có thể khác 0 hoặc 1.
Holger

52

Bản thân hàm là đúng, nhưng trong chương trình thử nghiệm của bạn, câu lệnh gọi hàm gây ra hành vi không xác định bằng cách sử dụng giá trị của một biến chưa được khởi tạo.

Lỗi này nằm ở chức năng gọi và nó có thể được phát hiện bằng cách xem lại mã hoặc phân tích tĩnh của chức năng gọi. Sử dụng liên kết trình thám hiểm trình biên dịch của bạn, trình biên dịch gcc 8.2 sẽ phát hiện ra lỗi. (Có lẽ bạn có thể nộp báo cáo lỗi chống lại tiếng kêu mà nó không tìm thấy vấn đề).

Hành vi không xác định có nghĩa là bất cứ điều gì có thể xảy ra, bao gồm chương trình bị sập một vài dòng sau khi sự kiện kích hoạt hành vi không xác định.

Lưu ý Câu trả lời cho "Hành vi không xác định có thể gây ra _____?" luôn luôn là "Có". Đó đúng là định nghĩa của hành vi không xác định.


2
Là mệnh đề đầu tiên có đúng không? Có phải chỉ đơn thuần là sao chép một boolkích hoạt chưa được khởi tạo UB?
Joshua Green

10
@JoshuaGreen xem [dcl.init] / 12 "Nếu một giá trị không xác định được tạo ra bởi một đánh giá, hành vi không được xác định ngoại trừ trong các trường hợp sau:" (và không có trường hợp nào trong số đó có ngoại lệ bool). Sao chép yêu cầu đánh giá nguồn
MM

8
@JoshuaGreen Và lý do cho điều đó là bạn có thể có một nền tảng gây ra lỗi phần cứng nếu bạn truy cập một số giá trị không hợp lệ cho một số loại. Chúng đôi khi được gọi là "đại diện bẫy".
David Schwartz

7
Itanium, trong khi tối nghĩa, là một CPU vẫn đang được sản xuất, có các giá trị bẫy và có ít nhất hai trình biên dịch C ++ bán hiện đại (Intel / HP). Nó thực sự có true, falsenot-a-thingcác giá trị cho booleans.
MSalters

3
Mặt khác, câu trả lời cho "Tiêu chuẩn có yêu cầu tất cả các trình biên dịch xử lý một cái gì đó theo một cách nhất định" thường là "không", thậm chí / đặc biệt trong trường hợp rõ ràng là bất kỳ trình biên dịch chất lượng nào cũng nên làm như vậy; điều gì đó càng rõ ràng thì càng cần ít các tác giả của Tiêu chuẩn thực sự nói điều đó.
supercat

23

Một bool chỉ được phép giữ các giá trị phụ thuộc thực hiện được sử dụng bên trong truefalsevà mã được tạo có thể cho rằng nó sẽ chỉ giữ một trong hai giá trị này.

Thông thường, việc triển khai sẽ sử dụng số nguyên 0cho false1cho true, để đơn giản hóa các chuyển đổi giữa boolintif (boolvar)tạo mã giống như if (intvar). Trong trường hợp đó, người ta có thể tưởng tượng rằng mã được tạo cho ternary trong bài tập sẽ sử dụng giá trị làm chỉ mục thành một mảng các con trỏ tới hai chuỗi, tức là nó có thể được chuyển đổi thành một cái gì đó như:

// the compile could make asm that "looks" like this, from your source
const static char *strings[] = {"false", "true"};
const char *whichString = strings[boolValue];

Nếu boolValuekhông được khởi tạo, nó thực sự có thể chứa bất kỳ giá trị nguyên nào, điều này sẽ gây ra truy cập bên ngoài giới hạn của stringsmảng.


1
@SidS Cảm ơn. Về mặt lý thuyết, các biểu diễn bên trong có thể trái ngược với cách chúng truyền tới / từ các số nguyên, nhưng điều đó sẽ là sai lầm.
Barmar

1
Bạn đã đúng, và ví dụ của bạn cũng sẽ sụp đổ. Tuy nhiên, "có thể thấy" đối với đánh giá mã rằng bạn đang sử dụng biến chưa được khởi tạo làm chỉ mục cho một mảng. Ngoài ra, nó sẽ sập ngay cả trong gỡ lỗi (ví dụ: một số trình gỡ lỗi / trình biên dịch sẽ khởi tạo với các mẫu cụ thể để dễ nhìn thấy hơn khi nó gặp sự cố). Trong ví dụ của tôi, phần đáng ngạc nhiên là việc sử dụng bool là vô hình: Trình tối ưu hóa quyết định sử dụng nó trong một phép tính không có trong mã nguồn.
Remz

3
@Remz Tôi chỉ sử dụng mảng để hiển thị những gì mã được tạo có thể tương đương với, không gợi ý rằng bất kỳ ai sẽ thực sự viết nó.
Barmar

1
@Remz Recast the boolto intwith *(int *)&boolValuevà in nó cho mục đích gỡ lỗi, xem nó có gì khác hơn 0hoặc 1khi nó gặp sự cố. Nếu đó là trường hợp, nó gần như xác nhận lý thuyết rằng trình biên dịch đang tối ưu hóa nội tuyến - nếu như là một mảng giải thích tại sao nó bị sập.
Havenard

2
@MSalters: std::bitset<8>không cho tôi những cái tên đẹp cho tất cả các cờ khác nhau của tôi. Tùy thuộc vào những gì họ đang có, đó có thể là quan trọng.
Martin Bonner hỗ trợ Monica

15

Tóm tắt câu hỏi của bạn rất nhiều, bạn đang hỏi Liệu tiêu chuẩn C ++ có cho phép trình biên dịch giả sử boolchỉ có thể có biểu diễn số bên trong là '0' hoặc '1' và sử dụng nó theo cách như vậy không?

Tiêu chuẩn không nói gì về đại diện nội bộ của a bool. Nó chỉ định nghĩa những gì xảy ra khi truyền a boolđến một int(hoặc ngược lại). Hầu hết, do các chuyển đổi tích hợp này (và thực tế là mọi người phụ thuộc khá nhiều vào chúng), trình biên dịch sẽ sử dụng 0 và 1, nhưng nó không phải (mặc dù nó phải tôn trọng các ràng buộc của bất kỳ ABI cấp thấp nào mà nó sử dụng ).

Vì vậy, trình biên dịch, khi nó thấy boolcó quyền xem xét rằng boolcó chứa một trong các mẫu bit ' true' hoặc ' false' và làm bất cứ điều gì nó cảm thấy thích. Vì vậy, nếu các giá trị cho truefalselần lượt là 1 và 0, trình biên dịch thực sự được phép tối ưu hóa strlenthành 5 - <boolean value>. Những hành vi vui vẻ khác là có thể!

Như được nhắc lại nhiều lần ở đây, hành vi không xác định có kết quả không xác định. Bao gồm nhưng không giới hạn trong

  • Mã của bạn hoạt động như bạn mong đợi
  • Mã của bạn bị lỗi tại các thời điểm ngẫu nhiên
  • Mã của bạn không được chạy ở tất cả.

Xem những gì mọi lập trình viên nên biết về hành vi không xác định

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.