Trôi theo thời gian php
Một bài đăng trước trên blog này là một lời nhắc nhở rằng tràn số học số nguyên có dấu trong C là hành vi không xác định. Ngược lại, hành vi tràn trong các chuyển đổi từ loại số nguyên sang loại số nguyên đã ký được xác định theo triển khai. Tiêu chuẩn C99 cho phép tăng tín hiệu do triển khai xác định nhưng trên thực tế, các nền tảng biên dịch phổ biến cung cấp hành vi bổ sung của hai. Và bạn có thể tin tưởng rằng họ sẽ tiếp tục làm như vậy vì nó được xác định theo triển khai. Các nhà sản xuất trình biên dịch không thể thay đổi quyết định của họ như thể đó là hành vi không xác định
Tràn dấu phẩy động trong CTiêu chuẩn C không bắt buộc số học dấu phẩy động IEEE 754. Trong thực tế, các nền tảng biên dịch hiện đại nếu chúng cung cấp các tính năng dấu phẩy động hoàn toàn cung cấp chính xác các định dạng và tính toán nhị phân 32 và nhị phân64 của IEEE 754 hoặc các định dạng giống nhau và gần đúng với các tính toán giống nhau Dấu phẩy động IEEE 754 xác định các giá trị int printf(const char * ...); int main() { int i = 0x1.0p31; printf("%d" i); }3 và int printf(const char * ...); int main() { int i = 0x1.0p31; printf("%d" i); }4 để bất kỳ số thực nào cũng có thể được tính gần đúng ở định dạng IEEE 754 mục tiêu (mặc dù khi nó kết thúc được biểu diễn dưới dạng vô cực không chính xác). Điều này có nghĩa là đối với các nền tảng biên dịch C triển khai IEEE 754 cho dấu phẩy động, điều kiện “giá trị có thể được biểu diễn theo kiểu mới” luôn đúng. Không có lý do gì để lo lắng về hành vi không xác định do tràn trong số học dấu phẩy động hoặc trong quá trình chuyển đổi int printf(const char * ...); int main() { int i = 0x1.0p31; printf("%d" i); }5 thành int printf(const char * ...); int main() { int i = 0x1.0p31; printf("%d" i); }6 Hoặc thực sự trong một hằng số. Xem xét cảnh báo của GCC tại đây $ cat t.c #include Số 2^5000 được biểu thị bằng C là int printf(const char * ...); int main() { int i = 0x1.0p31; printf("%d" i); }7 hoàn toàn nằm trong phạm vi của int printf(const char * ...); int main() { int i = 0x1.0p31; printf("%d" i); }5 mà tăng lên đến int printf(const char * ...); int main() { int i = 0x1.0p31; printf("%d" i); }9. Clang cũng cảnh báo tương tự rằng “độ lớn của hằng số dấu phẩy động quá lớn đối với kiểu double”. Một thông báo cảnh báo thích hợp sẽ là 2^5000 không thể được biểu diễn chính xác thay vì ngụ ý rằng nó hoàn toàn không thể được biểu diễn Dấu phẩy động ↔ tràn chuyển đổi số nguyên trong CNhưng đủ các cuộc thi sư phạm với trình biên dịch. Phạm vi của các biểu diễn dấu phẩy động là những gì chúng ta còn lại chỉ có tràn trong các chuyển đổi từ dấu phẩy động sang số nguyên để xem xét Hồi hộp… (dành cho bạn đọc không để ý tiêu đề) Tràn trong chuyển đổi từ dấu phẩy động sang số nguyên là hành vi không xác định. khoản 6. 3. 1. 4 trong tiêu chuẩn C99 khiến chúng trở nên như vậy
Điều gì có thể xảy ra trong thực tế khi một chương trình C gọi hương vị đặc biệt này của hành vi không xác định? Chương trình sau đây chuyển đổi biểu diễn int printf(const char * ...); int main() { int i = 0x1.0p31; printf("%d" i); }5 của 2^31 số nguyên dương nhỏ nhất không khớp với 32-bit $ frama-c -val t.c warning: overflow in conversion of 0x1.0p31 (2147483648.) from floating-point to integer. assert -2147483649 < 0x1.0p31 < 2147483648;2 thành $ frama-c -val t.c warning: overflow in conversion of 0x1.0p31 (2147483648.) from floating-point to integer. assert -2147483649 < 0x1.0p31 < 2147483648;2 int printf(const char * ...); int main() { int i = 0x1.0p31; printf("%d" i); } Phân tích giá trị của Frama-C cảnh báo về hành vi không xác định trong chương trình này $ frama-c -val t.c warning: overflow in conversion of 0x1.0p31 (2147483648.) from floating-point to integer. assert -2147483649 < 0x1.0p31 < 2147483648;
Máy Mac dựa trên PowerPC cũ kỹ (nhưng vẫn dũng cảm) của tôi dường như nghĩ rằng độ bão hòa là con đường để đi. biến $ frama-c -val t.c warning: overflow in conversion of 0x1.0p31 (2147483648.) from floating-point to integer. assert -2147483649 < 0x1.0p31 < 2147483648;5 được đặt thành $ frama-c -val t.c warning: overflow in conversion of 0x1.0p31 (2147483648.) from floating-point to integer. assert -2147483649 < 0x1.0p31 < 2147483648;6 $ gcc -std=c99 t.c && ./a.out 2147483647 Dillon Pariente là người đầu tiên thu hút sự chú ý của chúng tôi về tình trạng tràn trong các chuyển đổi dấu phẩy động thành số nguyên gây ra các ngoại lệ CPU trên CPU đích đối với mã mà anh ấy đang phân tích. Tôi hiểu rằng CPU mục tiêu cũng là PowerPC nên tôi nghi ngờ hành vi phải được định cấu hình trên kiến trúc đó
Để thực sự cho thấy những thứ kỳ lạ có thể xảy ra như thế nào trên bộ xử lý Intel, tôi cần sửa đổi chương trình thử nghiệm một chút int printf(const char * ...); volatile double v = 0; int main() { int i1 = 0x1.0p31; int i2 = 0x1.0p31 + v; printf("%d %d" i1 i2); } Bộ định loại loại $ frama-c -val t.c warning: overflow in conversion of 0x1.0p31 (2147483648.) from floating-point to integer. assert -2147483649 < 0x1.0p31 < 2147483648;8 ngăn cản việc tối ưu hóa nhưng không có phần cứng hoặc luồng để thay đổi giá trị của biến $ frama-c -val t.c warning: overflow in conversion of 0x1.0p31 (2147483648.) from floating-point to integer. assert -2147483649 < 0x1.0p31 < 2147483648;9. Hai biểu thức $ gcc -std=c99 t.c && ./a.out 21474836470 và $ gcc -std=c99 t.c && ./a.out 21474836471 đều là biểu thức kiểu int printf(const char * ...); int main() { int i = 0x1.0p31; printf("%d" i); }5 có giá trị là 2^31 GCC và Clang vẫn giống như một trình biên dịch duy nhất nghĩ rằng hai biểu thức này không nhất thiết phải dẫn đến cùng một giá trị khi được chuyển đổi thành $ frama-c -val t.c warning: overflow in conversion of 0x1.0p31 (2147483648.) from floating-point to integer. assert -2147483649 < 0x1.0p31 < 2147483648;2 $ gcc t.c && ./a.out 2147483647 -2147483648 $ clang t.c && ./a.out 2147483647 -2147483648 Các kết quả khác nhau vì một chuyển đổi được đánh giá tĩnh để được đặt trong $ gcc -std=c99 t.c && ./a.out 21474836474 (2147483647) trong khi chuyển đổi kia được đánh giá trong thời gian chạy trong $ gcc -std=c99 t.c && ./a.out 21474836475 với hướng dẫn $ gcc -std=c99 t.c && ./a.out 21474836476 $ clang -S -O t.c && cat t.s .. _main: ## @main .. movsd _v(%rip) %xmm0 addsd LCPI0_0(%rip) %xmm0 cvttsd2si %xmm0 %edx leaq L_.str(%rip) %rdi movl $2147483647 %esi ## imm = 0x7FFFFFFF xorb %al %al callq _printf .. L_.str: ## @.str .asciz "%d %d" Chỉ hành vi không xác định mới cho phép GCC và Clang tạo ra các giá trị khác nhau cho $ gcc -std=c99 t.c && ./a.out 21474836477 và $ gcc -std=c99 t.c && ./a.out 21474836478 tại đây. các giá trị của hai biến này được tính toán bằng cách áp dụng cùng một chuyển đổi cho cùng một số int printf(const char * ...); int main() { int i = 0x1.0p31; printf("%d" i); }5 ban đầu và phải giống hệt nhau nếu chương trình được xác định Nói chung, $ gcc -std=c99 t.c && ./a.out 21474836476 luôn tạo ra int printf(const char * ...); volatile double v = 0; int main() { int i1 = 0x1.0p31; int i2 = 0x1.0p31 + v; printf("%d %d" i1 i2); }1 trong trường hợp tràn. Điều đó gần giống như bão hòa ngoại trừ các số dấu phẩy động quá dương được gói thành int printf(const char * ...); volatile double v = 0; int main() { int i1 = 0x1.0p31; int i2 = 0x1.0p31 + v; printf("%d %d" i1 i2); }2. Người ta có thể coi nó là bão hòa đến int printf(const char * ...); volatile double v = 0; int main() { int i1 = 0x1.0p31; int i2 = 0x1.0p31 + v; printf("%d %d" i1 i2); }1 hoặc int printf(const char * ...); volatile double v = 0; int main() { int i1 = 0x1.0p31; int i2 = 0x1.0p31 + v; printf("%d %d" i1 i2); }4 và trong trường hợp sau bao quanh int printf(const char * ...); volatile double v = 0; int main() { int i1 = 0x1.0p31; int i2 = 0x1.0p31 + v; printf("%d %d" i1 i2); }1 vì phần bù của hai. Tôi không biết liệu lý do này có giống với lý do mà các kỹ sư của Intel đã sử dụng để biện minh cho lựa chọn của họ hay không Vì vậy, người ta có thể nghĩ rằng đây là kết thúc của câu chuyện. miễn là quá trình chuyển đổi được thực hiện trong thời gian chạy trên nền tảng Intel, trình biên dịch sẽ sử dụng lệnh $ gcc -std=c99 t.c && ./a.out 21474836476. Tràn nếu tràn có "bão hòa đến INT_MIN" như quy ước trên nền tảng này. Điều này có thể được xác nhận bằng thực nghiệm với biến thể chương trình sau #include Chương trình mới này lấy một số từ dòng lệnh và thêm nó vào 2^31 để không có cơ hội đánh giá thời gian biên dịch. Chúng tôi hy vọng việc chuyển đổi sẽ bão hòa đến int printf(const char * ...); volatile double v = 0; int main() { int i1 = 0x1.0p31; int i2 = 0x1.0p31 + v; printf("%d %d" i1 i2); }2 và thực tế là $ gcc -std=c99 t.c && ./a.out 1234 && ./a.out 12345 && ./a.out 123456 -2147483648 -2147483648 -2147483648 Chờ đợi. Nó vẫn thú vị hơn. Hãy để chúng tôi thay đổi chương trình không thể nhận thấy int main(int c char **v) { unsigned int i = 0x1.0p32 + strtod(v[1] 0); printf("%u" i); } Hành vi tràn thời gian chạy trong quá trình chuyển đổi từ int printf(const char * ...); int main() { int i = 0x1.0p31; printf("%d" i); }5 thành số nguyên thay đổi hoàn toàn int printf(const char * ...); int main() { int i = 0x1.0p31; printf("%d" i); }0 Tuy nhiên, lần này chuyển đổi lại bão hòa ở mức 0 cho cùng một chương trình khi nhắm mục tiêu IA-32 int printf(const char * ...); int main() { int i = 0x1.0p31; printf("%d" i); }1
Phần kết luậnTóm lại, tràn trong quá trình chuyển đổi từ dấu phẩy động sang số nguyên khá khó chịu trong phổ hành vi không xác định của C. Nó có vẻ hoạt động nhất quán nếu quá trình biên dịch nhắm đến một kiến trúc nơi (các) lệnh lắp ráp bên dưới bão hòa. Độ bão hòa là hành vi mà trình biên dịch GCC và Clang thực hiện khi chúng có thể đánh giá chuyển đổi tại thời điểm biên dịch. Trong những điều kiện này, một lập trình viên may mắn có thể không thực sự quan sát thấy điều gì lạ. Các đặc điểm riêng của các kiến trúc khác có thể dẫn đến các kết quả rất khác nhau đối với các chuyển đổi tràn tùy thuộc vào các tham số nằm ngoài tầm kiểm soát của lập trình viên (ví dụ: việc truyền liên tục hiệu quả hơn hoặc kém hơn tùy thuộc vào mức độ tối ưu hóa và có thể khó dự đoán như chúng ta đã phàn nàn khi thảo luận về Clang Sự nhìn nhận. Ngoài Dillon Pariente, tôi đã thảo luận chủ đề này với Boris Yakobowski John Regehr Stephen Canon và người dùng StackOverflow tenos Sander De Dycker và Mike Seymour trước khi viết bài đăng trên blog này |