Як я отримав значення, що перевищує 8 біт, з 8-бітного цілого числа?


118

Я відстежив надзвичайно неприємного помилку, що ховався за цією маленькою дорогоцінною каменем. Мені відомо, що за специфікацією C ++ підписані переповнення є невизначеною поведінкою, але лише тоді, коли переповнення виникає, коли значення розширюється на бітову ширину sizeof(int). Як я розумію, збільшення природи 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   // <= ..........

Перевірте це на ideone.


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 аргумент.


Це зрозуміле пояснення.
Девід Хілі

Компілятор виробляє хороший код із вимкненим оптимізатором. Тому пояснення з використанням "правил" та "визначень" не застосовуються. Це помилка в оптимізаторі.

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-бітні.


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

@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.


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

'c' - це 8-бітове значення, тому неможливо коли-небудь утримувати число, велике як -300.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.