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

Тонкости обзора начального кода C/C с поддержкой cppcheck

Anna | 24.06.2014 | нет комментариев

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

В этой статье будут рассмотрены вероятности cppcheck по вылавливанию утрат памяти, пригодные параметры для совершенствования обзора, а также экспериментальная вероятность по созданию собственных правил. Сегодня никаких сопоставлений анализаторов «кто отменнее», статья всецело посвящена работе с cppcheck.

Загрузка и установка

Загрузить cppcheck дозволено с официального сайта, для Windows есть инсталлятор, а для Linux я рекомендую скачать начальный код с гита, так как собирается он дюже легко и не имеет зависимостей. Сборка из начального кода дозволит включить экспериментальную вероятность, о которой будет рассказано в конце статьи.

В частности, для своей Linux-машины я форкнул на GitHub cppcheck и сделал git clone форка. Это дозволит в грядущем коммитить в репозиторий свои личные конфиги и собственноручно написанные правила проверки,периодично синхронизируясь с основным репозиторием, что, согласитесь, дюже комфортно (не говоря о вероятности отправлять патчи в план).

Собираем для Linux

Сборка в Linux весьма примитивна: скачать, распаковать, перейти в каталог и исполнить make:

unzip cppcheck-master.zip
cd cppcheck-master
make
Собираем для Windows

Сборка в Windows тоже не должна представлять сложностей — там есть план для VS. Открываем, собираем. Сам не пробовал, т. к. не имею такой надобности.

Cppcheck как плагин

По части плагинов для IDE — присутствуют плагины под Code::Blocks, CodeLite, Gedit и Eclipse, разумеется. Но этим дело не ограничивается, так как есть плагины для сборочных ферм Hudson, Jenkins, для систем управления версиями Tortoise SVN и Mercurial. Для Visual Studio плагина нет, но есть дюже милая фраза на основной странице:

There is no plugin for Visual Studio, but it is possible to add Cppcheck as an external tool. You can also try the proprietary PVS-Studio (there is a free trial), which is oriented for this environment.

В cppcheck присутствует GUI, написанный на Qt. Он ещё больше упрощает процесс обзора, но крепко вдаваться в его подробности не будем — грозные программисты не пользуются графическими интерфейсами:) Тем больше что GUI всецело повторяет вероятности командной строки (а в некоторых случаях — уступает) и разобраться в нём позже знакомства с cppcheck труда не составит.

Стоит подметить, что cppcheck распространяется под лицензией GNU GPL. Это разрешает без труда взять начальный код этой программы, утащить в свой репозиторий Git и допиливать там под всякие нужды, добавляя свои правила и библиотеки.

Настройка анализатора

Позитивным моментом cppcheck является вероятность тонкой настройки. Дозволено и ярус Восприимчивости настроить, и итог сообщений форматировать, и фильтровать некоторые назойливые сообщения.

честности ради подмечу, что всю информацию, высказанную ниже, дозволено получить из документации либо исполнив команду cppcheck –help, но я остановлюсь на особенно значимых нюансах поподробнее (и на русском языке:). Документация по cppcheck становится из года в год всё отменнее, следственно её благотворно изредка перечитывать.

Если вам кажется, что я занимаюсь пересказом документации — переходите к чтению дальнейшего раздела, т. к. тут информация для тех, кто никогда ранее не применял cppcheck и анализаторы в тезисе.

?русы ошибок

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

Пример того, как это работает. Представим, необходимо проанализировать дальнейший код:

void f() {
    char *a = malloc(100);
    process_a(a);
}

На 1-й взор, тут оплошность: нет free. Впрочем если функция process_a является библиотечной функцией, немыслимо с уверенностью сказать, что process_a где-то внутри не делает free для указателя a. Если внутри функции process_a переменная a подлинно освобождается — это ложное срабатывание, которое будет мешать обзору. Следственно cppcheck вначале испробует обнаружить реализацию функции process_a в коде анализируемой программы, убедится, что она не вызывает free, и только в этом случае выдаст ошибку. Если реализация не обнаружена, cppcheck полагает особенно благоприятный сценарий из всех допустимых:process_a освобождает память и следственно не выдаст ошибки. Однако, cppcheck дозволено «обучить» распознавать функции библиотек, тем самым увеличивая точность обзора — об этом будет рассказано ниже.

2-й пример:

void f() {
    char *a = malloc(100);
    if(random())
        g_exit(0);
    free(a)
}

