﻿# 64\-битный код в 2015 году: что нового в диагностике возможных проблем?

64\-битные ошибки достаточно тяжело обнаружить, так как они сродни бомбе замедленного действия: могут дать о себе знать далеко не сразу\. Статический анализатор PVS\-Studio облегчает задачу поиска и исправления подобных ошибок\. Однако в этом направлении были сделаны ещё несколько шагов: недавно были более внимательно пересмотрены 64\-битные диагностики, вследствие чего их распределение по уровням важности изменилось\. Речь в данной статье пойдёт об этих изменениях, а также о том, как это повлияло на работу с инструментом и на поиск ошибок\. Примеры 64\-битных ошибок из реальных приложений прилагаются\.

![0325_64bitsErrors_ru/image1.png](https://import.viva64.com/docx/blog/0325_64bitsErrors_ru/image1.png)

## О чём статья?

Для начала хотелось бы внести конкретики по содержанию\. В статье раскрываются следующие темы:

1. Изменения в анализаторе PVS\-Studio, затрагивающие поиск 64\-битных ошибок;
1. Обзор 64\-битных ошибок первого уровня, найденных анализатором PVS\-Studio, и краткие комментарии к ним;
1. Сравнение эффективности в поиске наиболее важных ошибок средствами PVS\-Studio и Microsoft Visual Studio 2013\.

Первый пункт говорит сам за себя: в нём будут рассмотрены основные изменения PVS\-Studio, касающиеся анализа 64\-битных ошибок, а также то, как они отразятся на работе с инструментом\.

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

В третьем разделе осуществляется сравнение в эффективности поиска этих ошибок статическим анализатором PVS\-Studio и средствами среды Microsoft Visual Studio 2013\. Причём, в случае Visual Studio, для поиска ошибок использовались как компилятор, так и статический анализатор\.

Не стоит забывать, что здесь выписаны только некоторые ошибки\. В реальном проекте их наверняка будет куда больше и они будут более разнообразными\. В конце статьи предложны ссылки, которые более полно познакомят вас с миром 64\-битных ошибок\.

## Изменения в PVS\-Studio, связанные с 64\-битными ошибками

Не так давно мы внимательно просмотрели 64\-битные диагностики и более аккуратно распределили их по уровням важности\.

Теперь распределение 64\-битных ошибок выглядит так:

**Уровень 1\.** Критические ошибки, которые причиняют вред в любом приложении\. Примером может служить хранение указателя в 32\-битной переменной типа int\. Если вы разрабатываете 64\-битное приложение, вы обязательно должны изучить и исправить предупреждения первого уровня\.

**Уровень 2\.** Ошибки, которые как правило проявляют себя только в приложениях, обрабатывающие большие массивы данных\. Пример \- использование для индексации огромного массива переменной типа 'int'\.

**Уровень 3\.** Всё остальное\. Как правило, эти предупреждения не актуальны\. Однако, для некоторых приложений, та или иная диагностика может оказаться крайне полезной\.

Таким образом, установив фильтрацию по 64\-битным ошибкам первого уровня, вы получите список сообщений, указывающих на участки кода, которые с большей вероятностью являются ошибочными\. Не стоит недооценивать эти предупреждения, так как последствия 64\-битных ошибок могут быть самыми разными, но явно неприятными и часто неожиданными\. Именно про них и пойдёт речь\.

Насколько тяжело было бы обнаружить подобные ошибки без такого инструмента, как PVS\-Studio, думаю, поймёте по мере прочтения статьи\.

## Анализ 64\-битных ошибок

Необходимо внимательно следить за правильным использованием типов данных\. С этого, пожалуй, и начнём\. 

```cpp
LRESULT CSaveDlg::OnGraphNotify(WPARAM wParam, LPARAM lParam)
{
  LONG evCode, evParam1, evParam2;
  while (pME && SUCCEEDED(pME->GetEvent(&evCode, 
    (LONG_PTR*)&evParam1, 
    (LONG_PTR*)&evParam2, 0))) 
  {
    ....
  }
  return 0;
}
```

Предупреждения анализатора:

* [V114](https://pvs-studio.ru/ru/docs/warnings/v114/) Dangerous explicit type pointer conversion: \(LONG\_PTR \*\) & evParam1 test\.cpp 8
* [V114](https://pvs-studio.ru/ru/docs/warnings/v114/) Dangerous explicit type pointer conversion: \(LONG\_PTR \*\) & evParam2 test\.cpp 8

Для того, чтобы понять суть ошибки, необходимо взглянуть на типы переменных 'evParam1', 'evParam2', а также на объявление метода 'GetEvent':

```cpp
virtual HRESULT STDMETHODCALLTYPE GetEvent( 
            /* [out] */ __RPC__out long *lEventCode,
            /* [out] */ __RPC__out LONG_PTR *lParam1,
            /* [out] */ __RPC__out LONG_PTR *lParam2,
            /* [in] */ long msTimeout) = 0;
```

Как видно из сообщения анализатора, выполняется опасное явное приведение типа\. Дело в том, что тип 'LONG\_PTR' является '[memsize\-типом](https://pvs-studio.ru/ru/blog/terms/0030/)', имеющим размер 32 бита на Win32 \(модель данных [ILP32](https://pvs-studio.ru/ru/blog/terms/0018/)\) и 64 бита на архитектуре Win64 \(модель данных [LLP64](https://pvs-studio.ru/ru/blog/terms/0026/)\)\. В то же время тип 'LONG' имеет размеры 32 бита на обеих архитектурах\. Так как на 64\-битной архитектуре вышеупомянутые типы имеют разный размер, возможна некорректная работа с объектами, на которые ссылаются эти указатели\.

Продолжим тему опасных приведений типов\. Взглянем на следующий код:

```cpp
BOOL WINAPI TrackPopupMenu(
  _In_      HMENU hMenu,
  _In_      UINT uFlags,
  _In_      int x,
  _In_      int y,
  _In_      int nReserved,
  _In_      HWND hWnd,
  _In_opt_  const RECT *prcRect
);

struct JABBER_LIST_ITEM
{
  ....
};

INT_PTR CJabberDlgGcJoin::DlgProc(....)
{
  ....
  int res = TrackPopupMenu(
    hMenu, TPM_RETURNCMD, rc.left, rc.bottom, 0, m_hwnd, NULL);
  ....
  if (res) {
    JABBER_LIST_ITEM *item = (JABBER_LIST_ITEM *)res;
    ....
  }
  ....
}
```

Предупреждение анализатора: [V204](https://pvs-studio.ru/ru/docs/warnings/v204/) Explicit conversion from 32\-bit integer type to pointer type: \(JABBER\_LIST\_ITEM \*\) res test\.cpp 57

Для начала неплохо было бы взглянуть на использовавшуюся в данном коде функцию 'TrackPopupMenu'\. Она возвращает идентификатор выбранного пользователем элемента меню, или нулевое значение в случае ошибки или если выбора не было\. Тип 'BOOL' для этих целей явно выбран неудачно, но что делать\.

Результат выполнения данной функции, как это видно из кода, заносится в переменную 'res'\. В случае, если какой\-то элемент пользователем всё же был выбран \(res\!\=0\), то данная переменная приводится к типу указателя на структуру\. Интересный подход, но так как в статье мы ведём беседу про 64\-битные ошибки, давайте подумаем, как этот код будет исполняться на 32 и 64\-битных архитектурах, и в чём может быть проблема? 

Загвоздка в том, что на 32\-битной архитектуре такие преобразования допустимы и осуществимы, так как типы 'pointer' и 'BOOL' имеют одинаковый размер\. Но грабли дадут о себе знать на 64\-битной архитектуре\. В Win64 приложениях вышеупомянутые типы имеют разный размер \(64 и 32 бита соответственно\)\. Потенциальная ошибка состоит в том, что могут быть потеряны значения старших бит в указателе\.

Продолжаем обзор\. Фрагмент кода:

```cpp
static int hash_void_ptr(void *ptr)
{
  int hash;
  int i;

  hash = 0;
  for (i = 0; i < (int)sizeof(ptr) * 8 / TABLE_BITS; i++)
  {
    hash ^= (unsigned long)ptr >> i * 8;
    hash += i * 17;
    hash &= TABLE_MASK;
  }
  return hash;
}
```

Предупреждение анализатора: [V205](https://pvs-studio.ru/ru/docs/warnings/v205/) Explicit conversion of pointer type to 32\-bit integer type: \(unsigned long\) ptr test\.cpp 76

Разберемся, в чём же заключается проблема приведения переменной типа 'void\*' к типу 'unsigned long' в этой функции\. Как уже говорилось, данные типы имеют различный размер в модели данных LLP64, где тип 'void\*' занимает 64 бита, а 'unsigned long' \- 32 бита\. В результате этого будут отсечены \(потеряны\) старшие биты, содержавшиеся в переменной 'ptr'\. Значение же переменной 'i' по мере прохождения итераций увеличивается, как следствие этого \- побитовый сдвиг вправо по мере прохождения итераций будет затрагивать всё большее количество бит\. Так как размер переменной 'ptr' был усечён, с некоторой итерации все содержащиеся в ней биты будут заполняться 0\. Как итог всего вышеописанного \- на Win64\-приложениях 'hash' будет составляться некорректно\. За счёт заполнения 'hash' нулями, возможно возникновение [коллизий](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BB%D0%BB%D0%B8%D0%B7%D0%B8%D1%8F_%D1%85%D0%B5%D1%88-%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8), то есть получение одинаковых хешей для различных входных данных \(в данном случае \- указателей\)\. В результате это может привести к неэффективной работе программы\. Если бы выполнялось приведение к 'memsize\-типу', усечения не произошло бы, и тогда сдвиг \(а следовательно \- составление хеша\) осуществлялся бы корректно\.

Посмотрим на следующий код:

```cpp
class CValueList : public CListCtrl
{
  ....
  public:
    BOOL SortItems(_In_ PFNLVCOMPARE pfnCompare, 
      _In_ DWORD_PTR dwData);
  ....
}; 

void CLastValuesView::OnListViewColumnClick(....)
{
  ....
  m_wndListCtrl.SortItems(CompareItems, (DWORD)this);
  ....
}
```

Предупреждение анализатора: [V220](https://pvs-studio.ru/ru/docs/warnings/v220/) Suspicious sequence of types castings: memsize \-\> 32\-bit integer \-\> memsize\. The value being cast: 'this'\. test\.cpp 87

Диагностика V220 сигнализирует о двойном опасном преобразовании данных\. В начале переменная 'memsize\-типа' превращается в 32\-битное значение, а затем сразу расширяется обратно до 'memsize\-типа'\. Фактически, это означает что будут "отрезаны" значения старших бит\. Почти всегда это ошибка\.

Продолжим раскрывать тему опасных преобразований:

```cpp
#define YAHOO_LOGINID "yahoo_id"
DWORD_PTR __cdecl CYahooProto::GetCaps(int type, HANDLE /*hContact*/)
{
  int ret = 0;
  switch (type)
  {
    ....
  case PFLAG_UNIQUEIDSETTING:
    ret = (DWORD_PTR)YAHOO_LOGINID;
    break;
    ....
  }
  return ret;
}
```

Предупреждение анализатора: [V221](https://pvs-studio.ru/ru/docs/warnings/v221/) Suspicious sequence of types castings: pointer \-\> memsize \-\> 32\-bit integer\. The value being cast: '"yahoo\_id"'\. test\.cpp 99

Заметил тенденцию, что с каждым примером преобразований становится больше и больше\. Тут их целых 3\. И 2 из них являются опасными, по тем же причинам, что и все, описанные выше\. Так как 'YAHOO\_LOGINID' является строковым литералом, его тип \- 'const char\*', имеющий в 64\-битной архитектуре тот же размер, что и тип 'DWORD\_PTR', так что явное преобразование корректно\. Но вот дальше начинаются нехорошие вещи\. Тип 'DWORD\_PTR' неявно приводится к целочисленному 32\-битному\. Но это не всё\. Так как возвращаемый функцией результат имеет тип 'DWORD\_PTR', будет выполнено ещё одно неявное преобразование, на этот раз обратно к 'memsize\-типу'\. Очевидно, что в таком случае использование возвращённого значения ведётся на свой страх и риск\.

Хочу отметить, что компилятор Visual Studio 2013 выдал предупреждение следующего вида:

warning C4244: '\=' : conversion from 'DWORD\_PTR' to 'int', possible loss of data 

Здесь будет возможен актуальный вопрос: почему предупреждение, выданное Visual Studio 2013, приведено только в этом примере? Вопрос справедливый, но наберитесь терпения, об этом будет написано ниже\.

А пока продолжим рассматривать ошибки\. Рассмотрим следующий код, содержащий иерархию классов:

```cpp
class CWnd : public CCmdTarget
{
  ....
  virtual void WinHelp(DWORD_PTR dwData, UINT nCmd = HELP_CONTEXT);
  ....
};

class CFrameWnd : public CWnd
{
  ....
};

class CFrameWndEx : public CFrameWnd
{
  ....
  virtual void WinHelp(DWORD dwData, UINT nCmd = HELP_CONTEXT);
  ....
};
```

Предупреждение анализатора: [V301](https://pvs-studio.ru/ru/docs/warnings/v301/) Unexpected function overloading behavior\. See first argument of function 'WinHelpA' in derived class 'CFrameWndEx' and base class 'CWnd'\. test\.cpp 122

Пример интересен тем, что взят из [отчёта](https://pvs-studio.ru/ru/blog/posts/cpp/0163/) при проверке библиотек Visual C\+\+ 2012\. Как видите, даже разработчики Visual C\+\+ допускают 64\-битные ошибки\.

Достаточно подробно об этой ошибке написано в [соответствующей статье](https://pvs-studio.ru/ru/blog/lessons/0012/)\. Здесь же я хотел объяснить суть вкратце\. На 32\-битной архитектуре данный код будет корректно отрабатываться, так как типы 'DWORD' и 'DWORD\_PTR' имеют одинаковый размер, в классе\-наследнике данная функция будет переопределена, и код будет выполняться корректно\. Но подводный камень никуда не делся и даст знать о себе на 64\-битной архитектуре\. Так как в этом случае типы 'DWORD' и 'DWORD\_PTR' будут иметь разные размеры, полиморфизм будет разрушен\. Мы будем иметь на руках 2 разные функции, что идёт вразрез с тем, что подразумевалось\.

И последний пример:

```cpp
void CSymEngine::GetMemInfo(CMemInfo& rMemInfo)
{
  MEMORYSTATUS ms;
  GlobalMemoryStatus(&ms);
  _ultot_s(ms.dwMemoryLoad, rMemInfo.m_szMemoryLoad,   
    countof(rMemInfo.m_szMemoryLoad), 10);
  ....
}
```

Предупреждение анализатора: [V303](https://pvs-studio.ru/ru/docs/warnings/v303/) The function 'GlobalMemoryStatus' is deprecated in the Win64 system\. It is safer to use the 'GlobalMemoryStatusEx' function\. test\.cpp 130

В принципе, особых пояснений не требуется, всё понятно из сообщения анализатора\. Необходимо использовать функцию 'GlobalMemoryStatusEx', так как функция 'GlobalMemoryStatus' может работать некорректно на 64\-битной архитектуре\. Подробнее об этом написано на портале MSDN в [описании соответствующей функции](https://msdn.microsoft.com/en-us/library/windows/desktop/aa366586%28v=vs.85%29.aspx)\.

**Примечание\.**

Обратите внимание на то, что все приведённые ошибки могут встретиться в самом обыкновенном прикладном программном обеспечении\. Чтобы они возникли, программе вовсе не обязательно работать с большим объемом памяти\. И именно поэтому диагностики, выявляющие эти ошибки, относятся к первому уровню\.

## Что нам скажет Visual Studio 2013?

### Предупреждения компилятора

Прежде чем рассказывать про результаты проверки статического анализатора среды Visual Studio 2013, хотелось бы остановиться на предупреждениях компилятора\. Внимательные читатели наверняка заметили, что в тексте было приведено только 1 такое предупреждение\.  В чём же дело, спросите вы? А дело в том, что больше никаких предупреждений, как\-то связанных с 64\-битными ошибками, попросту не было\. И это при 3\-м уровне выдачи предупреждений\. 

Но стоит скомпилировать этот пример при всех включённых предупреждениях \(EnableAllWarnings\), как получаем\.\.\.

![0325_64bitsErrors_ru/image2.png](https://import.viva64.com/docx/blog/0325_64bitsErrors_ru/image2.png)

Причём, совершенно неожиданно, предупреждения ведут в заголовочные файлы \(например,  winnt\.h\)\. Если не полениться и найти в этой куче предупреждений те, которые относятся к проекту, то всё же можно извлечь что\-то интересное, например:

warning C4312: 'type cast' : conversion from 'int' to 'JABBER\_LIST\_ITEM \*' of greater size

warning C4311: 'type cast' : pointer truncation from 'void \*' to 'unsigned long'

warning C4311: 'type cast' : pointer truncation from 'CLastValuesView \*const ' to 'DWORD'

warning C4263: 'void CFrameWndEx::WinHelpA\(DWORD,UINT\)' : member function does not override any base class virtual member function

В общем компилятор выдал 10 предупреждений в файле с этими примерами\. Только 3 предупреждения из этого списка явно указывают на 64\-битные ошибки \(предупреждения компилятора С4311 и С4312\)\. Среди этих предупреждений есть и такие, которые указывают на сужающее преобразование типов \(С4244\) или же на то, что виртуальная функция не будет переопределена \(С4263\)\. Эти предупреждения также косвенно указывают на 64\-битные ошибки\.

В итоге, исключив так или иначе повторяющие друг друга предупреждения, получим 5 предупреждений, касающихся рассматриваемых нами 64\-битных ошибок\.

Как видим, компилятору Visual Studio не удалось обнаружить все 64\-битные ошибки\. Напоминаю, что анализатор PVS\-Studio в том же файле нашёл 9 ошибок первого уровня\. 

"А как же статический анализатор, встроенный в Visual Studio 2013?" \- спросите вы\. Может быть он справился лучше и нашёл больше ошибок? Давайте посмотрим\.

### Статический анализатор, входящий в состав Visual Studio 2013

Результатом проверки этих примеров статическим анализатором, встроенным в среду Visual Studio 2013 стали 3 предупреждения:

* C6255 Unprotected use of alloca\_alloca indicates failure by raising a stack overflow exception\. Consider using \_malloca instead\.64BitsErrors \- test\.cpp \(Line 58\);
* C6384 Pointer size divisionDividing sizeof a pointer by another value\.64BitsErrors \- test\.cpp \(Line 72\);
* C28159 Consider using another function insteadConsider using 'GlobalMemoryStatusEx' instead of 'GlobalMemoryStatus'\. Reason: Deprecated\. See MSDN for details64BitsErrors \- test\.cpp \(Line 128\);

Но мы ведь смотрим 64\-битные ошибки, верно? Сколько ошибок из этого списка относится к 64\-битным? Только последняя \(использование функции, которая может возвращать некорректные результаты\)\.

Выходит, что статический анализатор Visual Studio 2013 нашёл 1 64\-битную ошибку против 9, найденных анализатором PVS\-Studio\. Впечатляет, не правда ли? Представьте, какова будет разница в больших проектах\.

А теперь ещё раз хочу напомнить, что по функциональности в плане обнаружения ошибок статические анализаторы кода, встроенные в среды Visual Studio 2013 и Visual Studio 2015 одинаковы \(о чём подробнее написано в [соответствующей заметке](https://pvs-studio.ru/ru/blog/posts/0319/)\)\.

## Что в итоге?

Нагляднее всего будет отразить результаты проверки примеров кода в виде таблицы\.

![0325_64bitsErrors_ru/image3.png](https://import.viva64.com/docx/blog/0325_64bitsErrors_ru/image3.png)

Таблица 1\. Результат поиска 64\-битных ошибок анализатором PVS\-Studio и средствами Microsoft Visual Studio 2013

Как видно из таблицы, с помощью PVS\-Studio было обнаружено 9 64\-битных ошибок, а общими средствами Microsoft Visual Studio 2013 \- 6\. Возможно, вы скажите, что не такая уж большая разница\. Не соглашусь\. Давайте прикинем, почему:

* Мы говорили только о наиболее критичных 64\-битных ошибках\. Даже 3 пропущенные ошибки, это уже много\. А если брать более редкие ошибки, для которых анализатор PVS\-Studio выдаёт предупреждения 2\-ого и 3\-его уровня, то он может найти гораздо больше, чем Visual Studio\. Некоторое представление об этом может дать [эта статья](https://pvs-studio.ru/ru/blog/posts/a0066/)\. Статья немного устарела, сейчас разрыв будет ещё больше\.
* Призывать на помощь компилятор с включённым 4\-м уровнем предупреждений не всегда возможно\. Но как на четвёртом, так и на третьем уровне предупреждений мы получим всего лишь 2 предупреждения \(средствами анализатора и компилятора\), связанные с 64\-битными ошибками\. Не слишком впечатляющий результат\.
* Если выставить флаг "/Wall", получаем кучу предупреждений, не имеющих отношения к проекту\. На практике воспользоваться "/Wall" будет затруднительно\. Можно отдельно включить некоторые предупреждения, но всё равно будет очень много лишнего шума\. 

Как видно из вышенаписанного, для того, чтобы обнаружить 64\-битные ошибки, найденные средствами Microsoft Visual Studio 2013, нужно проделать ещё определённый объём работы\. А теперь представьте, насколько он увеличится, будь это реальный, действительно большой проект\.

Что с PVS\-Studio? Запускаем диагностику, выставляем фильтрацию по 64\-битным ошибкам и нужным предупреждениям несколькими кликами мыши, получаем результат\.

## Заключение

Надеюсь, что мне удалось показать, что перенос приложений на 64\-битную архитектуру связан с рядом сложностей\.  Ошибки, подобные тем, что были описаны в этой статье, достаточно легко допустить, но при этом крайне тяжело найти\.  Добавим к этому тот факт, что не все подобные ошибки обнаруживаются средствами Microsoft Visual Studio 2013, да и для поиска тех нужно проделать определённый объём работ\. В то же время статический анализатор [PVS\-Studio](https://pvs-studio.ru/ru/pvs-studio/) справился с поставленной задачей, показав достойный результат\. При этом сам процесс поиска и фильтрации ошибок проще и удобнее\. Согласитесь, что в действительно больших проектах без подобного инструмента пришлось бы туго, так что в подобных случаях хороший статический анализатор попросту необходим\.

Разрабатываете 64\-битное приложение? [Скачайте](https://pvs-studio.ru/ru/pvs-studio/download/) триал PVS\-Studio, проверьте свой проект и посмотрите, сколько 64\-битных сообщений первого уровня у вас будет\. Если несколько всё же обнаружатся \- пожалуйста, исправьте их, и сделайте этот мир чуточку лучше\.

## Дополнительные материалы

Как и обещал, привожу перечень дополнительных материалов по теме 64\-битных ошибок:

* Терминология\. [64\-битная ошибка](https://pvs-studio.ru/ru/blog/terms/0002/);
* Андрей Карпов\. [Коллекция примеров 64\-битных ошибок в реальных программах](https://pvs-studio.ru/ru/blog/posts/cpp/a0065/)\.
* Андрей Карпов\. [C\+\+11 и 64\-битные ошибки](https://pvs-studio.ru/ru/blog/posts/cpp/0253/)\.
* Андрей Карпов, Евгений Рыжков\. [Уроки разработки 64\-битных приложений на языке Си/Си\+\+](https://pvs-studio.ru/ru/blog/lessons/)\.