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

6. 3. 1. 3 Số nguyên có dấu và không dấu

1 Khi một giá trị có kiểu số nguyên được chuyển đổi thành một kiểu số nguyên khác không phải là

int printf(const char *  ...); 
int main() 
{ 
  int i = 0x1.0p31; 
  printf("%d"  i); 
} 
2 nếu giá trị có thể được biểu diễn bằng kiểu mới thì giá trị đó không thay đổi

2 Mặt khác, nếu loại mới không được ký, giá trị được chuyển đổi bằng cách cộng hoặc trừ nhiều lần một giá trị lớn hơn giá trị tối đa có thể được biểu thị trong loại mới cho đến khi giá trị nằm trong phạm vi của loại mới

3 Mặt khác, loại mới được ký và giá trị không thể được biểu thị trong đó;

Tràn dấu phẩy động trong C

Tiê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  
int main() 
{ 
  double big = 0x1.0p5000; 
  printf("%f"  big); 
} 
$ gcc-172652/bin/gcc -std=c99 -Wall t.c && ./a.out  
t.c: In function ‘main’: 
t.c:5:3: warning: floating constant exceeds range of ‘double’ [-Woverflow] 
inf 

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 C

Như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

6. 3. 1. 4 Số thực và số nguyên

1 Khi một giá trị hữu hạn của kiểu số thực được chuyển đổi thành kiểu số nguyên khác với

int printf(const char *  ...); 
int main() 
{ 
  int i = 0x1.0p31; 
  printf("%d"  i); 
} 
2, phần phân số bị loại bỏ (i. e. giá trị bị cắt ngắn về 0). Nếu giá trị của phần tích phân không thể được biểu diễn bằng kiểu số nguyên thì hành vi không được xác định

Đ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; 

Nhân tiện, tinh chỉnh khẳng định

$ frama-c -val t.c 
warning: overflow in conversion of 0x1.0p31 (2147483648.)  
   from floating-point to integer. 
   assert -2147483649 < 0x1.0p31 < 2147483648; 
4 là một cuộc bạo loạn. Bạn có thấy tại sao không?

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 đó

Ví dụ của Dillon Pariente nằm dọc theo dòng của

$ frama-c -val t.c 
warning: overflow in conversion of 0x1.0p31 (2147483648.)  
   from floating-point to integer. 
   assert -2147483649 < 0x1.0p31 < 2147483648; 
7, điều này cũng rất vui nhộn nếu bạn thích kiểu hài hướ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  
2147483647 
0 và
$ gcc -std=c99 t.c && ./a.out  
2147483647 
1 đề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  
2147483647 
4 (2147483647) trong khi chuyển đổi kia được đánh giá trong thời gian chạy trong
$ gcc -std=c99 t.c && ./a.out  
2147483647 
5 với hướng dẫn
$ gcc -std=c99 t.c && ./a.out  
2147483647 
6

$ 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  
2147483647 
7 và
$ gcc -std=c99 t.c && ./a.out  
2147483647 
8 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  
2147483647 
6 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  
2147483647 
6. 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  
#include  
int main(int c  char **v) 
{ 
  int i = 0x1.0p31 + strtod(v[1]  0); 
  printf("%d"  i); 
} 

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

Bạn có một lời giải thích cho điều này? . Tác giả giải thích hoàn chỉnh nhanh nhất sẽ giành được giấy phép máy phân tích tĩnh

Phần kết luận

Tó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