Модераторы: Daevaorn
  

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> Немного о статическом анализе кода, Краткий обзор статических анализаторов 
:(
    Опции темы
CoderCPP
Дата 14.4.2015, 14:51 (ссылка) |    (голосов:2) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 7
Регистрация: 23.3.2015

Репутация: нет
Всего: нет



user posted image

В этой статье я хотел бы вкратце познакомить читателя с инструментами статического анализа на примере нескольких анализаторов - CppcheckPVS-StudioCppCat. Это не подробное их сравнение, но краткий обзор возможностей и функционала. В качестве примера работы приведены несколько ошибок, найденных при помощи анализаторов, а также даны краткие комментарии к ним.


О статическом анализе

Разработка программных продуктов включает несколько аспектов, таких как написание кода, его отладка и поиск ошибок. Значительную часть времени приходится уделять поиску ошибок и отладке. И хорошо, если ошибки легко обнаруживаются - куда печальнее дело обстоит с синтаксически верными, но логически ошибочными конструкциями. Особенно плохо, когда ошибки в подобных местах дают о себе знать не при каждом выполнении программы - тогда их поиск и исправление обходятся особенно дорого.  А если ошибка, присутствовавшая в начальных версиях проекта, дала знать о себе в более поздних, то на её исправление уходит ещё больше сил, ведь чем позже найдена ошибка, тем дороже её исправление.

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

О нескольких таких анализаторах и пойдёт речь в данной статье: PVS-StudioCppCat и Cppcheck. Это не столько основательный анализ преимуществ и недостатков, сколько краткий обзор анализаторов и демонстрация возможностей.


Статические анализаторы


Cppcheck

Очень простой и удобный инструмент, с которого можно начать знакомство со статическими анализаторами. Важным преимуществом является то, что он является open-source проектом и распространяется бесплатно.

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

Но вернёмся к Cppcheck. Он отлично подходит для первого знакомства с инструментами для анализа кода. Прелесть в том, что первый раз его можно запустить, вообще ничего не настраивая и не подготавливая. Просто указываем папку с проектом, и анализатор приступает к своей работе. Ненастроенный Cppcheck конечно даст менее полезный результат, но вся прелесть в том, что мы просто устанавливаем инструмент и говорим ему: "проверь вот эту папку с исходным кодом". И он её проверяет, и находит ошибки! Это может поразить человека, впервые пробующего статический анализатор кода. Он видит ошибки в своей программе и навсегда влюбляется в такие инструменты. После этого он будет готов читать руководства и настраивать инструменты статического анализа. Ведь он увидел, насколько они полезны. Инструменты, которые требуют первоначальной настройки, явно проигрывают Cppcheck в качестве привлечения новых пользователей. 

Можно сказать, что Cppcheck является большим популяризатором методологии статического анализа. Большое спасибо всем тем, кто развивает этот проект.

Рассмотрим пару примеров ошибок, которые умеет находить анализатор.

Код
static UStringPrepType
getValues(uint32_t result, int32_t* value, UBool* isIndex){
  ....
  }else{
    *isIndex = FALSE;
    *value = (int16_t)result;
    *value =  (*value >> 2);
  }
  if((result>>2) == _SPREP_MAX_INDEX_VALUE){
    type = USPREP_DELETE;
    isIndex =FALSE;
    value = 0;
  }
  ....
}


Анализатор заподозрил неладное, рассматривая вот эти строки:

Код
isIndex =FALSE;
value = 0;


И действительно, зачем менять значение указателей, переданных в качестве аргументов. Особенно нестыковка заметна для переменной 'isIndex'. Это указатель на тип 'UBool'. Этому указателю присваивается значение FALSE. Код компилируется только благодаря везению, так как FALSE на самом деле есть ни что иное как 0.

Явно, здесь должно быть написано:

Код
*isIndex =FALSE;
*value = 0;


Анализатор уведомляет о таких подозрительных местах сообщением "Assignment of function parameter has no effect outside the function".

Теперь пример, где случайно перепутан порядок аргументов:

Код
void CMeasureSection::SortSections( void )
{
  ....
  memset(sortarray,sizeof(CMeasureSection*)*128,0);
  ....
}


Сообщение Cppcheck: memset() called to fill 0 bytes of 'sortarray'. measure_section.cpp 180

Второй аргумент функции memset() задаёт значение, которым будет заполняться область память. Количество байт для заполнения задаётся третьим аргументом. Эти два параметра легко перепутать, что и произошло. С точки зрения языка Си/Си++ никакой ошибки здесь нет, и компилятор не выдаст никаких предупреждений. Но из-за ошибки  память не будет заполнена нулями. 

И последний пример. В целом, статические анализаторы хуже обнаруживают такие ошибки, как выход за границы массива или утечки ресурсов. Это прерогатива динамических анализаторов кода (подробности). Тем не менее, иногда статические анализаторы тоже весьма полезны. Рассмотрим пример, когда Cppcheck обнаруживает, что файл может быть не закрыт:

