Урок 11. Паттерн 3. Операции сдвига

24.01.2012

Легко сделать ошибку в коде, работающем с отдельными битами. Рассматриваемый паттерн 64-битных ошибок связан с операциями сдвига. Пример кода:

ptrdiff_t SetBitN(ptrdiff_t value, unsigned bitNum) {
  ptrdiff_t mask = 1 << bitNum;
  return value | mask;
}

Приведенный код работоспособен на 32-битной архитектуре и позволяет выставлять бит с номерами от 0 до 31 в единицу. После переноса программы на 64-битную платформу возникнет необходимость выставлять биты от 0 до 63. Но данный код никогда не выставит биты, с номерами 32-63. Обратите внимание, что числовой литерал "1" имеет тип int, и при сдвиге на 32 позиции произойдет переполнение, как показано на рисунке 1. Получим мы в результате 0 (рисунок 1-B) или 1 (рисунок 1-C) - зависит от реализации компилятора.

Рисунок 1 - a) корректная установка 31-ого бита в 32-битном коде; b,c) - Ошибка установки 32-ого бита на 64-битной системе (два варианта поведения)

Рисунок 1 - a) корректная установка 31-ого бита в 32-битном коде; b,c) - Ошибка установки 32-ого бита на 64-битной системе (два варианта поведения)

Для исправления кода необходимо сделать константу "1" того же типа, что и переменная mask:

ptrdiff_t mask = ptrdiff_t(1) << bitNum;

Заметим также, что неисправленный код приведет еще к одной интересной ошибке. При выставлении 31 бита на 64-битной системе результатом работы функции будет значение 0xffffffff80000000 (см. рисунок 2). Результатом выражения 1 << 31 является отрицательное число -2147483648. Это число представляется в 64-битной целой переменной как 0xffffffff80000000.

Рисунок 2 - Ошибка установки 31-ого бита на 64-битной системе.

Рисунок 2 - Ошибка установки 31-ого бита на 64-битной системе.

Следует помнить и учитывать эффекты сдвига значений различных типов. Для лучшего понимания и наглядности изложенной информации в таблице 1 приведен ряд интересных выражений со сдвигами в 64-битной системе.

Таблица 1 - Выражения со сдвигами и результаты в 64-битной системе (использовался компилятор Visual C++ 2005)

Таблица 1 - Выражения со сдвигами и результаты в 64-битной системе (использовался компилятор Visual C++ 2005)

Описанный вид ошибок можно считать опасным не только с точки зрения корректности работы программы, но и с точки зрения безопасности. Потенциально манипулируя с входными данными подобных некорректных функций можно получить недопустимо высокие права, если, например, происходит обработка масок прав доступа, заданных отдельными битами. Вопросы, связанные с использованием ошибок в 64-битном коде для взлома и компрометации приложений затронуты нами в статье "Безопасность 64-битного кода".

Рассмотрим теперь более тонкий пример:

struct BitFieldStruct {
  unsigned short a:15;
  unsigned short b:13;
};
BitFieldStruct obj;
obj.a = 0x4000;
size_t addr = obj.a << 17; //Sign Extension
printf("addr 0x%Ix\n", addr);
//Output on 32-bit system: 0x80000000
//Output on 64-bit system: 0xffffffff80000000

В 32-битной среде порядок вычисления выражения будет выглядеть, как показано на рисунке 3.

Рисунок 3 - Вычисление выражения в 32-битном коде

Рисунок 3 - Вычисление выражения в 32-битном коде

Обратим внимание, что при вычислении выражения "obj.a << 17" происходит знаковое расширение типа unsigned short до типа int. Более наглядно это может продемонстрировать следующий код:

#include <stdio.h>
template <typename T> void PrintType(T)
{
  printf("type is %s %d-bit\n",
          (T)-1 < 0 ? "signed" : "unsigned", sizeof(T)*8);
}
struct BitFieldStruct {
  unsigned short a:15;
  unsigned short b:13;
};
int main(void)
{
  BitFieldStruct bf;
  PrintType( bf.a );
  PrintType( bf.a << 2);
  return 0;
}
Result:
type is unsigned 16-bit
type is signed 32-bit

Теперь посмотрим, к чему приводит наличие знакового расширения в 64-битном коде. Последовательность вычисления выражения показана на рисунке 4.

Рисунок 4 - Вычисление выражения в 64-битном коде.

Рисунок 4 - Вычисление выражения в 64-битном коде.

Член структуры obj.a преобразуется из битового поля типа unsigned short в int. Выражение "obj.a << 17" имеет тип int, но оно преобразуется в ptrdiff_t и затем в size_t перед тем как будет присвоено переменной addr. В результате мы получим значение 0xffffffff80000000, вместо ожидаемого 0x0000000080000000.

Будьте внимательны при работе с битовыми полями. Для предотвращения описанной ситуации в нашем примере достаточно явно привести obj.a к типу size_t.

...
size_t addr = size_t(obj.a) << 17;
printf("addr 0x%Ix\n", addr);
//Output on 32-bit system: 0x80000000
//Output on 64-bit system: 0x80000000

Диагностика

Потенциально опасные сдвиги будут выявлены статическим анализатором PVS-Studio, когда он обнаружит неявное расширение 32-битного типа до memsize типа. Анализатор предупредит об опасной конструкции диагностическим сообщением V101. При этом сама по себе операция сдвига подозрения не вызывает. Но анализатор обнаруживает неявное расширение типа int до типа memsize при присваивании и информирует об этом программиста, который может обнаружить ошибку в коде. Соответственно, если расширения нет, то код считается анализатором безопасным. Пример: "int mask = 1 << bitNum;".

Авторы курса: Андрей Карпов (karpov@viva64.com), Евгений Рыжков (evg@viva64.com).

Правообладателем курса "Уроки разработки 64-битных приложений на языке Си/Си++" является ООО "Системы программной верификации". Компания занимается разработкой программного обеспечения в области анализа исходного кода программ. Сайт компании: http://www.viva64.com.

Контактная информация: e-mail: support@viva64.com, 300027, г. Тула, а/я 1800.