Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

C 11 и 64-битные ошибки

Anna | 24.06.2014 | нет комментариев
CryEngine 3 SDK and PVS-Studio
Мы решили сделать небольшую паузу в тематике статического обзора кода. чай блог C читают и те, кто пока еще не использует эту спецтехнологию. А между тем в мире C происходят явления, которые оказывают могущества на такую «устоявщуюся» тему, как 64-битный мир. Речь идет о том как эталон C 11 влияет и помогает (если есть чем) в разработке правильных 64-битных программ. Сегодняшняя статья как раз об этом.

64-битные компьютеры давным-давно и удачно применяются. Множество приложений стали 64-битными. Это разрешает им применять больший объем памяти, а также получить приход продуктивности за счёт архитектурных вероятностей 64-битных процессоров. Создание 64-битных программ на языке Си/Си требует от программиста наблюдательности. Существует масса причин, из-за которых код 32-битной программы отказывается правильно трудиться позже перекомпиляции для 64-битной системы. Про это написано много статей. Но теперь нам увлекателен иной вопрос. Давайте посмотрим, разрешает ли применение новых вероятностей, появившихся в C 11, облегчить жизнь программистов, создающих 64-битные программы.

Мир 64-битных ошибок

Существует уйма западней, в которые может попасть программист, создавая 64-битные приложения на языке Си/Си . Про это написано огромное число статей, следственно не будем повторяться. Тем, кто не знаком с нюансами разработки 64-битных программ либо тем, кто хочет освежить свою память, дозволено порекомендовать следующие источники:

Время не стоит на месте, и вот теснее программисты применяют обновлённую версию языка C , получившего наименования C 11. На данный момент множество новшеств, описанных в эталоне языка C 11, поддерживается современными компиляторами. Давайте посмотрим, могут ли эти нововведения как-то подмогнуть программисту избежать 64-битных ошибок.

Статья будет построена дальнейшим образом. Будет даваться короткое изложение нормальной 64-битной ошибки, и предлагаться методы, как её избежать, применяя C 11. Сразу подметим, что вдалеке не неизменно С 11 может хоть чем-то подмогнуть. Защитить от ошибок может только опрятное программирование. А новейший эталон лишь помогает в этом, но не решить все задачи за программиста.

Волшебные числа

Речь идёт об применении таких чисел, как 4, 32, 0x7FFFFFFF, 0xFFFFFFFF (подробнее). Нехорошо, если программист предположил, что размер указателя неизменно равен 4 байтам и написал вот такой код:

int **array = (int **)malloc(n * 4);

Тут эталон C 11 нам подмогнуть не может. Волшебные числа, это зло и исключительный метод избежать ошибок — это усердствовать их не применять.

Примечание. Да, malloc() это не C , а ветхий добродушный C. Гораздо отменнее применять оператор new либо контейнер std::vector. Но теперь это к делу не относится. Разговор про волшебные числа.

Однако, С 11 изредка помогает сократить число магических чисел. Некоторые волшебные числа в программе возникает из-за боязни (Зачастую необоснованной), что компилятор нехорошо оптимизирует код. В этом случае, стоит обратить внимание на generalized constant expressions (сonstexpr).

Механизм constexpr гарантирует инициализацию выражений во время компиляции. При этом, дозволено объявить функции, которые гарантированно развернутся в константу на этапе компиляции. Пример:

constexpr int Formula(int a) {
  constexpr int tmp = a * 2;
  return tmp   55;
}
int n = Formula(1);

Вызов функции Formula(1) превратится в число. Трактование безусловно слишком короткое. Подробнее про «constexpr» и другие нововведения дозволено прочитать, перейдя по ссылкам, приведённым в конце статьи.

Функции с переменным числом доводов

Речь идёт о неправильном применении таких функций, как printf, scanf (подробнее). Пример:

size_t value = ....;
printf("%u", value);

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

Функции с переменным числом доводов — пережиток языка Си. Их недочет в отсутствии контроля типов фактических доводов. В Си теснее давным-давно пора отказаться от них. Есть масса других методов форматирования строк. Скажем, дозволено заменить printf на cout, а sprintf на boost::format либо std::stringstream.

С языком C 11 жизнь стала ещё отменнее. В C 11 возникли образцы с переменным числом параметров (Variadic Templates). Это разрешает реализовывать вот такой неопасный вариант функции printf:

void printf(const char* s)
{
  while (s && *s) {
    if (*s=='%' && *  s!='%')
      throw runtime_error("invalid format: missing arguments");
    std::cout << *s  ;
  }
}
template<typename T, typename... Args>
void printf(const char* s, T value, Args... args)
{
  while (s && *s) {
    if (*s=='%' && *  s!='%') {
      std::cout << value;
      return printf(  s, args...);
    }
    std::cout << *s  ;
  }
}

