Làm cách nào để tôi nhận được giá trị có kích thước lớn hơn 8 bit từ số nguyên 8 bit?


118

Tôi đã lần ra một con bọ cực kỳ khó chịu ẩn sau viên ngọc nhỏ này. Tôi biết rằng theo thông số C ++, tràn có dấu là hành vi không xác định, nhưng chỉ khi tràn xảy ra khi giá trị được mở rộng đến chiều rộng bit sizeof(int). Theo tôi hiểu, việc tăng một charkhông bao giờ nên là hành vi không xác định miễn là sizeof(char) < sizeof(int). Nhưng điều đó không giải thích được cách cnhận được một giá trị bất khả thi . Là một số nguyên 8 bit, làm thế nào có thể cgiữ các giá trị lớn hơn chiều rộng bit của nó?

// Compiled with gcc-4.7.2
#include <cstdio>
#include <stdint.h>
#include <climits>

int main()
{
   int8_t c = 0;
   printf("SCHAR_MIN: %i\n", SCHAR_MIN);
   printf("SCHAR_MAX: %i\n", SCHAR_MAX);

   for (int32_t i = 0; i <= 300; i++)
      printf("c: %i\n", c--);

   printf("c: %i\n", c);

   return 0;
}

Đầu ra

SCHAR_MIN: -128
SCHAR_MAX: 127
c: 0
c: -1
c: -2
c: -3
...
c: -127
c: -128  // <= The next value should still be an 8-bit value.
c: -129  // <= What? That's more than 8 bits!
c: -130  // <= Uh...
c: -131
...
c: -297
c: -298  // <= Getting ridiculous now.
c: -299
c: -300
c: -45   // <= ..........

Kiểm tra nó trên Ideone.


61
"Tôi biết rằng theo thông số C ++, các lỗi tràn có dấu là không xác định." -- Đúng. Nói một cách chính xác, không chỉ giá trị là không xác định, mà còn là hành vi . Dường như nhận được kết quả không thể thực hiện được là một hậu quả hợp lệ.

@hvd Tôi chắc rằng ai đó có lời giải thích về cách các triển khai C ++ phổ biến gây ra hành vi này. Có lẽ nó phải làm với căn chỉnh hoặc làm thế nào printf()để chuyển đổi?
rliu

Những người khác đã giải quyết vấn đề chính. Nhận xét của tôi là tổng quát hơn và liên quan đến các phương pháp chẩn đoán. Tôi tin rằng một phần lý do tại sao bạn tìm thấy một câu đố như vậy là niềm tin cơ bản rằng nó không thể giải quyết được. Rõ ràng, nó không phải là bất khả thi, vì vậy chấp nhận điều đó và tìm kiếm một lần nữa
Tim X

@TimX - Tôi đã quan sát hành vi và rõ ràng rút ra kết luận rằng nó không phải là không thể theo nghĩa đó. Việc sử dụng từ của tôi để chỉ số nguyên 8 bit chứa giá trị 9 bit, đó là một định nghĩa không thể xảy ra. Thực tế là điều này đã xảy ra cho thấy rằng nó không được coi là một giá trị 8-bit. Như những người khác đã giải quyết, đây là do lỗi trình biên dịch. Các chỉ dường như bất khả thi ở đây là một giá trị 9-bit trong một không gian 8-bit, và bất khả thi rõ ràng này được giải thích bởi không gian thực sự là "lớn hơn" so với báo cáo.
Chưa ký

Tôi vừa thử nghiệm nó trên mechine của mình, và kết quả đúng như mong muốn. c: -120 c: -121 c: -122 c: -123 c: -124 c: -125 c: -126 c: -127 c: -128 c: 127 c: 126 c: 125 c: 124 c: 123 c: 122 c: 121 c: 120 c: 119 c: 118 c: 117 Và môi trường của tôi là: Ubuntu-12.10 gcc-4.7.2
VELVETDETH

Câu trả lời:


111

Đây là một lỗi trình biên dịch.

Mặc dù không thể nhận được kết quả cho hành vi không xác định là một hệ quả hợp lệ, nhưng thực tế không có hành vi không xác định nào trong mã của bạn. Điều đang xảy ra là trình biên dịch nghĩ rằng hành vi là không xác định và tối ưu hóa cho phù hợp.

