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

Переполнение переполнения GCC

Следующая небольшая программа очень неудобна с использованием GCC версии 4.2.1 (Apple Inc. build 5664) на Mac.

#include <stdio.h>

int main(){
        int x = 1 << 32;
        int y = 32;
        int z = 1 << y;
        printf("x:%d, z: %d\n", x, z);
}

Результат x:0, z: 1.
Любая идея, почему значения x и z различны?
Большое спасибо.

4b9b3361

Ответ 1

Краткий ответ: процессор Intel маскирует число сдвигов до 5 бит (максимум 31). Другими словами, фактически выполненное смещение составляет 32 и 31, что равно 0 (без изменений).

Тот же результат появляется при использовании gcc на 32-битном ПК с Linux.

Я собрал более короткую версию этой программы, потому что был озадачен тем, почему сдвиг влево на 32 бита должен привести к ненулевому значению вообще:

int main(){
    int y = 32;
    unsigned int z = 1 << y;
    unsigned int k = 1;
    k <<= y;
    printf("z: %u, k: %u\n", z, k);
}

..использование команды gcc -Wall -o as -S deleteme.c (комментарии мои)

main:
leal    4(%esp), %ecx
andl    $-16, %esp
pushl   -4(%ecx)
pushl   %ebp
movl    %esp, %ebp
pushl   %ecx
subl    $36, %esp
movl    $32, -16(%ebp)  ; y = 32
movl    -16(%ebp), %ecx ; 32 in CX register
movl    $1, %eax        ; AX = 1
sall    %cl, %eax       ; AX <<= 32(32)
movl    %eax, -12(%ebp) ; z = AX
movl    $1, -8(%ebp)    ; k = 1
movl    -16(%ebp), %ecx ; CX = y = 32
sall    %cl, -8(%ebp)   ; k <<= CX(32)
movl    -8(%ebp), %eax  ; AX = k
movl    %eax, 8(%esp)
movl    -12(%ebp), %eax
movl    %eax, 4(%esp)
movl    $.LC0, (%esp)
call    printf
addl    $36, %esp
popl    %ecx
popl    %ebp
leal    -4(%ecx), %esp
ret

Хорошо, так что это значит? Это инструкция, которая озадачивает меня:

sall    %cl, -8(%ebp)   ; k <<= CX(32)

Ясно, что k смещается влево на 32 бита.

Вы меня поймали - оно использует инструкцию sall которая является арифметическим сдвигом. Я не знаю, почему поворот на 32 приводит к повторному появлению бита в исходном положении. Моя первоначальная гипотеза состояла бы в том, что процессор оптимизирован для выполнения этой инструкции за один такт - это означает, что любой сдвиг более чем на 31 будет рассматриваться как неважно. Но мне любопытно найти ответ на этот вопрос, потому что я ожидаю, что вращение должно привести к тому, что все биты будут падать с левого конца типа данных.

Я нашел ссылку на http://faydoc.tripod.com/cpu/sal.htm, которая объясняет, что счетчик сдвига (в регистре CL) маскируется до 5 битов. Это означает, что если вы попытаетесь сдвинуться на 32 бита, фактический сдвиг будет равен нулю (т.е. без изменений). Там ответ!

Ответ 2

Если ваш ints равен 32 битам или короче, поведение undefined... и поведение undefined невозможно объяснить.

В стандарте говорится:

6.5.7/3 [...] Если значение правого операнда отрицательное или больше или равно ширине продвинутого левого операнда, поведение undefined.


Вы можете проверить размер бит int width, например:

#include <limits.h>
#include <stdio.h>
int main(void) {
    printf("bits in an int: %d\n", CHAR_BIT * (int)sizeof (int));
    return 0;
}

И вы можете проверить свою ширину int (могут быть биты заполнения), например:

#include <limits.h>
#include <stdio.h>
int main(void) {
    int width = 0;
    int tmp = INT_MAX;
    while (tmp) {
        tmp >>= 1;
        width++;
    }
    printf("width of an int: %d\n", width + 1 /* for the sign bit */);
    return 0;
}

Стандарт 6.2.6.2/2: для знаковых целых типов биты представления объекта должны быть разделены на три группы: биты значений, биты заполнения и знаковый бит. Не должно быть никаких битов заполнения; должен быть ровно один знаковый бит

Ответ 3

В стандарте C99 говорится, что результат сдвига числа по ширине в битах (или более) операнда undefined. Почему?

Ну, это позволяет компиляторам создавать наиболее эффективный код для конкретной архитектуры. Например, инструкция сдвига i386 использует поле шириной в пять бит для количества бит, чтобы сдвинуть 32-разрядный операнд. Стандарт C99 позволяет компилятору просто взять нижние пять бит счетчика сдвига и поместить их в поле. Очевидно, это означает, что сдвиг 32 бит (= 100000 в двоичном формате), таким образом, идентичен сдвигу 0, и поэтому результат будет левым операндом без изменений.

Другая архитектура ЦП может использовать более широкое поле бит, например 32 бита. Компилятор все равно может поместить счетчик сдвига непосредственно в поле, но на этот раз результат будет равен 0, потому что сдвиг 32 бит сдвинет все биты из левого операнда.

Если C99 определил одно или другое из этих действий как правильное, либо компилятор для Intel должен поставить специальную проверку для слишком большого количества сдвигов, либо компилятор для i386 должен замаскировать счетчик сдвигов.

Причина, по которой

   int x = 1 << 32;

и

   int z = 1 << y;

дают разные результаты, потому что первый расчет является постоянным выражением и полностью выполняется компилятором. Компилятор должен вычислять константные выражения, используя 64-битную арифметику. Второе выражение вычисляется кодом, сгенерированным компилятором. Так как тип y и z int, код генерирует вычисление с использованием 32-битного шифра int (int 32 бит на i386 и x86_64 с gcc на Apple).

Ответ 4

В моем уме "int x = y < 32;" не имеет смысла, если sizeof (int) == 4.

Но у меня была аналогичная проблема:

long y =... long x = y < 32;

Где я получил предупреждение "предупреждение: количество сдвигов влево >= ширина типа", хотя sizeof (long) было 8 на рассматриваемой цели. Я избавился от предупреждения, сделав это вместо этого:

long x = (y < 16) < 16;

И это, казалось, сработало.

В 64-битной архитектуре не было предупреждений. На 32-битной архитектуре было.