Тут теснее очевидно есть утрата памяти, так как g_exit — обёртка библиотеки GLib над стандартной функциейexit. Но cppcheck не знает, что g_exit прерывает выполнение программы, следственно анализатор не сумеет распознать тут ошибки. Необходимо как-то предоставить cppcheck информацию о том, что функция g_exitможет прерывать программу, Дабы анализатор обучился распознавать такие ошибки.

Из примеров видно, что cppcheck перестраховывается, причём планка адекватности высока. Множество проверок cppcheck по умолчанию не включает. Среди них следующие категории проверок, всякая из которых может включаться/выключаться самостоятельно:

  • error — очевидные ошибки, которые анализатор считает скептическими и традиционно они приводят к багам (включено по умолчанию);
  • warning — предупреждения, тут даются сообщения о небезопасном коде;
  • style — стилистические ошибки, сообщения возникают в случае неаккуратного кодирования (огромнее схоже на рекомендации);
  • performance — задачи продуктивности, тут cppcheck предлагает варианты, как сделать код стремительней (но это не неизменно даёт приход продуктивности);
  • portability — ошибки совместимости, традиционно связано с разным поведением компиляторов либо систем различной разрядности;
  • information — информационные сообщения, возникающие в ходе проверки (не связаны с ошибками в коде);
  • unusedFunction — попытка вычислить неиспользуемые функции (мёртвый код), не может трудиться в многопоточном режиме;
  • missingInclude — проверка на недостающий #include (скажем, используем random, а подключитьstdlib.h позабыли).

Включаются проверки параметром –enable, список категорий проверок перечисляется через запятую. Скажем:

cppcheck -q -j4 --enable=warning,style,performance,portability ./source

Таким образом я традиционно включаю особенно значимые проверки. Существует ключевое слово all, которое включает все перечисленные проверки.

Примечание. Параметры -j и режим проверки unusedFunction несовместимы, следственно -j отключит проверку unusedFunction, даже если она указана очевидно.

Пример команды, которая «гоняет» код по каждому правилам:

cppcheck -q --enable=all ./source

И это ещё не всё. Если ваша программа безошибочна с точки зрения анализатора, испробуйте запустить cppcheck с параметром –inconclusive. Данный режим подлинно включает все допустимые проверки, даже ошибки с малой вероятностью, которые cppcheck пропускает по умолчанию.

Таким образом, самый подробнейший режим проверки:

cppcheck -q --enable=all --inconclusive ./source
Не забывайте о кроссплатформенности!

Cppcheck первоначально создавался как инструмент, работающий с различными операционными системами и платформами. Следственно необходимо непременно следить за тем, для какой платформы написана программа и какой режим проверки использует cppcheck. Платформа переключается параметром –platform.

Разные платформы:

  • unix32 — все 32-разрядные *nix (включая Linux);
  • unix64 — все 64-разрядные *nix;
  • win32A — семейство 32-разрядных Windows с кодировкой ASCII;
  • win32W — семейство 32-разрядных Windows с кодировкой UNICODE;
  • win64 — семейство 64-разрядных Windows.

Если необходимо проверить код, тот, что был написан для Win32, применяя Linux, необходимо непременно указать платформу:

cppcheck --platform=win32A ./source

Дозволено указать, по какому эталону написан начальный код, что изредка разрешает уточнить проверку и выловить новые ошибки. Используйте параметр –std со следующими допустимыми вариантами:

  • posix — для ОС, совместимых со эталоном POSIX (включая Linux);
  • c89 — язык Си, эталон 89-го года;
  • c99 — язык Си, эталон 99-го года;
  • c11 — язык Си, эталон 11-го года (по умолчанию для Си);
  • c 03 — язык Си , эталон 03-го года;
  • c 11 — язык Си , эталон 11-го года (по умолчанию для Си ).

Дозволено применять сразу два эталона:

cppcheck --std=c99 --std=posix ./source
Пригодные доводы в командной строке

Допустимо теснее кидается в глаза, что я всюду использую параметры -q-j. Для чего они необходимы? Разглядим особенно увлекательные.

-j — дюже пригодный параметр, дозволяющий запускать проверку в многопоточном режиме. Применять дюже легко — в качестве параметра передаётся число процессоров и проверка пойдёт веселее.
-q — тихий режим. По умолчанию cppcheck выдаёт информационные сообщения о ходе проверки (которых может быть дюже много). Данный параметр всецело выключает информационные сообщения, остаются только сообщения об ошибках.
-f либо –force — включить перебор всех вариантов директив ifdef (по умолчанию cppcheck проверяет дюжину вариантов). Что это такое — потом будет рассмотрено отдельно.
-v — режим отладки — cppcheck выдаёт внутреннюю информацию о ходе проверки.
–xml — выводить итог проверки в формате XML.
–template=gcc — выводить ошибки в формате предупреждений компилятора gcc (комфортно для интеграции с IDE, поддерживающей такой компилятор).
–suppress — режим подавления ошибок с указанными идентификаторами (необходим повторный обзор).
-h — выдаёт справку по каждому параметрам на чистейшем английском языке.