Код
int SmdExportClass::DoExport(....) 
{
  ....
  FILE *pFile;
  if ((pFile = fopen(szFile, "w")) == NULL)
    return FALSE;
  ....

  if (!CollectNodes(pexpiface))
    return 0;
  ....
}


Предупреждение Cppcheck: Resource leak: pFile smdlexp.cpp 164

Файл открывается, затем где-то ниже закрывается. Но обратите внимание на этот фрагмент кода:

Код
if (!CollectNodes(pexpiface))
  return 0;


Если не удалось собрать 'Nodes', то происходит выход из функции. При этом файл не закрывается. Правильным вариантом будет:

Код
if (!CollectNodes(pexpiface))
{
  fclose(pFile);
  return 0;
}


Итог. Если кто-то спросит, с какого статического анализатора ему стоит начать, посоветуйте ему Cppcheck. Он прост и бесплатен. Незаменимые качества при знакомстве с чем-то новым.


CppCat

По сути CppCat, это сокращённый вариант PVS-Studio. В нем убрано всё, что вряд ли понадобится программисту, работающему в Visual Studio. Дополнительно к этому за программиста принято решение, какие диагностические предупреждения ему стоит смотреть, а какие - нет. Это позволяет начать использовать статический анализатор без тяжких раздумий над параметрами настройки. К сожалению, статические анализаторы грешат тягой к сложности своих настроек. Это может оттолкнуть программиста, который ещё не имел дело с такими инструментами. А что ещё хуже - обилие настроек может приводить к тому, что программист случайно выставит не ту галочку и получит безобразные результаты. Он не сможет понять, кто виноват и с большой вероятностью это надолго отобьёт желание работать со статическими анализаторами.

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


PVS-Studio

Статический анализатор кода, умеющий обнаруживать широкий спектр разнообразных ошибок. Анализатор может использоваться как из среды Visual Studio, так и отдельно. Поддерживает языки: C, C++, C++11, C++/CLI, C++/CX. Имеется функционал, позволяющий разнообразными способами интегрировать анализатор в процесс разработки. Например, можно выполнять автоматический анализ изменённых файлов, присутствует  поддержка MSBuild, можно отслеживать запуск компилятора и собирать информацию, необходимую для проверки проекта.

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

По поводу опечаток. Программист знает, что они существуют. Но на практике очень сложно помнить про них и делать что-то, чтобы их избежать. Внимание программиста сосредоточено на том, чтобы не забыть проверить указатель на равенство NULL перед использованием или, например, инициализировать все члены класса. Факт того, что в ходе написания кода легко допустить опечатку, как правило, не учитывается. К сожалению, как раз на устранение ошибок, связанных с опечатками, уходит много времени. И анализатор может существенно упростить жизнь программиста, взяв на себя большую часть работы по их выявлению.

Приведу пример, выявленной опечатки. Закрывающаяся скобка стоит не на своём месте:

Код
template<typename Scalar> EIGEN_DEVICE_FUNC
inline bool isApprox(const Scalar& x, const Scalar& y,
  typename NumTraits<Scalar>::Real precision =
    NumTraits<Scalar>::dummy_precision())

template< .... >
void evalSolverSugarFunction(....)
{
  ....
  const Scalar psPrec = sqrt( test_precision<Scalar>;() );
  ....
  if (internal::isApprox(
        calc_realRoots[i], real_roots[j]), psPrec)
  {
    found = true;
  }
  ....
}


Функция isApprox() имеет два обязательных аргумента и один необязательный. Это послужило поводом к возникновению ошибки.

Запятая перед третьим аргументом в данном случае является оператором, а переменная 'psPrec' стала его правым операндом.

Поясним для тех, кто плохо знаком с оператором запятая. Операция запятая имеет самый низкий приоритет среди всех операций языка Си++. У этой операции есть 2 операнда (левый и правый). Вначале вычисляется левый операнд, затем правый, который и возвращается в качестве результата.

Получается, что изначально вызывается функция isApprox() с двумя аргументами, но результат её работы никак не используется. Оператор запятая возвращает значение переменной 'psPrec', в результате чего, код работает так:

Код
internal::isApprox(_realRoots[i], real_roots[j]);
if (psPrec)


Правильный вариант кода:

Код
if (internal::isApprox(
      calc_realRoots[i], real_roots[j], psPrec))



Анализатор PVS-Studio выдал для этого кода предупреждение: V639 Consider inspecting the expression for 'isApprox' function call. It is possible that one of the closing ')' brackets was positioned incorrectly. polynomialsolver.cpp 123

