1 янв. 2012 г.

Статический анализ кода на C в Linux'е

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

К сожалению, поиски того, где же ты напортачил, могут затянуться если не использовать анализаторы кода. Там, где человек от усталости вполне может и пропустить какую-то, не вполне очевидную, ошибку, программа ничего не упустит и предупредит о том, что "здесь что-то не так".
Поскольку я пишу на C и использую GNUтые или же просто опенсорсные средства разработки (GNU Emacs, GNU GCC, Doxygen и так далее), то меня интересовали, в первую очередь, средства для статического анализа кода с открытыми исходниками. О них то, по меньшей мере о тех, которые мне приглянулись, и пойдет речь в данном посте.

GNU GCC

Во-первых, как внезапно оказалось, некоторый анализ кода можно проводить и средствами самого компилятора gcc. Достаточно лишь задать ему набор "волшебных" ключиков, приняв которые он начнет относиться к вашему коду придирчиво и педантично.
Список ключей я сформировал на основе вот этой статьи и на основе официальной документации по компилятору gcc:
  • -Wall включает предупреждения о выходах за границы массива, о неиспользуемых переменных, функциях и так далее;
  • -Wextra включает предупреждения о пустых блоках в операторе if, о сравнениях signed и unsigned;
  • -pedantic включает строгое следование стандарту ISO C. Всякие расширения компилятора, наподобие long long игнорируются;
  • -Wconversion -Wsign-conversion дает предупреждение о преобразовании типа, при котором значение может поменяться;
  • -Werror трактует все предупреждения как ошибки. Пока все предупреждения компилятора не будут исправлены - программа не скомпилируется;
  • -Winit-self проверяет на наличие ошибок "самоинициализации", вида int i = i;
  • -Wunreachable-code ищет участки кода, которые никогда не будут выполнены;
  • -Wformat-y2k работает только вместе с -Wall и предупреждает об использовании в функции strftime() и подобных об использовании формата, который дает только последние две цифры года, вместо четырех;
  • -Wformat-nonliteral предупреждает об использовании в качестве строки формата чего-то, что не является строковым литералом;
  • -Wformat-security предупреждает о небезопасных применениях функций, использующих заданные форматы для работы со строками, например printf() или scanf();
  • -Wformat=2 является аналогом -Wformat (включен в -Wall) -Wformat-y2k -Wformat-nonliteral -Wformat-security;
  • -Wmissing-include-dirs вырабатывает предупреждение, если предоставленный пользователем каталог для заголовочных файлов не существует;
  • -Wswitch-default предупреждает об отсутствии ветви default в операторе множественного выбора switch. Я встречал рекомендации всегда использовать ветвь default, поскольку нельзя гарантировать сейчас, а тем более и в будущем, что переменная, проверяемая в switch() всегда будет принимать те и только те значения, которые вы задали через case'ы;
  • -Wtrigraphs предупреждает об использовании триграфов в вашей программе;
  • -Wstrict-overflow=4 активно ищет, чтобы такого упростить в вашей программе. Например, компилятор теперь не пройдет мимо x + 1 > x (всегда TRUE), x + 1 > 1 (можно записать как x > 0), (x * 10) / 5 (лучше записать короче, как x * 2);
  • -Wstrict-overflow=5 с этой опцией компилятор становиться еще более параноидальным и теперь он не пройдет мимо x + 2 > y, не предложив упростить до x + 1 >= y. Как мне кажется, оба варианта имеют свое право на существование, поэтому я не использую данную опцию;
  • -Wsuggest-attribute=[pure|const|noreturn] данная опция проверяет наш код на наличие функций, которым не помешали бы атрибуты pure, const или noreturn. Работает вместе с опцией -fipa-pure-const (у меня почему-то не заработало ни с -fipa-pure-const ни с -O1 - достаточно новый gcc-4.5.2 просто ругается на unrecognized command line option);
  • -Wfloat-equal ругается, если кто-то пытается производить некоторые проверки на равенство, используя при этом переменные типа float;
  • -Wundef предупреждает если используется неизвестный идентификатор в #if;
  • -Wshadow предупреждает если локальная переменная скрывает собой некую, лежащую выше по области видимости, переменную, с таким же названием;
  • -Wbad-function-cast предупреждает, если результат функции приводится к неподходящему типу;
  • -Wcast-qual предупреждает о приведении типа указателя, вызывающем потерю некоторого атрибута типа. Например, будет выработано предупреждение о приведении const char * к char *;
  • -Wcast-align предупреждает, если указатель приводится к типу, чей размер больше, чем размер исходного типа. Например: char * к int *, если int 2 или 4-байтный;
  • -Wwrite-strings вырабатывает предупреждение, если мы попытаемся присвоить адрес строковой константы const char[] указателю char *;
  • -Wlogical-op предупреждает о странном использовании логических операторов в выражениях. Например, использование логических операторов в контексте, где более подошло бы использование побитовых операторов.