Фильтрация сообщений и исключения

Как всякий уважающий себя анализатор cppcheck разрешает при проверке эластично настроить отображение ошибок. Особенно пригодной является допустимо отключить определённое предупреждение в определённом файле (и допустимо в определённой строке). Отключение сообщений реализовано параметром –suppress, где необходимо указать исключение, либо –suppress-file с указанием текстового файла, где содержится список исключений. Дабы передать несколько исключений в командной строке дозволено указать несколько параметров –suppress подряд, но отменнее для таких целей завести файл с исключениями.

Формат исключений:

id[:file:[line]]

Непременный параметр id (идентификатор ошибки), следом через двоеточие дозволено опционально указать имя файла, позже имени файла, также опционально, дозволено указать номер строки.

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

The scope of the variable '%VAR' can be reduced.

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

cppcheck -q -j4 --enable=all --suppress=variableScope ./source

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

Узнать список всех допустимых ошибок поможет параметр –errorlist, тот, что выдаёт полный список в формате XML. Но я могу порекомендовать иной способ определения «неугодных» сообщений. Для этого понадобится изменить формат итога сообщений с поддержкой параметра –template:

cppcheck -q -j4 --enable=all --template='{id} {file}:{line} {message}' ./source

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

Пример итога

variableScope geany/src/document.c:1099 The scope of the variable 'use_ft' can be reduced.
variableScope geany/src/document.c:1257 The scope of the variable 'filename' can be reduced.
variableScope geany/src/document.c:2306 The scope of the variable 'keywords' can be reduced.
variableScope geany/src/document.c:3011 The scope of the variable 'old_status' can be reduced.
variableScope geany/src/editor.c:194 The scope of the variable 'specials' can be reduced.
variableScope geany/src/editor.c:248 The scope of the variable 'ptr' can be reduced.
variableScope geany/src/editor.c:1545 The scope of the variable 'text' can be reduced.
variableScope geany/src/editor.c:4309 The scope of the variable 'tab_str' can be reduced.

И наконец, рецепт механического создания файла для применения в параметре –suppress-file. В командной строке это делается в два счёта:

cppcheck -q --enable=all --template='{id}:{file}:{line}' ./source > suppress-list.txt

Сейчас полученный файл дозволено подать на вход cppcheck и в итоге не окажется ни одной ошибки. Это пригодно, когда обзор завершён и все срабатывания анализатора — ложные.

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

Разбираемся с include и define

Cppcheck понимает некоторые параметры компиляторов, что разрешает уточнить, по которому пути идёт проверка. Так как cppcheck не пользуется службами компилятора, он имеет свой личный препроцессор. Данный препроцессор не требует ни наличия всех заголовочных файлов, ни корректности начального кода. Если где-то встречается неведомый include — cppcheck его легко не обрабатывает.

Неизвестность может сыграть гневную шутку. Обыкновенной практикой в библиотеке GLib является проверка доводов:

void f(gchar *s1, gchar *s1) {
    g_return_if_fail(s1);
    gchar *a = g_strdup(s1);
    g_return_if_fail(s2);
    gchar *b = g_strdup(s2);
}

Всё отлично за исключением того, что g_return* — это макросы, которые прерывают выполнение функции в случает ошибки. Таким образом, если 1-й довод функции f окажется правильным, а 2-й — нет, появляется утрата памяти. Cppcheck об этом не догадывается, так как считает g_return_if_fail по умолчанию «отличной функцией», а не макросом.

Поведение cppcheck дозволено изменить, если подключить нужные заголовочные файлы, Дабы препроцессор сделал всю нужную работу: он найдёт реализацию макроса g_return_if_fail, раскроет его, а cppcheck увидит воображаемый return без free, что является паттерном утраты памяти.

Для того Дабы принудить препроцессор трудиться как следует, необходимо указать пути, где искать заголовочные файлы. За это отвечает параметр -I, тот, что аналогичен одноименному параметру компилятора gcc. Для GLib и Linux это абсолютно предсказуемый путь:

cppcheck -q -I/usr/include/glib-2.0/ ./source