Но все-таки почему я говорю, что программисты не подозревают об опечатках? Потому что они встречаются в проектах куда чаще, чем этого можно было бы ожидать. Посмотрите, например, вот эту статью: "Эффект последней строки". Учтите, что эта статья написана по мотивам анализа отлаженного и в целом работающего кода. Что же говорить, про только что написанный свежий код.

Ещё один вид скрытых ошибок, когда программа ведёт себя не так, как думает программист - конструкции, приводящие к неопределённому поведению. Но про них говорить не будем. Есть и более интересные ситуации.

Анализатор обладает обширной базой знаний, что позволяет   ему выдавать предупреждения об опасных ситуациях, о которых программист вообще может и не догадываться. Очень хорошая и полезная помощь. Немногие программисты знают, что в этом коде, вызов функции memset() может исчезнуть:

Код
char* crypt_md5(const char* pw, const char* salt)
{
  MD5_CTX ctx,ctx1;
  unsigned long l;
  int sl, pl;
  u_int i;
  u_char final[MD5_SIZE];
  static const char *sp, *ep;
  ....
  /* Don't leave anything around in vm they could use. */
  memset(final,0,sizeof final);
  return passwd;
}


Перед выходом из функции требуется очистить буфер, содержащий приватные данные. Для этого используется функция memset(). Это неправильно. Компилятор знает, что после функции memset() буфер 'final' больше не используется. Он имеет полное право удалить вызов функции memset(). Более того - он так и сделает при сборке release-версии. Подробнее про это можно почитать в статье "Перезаписывать память - зачем?" и в описании диагностики V597.

Чтобы этого не произошло, следует использовать специальные функции, которые компилятор не имеет права удалять.

Анализатор PVS-Studio выдал для этого кода предупреждение: V597 The compiler could delete the 'memset' function call, which is used to flush 'final' buffer. The RtlSecureZeroMemory() function should be used to erase the private data. md5.c 342

Надеюсь, читателю было интересно познакомиться с анализатором PVS-Studio. Подробнее о нём можно почитать в статье: "PVS-Studio для Visual C++". Если интересно посмотреть примеры ошибок, выявленных  анализатором, можно заглянуть вот на эту страницу: "Ошибки, обнаруженные в Open Source проектах".


Заключение

В заключение хотелось бы напомнить, что данная статья носит скорее ознакомительный характер и ставит целью знакомство читателя с инструментами статического анализа для проектов, написанных на языках C/C++.  Думаю, сложно переоценить ценность статических анализаторов, особенно в больших проектах, где на поиск не бросающейся в глаза ошибки может уйти достаточно много времени.  Таким образом, анализаторы экономят как время и силы программистов, так и стоимость разработки. А краткий обзор трёх из них - PVS-StudioCppCatCppcheck поможет вам выбрать тот или иной продукт для своих нужд.

И помните! Самое главное - использовать статический анализ регулярно, а не от случая к случаю. В этом вся его суть и польза.


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

Привожу перечень материалов по теме статического анализа, которые могут быть полезны заинтересовавшимся:


Это сообщение отредактировал(а) CoderCPP - 14.4.2015, 15:02
PM MAIL   Вверх
antontitl00
Дата 10.5.2017, 18:14 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 1
Регистрация: 10.5.2017

Репутация: нет
Всего: нет



Спасибо за черновики C++ очень полезная инфа!!! Анализ программного обеспечения, производимый (в отличие от динамического анализа) без реального выполнения исследуемых программ. В большинстве случаев анализ производится над какой-либо версией исходного кода, хотя иногда анализу подвергается какой-нибудь вид объектного кода, например, P-код или код на MSIL. Термин обычно применяют к анализу, производимому специальным программным обеспечением (ПО), тогда как ручной анализ называют «program understanding», «program comprehension» (пониманием или постижением программы).  http://www.a1qa.ru/services/security_testing/

Это сообщение отредактировал(а) antontitl00 - 23.5.2017, 15:38
PM MAIL   Вверх
  
Ответ в темуСоздание новой темы Создание опроса
Правила форума "С++:Общие вопросы"
Earnest Daevaorn

Добро пожаловать!

  • Черновик стандарта C++ (за октябрь 2005) можно скачать с этого сайта. Прямая ссылка на файл черновика(4.4мб).
  • Черновик стандарта C (за сентябрь 2005) можно скачать с этого сайта. Прямая ссылка на файл черновика (3.4мб).
  • Прежде чем задать вопрос, прочтите это и/или это!
  • Здесь хранится весь мировой запас ссылок на документы, связанные с C++ :)
  • Не брезгуйте пользоваться тегами [code=cpp][/code].
  • Пожалуйста, не просите написать за вас программы в этом разделе - для этого существует "Центр Помощи".
  • C++ FAQ

Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, Earnest Daevaorn

 
1 Пользователей читают эту тему (1 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | C/C++: Общие вопросы | Следующая тема »


 




[ Время генерации скрипта: 0.1140 ]   [ Использовано запросов: 21 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.