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

118

Я обнаружил чрезвычайно неприятную ошибку, скрывающуюся за этой маленькой жемчужиной. Я знаю, что согласно спецификации C ++ подписанные переполнения являются неопределенным поведением, но только когда переполнение происходит, когда значение расширяется до разрядности sizeof(int). Насколько я понимаю, увеличение a charникогда не должно быть неопределенным, пока 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's more than 8 bits!
c: -130  // <= Uh...
c: -131
...
c: -297
c: -298  // <= Getting ridiculous now.
c: -299
c: -300
c: -45   // <= ..........

Зацените на идеоне.

неподписанный
источник
61
«Я знаю, что согласно спецификации C ++ подписанные переполнения не определены». -- Правильно. Если быть точным, не определено не только значение , но и поведение . Появление возможности получить физически невозможные результаты - серьезное последствие.
@hvd Я уверен, что у кого-нибудь есть объяснение того, как распространенные реализации C ++ вызывают такое поведение. Возможно, это связано с выравниванием или как printf()преобразование?
rliu
Другие обратились к основной проблеме. Мой комментарий более общий и касается диагностических подходов. Я считаю, что отчасти вы сочли это такой загадкой безосновательное убеждение, что это возможно. Очевидно, это не невозможно, так что примите это и посмотрите еще раз
Тим Икс
@TimX - Я наблюдал за поведением и, очевидно, пришел к выводу, что в этом смысле это не невозможно. Мое использование этого слова относится к 8-битному целому числу, содержащему 9-битное значение, что по определению невозможно. Тот факт, что это произошло, предполагает, что это не рассматривается как 8-битное значение. Как уже говорили другие, это связано с ошибкой компилятора. Единственная кажущаяся невозможность здесь - это 9-битное значение в 8-битном пространстве, и эта очевидная невозможность объясняется тем, что пространство на самом деле «больше», чем указано.
Без подписи
Я только что протестировал его на своем механизме, и результат оказался таким, каким он должен быть. 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 И моя среда: Ubuntu-12.10 gcc-4.7.2
VELVETDETH

Ответы:

111

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

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

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


источник
Я бы не назвал это «ошибкой». Поскольку подписанное переполнение вызывает неопределенное поведение, компилятор имеет полное право предположить, что этого не произойдет, и оптимизировать цикл, чтобы сохранить промежуточные значения cв более широком типе. Предположительно, вот что здесь происходит.
Майк Сеймур,
4
@MikeSeymour: Единственное переполнение здесь связано с (неявным) преобразованием. Переполнение при преобразовании со знаком не имеет неопределенного поведения; он просто дает результат, определяемый реализацией (или вызывает сигнал, определяемый реализацией, но, похоже, здесь этого не происходит). Разница в определенности между арифметическими операциями и преобразованиями странная, но именно так ее определяет стандарт языка.
Кейт Томпсон
2
@KeithThompson Это то, что отличает C от C ++: C допускает сигнал, определяемый реализацией, а C ++ - нет. C ++ просто говорит: «Если тип назначения подписан, значение не меняется, если оно может быть представлено в типе назначения (и ширине битового поля); в противном случае значение определяется реализацией».
Как оказалось, я не могу воспроизвести странное поведение на g ++ 4.8.0.
Даниэль Ландау
2
@DanielLandau См. Комментарий 38 в этой ошибке: «Исправлено в 4.8.0». :)
15

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

В этом случае это похоже на ошибку соответствия. Выражение 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 аргумент.

Kaz
источник
Это ясное объяснение.
Дэвид Хили
Компилятор производит хороший код с выключенным оптимизатором. Следовательно, объяснения с использованием «правил» и «определений» неприменимы. Это ошибка оптимизатора.
14

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

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

Я протестировал код, опубликованный на 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

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

user123
источник
4
Из-за целостных акций, c = c - 1значит c = (int8_t) ((int)c - 1. Преобразование вне диапазона intв int8_tопределенное поведение, но результат определяется реализацией. На самом деле, разве не c--должны выполняться те же преобразования?
12

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


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

Золтан
источник
1
Ух ты. Я забыл, что скомпилированная сборка по возможности будет хранить локальные переменные в регистрах. Это кажется наиболее вероятным ответом, printfне говоря уже о том, sizeofчтобы не заботиться о значениях формата.
rliu
3
@roliu Запустите g ++ -O2 -S code.cpp, и вы увидите сборку. Более того, printf () - это функция с переменным аргументом, поэтому аргументы, ранг которых меньше int, будут преобразованы в int.
@nos Я бы хотел. Мне не удалось установить загрузчик UEFI (в частности, rEFInd), чтобы запустить Archlinux на моем компьютере, поэтому я уже давно не кодировал с помощью инструментов GNU. Я доберусь до этого ... в конце концов. На данный момент это просто C # в VS и попытки запомнить C / выучить немного C ++ :)
rliu
@rollu Запустите его на виртуальной машине, например VirtualBox
@nos Не хочу сорвать тему, но да, могу. Я также мог просто установить Linux с помощью загрузчика BIOS. Я просто упрям, и если я не могу заставить его работать с загрузчиком UEFI, я, вероятно, не получу его вообще: P.
rliu
11

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

: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

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


источник
4

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

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

Zotta
источник
1
Я могу воспроизвести исходное поведение моей системы, используя g++ -O3. Переход %iна %hhdничего не меняет.
Кейт Томпсон
3

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

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

Всеволод
источник
Потом отключите оптимизацию. или сделайте что-то подобное:(int8_t)(c & 0x0000ffff)--
Всеволод
1

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

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

Ижар Аазми
источник
0

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

printf("c: %i\n", c);
r.mirzojonov
источник
'c' - это 8-битное значение, поэтому оно не может содержать число, равное -300.