Теперь, приведу небольшой пример использования этих ключей для проверки кода некоторого проекта.
У меня есть один проект на С, содержащий 399 строчек кода, которые спокойно, без ошибок и предупреждений компилируются с ключами -Wall -Werror. Если же к опциям компилятора добавить еще и -Wextra -Wconversion -Wsign-conversion -Winit-self -Wunreachable-code
-pedantic -Wformat=2 -Wswitch-default -Wtrigraphs -Wstrict-overflow=4 -Wfloat-equal -Wundef -Wshadow -Wbad-function-cast -Wcast-qual -Wcast-align -Wlogical-op, то мы получим следующий список ошибок:
Как видно, здесь есть над чем поработать. Во-первых, нужно удалить неиспользуемые параметры функции main() - их всегда можно добавить обратно, когда это будет необходимо.
С функциями-обработчиками сигналов все будет несколько сложнее. Как следует из документации, они должны иметь один параметр типа int - номер сигнала, который вызвал срабатывание обработчика. Но у нас, внутри наших обработчиков, этот параметр совершенно не используется. Как же удовлетворить требование документации к наличию обязательного параметра для обработчика сигнала и одновременно удовлетворить требование компилятора к отсутствию неиспользуемых параметров функций?
Конечно, можно просто отключить проверку на наличие неиспользуемых параметров. Но тут мы сталкиваемся с тем, что обработчиков сигналов в нашем коде всего два. А функций, для которых по-прежнему нужно контролировать использование параметров, гораздо больше.
Выход все же есть - нужно всего лишь указать компилятору, что эти переменные действительно не используются программистом, что это не ошибка - кто-то взял и объявил переменные, а потом забыл о них. Делается это при помощи атрибута unused:
Последние, оставшиеся не рассмотренными, ошибки связаны с тем, что внутри функций readn и writen все служебные переменные типа size_t, а возвращают эти фукнции тип ssize_t, как и обычные функции read/write. Естественно, что приведение size_t к ssize_t может вызвать ошибку для величин, больших чем MAX(size_t) / 2. А обратное приведение - ssize_t к size_t может вызвать потерю значащего разряда для числа. Решение, я здесь приводить не буду, отмечу лишь, что в каждом случае оно свое...
Один из плюсов использования gcc для статического анализа кода состоит в том, что его (gcc) не нужно специально интегрировать с Emacs'ом. Все предупреждения и ошибки будут выведены в буфере *compilation* и затем можно как обычно прыгать по строчкам, исправляя ошибки.

Splint

Пришло время рассмотреть специализированные утилиты для статического анализа кода. Список статических анализаторов из мира Unix можно посмотреть здесь, либо же здесь.
Из всего множества программ я выбрал splint, во многом из-за того, что эта утилита является наследницей юниксового lint'а. Домашняя страница утилиты Splint - http://www.splint.org/.

Для демонстрации работы утилиты я буду использовать все тот же проект, уже прошедший через проверки gcc. Для начала запустим splint без каких бы то ни было параметров, сразу натравив его на наши исходники:
Как видно, ничего у нас не работает. Сначала, нужно указать splint, что мы используем функции из библиотеки POSIX через +posixlib, затем еще нужно обойти непонимание отдельных расширений языка C от GCC, через опцию -D__gnuc_va_list=va_list, а напоследок еще нужно исключить из области видимости проверки заголовочный файл unistd.h, через обертывание директивы его включения в #ifndef S_SPLINT_S ... #endif, иначе мы будем сталкиваться с ошибкой вида:

Теперь, после всех этих действий, мы можем увидеть список аж из 47 вероятных ошибок, которые пропустил gcc:

Впрочем, как видно, пропустил он не все. Часть предупреждений снова предупреждает нас о неиспользуемых параметрах внутри обработчиков сигналов - splint не понимает расширений gcc, которыми мы указали (впрочем, у него есть свои: splint -help annotations), что отмеченные параметры действительно являются неиспользуемыми и обращать внимания на них не надо. А часть предупреждений и вовсе является мусором, наподобие предупреждений о не распознанных setitimer() и putenv().

Плюсом splint является то, что формат ее вывода совпадает с выводом gcc и опять, интегрировать splint с Emacs'ом при помощи специальных плагинов нет никакой необходимости:

На этом, я завершаю обзор средств статического анализа кода и отправляюсь разбираться в выводе splint'а =).