Переполнение в битовых полях
Могу ли я доверять тому, что компилятор C по модулю 2 ^ n каждый раз, когда я обращаюсь к битному полю?
Или существует какой-либо компилятор/оптимизация, где код, подобный приведенному ниже, не будет распечатывать переполнение?
struct {
uint8_t foo:2;
} G;
G.foo = 3;
G.foo++;
if(G.foo == 0) {
printf("Overflow\n");
}
Спасибо, Advance, Флориан
Ответы
Ответ 1
Да, вы можете доверять компилятору C, чтобы делать правильные вещи здесь, если бит-бит объявлен с неподписанным типом, который у вас есть с uint8_t
. Из стандарта C99 §6.2.6.1/3:
Значения, хранящиеся в неподписанных битовых полях и объектах типа unsigned char, должны быть представлены с использованием чистой двоичной нотации. 40)
Из §6.7.2.1/9:
Битовое поле интерпретируется как целое число с подписью или без знака, состоящее из указанного количества бит. 104) Если значение 0 или 1 сохраняется в битовом поле с ненулевой шириной type _Bool
, значение бит-поля должно сравниваться с сохраненным значением.
И из §6.2.5/9 (акцент мой):
Диапазон неотрицательных значений знакового целочисленного типа является поддиапазоном соответствующего беззнакового целочисленного типа, а представление одного и того же значения в каждом типе одинаково. 31) A вычисление с использованием неподписанных операндов никогда не может переполняться, потому что результат, который не может быть представлен результирующим беззнаковым целочисленным типом, уменьшается по модулю по числу, которое больше, чем наибольшее значение, которое может быть представлено результирующим типом.
Итак, вы можете быть уверены, что любой совместимый со стандартами компилятор будет иметь переполнение G.foo
до 0 без каких-либо других нежелательных побочных эффектов.
Ответ 2
Нет. Компилятор выделяет 2 бита в поле, а приращение 3 приводит к 100b, которое при размещении в двух битах приводит к 0.
Ответ 3
Короткий ответ: да, вы можете доверять модулю 2 ^ n, чтобы произойти.
В вашей программе,
G.foo++;
на самом деле эквивалентен G.foo = (unsigned int)G.foo + 1
.
Unsigned int арифметика всегда производит результат 2 ^ (размер unsigned int в битах). Два бита наименьшего веса затем сохраняются в G.foo
, производя нуль.
Ответ 4
Да. Мы можем получить ответ от сборки.
Вот пример, который я кодирую в Ubuntu 16.04, 64bit, gcc.
#include <stdio.h>
typedef unsigned int uint32_t;
struct {
uint32_t foo1:8;
uint32_t foo2:24;
} G;
int main() {
G.foo1 = 0x12;
G.foo2 = 0xffffff; // G is 0xfffff12
printf("G.foo1=0x%02x, G.foo2=0x%06x, G=0x%08x\n", G.foo1, G.foo2, *(uint32_t *)&G);
G.foo2++; // G.foo2 overflow
printf("G.foo1=0x%02x, G.foo2=0x%06x, G=0x%08x\n", G.foo1, G.foo2, *(uint32_t *)&G);
G.foo1 += (0xff-0x12+1); // // G.foo1 overflow
printf("G.foo1=0x%02x, G.foo2=0x%06x, G=0x%08x\n", G.foo1, G.foo2, *(uint32_t *)&G);
return 0;
}
Скомпилируйте его с помощью gcc -S <.c file>
. Вы можете получить файл сборки .s
. Здесь я показываю сборку G.foo2++;
и пишу некоторые комментарии.
movl G(%rip), %eax
shrl $8, %eax # 0xfffff12-->0x00ffffff
addl $1, %eax # 0x00ffffff+1=0x01000000
andl $16777215, %eax # 16777215=0xffffff, so eax still 0x01000000
sall $8, %eax # 0x01000000-->0x00000000
movl %eax, %edx # edx high-24bit is fool2
movl G(%rip), %eax # G.foo2, tmp123
movzbl %al, %eax # so eax=0x00000012
orl %edx, %eax # eax=0x00000012 | 0x00000000 = 0x00000012
movl %eax, G(%rip) # write to G
Мы видим, что компилятор будет использовать инструкции сдвига, чтобы гарантировать, что вы говорите. (Примечание: здесь расположение памяти G:
----------------------------------
| foo2-24bit | foo1-8bit |
----------------------------------
Конечно, результатом вышеупомянутого является:
G.foo1=0x12, G.foo2=0xffffff, G=0xffffff12
G.foo1=0x12, G.foo2=0x000000, G=0x00000012
G.foo1=0x00, G.foo2=0x000000, G=0x00000000