Nếu cđược định nghĩa là int8_t, và int8_tthăng hạng thành int, thì c--được cho là thực hiện phép trừ c - 1trong intsố học và chuyển đổi kết quả trở lại int8_t. Phép trừ trong intkhông bị tràn và việc chuyển đổi các giá trị tích phân ngoài phạm vi thành một loại tích phân khác là hợp lệ. Nếu kiểu đích được ký, kết quả được xác định bởi việc triển khai, nhưng nó phải là một giá trị hợp lệ cho kiểu đích. (Và nếu kiểu đích không có dấu, kết quả được xác định rõ, nhưng điều đó không áp dụng ở đây.)


Tôi sẽ không mô tả nó như một "lỗi". Vì tràn đã ký gây ra hành vi không xác định, trình biên dịch hoàn toàn có quyền giả định rằng điều đó sẽ không xảy ra và tối ưu hóa vòng lặp để giữ các giá trị trung gian cở kiểu rộng hơn. Có lẽ, đó là những gì đang xảy ra ở đây.
Mike Seymour

4
@MikeSeymour: Sự cố tràn duy nhất ở đây là về chuyển đổi (ngầm định). Tràn trên chuyển đổi đã ký không có hành vi không xác định; nó chỉ mang lại kết quả do triển khai xác định (hoặc tăng tín hiệu do triển khai xác định, nhưng điều đó dường như không xảy ra ở đây). Sự khác biệt về định nghĩa giữa các phép toán số học và chuyển đổi là kỳ lạ, nhưng đó là cách tiêu chuẩn ngôn ngữ định nghĩa nó.
Keith Thompson

2
@KeithThompson Đó là điều khác biệt giữa C và C ++: C cho phép một tín hiệu được xác định bởi việc triển khai, C ++ thì không. C ++ chỉ nói "Nếu kiểu đích được ký, giá trị không thay đổi nếu nó có thể được biểu diễn trong kiểu đích (và độ rộng trường bit); nếu không, giá trị được xác định bằng cách triển khai."

Khi nó xảy ra, tôi không thể tạo lại hành vi kỳ lạ trên g ++ 4.8.0.
Daniel Landau

2
@DanielLandau Xem nhận xét 38 trong lỗi đó: "Đã sửa cho 4.8.0." :)

15

Một trình biên dịch có thể có các lỗi khác ngoài sự không phù hợp với tiêu chuẩn, bởi vì có các yêu cầu khác. Một trình biên dịch phải tương thích với các phiên bản khác của chính nó. Nó cũng có thể tương thích theo một số cách với các trình biên dịch khác và cũng phù hợp với một số niềm tin về hành vi được đa số người dùng nắm giữ.

Trong trường hợp này, nó có vẻ là một lỗi tuân thủ. Biểu thức c--nên thao tác ctheo một cách tương tự như c = c - 1. Ở đây, giá trị của cbên phải được thăng cấp thành kiểu int, và sau đó phép trừ diễn ra. Vì cnằm trong phạm vi của int8_t, phép trừ này sẽ không bị tràn, nhưng nó có thể tạo ra giá trị nằm ngoài phạm vi của int8_t. Khi giá trị này được chỉ định, một chuyển đổi sẽ diễn ra trở lại loại int8_tđể kết quả khớp trở lại c. Trong trường hợp nằm ngoài phạm vi, chuyển đổi có giá trị do việc triển khai xác định. Nhưng giá trị nằm ngoài phạm vi int8_tkhông phải là giá trị được triển khai hợp lệ xác định. Việc triển khai không thể "định nghĩa" rằng một loại 8 bit đột nhiên giữ 9 bit trở lên. Đối với giá trị được xác định bởi triển khai có nghĩa là một cái gì đó trong phạm vi của int8_tđược tạo ra và chương trình tiếp tục. Tiêu chuẩn C do đó cho phép thực hiện các hành vi như số học bão hòa (phổ biến trên DSP) hoặc quấn quanh (kiến trúc chính thống).

