Этот вопрос мотивирован тем, что я внедряю криптографические алгоритмы (например, SHA-1) в C/С++, записывая переносимый платформо-агностический код и полностью избегая поведения undefined.
Предположим, что стандартизованный криптоалгоритм просит вас реализовать это:
b = (a << 31) & 0xFFFFFFFF
где a
и b
- неподписанные 32-битные целые числа. Обратите внимание, что в результате мы отбрасываем любые биты выше наименее значимых 32 бит.
В качестве первого наивного приближения можно предположить, что на большинстве платформ int
имеет ширину 32 бита, поэтому мы будем писать:
unsigned int a = (...);
unsigned int b = a << 31;
Мы знаем, что этот код не будет работать повсеместно, потому что int
имеет ширину 16 бит в некоторых системах, 64 бит на других и, возможно, даже 36 бит. Но используя stdint.h
, мы можем улучшить этот код с помощью типа uint32_t
:
uint32_t a = (...);
uint32_t b = a << 31;
Итак, мы закончили, да? Это то, что я думал годами.... Не совсем. Предположим, что на некоторой платформе мы имеем:
// stdint.h
typedef unsigned short uint32_t;
Правило для выполнения арифметических операций в C/С++ состоит в том, что если тип (например, short
) уже, чем int
, тогда он расширяется до int
, если все значения могут соответствовать, или unsigned int
в противном случае.
Скажем, что компилятор определяет short
как 32 бита (подпись) и int
как 48 бит (подписанный). Затем эти строки кода:
uint32_t a = (...);
uint32_t b = a << 31;
будет эффективно означать:
unsigned short a = (...);
unsigned short b = (unsigned short)((int)a << 31);
Обратите внимание, что a
продвигается до int
, потому что все ushort
(т.е. uint32
) вписываются в int
(т.е. int48
).
Но теперь у нас есть проблема: смещение ненулевых битов, оставшихся в бите знака целочисленного типа со знаком, - undefined поведение. Эта проблема возникла из-за того, что наш uint32
был повышен до int48
- вместо того, чтобы быть продвинутым до uint48
(где смещение влево было бы в порядке).
Вот мои вопросы:
-
Является ли мое рассуждение правильным, и является ли это законной проблемой в теории?
-
Можно ли игнорировать эту проблему, потому что на каждой платформе следующий целочисленный тип имеет двойную ширину?
-
Хорошая идея правильно защититься от этой патологической ситуации, предварительно маскируя ввод следующим образом:
b = (a & 1) << 31;
. (Это обязательно будет правильно на каждой платформе, но это может сделать криптографический алгоритм с критической скоростью медленнее, чем необходимо.)
Разъяснения/редактирование:
-
Я буду принимать ответы для C или С++ или обоих. Я хочу знать ответ хотя бы на одном из языков.
-
Логика предварительной маскировки может повредить вращение бит. Например, GCC будет компилировать
b = (a << 31) | (a >> 1);
в 32-разрядную инструкцию бит-вращения на языке ассемблера. Но если мы предварительно замаскируем левый сдвиг, возможно, что новая логика не переведена в бит вращение, а это означает, что теперь выполняется 4 операции вместо 1.