LINT LINT СОДЕРЖАНИЕ 1. Введение 2 2. Запуск утилиты lint 3 3. Типы сообщений lint'а 5 3.1. Неиспользуемые переменные и функции 5 3.2. Информация об определениях/использованиях значений 6 3.3. Поток управления 6 3.4. Значения функций 7 3.5. Контроль типов 8 3.6. Явные преобразования типов 9 3.7. Машинно-зависимое использование символов 10 3.8. Присваивание целым переменным long-значений 10 3.9. Странные конструкции 11 3.10. Устаревший синтаксис 12 3.11. Выравнивание указателей 13 3.12. Многократные использования и побочные эффекты 13 1. ВВЕДЕНИЕ Утилита lint исследует тексты на языке C, выявляя некоторые ошибки и сомнительные конструкции. lint более строго, чем C- компилятор, контролирует правила типизации. lint можно приме- нять и для того, чтобы проверить выполнение ряда ограничений, повышающих мобильность программ, если имеется в виду перенос на другой компьютер или в другую операционную среду. lint также позволяет выявить использование формально корректных, но расто- чительных или рискованных конструкций. Наконец, совокупность файлов с исходными текстами и спецификаций библиотек проверяет- ся lint'ом на совместимость. Утилита lint является мощным средством отладки, поскольку за- частую нарушенные правила типизации или употребленные рискован- ные конструкции являются источником коварных ошибок. Следует настоятельно рекомендовать верификацию комплексов C-программ до их первого запуска, поскольку это существенно сократит время отладки, да и приучит к аккуратному стилю программирования. 2. ЗАПУСК УТИЛИТЫ LINT Вызов утилиты lint имеет вид |lint [опции] файлы описатели_библиотек Опции определяют набор проверок, выполняемых lint'ом, и степень подробности выдаваемых сообщений. Имена верифицируемых файлов должны оканчиваться на .c или .ln. Описатели-библиотек - это список имен библиотек, которые должны использоваться при анали зе программы. Опциям утилиты lint приписан следующий смысл: -a Не выдавать сообщений о присваиваниях long-значе- ний переменным, не описанным как long. -b Не выдавать сообщений о недостижимых операторах break. -c Ограничиться проверкой ошибок в пределах каждого из .c-файлов; поместить информацию о внешних свой- ствах в файлы, оканчивающиеся на .ln. -h Не применять эвристики (предназначенные для того, чтобы попытаться обнаружить ошибки, улучшить стиль и уменьшить сложность программы). -n Не проверять совместимость со стандартной или мо- бильной lint-библиотеками. -o библ Создать из исходных файлов lint-библиотеку с име- нем llib-lбибл.ln. -p Попытаться проверить мобильность программы. -u Не выдавать сообщения о функциях и внешних пере- менных, используемых, но не определенных, или оп ределенных, но не используемых. -v Не выдавать сообщения о неиспользуемых аргумент функций. -x Не сообщать о внешних переменных, которые нигде не используются. Если требуется указать более одной опции, их следует объединять в единый аргумент, такой как -ab или -xha. Имена файлов, содержащих исходные C-тексты, должны оканчиваться на .c, что обязательно не только для lint'а, но и для C-компи- лятора. Помимо перечисленных, утилита lint поддерживает опции вида |-lбибл Они специфицируют библиотеки, содержащие функции, используемые в анализируемой C-программе. Исходный текст программы проверя- ется на совместимость с этими библиотеками. Проверка выполняет- ся на основе файлов описания библиотек, имена которых формиру ются исходя из указанных аргументов. Данные файлы всегда откры ваются комментарием |/* LINTLIBRARY */ и содержат серию фиктивных определений функций. Наиболее су- щественными частями в этих определениях являются указания, возвращает ли функция значение и если да, то какого типа, како- вы число и тип аргументов. Чтобы специфицировать особые свойст- ва некоторых библиотечных функций, можно воспользоваться ком- ментариями VARARGS и ARGSUSED. Как это сделать, написано в сле- дующем разделе, ТИПЫ СООБЩЕНИЙ LINT'А. lint-библиотеки обрабатываются практически так же, как и обыч ные файлы с исходными текстами. Единственное отличие заключает- ся в том, что функции, определенные в библиотечном файле, но не используемые в исходном, не вызывают выдачи сообщений. lint не воспроизводит алгоритм полного поиска в библиотеках и выдает соответствующие сообщения, если исходные файлы содержат переоп- ределения библиотечных функций. По умолчанию lint проверяет программы совместно со стандартной lint-библиотекой, содержащей описания функций, обычно загружае мых при выполнении C-программ. Если указана опция -p, проверя ется еще один файл, содержащий описания функций из стандартной библиотеки, которая считается мобильной. Чтобы подавить всякую проверку библиотек, можно воспользоваться опцией -n. Дополнительную информацию об опциях команды lint можно найти в статье lint(1) Справочника пользователя. 3. ТИПЫ СООБЩЕНИЙ LINT'А В последующих пунктах описываются основные группы сообщений, выдаваемых lint'ом. 3.1. Неиспользуемые переменные и функции По мере того, как комплекс программ растет и развивается, необ ходимые ранее переменные и аргументы функций могут перестать использоваться. Нет ничего необычного в том, что внешние пере- менные и даже функции целиком станут ненужными, но еще не будут удалены из исходного текста. Этот тип ошибок редко выводит из строя работающие программы, однако является источником неэффек тивности и затрудняет понимание и модификацию программ. Кроме того, информация о неиспользуемых переменных и функциях может порой способствовать обнаружению ошибок. lint печатает сообщения о переменных и функциях, определенных, но нигде не используемых, в случае, если не указаны опции -u или -x, подавляющие подобные сообщения. Программисты иногда прибегают к приему, заключающемуся в опре- делении функций с интерфейсом, в котором часть аргументов функ- ции является необязательной. Обычно lint печатает сообщение о неиспользуемых аргументах, однако предусмотрена опция -v, по- давляющая вывод таких сообщений. Если опция -v указана, сообще- ния о неиспользуемых аргументах не выдаются, если только такой аргумент не является регистровым. Считается, что в последнем случае имеет место напрасная (хотя и не фатальная) трата машин- ных ресурсов. Сообщения о неиспользуемых аргументах можно подавить в пределах одной функции, добавив в исходный текст программы перед началом нужной функции комментарий |/* ARGSUSED */ Комментарий действует так же, как и опция -v, но только в пре- делах одной функции. Другой комментарий |/* VARARGS */ можно использовать, чтобы подавить сообщения о переменном числе аргументов в вызовах данной функции. Комментарий следует вста- вить перед определением функции. В некоторых случаях желательно проверить несколько первых аргументов, а остальные аргументы оставить непроверенными. Это можно сделать, включив в коммента рий цифры, определяющие число аргументов, требующих проверки. Например, комментарий |/* VARARGS2 */ вызывает проверку двух первых аргументов. Если lint применить к подмножеству полной сосвокупности файлов, составляющих единую программу, в его сообщениях может быть вы- ражено неудовольствие по поводу якобы неиспользуемых или неоп- ределенных переменных. Такая информация, конечно, скорее сбива- ет с толку, чем помогает. Функции и переменные, определенные в этих файлах, вполне могут в них не использоваться; и наоборот, могут использоваться функции и переменные, определенные где-то в другом месте. Ложные сообщения подавляет опция -u. 3.2. Информация об определениях/использованиях значений lint стремится выявлять ситуации, когда значения переменных ис- пользуются до того, как определяются (присваиваются). lint вы- являет локальные переменные (принадлежащие автоматическому и регистровому классам памяти), первое использование которых при выполнении программы происходит раньше, чем им присваивается какое-либо значение. Считается, что вычисление адреса перемен ной составляет ее "использование", поскольку фактическое ис- пользование может произойти в любой последующий момент пос- редством указателя. Ограничение вхождений переменных рамками одного файла позволяет очень просто и быстро реализовать соответствующий алгоритм, так как не нужно выявлять истинный поток управления. Это означает, что lint может печатать сообщения, относящиеся к корректным фрагментам программ, стиль которых тем не менее следует приз нать плохим. Поскольку статические и внешние переменные инициа лизируются нулями, никакой содержательной информации об исполь зовании их значений получить нельзя. Утилита lint обрабатывает и инициализируемые автоматические переменные. Информация об определении/использовании значений позволяет так- же обнаружить локальные переменные, значения которых определя- ются, но нигде не используются. Это часто оказывается источни- ком неэффективности и, кроме того, может быть проявлением ошиб- ки. 3.3. Поток управления lint стремится выявлять недостижимые фрагменты программы. Он выдает сообщения об операторах без меток, следующих непосредст венно за операторами goto, break, continue и return. lint пыта ется выявлять бесконечные циклы (частные случаи - циклы с заго ловками while(1) и for(;;)) и трактует следующие за ними опера- торы как недостижимые. lint также выдает сообщения о циклах, в которые нельзя попасть через заголовок. В корректных программах могут быть такие циклы, однако подобный стиль считается плохим. Для подавления сообщений о недостижимых фрагментах программы служит опция -b. lint не обладает информацей, достаточной для того, чтобы опре делить, какие из вызываемых функций никогда не возвращают уп равление. Так, вызов exit() может стать причиной недостижимости некоторого фрагмента, чего lint, однако, не обнаружит. Наиболее серьезные последствия этого связаны с опредлением возвращаемых функцией значений (см. пункт Значения функций). Если некоторый фрагмент в программе мыслится как недостижимый, причем lint'у это непонятно, в соответствующее место программы можно вставить комментарий |/* NOTREACHED */ который будет информировать lint о том, что данный фрагмент не- достижим (без выдачи диагностического сообщения). Программы, сгенерированные при помощи yacc'а и, в особенности, lex'а, могут содержать сотни недостижимых операторов break, но пользы от подобных сообщений немного. Обычно с такими операто рами ничего поделать нельзя, и сообщения загромождали бы выдачу lint'а. Рекомендуется при работе с подобными исходными текстами вызывать lint с опцией -b. 3.4. Значения функций Иногда функции возвращают значения, которые нигде не использу ются; иногда в программах делается попытка использовать значе ния функций, которых они не возвращают. lint рассматривает дан- ную проблему с нескольких точек зрения. Если в теле функции встречается как оператор |return (выражение); так и оператор |return ; это настораживает; lint выдаст сообщение |function name has return(e) and return Сложнее всего проанализировать тот возврат из функции, который происходит при достижении потоком управления ее конца. Рассмот- рим пример. |f (a) |{ | if (a) | return (3); | g (); |} Если условие ложно, f вызовет g, а затем завершится, не возвра- тив никакого определенного результата; такая конструкция будет причиной сообщения lint'а. Если g, подобно exit, никогда управ- ления не возвращает, сообщение тем не менее будет выдаваться, хотя на самом деле никакой ошибки нет. Комментарий |/* NOTREACHED */ в исходном тексте будет подавлять данное сообщение. При анализе программы в целом lint выявляет ситуации, когда функция возвращает значения, которые иногда (или никогда) не используются. Если возвращаемые значения никогда не используют- ся, в определении функции имеется некоторая неэффективность. Можно "легально" проигнорировать результат функции при помощи явного преобразования к типу void, например |(void) fprintf (stderr, "Файл занят. Попробуйте позже!\n"); Игнорирование результата может быть просто проявлением дурного стиля (например, частичное отсутствие проверок неудачного за- вершения функции). Обнаруживаются и противоположные ошибки, то есть использование результата функции, который не возвращается. Проявления таких ошибок могут быть самыми серьезными и, что гораздо хуже, неста- бильными, а их поиск с помощью отладчика весьма труден. 3.5. Контроль типов lint, по сравнению с C-компилятором, выполняет более строгий контроль типов. Дополнительный контроль затрагивает четыре ос- новные области: Согласование типов в некоторых бинарных операциях и присваиваниях. Операции выбора элементов структур и объединений. Соответствие определения и использований функций. Использование перечислимых типов. Некоторые операции выполняют определенные преобразования, при- водящие в соответствие типы операндов. Этим свойством обладают присваивания, условная операция (?:) и операции отношения. Ар- гумент оператора return и выражения, используемые для инициали- зации, допускают аналогичные преобразования. В этих конструкци- ях типы char, short, int, long, unsigned, float и double можно без ограничений смешивать. Указательные типы должны в точности согласовываться, конечно же, за исключением того, что массивы x можно смешивать с указателями на x. Правила контроля типов требуют, чтобы при работе со структурами левый операнд операции -> являлся указателем на структуру, ле- вый операнд операции . - структурой, а правый операнд этих опе раций - элементом, предусмотренным для структурного типа, соот- ветствующего левому операнду. Аналогичные проверки выполняются при работе с объединениями. Контролируется соответствие типов аргументов функций и возвра- щаемых значений. Без ограничений сопоставляются типы float и double, так же, как и типы char, short, int и unsigned. Анало- гично, указатели могут сопоставляться с соответствующими масси- вами. Во всех остальных случаях типы фактических аргументов должны согласовываться с описаниями соответствующих им формаль- ных. Для перечислимых типов контролируется, что переменные и значе- ния таких типов не употребляются вместе с переменными других типов, в том числе и перечислимых, и что единственные операции, к ним применяемые, - это =, инициализация, ==, !=, передача в качестве фактических аргументов и возврат в качестве результата функции. Если требуется отключить строгий контроль типов в конкретном выражении, в исходный текст программы непосредственно перед этим выражением нужно добавить комментарий |/* NOSTRICT */ Данный комментарий отменяет строгий контроль типов только в следующей строке программы 3.6. Явные преобразования типов Возможность явного преобразования типов в языке C предназначена в значительной степени для того, чтобы повысить мобильность программ. Рассмотрим присваивание |p = 1; в котором p - это указатель на char. lint, обнаружив такую конструкцию, будет выдавать диагностическое сообщение. В прис- ваивании |p = (char *) 1; использована операция явного преобразования целого значения в указатель на символ. У программиста наверняка были веские при- чины написать именно такую конструкцию и явно об этом сигнали- зировать. Тем не менее, если указана опция -p, lint, как и прежде, будет выдавать сообщение о несоответствии типов. 3.7. Машинно-зависимое использование символов В ряде систем символы представляются величинами со знаком в ди- апазоне от -128 до 127. В других реализациях языка C символы принимают только положительные значения. Поэтому lint выдает сообщения о некоторых ситуациях сравнения и присваивания как о некорректных или машинно-зависимых. Например, фрагмент |char c; | if ((c = getchar ()) < 0) | . . . на одном компьютере может быть работоспособен, а на другом, где символы представляются только положительными числами, мы имеем заведомо ложное условие. Во всех случаях lint будет выдавать сообщение |nonportable character comparison Лучший выход - описать c как целую переменную, поскольку getc har() в действительности возвращает целочисленные значения. Аналогичные трудности возникают в связи с битными полями. При присваивании константного значения битному полю последнее может оказаться слишком маленьким, чтобы вместить значение. Чаще все- го это происходит из-за того, что некоторые машины рассматрива ют битные поля как знаковые величины. Поскольку естественно считать, что поле из двух бит, описанное как int, не мож вместить значение 3, проблема разрешается, если описать битное поле как unsigned. 3.8. Присваивание целым переменным long-значений Присваивание переменным, описанным как int, значений типа long, может вызвать ошибки, поскольку оно сопровождается усечением значения. Это может произойти в программах, которые не пол- ностью переработаны применительно к использованию конструкций typedef. Когда тип переменной, определяемый посредством type- def, изменяется с int на long, программа может перестать рабо- тать, если часть промежуточных результатов заносится в int-пе- ременные и при этом усекается. Чтобы подавить сообщения о прис- ваивании long-значений int-переменным, можно использовать опцию -a. 3.9. Странные конструкции lint выявляет ряд конструкций, являющихся вполне корректными, но несколько странными. Предполагается, что сообщения, выдавае- мые при этом, должны поощрять лучшее качество программ, более ясный стиль, а иногда и способствовать обнаружению ошибок. (Чтобы отменить эти проверки, используется опция -h). Например, в операторе |*p++; * ничего не дает. Такая конструкция вызывает сообщение lint'а: |null effect В следующем фрагменте программы |unsigned x; | . . . |if (x < 0) | . . . условие никогда не будет выполнено. В таком случае lint выдает сообщение |degenerate unsigned comparison Аналогично, для того же x, проверка |if (x > 0) | . . . эквивалентна |if (x != 0) | . . . Поэтому lint спрашивает: |unsigned comparison with 0? Если программа содержит нечто, подобное фрагменту |if (1 != 0) lint выдает сообщение |constant in conditional context поскольку сравнение 1 с 0 дает неизменный результат. В некоторых конструкциях lint выявляет свойства, связанные со старшинством операций. Ошибки, вызванные неправильным понимани- ем старшинства, могут быть замаскированы использованием пробе- лов и другими чисто внешними особенностями расположения операн- дов, что делает обнаружение подобных ошибок исключительно слож- ным. Например, операторы |if (x&077 == 0) | . . . и |x<<2 + 40 имеют, скорее всего, совсем не тот смысл, который имелся в ви- ду. Лучшее решение - использовать в таких выражениях скобки; к этому и призывает соответствующее сообщение lint'а. 3.10. Устаревший синтаксис Ряд конструкций более ранней версии языка C теперь недопустимы. Они распадаются на два класса: операции присваивания и конст- рукции инициализации. Устаревшие формы операций присваивания (=+, =- и т.д.) могли привести к неоднозначным выражениям, например, таким: |a =-1; Это выражение можно трактовать как |a =- 1; или как |a = -1 ; Ситуация становится в особенности запутанной, если неоднознач- ные конструкции данного сорта появляются в результате макро подстановки. Более новые и предпочтельные операции (например, +=, -=, ...) не приводят к таким неоднозначностям. lint выдает сообщения, призывающие отказаться от устаревших конструкций. Аналогичные проблемы возникают с инициализацией. Раньше язык допускал описание вида |int x 1; инициализировавшее x единицей. Это приводило к трудностям в ин- терпретации других конструкций. Например, инициализация |int x (-1); до некоторой степени походит на начало определения функции: |int x (y) | . . . и компилятор должен прочесть входной текст дальше x, чтобы до- копаться до истинного смысла. И вновь затруднения еще более возрастают, когда инициализатор содержит макросы. В современном синтаксисе между именем переменной и инициализатором требуется знак =: |int x = -1; В этом уже нет никакой синтаксической неоднозначности. 3.11. Выравнивание указателей Некоторые конструкции присваивания указателей могут быть прием- лемыми на одних машинах и недопустимыми на других в силу огра- ничений, связанных с их выравниванием. lint пытается выявить присваивания указателей, которые могут привести к проблемам по- добного сорта. В такой ситуации выдается сообщение |possible pointer alignment problem 3.12. Многократные использования и побочные эффекты Для сложных выражений наилучшая последовательность обработки подвыражений может быть существенно машинно-зависимой. Напри- мер, если стек растет в сторону уменьшения адресов, целесооб разно обрабатывать аргументы функций справа налево. Там же, гд стек растет в сторону увеличения адресов, предпочтительным ка- жется обратный порядок обработки аргументов. Далее, вызовы функций, указанные в качестве фактических аргументов других функций, могут либо считаться обычными аргументами, либо обра- батываться особым образом. Аналогичные проблемы связаны и с другими операциями, имеющими побочный эффект, такими как опера- ции присваивания, увеличения и уменьшения. Чтобы не жертвовать сверх меры эффективностью реализации языка C на конкретной машине, оговаривается, что компилятор волен вы- бирать порядок вычисления сложных выражений. И многие компиля торы этим правом пользуются. В частности, если какая-либо пере менная изменяется вследствие побочного эффекта и, кроме того, используется в этом выражении еще раз, результат, вообще гов ря, не определен. lint проверяет важный частный случай - манипуляции со скалярны- ми переменными. Например, оператор |a [i] = b [i++]; вызовет следующее сообщение lint'а: |warning: i evaluation order undefined Оно должно обратить внимание программиста на то, что порядок, в котором будут производиться действия над переменной i, не опре- делен.