Trình biên dịch đang sử dụng kiểu máy cơ bản rộng hơn khi thao tác các giá trị của kiểu số nguyên nhỏ như int8_thoặc char. Khi số học được thực hiện, các kết quả nằm ngoài phạm vi của kiểu số nguyên nhỏ có thể được ghi lại một cách đáng tin cậy trong kiểu rộng hơn này. Để duy trì hành vi có thể nhìn thấy bên ngoài mà biến là loại 8 bit, kết quả rộng hơn phải được cắt bớt thành phạm vi 8 bit. Cần có mã rõ ràng để làm điều đó vì vị trí lưu trữ của máy (thanh ghi) rộng hơn 8 bit và hài lòng với các giá trị lớn hơn. Ở đây, trình biên dịch đã bỏ qua việc chuẩn hóa giá trị và chỉ chuyển nó về nguyên trạng printf. Trình chỉ định chuyển đổi %itrong printfkhông có ý tưởng rằng đối số ban đầu đến từ các int8_tphép tính; nó chỉ làm việc với mộtint tranh luận.


Đây là một lời giải thích sáng suốt.
David Healy

Trình biên dịch tạo ra mã tốt khi tắt trình tối ưu hóa. Do đó, các giải thích sử dụng "quy tắc" và "định nghĩa" không được áp dụng. Đó là một lỗi trong trình tối ưu hóa.

14

Tôi không thể phù hợp với điều này trong một bình luận, vì vậy tôi đăng nó như một câu trả lời.

Vì một số lý do rất kỳ quặc, --nhà điều hành tình cờ là thủ phạm.

Tôi đã kiểm tra mã được đăng trên Ideone và thay thế c--bằng c = c - 1và các giá trị vẫn nằm trong phạm vi [-128 ... 127]:

c: -123
c: -124
c: -125
c: -126
c: -127
c: -128 // about to overflow
c: 127  // woop
c: 126
c: 125
c: 124
c: 123
c: 122

Quái đản? Tôi không biết nhiều về những gì trình biên dịch làm với các biểu thức như i++hoặc i--. Nó có khả năng thúc đẩy giá trị trả về cho một intvà chuyển nó. Đó là kết luận hợp lý duy nhất mà tôi có thể đưa ra bởi vì trên thực tế, bạn ĐANG nhận các giá trị không thể vừa với 8-bit.