Увлекательная специфика (которая крепко увеличивает время обзора) — перебор ifdef-вариантов. Если в программе есть один ifdef — cppcheck сделает два варианта препроцессинга и просканирует оба варианта начального кода. Чем огромнее в начальном коде ifdef-ветвлений, тем огромнее вариантов необходимо перебирать. Руководить этим поведением дозволено параметрами -D и -U. Параметр -DA обозначает, что макрос A определён. Параметр -UB обозначает, что макрос B не определён.

По умолчанию проверяется только дюжина конфигураций. Изменить это число дозволено параметром –max-configs. Дабы проверять только одну конфигурацию, дозволено задать проверку одной конфигурации. Параметр –force проверит все конфигурации (дюже медлительно).

Cppcheck не отличает макросов из заголовочных файлов от макросов, определённых в начальном коде. Если обзор усилить препроцессингом всех #include, указав каталог с заголовочными файлами — приготовьтесь кдюже долгому обзору — cppcheck будет шелушить все макросы из всех заголовочных файлов, до которых дотянулся препроцессор.

Применение препроцессора — самый примитивный метод усовершенствовать проверку, но видимо, что время этой проверки крепко возрастает, так как препроцессинг раздувает начальный код до сотен мегабайт. На примере GLib будет показан больше результативный метод ловить утраты памяти, применяя иную вероятность cppcheck предоставления информации о сторонних библиотеках.

Пишем реализации функций независимо

Стоит подметить, что параметр -I лишь информирует cppcheck, где искать заголовочные файлы и подключает их только в случае, если в начальном файле есть соответствующий #include. Дозволено применять чуть больше затратный альтернативный вариант: реализовать особенно частые макросы и функции вручную и подключить их ко каждому файлам плана. Придётся немножко поработать над созданием такого файла, но обзор пойдёт гораздо стремительней и вернее. Больше того, дозволено применять некоторые увлекательные трюки с макросами.

Подключение файла с реализацией функций реализуется параметром –append. Указанный файл механически вставляется в конец всякого файла плана.

Подключение файла с макроопределениями реализуется параметром –include. Указанный файл механически вставляется в предисловие всякого файла плана.

Скажем, программа использует библиотеку GLib и необходимо известить cppcheck, что g_return_if_fail — это макрос.

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

void f(char *s1, char *s1) {
    g_return_if_fail(s1);
    char *a = g_strdup(s1);
    g_return_if_fail(s2);
    char *b = g_strdup(s2);
    free(a);
    free(b);
}

Запускаем cppcheck:

cppcheck -q test.c

Ничего.

Создаём файл gtk.h дальнейшего оглавления:

#define g_return_if_fail(expr) do{if(!(expr)){return;}}while(0)

Так как это макрос, его необходимо включать в предисловие:

cppcheck -q test.c --include=gtk.h

Хм. Вновь ничего? Если присмотреться к коду в примере, дозволено подметить функцию g_strdup, о которой cppcheck пока ничего не знает. Испробуем написать простейшую реализацию (файл gtk.c):

char * g_strdup(const char *s) {
	return strdup(s);
}

Обратите внимание, что это функция, а не макрос.
Анализируем. Файл с реализацией функции вставляется параметром –append;

cppcheck -q test.c --include=gtk.h --append=gtk.c
[test.c:4]: (error) Memory leak: a

Готово! Сейчас cppcheck обучился обнаруживать новые утраты памяти. Разумный анализатор забрался вовнутрь кода функции g_strdup, сделал трассировку возвращаемого значения и нашел там strdup, пометив указатели как указатели на память, которую необходимо освободить.

Данный пример взят не с потолка: это одна из самых частых ошибок в GLib-программах, которые я нахожу непрерывно.

Трюки с макросами и реализациями

Механизм подключения файлов к плану — дюже эластичный инструмент. С его поддержкой дозволено проделывать такие вещи как исключение функций с знаменитым поведением, подмена доводов функций, определение недостающих typedef-ов.

Во многих случаях cppcheck не нуждается в том, Дабы присутствовал какой-либо заголовочный файл либо был определён тип данных/класс. Следственно писать макросы имеет толк только в том случае, если они улучшают проверку.

Нестандартное выделение памяти

Представим, есть функция my_alloc, которая выделяет память внутри своих доводов:

my_alloc(char **a);

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

void f(){
	char *a, *b;
	my_alloc(&a, &b);
}

Добавим сейчас реализацию:

void my_alloc(char **a, char **b)
{
	*a = malloc(13);
	*b = malloc(42);
}

и проверка выдаст следующее:

