Подтвердить что ты не робот

Как я получил значение размером более 8 бит от 8-битного целого?

Я выследил чрезвычайно неприятную ошибку, скрывающуюся за этим маленьким драгоценным камнем. Я знаю, что в спецификации С++ подписанные переполнения - это поведение undefined, но только тогда, когда переполнение происходит, когда значение расширяется до ширины бита sizeof(int). Насколько я понимаю, приращение char никогда не должно быть undefined, если sizeof(char) < sizeof(int). Но это не объясняет, как c получает невозможное значение. Как 8-разрядное целое число, как c может удерживать значения, превышающие его битовую ширину?

код

// 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;
}

Выход

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 more than 8 bits!
c: -130  // <= Uh...
c: -131
...
c: -297
c: -298  // <= Getting ridiculous now.
c: -299
c: -300
c: -45   // <= ..........

Посмотрите на идеон.

4b9b3361

Ответ 1

Это ошибка компилятора.

Хотя получение невозможных результатов для поведения undefined является допустимым следствием, в вашем коде фактически нет поведения undefined. Что происходит, так это то, что компилятор считает, что поведение undefined, и соответственно оптимизируется.

Если c определяется как int8_t, а int8_t продвигается до int, то c-- должен выполнять арифметику вычитания c - 1 в int и преобразовывать результат обратно в int8_t. Вычитание в int не переполняется, и допустимо преобразование интегральных значений вне диапазона в другой интегральный тип. Если тип назначения подписан, результат определяется реализацией, но он должен быть допустимым значением для типа назначения. (И если тип назначения не указан, результат будет определен, но здесь это не применимо.)

Ответ 2

У компилятора могут быть ошибки, отличные от несоответствий стандарту, поскольку существуют другие требования. Компилятор должен быть совместим с другими версиями. Также можно ожидать, что некоторые из них будут совместимы с другими компиляторами, а также будут соответствовать некоторым убеждениям о поведении, которые держат большинство его пользовательской базы.

В этом случае он выглядит как ошибка соответствия. Выражение c-- должно манипулировать c способом, аналогичным c = c - 1. Здесь значение c справа повышается до типа int, а затем происходит вычитание. Так как c находится в диапазоне int8_t, это вычитание не будет переполняться, но оно может вызвать значение, выходящее за пределы диапазона int8_t. Когда это значение назначено, преобразование происходит обратно к типу int8_t, поэтому результат возвращается обратно в c. В случае вне диапазона преобразование имеет значение, определенное реализацией. Но значение из диапазона int8_t не является допустимым значением, определяемым реализацией. Реализация не может "определить", что 8-битный тип внезапно содержит 9 или более бит.. Значение, которое должно быть определено реализацией, означает, что создается что-то в диапазоне int8_t, и программа продолжается. Стандарт C, таким образом, допускает поведение, такое как арифметика насыщения (общая для DSP) или обтекание (основные архитектуры).

При манипулировании значениями небольших целых типов, таких как int8_t или char, компилятор использует более широкий базовый тип машины. Когда выполняется арифметика, результаты, которые находятся вне диапазона малого целочисленного типа, могут быть надежно зафиксированы в этом более широком типе. Чтобы сохранить внешне видимое поведение переменной 8 бит, более широкий результат должен быть усечен в 8-битный диапазон. Для этого требуется явный код, поскольку места хранения (регистры) хранилища более 8 бит и довольны большими значениями. Здесь компилятор забыл нормализовать значение и просто передал его printf как есть. Спецификатор преобразования %i в printf не имеет представления о том, что аргумент изначально исходил из расчетов int8_t; он просто работает с аргументом int.

Ответ 3

Я не могу вставить это в комментарий, поэтому я отправляю его как ответ.

По какой-то очень странной причине оператор -- оказывается виновником.

Я протестировал код, отправленный на Ideone, и заменил c-- на c = c - 1, а значения остались в диапазоне [-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

Freaky ey? Я не очень разбираюсь в том, что делает компилятор для выражений типа i++ или i--. Вероятно, это способствовало возврату значения int и передало его. Единственный логический вывод, который я могу придумать, потому что вы на самом деле получаете значения, которые не могут вписаться в 8 бит.

Ответ 4

Я предполагаю, что базовое оборудование все еще использует 32-разрядный регистр для хранения этого int8_t. Поскольку спецификация не налагает поведение для переполнения, реализация не проверяет наличие переполнения и позволяет хранить более крупные значения.


Если вы помечаете локальную переменную как volatile, вы вынуждаете использовать память для нее и, следовательно, получать ожидаемые значения в пределах диапазона.

Ответ 5

Код ассемблера показывает проблему:

: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 следует использовать с пост-декрементом FF, или только BL следует использовать с остатком EBX. Любопытно, что он использует sub вместо dec. -45 таинственный. Это побитовая инверсия 300 и 255 = 44. -45 = ~ 44. Там где-то есть связь.

Это работает намного больше, используя 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

Затем он использует только низкую часть RAX, поэтому он ограничивается -128 до 127. Параметры компилятора "-g -O2".

Без оптимизации он производит правильный код:

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

Так что это ошибка в оптимизаторе.

Ответ 6

Используйте %hhd вместо %i! Должно решить вашу проблему.

То, что вы видите, является результатом оптимизации компилятора в сочетании с тем, что вы сообщаете printf, чтобы напечатать 32-битное число, а затем нажав (предположительно 8-битный) номер на стек, который действительно является размером указателя, потому что это то, как код операции push x86 работает.

Ответ 7

Я думаю, что это делается путем оптимизации кода:

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

Компилятор использует переменную int32_t i как для i, так и для c. Отключите оптимизацию или сделайте прямой листинг printf("c: %i\n", (int8_t)c--);

Ответ 8

c сам определяется как int8_t, но при работе ++ или -- над int8_t он неявно преобразовывается сначала в int, а результат операции внутреннее значение c печатается с помощью printf, который бывает int.

См. текущее значение c после целого цикла, особенно после последнего декремента

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

его правильное значение, напоминающее поведение -128 + 1 = 127

c начинает использовать память размера int, но печатается как int8_t при печати как само, используя только 8 bits. Использует все 32 bits при использовании в качестве int

[Ошибка компилятора]

Ответ 9

Я думаю, это произошло потому, что ваш цикл будет идти до тех пор, пока int я не станет 300, а c станет -300. И последнее значение связано с тем, что

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