4
Vì các chương trình khuyến mãi không thể thiếu, c = c - 1phương tiện c = (int8_t) ((int)c - 1. Chuyển đổi ngoài phạm vi intthành int8_tcó hành vi đã xác định nhưng là kết quả do triển khai xác định. Trên thực tế, không phải c--cũng được thực hiện những chuyển đổi tương tự?

12

Tôi đoán rằng phần cứng bên dưới vẫn đang sử dụng thanh ghi 32-bit để giữ int8_t đó. Vì đặc tả không áp đặt hành vi cho tràn, việc triển khai không kiểm tra tràn và cũng cho phép lưu trữ các giá trị lớn hơn.


Nếu bạn đánh dấu biến cục bộ là volatilebạn đang buộc sử dụng bộ nhớ cho nó và do đó nhận được các giá trị mong đợi trong phạm vi.


1
Tuyệt vời. Tôi quên rằng hợp ngữ đã biên dịch sẽ lưu trữ các biến cục bộ trong thanh ghi nếu có thể. Đây có vẻ là câu trả lời có khả năng nhất cùng với việc printfkhông quan tâm đến các sizeofgiá trị định dạng.
rliu

3
@roliu Chạy mã g ++ -O2 -S.cpp và bạn sẽ thấy lắp ráp. Hơn nữa, printf () là một hàm đối số biến, vì vậy các đối số có thứ hạng nhỏ hơn int sẽ được thăng cấp thành int.
nos

@nos Tôi muốn. Tôi đã không thể cài đặt bộ tải khởi động UEFI (cụ thể là rEFInd) để chạy Archlinux trên máy của mình, vì vậy tôi đã không thực sự viết mã bằng các công cụ GNU trong một thời gian dài. Cuối cùng thì tôi cũng sẽ làm được. Còn bây giờ nó chỉ là C # trong VS và cố nhớ lại C / tìm hiểu một số C ++ :)
rliu

@rollu Chạy nó trong một máy ảo, ví dụ như VirtualBox
nos

@nos Không muốn chủ đề bị trật, nhưng vâng, tôi có thể. Tôi cũng có thể chỉ cần cài đặt linux với bộ nạp khởi động BIOS. Tôi chỉ cứng đầu và nếu tôi không thể làm cho nó hoạt động với bộ nạp khởi động UEFI thì có lẽ tôi sẽ không làm cho nó hoạt động được: P.
rliu

11

Mã trình hợp dịch tiết lộ vấn đề:

:loop
mov esi, ebx
xor eax, eax
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
sub ebx, 1
call    printf
cmp ebx, -301
jne loop

mov esi, -45
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
xor eax, eax
call    printf

EBX nên được cân bằng với FF sau khi giảm, hoặc chỉ BL nên được sử dụng với phần còn lại của EBX rõ ràng. Tò mò rằng nó sử dụng phụ thay vì dec. -45 là bí ẩn phẳng. Đó là sự nghịch đảo bitwise của 300 & 255 = 44. -45 = ~ 44. Có một kết nối ở đâu đó.

Nó trải qua nhiều công việc hơn khi sử dụng c = c - 1:

mov eax, ebx
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
add ebx, 1
not eax
movsx   ebp, al                 ;uses only the lower 8 bits
xor eax, eax
mov esi, ebp

Sau đó, nó chỉ sử dụng phần thấp của RAX, vì vậy nó bị hạn chế từ -128 đến 127. Tùy chọn trình biên dịch "-g -O2".

Không có tối ưu hóa, nó tạo ra mã chính xác:

movzx   eax, BYTE PTR [rbp-1]
sub eax, 1
mov BYTE PTR [rbp-1], al
movsx   edx, BYTE PTR [rbp-1]
mov eax, OFFSET FLAT:.LC2   ;"c: %i\n"
mov esi, edx

Vì vậy, đó là một lỗi trong trình tối ưu hóa.


4

Sử dụng %hhdthay vì %i! Nên giải quyết vấn đề của bạn.

Những gì bạn thấy ở đó là kết quả của việc tối ưu hóa trình biên dịch kết hợp với việc bạn yêu cầu printf in một số 32bit và sau đó đẩy một số (được cho là 8bit) vào ngăn xếp, có kích thước thực sự là con trỏ, bởi vì đây là cách hoạt động của push opcode trong x86.


1
Tôi có thể tạo lại hành vi ban đầu trên hệ thống của mình bằng cách sử dụng g++ -O3. Thay đổi %ithành %hhdkhông thay đổi bất cứ điều gì.
Keith Thompson

3

Tôi nghĩ rằng điều này được thực hiện bằng cách tối ưu hóa mã:

for (int32_t i = 0; i <= 300; i++)
      printf("c: %i\n", c--);

Trình biên dịch sử dụng int32_t ibiến cho cả ic. Tắt tối ưu hóa hoặc truyền trực tiếp printf("c: %i\n", (int8_t)c--);


Sau đó tắt tối ưu hóa. hoặc làm điều gì đó như thế này:(int8_t)(c & 0x0000ffff)--
Vsevolod

1

cbản thân nó được định nghĩa là int8_t, nhưng khi hoạt động ++hoặc --hơn int8_tnó được chuyển đổi ngầm trước tiên thành intkết quả của hoạt động thay vào đó giá trị bên trong của c được in bằng printf, điều này xảy ra int.

Xem giá trị thực của csau toàn bộ vòng lặp, đặc biệt là sau lần giảm cuối cùng

-301 + 256 = -45 (since it revolved entire 8 bit range once)

nó là giá trị chính xác tương tự như hành vi -128 + 1 = 127

cbắt đầu sử dụng intbộ nhớ kích thước nhưng được in như int8_tkhi chỉ sử dụng chính nó 8 bits. Sử dụng tất cả 32 bitskhi được sử dụng nhưint

[Lỗi trình biên dịch]


0

Tôi nghĩ rằng nó đã xảy ra bởi vì vòng lặp của bạn sẽ đi cho đến khi int i sẽ trở thành 300 và c trở thành -300. Và giá trị cuối cùng là vì

printf("c: %i\n", c);

'c' là một giá trị 8 bit, do đó nó không thể chứa một số lớn như -300.
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.