[test.c:4]: (error) Memory leak: a
[test.c:4]: (error) Memory leak: b
Исключаем функцию из проверки

Если есть какая-то функция, которую мы не хотим проверять, её легко спрятать:

#define unused_func(arg...)
Выделение памяти с флагом ошибки

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

void f() {
	int is_ok;
	char *a = my_alloc(&is_ok);
	if(is_ok)
		free(a);
}

Впрочем если в базе cppcheck my_alloc значится как функция выделения памяти, итогом проверки будет оплошность. Дабы её избежать, дозволено применять следующую хитрость:

char *my_alloc(int *ok)
{
	char *a = malloc(42);
	if(a) *ok = 1;
	else *ok = 0;
	return a;
}

Обзор с учётом особенностей библиотек

Множество приложений применяют какие-либо библиотеки, в частности, glibc. Анализатору порой необходима некоторая информация о библиотечных функциях. Скажем, malloc выделяет память, а free — освобождает. Если есть malloc, но нет free, нередко это обозначает утрату памяти. Данные функции входят в glibc и являются частью эталона, следственно анализатор (да и компилятор) о них знать легко обязан.

Что же касается каждого остального, то в идеале для всякой применяемой библиотеки анализатор должен иметь некоторое изложение её особенностей, Дабы уметь обнаруживать ошибки, допущенные не только при применении glibc.

Отчего это так значимо? Если встречается неведомая функция — cppcheck полагает, что она находится где-то в сторонней библиотеке и строит касательно неё самые благоприятные прогнозы (она освобождает за собой память, не прерывает выполнение программы и т. п.). Если где-то был найден паттерн утраты памяти либо неинициализированной переменной, но внутри находится функция неведомого назначения, cppcheck заблокирует сообщение об ошибке.

Разглядим примитивный пример. В ОС Linux многие графические приложения применяют библиотеку GLib. Данная библиотека фактически дублирует glibc, в том числе она имеет свои реализации malloc/free. Традиционно код, использующий GLib, выглядит так:

gchar *s = g_strdup("test");
gint *a = g_malloc(sizeof(int) * 10);

То, что позже g_strdup и g_malloc память необходимо освобождать, человек додумается подсознательно, даже не имея ранее навыка работы с GLib. Чего не скажешь об анализаторе: реализация функции g_malloc спрятана внутри двоичного кода библиотеки — кто её знает, что она там делает?

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

Самое славное в cppcheck то, что он имеет такую пополняемую базу данных. Это обозначает, что сообщество может улучшать cppcheck легко добавляя всё новую информацию о стандартных библиотеках, которую cppcheck потом будет применять при обзоре.

Cppcheck не загружает механически нужные библиотеки, это необходимо делать вручную. Библиотеки указываются параметром –library, дозволено указывать несколько библиотек, разделяя их запятыми. cppcheck вначале ищет файл с именем библиотека.cfg в нынешнем каталоге (комфортно так беречь библиотеку для своего плана), потом пытается обнаружить её в своей базе библиотек. Если библиотеки нигде не нашлось, cppcheck выдаст ошибку.

Пример обзора плана с применением библиотеки gtk:

cppcheck -q --library=gtk ./source

Дозволено очевидно указать, в каком файле лежит библиотека (нужно сказать, не посмотрев в исходники, я бы не додумался, что так дозволено делать):

cppcheck -q --library=my/path/mylib.xml ./source

На сегодняшний день cppcheck поддерживает вовсе немножко библиотек:

  • gtk (на самом деле, glib, gtk там не поддерживается)
  • Qt
  • windows (любые scanf_s…)
  • posix
  • glibc (стандартная библиотека перестаёт быть hard-coded и понемногу вытесняется в базу)

Список очевидно небогат. Больше того, от содержимого этой базы хочется пустить скупую слезу, вот, скажем, база POSIX:

<?xml version="1.0"?>
<def>
  <function name="usleep"> <noreturn>false</noreturn> <arg nr="1"><not-bool/><valid>0-999999</valid></arg> </function>
  <function name="_exit"> <noreturn>true</noreturn> </function>
</def>

Из этого файла дозволено почерпнуть только одно: cppcheck для растяжения своей базы использует XML.

Что на данный момент cppcheck может «выуживать» из базы данных библиотеки:

  • список функций выделения памяти и соответствующий список освобождения, позже изложения может ловить утраты памяти и несоответствие конструктора/деструктора — на мой взор самая пригодная вероятность;
  • функция, прерывающая выполнение (как бы exit);
  • функция, которую необходимо пропускать при проверке информационного потока утраты памяти (скажем, функция strlen никак не модифицирует полученный указатель и её дозволено не рассматривать при проверке на утрату памяти);
  • проверка корректности доводов функции, включая передачу на вход функции неинициализированной переменной;
  • проверка строки-формата и списка доводов на соответствие (как в printf);
