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 bool
như một hàm trong một thanh ghi được biểu diễn bằng các mẫu bit false=0
vàtrue=1
trong 8 bit thấp của thanh ghi 1 . Trong bộ nhớ, bool
là 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 bool
cho bất kỳ kiến trúc nào (không chỉ x86). Nó cho phép tối ưu hóa như !mybool
với xor eax,1
việ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&&b
thành bitwise AND cho bool
cá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 memcpy
là 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 bool
mộ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 main
vớ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 garbage
có 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=0
và true=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 memcpy
ing bool
và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 inline
từ khóa.)
Vì vậy, một trình biên dịch có thể chỉ phát ra một ret
hoặ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 main
cuộ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 ud2
trê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 void
hàm không hoạt động, gcc đôi khi sẽ bỏ qua một ret
lệ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 -Wall
và 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_MIN
không tối ưu hóa a<0
luôn luôn là sai, chỉ có điều đó tmp
luôn luôn âm. (Vì INT_MIN
+ a=INT_MAX
là âm trên mục tiêu bổ sung của 2 này và a
khô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 & 0x01010101
trong 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 &1
vì nó đã làm điều đó với byte thấp như một phần của and edi, 0x01010101
nó 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, dil
trong 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 mov
ngay 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 mov
gấp 2 lần + cmov
và 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 movsb
có thể là tối ưu. glibc memcpy
có 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 -pie
việ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 -O2
trong 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 memcpy
với length
tí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.