Данный код легко «достает» 1-й довод, не являющийся форматной строкой, и после этого вызывает себя рекурсивно. Когда таких доводов огромнее не останется, будет вызвана первая (больше простая) версия способа printf().

Тип Args… определяет так называемую «группу параметров» («parameter pack»). По сути, это последовательность пар тип/значение, из которых вы можете «доставать» доводы, начиная с первого. При вызове функции printf() с одним доводом, будет выбран 1-й способ (printf(const char*)). При вызове функции printf() с двумя либо больше доводами, будет выбран 2-й способ (printf(const char*, T value, Args… args)), с первым параметром s, вторым – value, и оставшиеся параметры (если они есть) будут запакованы в группу параметров args, для дальнейшего применения. При вызове:

printf(  s, args...); 

Группа параметров args сдвигается на один, и дальнейший параметр может быть обработан в виде value. И так продолжается до тех пор, пока args не станет пустым (и будет вызвана первая версия способа printf()).

Некорректные операции сдвига

Числовой литерал 1 имеет тип int. Значит, его невозможно сдвигать больше чем на 31 разряд (подробнее). Про это Зачастую забывают, и в программах дозволено встретить вот такой код:

ptrdiff_t mask = 1 << bitNum;

Если, значение bitNum будем равно, скажем 40, то итог будет непредсказуем. Официально, это приведёт к undefined behavior (подробнее).

Может нам подмогнуть C 11? К сожалению, ничем.

Рассинхронизация виртуальных функций

Пускай в базовом классе объявлена виртуальная функция:

int A(DWORD_PTR x);

А в классе преемнике есть функция:

int A(DWORD x);

В 32-битной программе типы DWORD_PTR и DWORD совпадают. Впрочем, в 64-битной программе, это теснее два различных типа (подробнее). В итоге, вызов функции A из базового класса будет приводить к разным итогам в 32-битной и 64-битной программе.

Бороться с сходственными ошибками могут подмогнуть новые ключевые слова, появившиеся в C 11.

Сейчас у нас есть ключевое слово override, которое разрешает программисту очевидно выражать свои намерения насчет переопределения функций. Объявление функции с ключевым словом override является правильным, только если существует функция для переопределения.

Данный код не скомпилируется в 64-битном режиме и тем самым оплошность будет обезврежена:

struct X
{
  virtual int A(DWORD_PTR) { return 1; }
};
struct Y : public X
{
  int A(DWORD x) override { return 2; }
};

Смешанная арифметика

Это довольно значимая и обширная тема. Предлагаю познакомиться с соответствующим заделом «64-битных уроков»: Смешанная арифметика.

Вовсе коротко:

  1. Программисты Зачастую забывают, что итог перемножения и сложения 2-х переменных типа ‘int’, тоже имеет тип ‘int’. При этом может появиться переполнение. И не значимо, как потом применяется итог умножения и сложения.
  2. Небезопасно смешивать 32-битные и 64-битные типы данных. Итоги: неправильные данные, нерушимые циклы.

Разглядим несколько примитивных примеров про переполнение

char *p = new char[1024*1024*1024*5];

Программист пытается выделить массив 5 гигабайт памяти, но выделит значительно поменьше. Дело в том, что выражение «1024*1024*1024*5» имеет тип int. В итоге произойдёт переполнение, и выражение будет равно 1073741824 (1 гигабайт). После этого, при передачи в оператор ‘new’, число 1073741824 будет расширено до типа size_t, но это не имеет значения (теснее поздно).

Если задача не внятна, то вот иной подобный пример:

unsigned a = 1024, b = 1024, c = 1024, d = 5;
size_t n = a * b * c * d;

Итог выражения помещается в переменную типа ‘size_t’. Она способна беречь значения огромнее, чем UINT_MAX. Но при перемножении переменных типа ‘unsigned’ появляется переполнение и итог будет некорректен.

Отчего мы называем всё это 64-битными ошибками? Дело в том, что в 32-битной программе немыслимо выделить массив размером больше 2 гигабайт. А значит, переполнения легко не появляются. Проявляют себя такие ошибки только в 64-битных программах, когда они начинают трудиться с огромными объёмами памяти.

Сейчас пара примеров про сопоставление

size_t Count = BigValue;
for (unsigned Index = 0; Index < Count;   Index)
{ ... }  