Формат базы данных (вновь немножко документации)

База формируется как XML-файл, тот, что кладётся в каталог cfg плана cppcheck. Вот отчего я рекомендовал в начале статьи загрузить начальный код программы. cfg-файлы прибиты гвоздями к подкаталогу cfg рядом с исполняемым файлом и это пока не поправили. Внутри каталога cfg теснее есть несколько библиотек, которыми дозволено пользоваться как примерами для создания своей библиотеки — документации по данной теме не дюже много.

Всякий файл начинается с заголовка:

<?xml version="1.0"?>

Каждая база оборачивается в тег def. Разных тегов внутри def может быть три:

  • memory — информация о функциях, работающих с памятью;
  • resource — подобно, но для источников (открыт/закрыт), классический поверенный — open/close;
  • function — легко функция.

Что касается memory и resource, внутренняя конструкция у них идентичная:

  • alloc — внутри тега размещается аллокатор, дозволено добавить признактега init=«true|false» — указывает, инициализирует ли аллокатор память;
  • dealloc — внутри тега размещается соответствующая функция освобождения памяти каждому функциям выделения памяти в блоке (дозволено комбинировать несколько alloc/dealloc в одном блоке);
  • use — функция, которая должна применять объект, выделенный аллокатором (теоретически, так как вероятность не описана в документации и не реализована в исходниках). У меня предположение, что это будет применяться для счётчиков ссылок.

Всякий блок memory и resource необходимо дублировать для различных групп функций. Скажем, если память, выделенную функцией malloc необходимо освобождать при помощи free, а g_malloc — при помощи g_free, их необходимо попарно поместить в различных тегах memory.

Функция (function) — владеет наибольшим числом вероятностей. Имя функции указывается как признак nameтега function. Внутри тега дозволено применять:

  • noreturn — если true, данная функция не прерывает выполнение;
  • leak-ignore — непартный тег, обозначает, что данная функция определённо не освобождает указатели и её дозволено игнорировать при проверке на утраты памяти;
  • arg — проверка довода, номер довода уточняется признаком nr (детально описано в документации).

Примечание. Тег noreturn для функции необходимо ставить только тогда, когда она безоговорочно прерывает выполнение. Может показаться, что данный тег описывает не-void функции, но это не так. Пример такой функции — exit. Из-за этой особенности при составлении правил для библиотеки GLib появлялись крупные странности.

Описываем библиотеку GLib

GLib — библиотека языка Си, лежащая в основе GTK и реализующая объектно-ориентированное программирование в Си (а с недавних пор возникла интроспекция). На ней построено дюже много планов: GIMP, GNOME, Xfce, даже Chromium/Firefox в той либо другой мере её применяют.

GLib является кроссплатформенной, следственно даже такие функции как malloc/free либо printfпродублированы внутри GLib, Дабы не зависеть от определенной реализации либо версии glibc поддерживаемых платформ. Как следствие, в GLib десятки функций, работающие с памятью, свой обработчик ошибок, в целом, всё то, о чём cppcheck не подозревает.

В cppcheck лишь относительно незадолго возникли зачатки поддержки библиотеки GLib. Скажем, такие очевидные утраты памяти cppcheck ловить может:

void f() {
    g_malloc(42);
}
cppcheck -q test.c --library=gtk
[test.c:3]: (error) Return value of allocation function g_malloc is not used.

Впрочем в GLib сотни функций, и такое хитросплетение cppcheck теснее не возьмёт:

void f() {
    gchar *a = g_strdup(s);
    g_strdown(a);
}

Всё потому, что функция g_strdown незнакома. Внезапно она освобождает память сама?

Библиотеку GLib дозволено условно разбить на комбинированные части:

  • функции-аллокаторы всеобщего назначения, все они освобождаются одной функцией g_free;
  • конструкторы и деструкторы объектов;
  • функции, «впитывающие» в себя указатели (хэш-таблицы, списки) — для таких не необходимо извещать об утратах памяти — будет много неверных срабатываний;
  • каждого одна функция, прерывающая выполнение — g_exit;
  • все остальные функции, которые дозволено игнорировать. Данный список особенно значимый, так как именно он уведомляет cppcheck не прятать ошибку из-за функции, которая ничего не делает с указателями.

