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

О чём статья?


Для начала хотелось бы внести конкретики по содержанию. В статье раскрываются следующие темы:
  1. Изменения в анализаторе PVS-Studio, затрагивающие поиск 64-битных ошибок;
  2. Обзор 64-битных ошибок первого уровня, найденных анализатором PVS-Studio, и краткие комментарии к ним;
  3. Сравнение эффективности в поиске наиболее важных ошибок средствами 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-битных ошибок


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

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 Dangerous explicit type pointer conversion: (LONG_PTR *) & evParam1 test.cpp 8
  • V114 Dangerous explicit type pointer conversion: (LONG_PTR *) & evParam2 test.cpp 8

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

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-типом', имеющим размер 32 бита на Win32 (модель данных ILP32) и 64 бита на архитектуре Win64 (модель данных LLP64). В то же время тип 'LONG' имеет размеры 32 бита на обеих архитектурах. Так как на 64-битной архитектуре вышеупомянутые типы имеют разный размер, возможна некорректная работа с объектами, на которые ссылаются эти указатели.

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

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 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 бита соответственно). Потенциальная ошибка состоит в том, что могут быть потеряны значения старших бит в указателе.

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

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 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' нулями, возможно возникновение коллизий, то есть получение одинаковых хешей для различных входных данных (в данном случае — указателей). В результате это может привести к неэффективной работе программы. Если бы выполнялось приведение к 'memsize-типу', усечения не произошло бы, и тогда сдвиг (а следовательно — составление хеша) осуществлялся бы корректно.

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

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

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

Предупреждение анализатора: V220 Suspicious sequence of types castings: memsize -> 32-bit integer -> memsize. The value being cast: 'this'. test.cpp 87

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

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

#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 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, приведено только в этом примере? Вопрос справедливый, но наберитесь терпения, об этом будет написано ниже.

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

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 Unexpected function overloading behavior. See first argument of function 'WinHelpA' in derived class 'CFrameWndEx' and base class 'CWnd'. test.cpp 122

Пример интересен тем, что взят из отчёта при проверке библиотек Visual C++ 2012. Как видите, даже разработчики Visual C++ допускают 64-битные ошибки.

Достаточно подробно об этой ошибке написано в соответствующей статье. Здесь же я хотел объяснить суть вкратце. На 32-битной архитектуре данный код будет корректно отрабатываться, так как типы 'DWORD' и 'DWORD_PTR' имеют одинаковый размер, в классе-наследнике данная функция будет переопределена, и код будет выполняться корректно. Но подводный камень никуда не делся и даст знать о себе на 64-битной архитектуре. Так как в этом случае типы 'DWORD' и 'DWORD_PTR' будут иметь разные размеры, полиморфизм будет разрушен. Мы будем иметь на руках 2 разные функции, что идёт вразрез с тем, что подразумевалось.

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

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

Предупреждение анализатора: 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 в описании соответствующей функции.

Примечание.

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

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


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


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

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



Причём, совершенно неожиданно, предупреждения ведут в заголовочные файлы (например, 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 division
    Dividing sizeof a pointer by another value.
    64BitsErrors — test.cpp (Line 72);
  • C28159 Consider using another function instead
    Consider using 'GlobalMemoryStatusEx' instead of 'GlobalMemoryStatus'. Reason: Deprecated. See MSDN for details
    64BitsErrors — test.cpp (Line 128);

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

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

А теперь ещё раз хочу напомнить, что по функциональности в плане обнаружения ошибок статические анализаторы кода, встроенные в среды Visual Studio 2013 и Visual Studio 2015 одинаковы (о чём подробнее написано в соответствующей заметке).

Что в итоге?


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



Как видно из таблицы, с помощью PVS-Studio было обнаружено 9 64-битных ошибок, а общими средствами Microsoft Visual Studio 2013 — 6. Возможно, вы скажите, что не такая уж большая разница. Не соглашусь. Давайте прикинем, почему:
  • Мы говорили только о наиболее критичных 64-битных ошибках. Даже 3 пропущенные ошибки, это уже много. А если брать более редкие ошибки, для которых анализатор PVS-Studio выдаёт предупреждения 2-ого и 3-его уровня, то он может найти гораздо больше, чем Visual Studio. Некоторое представление об этом может дать эта статья. Статья немного устарела, сейчас разрыв будет ещё больше.
  • Призывать на помощь компилятор с включённым 4-м уровнем предупреждений не всегда возможно. Но как на четвёртом, так и на третьем уровне предупреждений мы получим всего лишь 2 предупреждения (средствами анализатора и компилятора), связанные с 64-битными ошибками. Не слишком впечатляющий результат.
  • Если выставить флаг "/Wall", получаем кучу предупреждений, не имеющих отношения к проекту. На практике воспользоваться "/Wall" будет затруднительно. Можно отдельно включить некоторые предупреждения, но всё равно будет очень много лишнего шума.

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

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

Заключение


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

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

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


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

Эта статья на английском


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Sergey Vasiliev. 64-Bit Code in 2015: New in the Diagnostics of Possible Issues.

Прочитали статью и есть вопрос?
Часто к нашим статьям задают одни и те же вопросы. Ответы на них мы собрали здесь: Ответы на вопросы читателей статей про PVS-Studio, версия 2015. Пожалуйста, ознакомьтесь со списком.

Комментарии (0)