Это пример нерушимого цикла, если Count > UINT_MAX. Представим, что на 32-битных системах данный код исполнял менее повторения, менее чем UINT_MAX раз. Но 64-битный вариант программы может обрабатывать огромнее данных, и ему может понадобиться большее число итераций. От того что значения переменной Index лежат в диапазоне [0..UINT_MAX], то условие «Index < Count» неизменно выполняется, что и приводит к безмерному циклу.

Ещё один пример:

string str = .....;
unsigned n = str.find("ABC");
if (n != string::npos)

Данный код некорректен. Функция find() возвращает значение типа string::size_type. Всё будет отменно трудиться в 32-битной системе. Но давайте посмотрим, что произойдет в 64-битной программе.

В 64-битной программе string::size_type и unsigned перестают совпадать. Если подстрока не находится, функция find() возвращает значение string::npos, которое равно 0xFFFFFFFFFFFFFFFFui64. Это значение урезается до величины 0xFFFFFFFFu и помещается в 32-битную переменную. Вычисляется выражение: 0xFFFFFFFFu != 0xFFFFFFFFFFFFFFFFui64. Получается, что условие (n != string::npos) неизменно правдиво!

Может тут как-то подмогнуть C 11?

Результат — и да, и нет.

В некоторых случаях, нам может подмогнуть новое ключевое слово auto. А в некоторых оно может только запутать программиста. Следственно, давайте наблюдательно разберёмся.

Если объявить «auto a = …..», то её тип будет вычислен механически. Дюже значимо не запутаться и не написать вот такой неверный код: «auto n = 1024*1024*1024*5;».

Побеседуем о ключевом слове auto. Разглядим дальнейший пример:

auto x = 7;

В данном случае тип переменной ‘x’ будет ‘int’, потому что именно такой тип имеет ее инициализатор. В всеобщем случае мы можем написать:

auto x = expression;

И тип переменной ‘x’ будет равен типу значения, полученному в итоге вычисления выражения.

Ключевое слово ‘auto’ для итога типа переменной из ее инициализатора, особенно благотворно, когда точный тип выражения не знаменит, либо труден в написании. Разглядим пример:

template<class T> void printall(const vector<T>& v)
{
  for (auto p = v.begin(); p!=v.end();   p)
    cout << *p << "n";
}

В С 98, вам бы пришлось писать способено больше длинный код:

template<class T> void printall(const vector<T>& v)
{
    for (typename vector<T>::const_iterator p = v.begin(); 
         p!=v.end();   p)
      cout << *p << "n";
}

Дюже пригодное новшество в языке C 11.

Возвратимся к нашей задаче. Выражение «1024*1024*1024*5» имеет тип ‘int’. Так что для теперь нам ‘auto’ никак не поможет.

Не поможет нам ‘auto’ и в случае цикла:

size_t Count = BigValue;
for (auto Index = 0; Index < Count;   Index)

Стало отменнее? Нет. Число 0 имеет тип ‘int’. Значит переменная Index сейчас будет иметь тип не ‘unsigned’, а «int’. Вероятно, стало даже дрянней.

Так есть ли хоть какой-то нам прок от ‘auto’? Да, есть. Скажем, тут:

string str = .....;
auto n = str.find("ABC");
if (n != string::npos)

Переменная ‘n’ будет иметь тип string::size_type. Сейчас всё отлично.

Вот наконец нам и сгодилось новое ключевое слово ‘auto’. Впрочем, будьте опрятны. Необходимо понимать, что и для чего вы делаете. Не нужно верить побороть все ошибки, связанные со смешанной арифметикой, применяя повсюду ‘auto’. Это каждого лишь один из инструментов, а не панацея.

Кстати, есть ещё один метод защититься от обрезания типа в рассмотренном ранее примере:

unsigned n = str.find("ABC");

Дозволено применять новейший формат инициализации переменных, тот, что предотвращает сужение (narrowing) типов. Задача заключается в том, что языки С и С неявно обрезают некоторые типы:

int x = 7.3;  // Ой!
void f(int);
f(7.3);  // Ой!

Впрочем списки инициализации С 11 не разрешают сужение (narrowing) типов:

int x0 {7.3}; //compilation error
int x1 = {7.3}; //compilation error
double d = 7;
int x2{d}; //compilation error

Для нас теперь больше увлекательны вот эти пример:

size_t A = 1;
unsigned X = A;
unsigned Y(A);
unsigned Q = { A }; //compilation error
unsigned W { A }; //compilation error

Предположим, что код написан так:

unsigned n = { str.find("ABC") };
   либо так
unsigned n{str.find("ABC")};

Данный код будет компилироваться в 32-битном режиме и перестанет компилироваться в 64-битном режиме.

Вновь это не панацея от всех ошибок. Легко ещё один метод писать больше надёжные программы.

Адресная арифметика