Приступим к образованию списка правил. В GLib и GTK больше четырёх тысяч функций, разумно, что вручную их собрать фактически немыслимо. К счастью, в новых версиях GLib возникла интроспекция, что дозволит без усилий вытянуть все метsendto-email.XXXXXX”); if (G_UNLIKELY (mkdtemp (tmpdir) == NULL)) { error = g_error_new_literal (G_FILE_ERROR, g_file_error_from_errno (errno), g_strerror (errno)); tse_error (error, _(“Failed to create temporary directory”)); g_error_free (error); return FALSE; } /* где-то дюже вдалеке */ g_free(tmpdir);

Схожие примеры

  escaped_file_name = g_shell_quote (file_name);
  switch (desktop_type)
    {
      case DESKTOP_TYPE_XFCE:
        ...
        break;

      case DESKTOP_TYPE_NAUTILUS:
     ...
        break;

      default:
        return; /* как, уходите? А как же break??? */
        break;
    }
  g_free (escaped_file_name);
  GString            *command_line = g_string_new (NULL);
  GList              *lp;
  gchar              *dirname;
  gchar              *quoted;
  gchar              *path;
  gchar              *uri;

  g_return_val_if_fail (THUNAR_UCA_IS_MODEL (uca_model), FALSE);
  g_return_val_if_fail (iter->stamp == uca_model->stamp, FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

Несоответствие конструктора/деструктора. На самом деле это утрата памяти, так как внутренняя конструкция массива не освобождается. Код ниже будет восхитительно трудиться без ошибок, но будет течь в элементах массива:

  gchar     **attrs;
...
  attrs = g_file_info_list_attributes (info1, NULL);
...
  g_free (attrs);

Указатель возвращается позже освобождения:

  GtkTreePath *path = NULL;
  GtkTreeIter  iter;
  ThunarFile  *file = NULL;

  path = (*THUNAR_STANDARD_VIEW_GET_CLASS (standard_view)->get_path_at_pos) (standard_view, x, y);
  if (G_LIKELY (path != NULL))
    {
      gtk_tree_model_get_iter (GTK_TREE_MODEL (standard_view->model), &iter, path);
      file = thunar_list_model_get_file (standard_view->model, &iter);

      if (!thunar_file_is_directory (file) && !thunar_file_is_executable (file))
        {
          g_object_unref (G_OBJECT (file)); /* Тут объект может быть удалён! */
          gtk_tree_path_free (path);
          path = NULL;
        }
    }
  return file;

Это спорное предупреждение, так как g_object_unref считает ссылки и будет ли удалён объект неведомо, но к ошибке стоит присмотреться.

Ложные срабатывания. Были и такие — некоторые функции gtk разрешают отчуждать объект, тот, что уничтожится механически совместно с родителем, если их не исключить — cppcheck будет браниться.

Примеры

static void
manage_actions (GtkWindow *window)
{
  GtkWidget *dialog;

  dialog = g_object_new (THUNAR_UCA_TYPE_CHOOSER, NULL);
  gtk_window_set_transient_for (GTK_WINDOW (dialog), window);
  gtk_widget_show (dialog);
}
  label = g_object_new (GTK_TYPE_LABEL, "label", _("Appears if selection contains:"), "xalign", 0.0f, NULL);
  gtk_table_attach (GTK_TABLE (table), label, 0, 2, 2, 3, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0);

Тут применяется иная функция освобождения, что всё равно правильно, так как g_object легко считает ссылки:

  dialog = g_object_new (THUNAR_TYPE_COLUMN_EDITOR, NULL);
...
  gtk_widget_destroy (dialog);

От этого ложного срабатывания трудно избавиться, так как g_new Зачастую применяется Дабы выделить массив строк:

  devices = g_new0 (gchar *, length   2);
...
  g_strfreev (devices);

Здесь cppcheck запутался в условиях. Баг в cppcheck:

  /* be sure to not overuse the stack */
  if (G_LIKELY (length < 2000))
    {
      old_order = g_newa (GSequenceIter *, length);
      new_order = g_newa (gint, length);
    }
  else
    {
      old_order = g_new (GSequenceIter *, length);
      new_order = g_new (gint, length);
    }
...
  /* clean up if we used the heap */
  if (G_UNLIKELY (length >= 2000))
    {
      g_free (old_order);
      g_free (new_order);
    }

Но прелесть этих неверных срабатываний в том, что их легко поправить, пропатчив XML-файл с библиотекой. Таким образом, удалось сократить число ошибок до 14. Со временем интроспекция в GLib будет усовершенствована и дозволено будет в механическом режиме собрать информацию о библиотеке.

Пишем правила для cppcheck

Напоследок— самое аппетитное. Тут пойдёт речь о том, как реализовать свои личные проверки для cppcheck. Наверно, вы теснее находили у себя какой-то баг и хотели бы проверить каждый план на присутствие схожих ошибок. Иная обстановка — вы тимлид и программисты в вашей команде регулярно лепят типовые ошибки, которые хотелось бы пресекать в механическом режиме. Удерживать свой личный код «в узде» пригодно, Дабы разрабатывать результативные и безвредные программы. Обнаружили ошибку — написали к ней не только регрессионный тест, но и правило анализатора.

Что под капотом?

Для начала пара слов о том, как работает cppcheck. Перед непринужденно обзором cppcheck прогоняет препроцессор, подобно компилятору, позже чего следует этап облегчения начального кода. То есть: убираются все лишние отступы и пробелы, всякая лексическая конструкция языка разделяется ровно одним пробелом. Все константы, которые могут быть упрощены во время препроцессинга, — вычисляются. Всюду расставляются блоки {}, даже если они опущены. Если присутствует объявление либо присвоение переменной внутри блока if/for/while — оно будет вынесено снаружи этого блока.

Так как жанр кодирования у всех различный, cppcheck тяготится сделать так, Дабы код был приведён к своего рода «типичной форме». Скажем, один программист пишет цикл так:

for(int i = 0; i < 10; i  )
    if(i % 2)
        printf("%d\n", i);

А иной — так:

int i;
for(i = 0; i < 10; i  ) {
    if(i % 2)
        printf("%d\n", i);
}

Cppcheck приведёт это всё к виду:

int i ; for ( i = 0 ; i < 10 ; i    ) { if ( i % 2 ) { printf ( "%d\n" , i ) ; } }

Не дюже читабельно, но легко исследовать. Все лексические конструкции старательно поделены пробелами и не имеют ничего лишнего.

Cppcheck использует несколько ярусов облегчения: примитивный, типичный и начальный. Скажем, на «простом» ярусе оператор sizeof раскрыт как число, а обыкновенном ярусе он остаётся оператором. Это изредка пригодно для поиска ошибок, связанных с определенным оператором. Начальный ярус — это код в первозданном виде, над которым ещё не поработал препроцессор и он только приведён к типичной форме.

Таким образом, cppcheck строит на основе начального кода некоторую модель (класс Tokenizer), где все токены легко поделены пробелом, дозволяя анализирующим модулям легко пользоваться этими токенами. Анализирующий модуль может в свою очередь строить свою модель, если ему это нужно, перемещаться по токенам, определять тип токена и т. п. На данный момент есть базовая модель (разбиение на токены, она применяется фактически всюду), модель ValueFlow — для проверки утрат памяти и экспериментальная модуль AST (синтаксическое дерево). Разработчики правил могут применять всякую из этих моделей.

Дабы не крушить голову, как cppcheck оптимизирует те либо иные конструкции, дозволено воспользоваться отладочным режимом, в котором cppcheck выдаст на экран упрощённую версию кода:

cppcheck --debug ./file.cpp
Правила на основе регулярных выражений

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

Перед тем как пользоваться данной вероятностью, необходимо перекомпилировать cppcheck (вот отчего я рекомендую загрузить версию исходников из гита), включив экспериментальную поддержку регулярных выражений. Для этого понадобится библиотека pcre. Компилируется всё так же легко:

make HAVE_RULES=yes

Сейчас у cppcheck появится два новых параметра: –rule — дозволено задать регулярное выражение прямо в командной строке и –rule-file — XML-база с вашими собственными правилами проверки.

Наглядно познакомиться с новой вероятностью дозволено дальнейшим образом. Тестовый пример:

void f() {
	if(a) free(a);
}

Дальше проверка. Составим регулярное выражение, которое подмиs_lqvmk!/pre>

Как вестимо, на XML у меня аллергия, следственно я сотворил репозиторий с немножко упрощённым форматом базы и компилятором на Питоне. XML-база, собранная мною, содержит основные рекомендации CERT классного тона программирования на Си (не Си !) и помаленьку пополняется. Как говорится, contibutions are welcome.

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

Если хочется узнать побольше, по теме создания правил для cppcheck есть недурная документация: раздватри (третья часть посвящена разработке правил на языке Си ). Вы можете крепко подмогнуть плану, отправляя разработчикам патчи, сообщения о неверных срабатываниях, багах, и не стесняйтесь делать PR (нет, не тот, а Pull Request:).

 

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

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