Задача во многом схожа с тем, что мы разглядели в разделе „Смешанная арифметика“. Различие лишь в том, что переполнение появляется при работе с указателями (подробнее).

Разглядим пример:

float Region::GetCell(int x, int y, int z) const {
  return array[x   y * Width   z * Width * Height];
}

Данный код взят из реальной программы математического моделирования, в которой значимым источником является объем оперативной памяти. В программах данного класса для экономии памяти Зачастую применяют одномерные массивы, осуществляя работу с ними, как с трехмерными массивами. Для этого существуют функции, схожие GetCell, обеспечивающие доступ к нужным элементам. Но приведенный код будет правильно трудиться только с массивами, содержащими менее INT_MAX элементов. Повод — применение 32-битных типов int для вычисления индекса элемента.

Может тут как-то подмогнуть C 11? Нет.

Метаморфоза типа массива и упаковка указателей.

Изредка в программах нужно (либо легко комфортно) представлять элементы массива в виде элементов иного типа (подробнее). Ещё бывает комфортно беречь указатели в переменных целочисленного типа (подробнее).

Ошибки появляются из-за неправильных очевидных приведений типов. С новым эталоном С 11 тут никакой связи нет. Очевидные приведения типов неизменно делались на свой личный ужас и риск.

Следует ещё упомянуть про работу с данными, находящимися в объединениях (union). Такая работа с данными является низкоуровневой и также зависит только от знаний и познаний программиста (подробнее).

Сериализация и обмен данными

В плане может появиться надобность создания совместимого формата данных. То есть один комплект данных должен обрабатываться как 32-битной, так и 64-битной версией программы. Трудность заключается в том, что меняются размеры некоторых типов данных (подробнее).

Эталон C 11 немножко облегчил жизнь, введя типы фиксированного размера. Прежде программисты объявляли такие типы независимо либо применяли типы, объявленные в одной из системной библиотек.

Сейчас есть следующие типы фиксированного размера:

  • int8_t
  • int16_t
  • int32_t
  • int64_t
  • uint8_t
  • uint16_t
  • uint32_t
  • uint64_t

Помимо размера изменяются выравнивание данных в памяти (data alignment). Это тоже может предоставить определённые трудности (подробнее).

Касательно этой темы, стоит упомянуть возникновение в С 11 нового ключевого слова ‘alignment’. Сейчас дозволено написать вот такой код:

// массив символов, выровнен для хранения типов double
alignas(double) unsigned char c[1024]; 
// выравнивание по 16 байтной границе
alignas(16) char[100];

Существует также оператор ‘alignof’, тот, что возвращает выравнивание для указанного довода (довод должен быть типом). Пример:

constexpr int n = alignof(int);

Перегруженные функции

При переносе 32-битных программ на 64-битную платформу может отслеживаться метаморфоза логики ее работы, связанное с применением перегруженных функций. Если функция перекрыта для 32-битных и 64-битных значений, то обращение к ней с доводом, скажем, типа size_t будет транслироваться в разные вызовы на разных системах (подробнее).

Я затрудняюсь ответить, дозволено ли применять какое-то из новых свойств языка для борьбы с такими ошибками.

Проверки размеров типа

Бывают случаи, когда нужно проверить размеры типов данных. Это необходимо, Дабы не получить глючную программу позже перекомпиляции кода для новой системы.

Зачастую это делают неправильным методом. Скажем, так:

assert(sizeof(unsigned) < sizeof(size_t));
assert(sizeof(short) == 2);

Дрянной метод. Во-первых, программа всё равно компилируется. Во-вторых, эти проверки проявят себя только в отладочной версии.

Значительно отменнее останавливать компиляцию, если нужные данные не выполняются. Для этого существует уйма решений. Скажем, дозволено применять макрос _STATIC_ASSERT, тот, что доступен разработчикам, использующим Visual Studio. Пример применения:

_STATIC_ASSERT(sizeof(int) == sizeof(long));

C 11 стандартизировал метод, как остановить компиляцию, если что-то вульгарно не так. В язык введены заявления времени компиляции (static assertions).

Статические заявления (заявления времени компиляции) содержат константное выражение и строковый литерал:

static_assert(expression, string);

Компилятор вычисляет выражение, и если итог вычисления равен false (т.е. заявление нарушено), выводит строку в качестве сообщения об ошибке. Примеры:

static_assert(sizeof(long)>=8, 
  "64-bit code generation required for this library.");

struct S { X m1; Y m2; };
static_assert(sizeof(S)==sizeof(X) sizeof(Y),
  "unexpected padding in S");

Завершение

Написание кода с максимальным применением новых конструкций языка C 11 совсем не гарантирует отсутствия 64-битных ошибок.

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB