Производственно-внедренческий кооператив

"И Н Т Е Р Ф Е Й С"













Диалоговая Единая Мобильная

Операционная Система

Демос/P 2.1










Верификатор программ

на языке Си

LINT












Москва

1988















АННОТАЦИЯ

Документ содержит описание применния верификатора прог-
рамм на языке Си lint. Описана реализация программы lint.






























































ВВЕДЕНИЕ

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

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

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

1. Вызов программы

Предположим, что имеются два исходных файла на языке Си
file1.c, file2.c, компилируемые обычным образом и загружае-
мые вместе. Тогда команда

lint file1.c file2.c

порождает сообщения, описывающие несовместимые и неэффектив-
ные элементы в данных программах. Программа lint осуществ-
ляет более строгую проверку правил согласования типов языка
Си, чем Си-компиляторы (что объясняется историческими и
практическими причинами). Команда

lint -p file1.c file2.c

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

Замена флага -p на -h вызовет появление сообщения о
различных неэффективных и могущих приводить к ошибкам


- 3 -










конструкциях, которые, строго говоря, не являются ошибоч-
ными. Задание -hp приводит к выдаче всех вышеупомянутых
сообщений.

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

2. Некоторые замечания

Может оказаться, что многие факты, необходимые прог-
рамме lint, невозможно обнаружить. Например, вопрос о том,
произойдет ли когда-либо в некоторой программе обращение к
некоторой конкретной функции, может зависеть от входных дан-
ных. Решение вопроса о том, будет ли когда-либо вызвана
функция exit, эквивалентно знаменитой "проблеме останова",
которая является рекурсивно неразрешимой.

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

Программа lint старается выдавать информацию с высокой
степенью соответствия. Сообщения типа "ххх может быть ошиб-
кой" легки для генерации, однако они приемлемы только в том
случае, если они раскрывают достаточно большой процент
истинных ошибок. Если же доля истинных ошибок слишком мала,
к подобным сообщениям теряется доверие и они лишь загромож-
дают выходные сообщения, затрудняя этим анализ действительно
важных сообщений.

Имея все это в виду, рассмотрим теперь более подробно
классы сообщений, выдаваемых программой lint.

3. Неиспользованные переменные и функции

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



- 4 -










Программа lint сообщает о таких переменных и функциях -
описанных, но не упомянутых каким-либо иным способом. Иск-
лючение составляют переменные, объявленные с помощью явного
оператора extern, но на которые не были произведены ссылки;
так, оператор

extern float sin();

не вызовет никакого коментария, если идентификатор sin нигде
не был использован. Заметим, что это согласуется с семанти-
кой компилятора языка Си. Однако, в некоторых случаях эти
неиспользованные внешние переменные могут представлять инте-
рес; они могут быть обнаружены посредством указания флага -x
при вызове lint.

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

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

4. Информация об инициализации и использовании переменных

Программа lint делает попытки обнаружения случаев
использования переменных до присваивания им начальных значе-
ний. Сделать это очень не легко; многие соответствующие
алгоритмы расходуют значительное количество времени и памяти
и, все же, иногда выдают сообщения о совершенно правильных
программах. Программа lint обнаруживает локальные перемен-
ные (из классов автоматической и регистровой памяти), первое
использование которых появляется во входном файле раньше,
чем первначальное присваивание этим переменным. Программа
полагает, что взятие адреса переменной представляет ее
использование, тогда как действительное использование этой
переменной может произойти в любой момент и зависеть от дан-
ных.



- 5 -










Такое ограничение физическим появлением переменных в
файле делает соответствующий алгоритм быстрым и легко реали-
зуемым, поскольку при этом нет необходимости прослеживать
фактический поток управления. Это означает, что lint может
выдавать сообщения о программах, которые являются правиль-
ными, но которые следовало бы считать плохими по стилисти-
ческим особенностям (например, содержат по крайней мере два
оператора goto). Поскольку статические и внешние переменные
инициализируются значением нуль, то не может быть получена
никакая осмысленная информация об их использовании. Вместе
с тем алгоритм правильно обрабатывает инициализированные
автоматические переменные, а также переменные, используемые
в выражениях, в которых им сначала присваиваются значения.

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

5. Поток управления

Программа lint делает попытки обнаружить недостигаемых
участков в обрабатываемых программах. Она сообщает о нали-
чии непомеченных операторов, непосредственно следующих за
операторами goto, break, continue, или return. Делается
также попытка обнаружить циклы, из которых никогда не проис-
ходит выход по концу тела цикла; при этом обнаруживаются
специальные случаи использования бесконечных циклов while(1)
и for(;;). Программа lint сообщает также о циклах, в кото-
рые невозможно войти через их заголовок; такие циклы встре-
чаются во многих правильных программах, что в лучшем случае
свидетельствует о плохом стиле их написания, а в худшем - об
ошибках.

Следует отметить, что алгоритм проверки потока управле-
ния программы lint имеет значительное "слепое пятно": нет
возможности обнаружения функций, которые вызываются, но не
возвращают управление в вызывающую программу. Так, вызов
exit может служить причиной недостижимости некоторой части
программы, что не обнаруживается программой lint. Наиболее
серьезно это сказывается на определении возвращаемых значе-
ний функций (см.следующий раздел).

Имеется один вид недостигаемого оператора, не вызываю-
щего обычно сообщений lint: это - недостигаемый оператор
break. Заметим, что программы, полученные с помощью yacc[2]
и, особенно, lex[3], могут включать буквально сотни недости-
гаемых операторов break. Использование флага -O в Си-
компиляторе приводит к устранению неэффективности результи-
рующего объектного кода. Таким образом, эти недостигаемые
операторы не играют большой роли, и обычно пользователь
ничего не может с ними поделать. Т.о. сообщения о них


- 6 -










только бы загромождали выдачу программы lint. Если эти
сообщения все же желательны, следует вызывать lint с ключом
-b.

6. Значения функций

Иногда функции возвращают значения, которые никогда не
используются; иногда программы некорректно используют "зна-
чения" функций, которые не были возвращены. Программа lint
разрешает эту проблему несколькими способами.

На локальном уровне, внутри некоторого определения
функции, одновременное появление операторов

return(выражение);
и
return;

служит поводом для тревоги; программа lint выдает сообщение
function name contains ruturn(e) and return" ("функция с
именем name содержит return(выражение) и return"). При этом
наиболее серьезную трудность вызывает случай, когда возвра-
щаемое значение зависит от потока управления, который дости-
гает конца определения функции. Это может быть продемонст-
рировано на простом примере:

f(a){
if(a) return(3);
g
}

Заметим, что если a ложно, то функция f вызывает функцию g и
затем возвращает управление, не определив никакого возвраща-
емого значения; это вызовет сообщение программы lint. Если
же функция g, подобно exit, не возвращает управления, сооб-
щение все же будет порождено, даже если в действительности
все верно.

С помощью этой возможности можно обнаружить многие
потенциально серьезные ошибки; она же несет ответственность
за значительную часть "шумовых" сообщений, выдаваемых прог-
раммой lint.

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




- 7 -










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

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

Существует некоторое число операций, предполагающих
соответствие типов операндов. Таким свойством обладают
присваивания, условная операция (?:) и операции отношения.
В этих операциях типы char, short, int, long, unsigned,float
и double могут быть произвольно смешаны. Типы указателей
должны тщательно согласовываться, при этом, разумеется, мас-
сивы элементов типа х могут смешиваться с указателями на тип
х. Правила контроля типов требуют также, чтобы в обращениях
к структуре левый операнд ->> был указателем на структуру, а
левый операнд в операции . (точка) был структурой и правый
операнд этих операций был членом структуры, к которому обра-
щается левый операнд. Аналогичный контроль делается и для
обращений к объединениям.

Строгие правила накладываются на согласование аргумен-
тов функций и согласование возвращаемых значений. Типы
float и double свободно согласуются, равно как типы char,
short, int и unsigned. Кроме того, указатели могут согласо-
вываться с соответствующими массивами. За исключением ука-
занных, все фактические аргументы должны согласовываться по
типу с соответствующими объявленными аргументами.

Для перечислений проверяется, чтобы переменные или
члены перечислений не смешивались с другими типами или дру-
гими перечислениями; они могут только использовать в
качестве операций =, ==, !=, аргументы функций и возвращае-
мые значения.

7. Изменения типов

Возможность изменения типов в языке Си была широко вве-
дена в качестве средства получения более переносимых прог-
рамм. Пусть имеется присваивание

p=1;

где p - указатель на символы. Программа lint вполне обосно-
ванно выдаст предостерегающее сообщение. Теперь рассмотрим
присваивание


- 8 -










p = (char*) 1;

в котором операция перевода типа была использована для пре-
образования целого в указатель на символы. Очевидно, прог-
раммист имел серьезное основание сделать это и ясно выразил
свои намерения. Поэтому представляется слишком суровым со
стороны программы lint, если она будет продолжать выдавать
сообщения об этом. С другой стороны, при переносе данной
программы на другую машину к ней следует отнестись с осто-
рожностью. Флаг -c управляет печатью комментариев об изме-
нении типов. Если флаг -c включен, переводы типов вызывают
выдачу предостерегающих сообщений, в противном случае все
допустимые переводы типов не вызывают выдачи комментирующих
сообщений, каким бы странным ни казалось данное смешение
типов.

8. Использование символов, нарушающее переносимость.

В машинах типа СМ-1420 символы являются величинами со
знаком, в диапазоне от -128 до 127. В большинстве других
реализаций языка Си символы принимают только положительные
значения. Поэтому lint сигнализирует о некоторых сравнениях
и присваиваниях как о недопустимых или непереносимых. Нап-
ример, фрагмент

char c;
...
if ((c=getchar())<0)...

будет работать на СМ-1420, но приведет к ошибке на машинах,
в которых символы принимают только положительные значения.
Правильное решение заключается в объявлении с целым числом,
так как функция getchar будет возвращать целые значения. Во
всяком случае программа lint выведет сообщение "nonportable
character comparison" ("непереносимое символьное сравне-
ние"). Подобная ситуация возникает и с полями битов; при
выполнении присваивания полю битов постоянного значения,
данное поле может оказаться слишком малым для хранения этого
значения. Это особенно верно, поскольку на некоторых маши-
нах поля битов рассматриваются как величины со знаком.
Можно долго ломать голову над тем, почему двухбитовое поле,
объявленное как int, не может хранить значение 3; эта труд-
ность устраняется, если поле битов объявляется с типом
unsigned.

9. Присваивание целым типа int значений типа long

Ошибки могут возникать в результате присваиваний вели-
чин типа long переменным типа int, при которых происходит
потеря точности. Такая ситуация может иметь место в прог-
раммах, не полностью преобразованных для использования опре-
делений типа typedef. При изменении некоторой typedef -
переменной из int в long, программа может прекратить работу,


- 9 -










поскольку некоторые промежуточные результаты могут присваи-
ваться переменным типа int, что приводит к потере точности.
Поскольку имеется ряд разумных ситуаций, когда необходимо
присваивание величин типа long переменным типа int, то фик-
сация таких присваиваний производится только при задании
флага -a.

10. Странные конструкции

Программа lint обнаруживает некоторые совершенно пра-
вильные, но несколько странные конструкции. Существует
надежда, что сообщения о таких конструкциях побуждают к
написанию более ясных и качественных программ и могут даже
указывать на ошибки. Включение подобных проверок осуществ-
ляется с помощью флага -h. Так например, в операторе *p++
операция * не приводит ни к каким действиям. по этому
поводу lint выдает сообщение "null effect" ("нулевой
эффект"). Фрагмент программы

unsigned x ;
if (x<0) ...

явно несколько странен, поскольку данное сравнение никогда
не будет истинным. Подобно этому, сравнение

if (x>0) ...

эквивалентно сравнению

if (x!=0)

что может и не быть желаемым действием. В таких случаях
выдается сообщение lint: "degenerate unsigned comparison"
("вырожденное сравнение величин типа unsigned"). Если
используется выражение

if (1!=0)...

то программа lint напечатает "constant in conditional con-
text" ("константа в условном контексте"), поскольку сравне-
ние 1 с 0 дает постоянный результат.

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

if (x & 077 ==0) ...
или
x<<2 + 40



- 10 -










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

Наконец, при использовании флага -h программа lint
сообщает о переменных, переопределенных во внутренних блоках
таким способом, что это противоречит их использованию в
объемлющих блоках. И хотя такая ситуация является допусти-
мой, она, по мнению многих свидетельствует о плохом стиле
написания программы и обычно не являясь необходимой, зачас-
тую говорит о наличии ошибки.

11. О ранних версиях языка

Существует несколько форм старого синтаксиса, примене-
ние которых официально не одобряется. Они делятся на два
класса - операции присваивания и инициализация.

Применение старых форм операций присваивания (например,
=+, =-, ...)может породить двусмысленные выражения, подобные

a=-i;

что может трактоваться либо как

a =- 1;
либо как
a = -1;

Подобная ситуация особенно запутывает в том случае, если
такая двусмысленность возникает в результате макроподста-
новки. Отметим, что в последней версии языка подобные опе-
рации (+=, -= и т.д.), который являются более предпочтитель-
ными, не порождают таких двусмысленностей. С целью побужде-
ния удаления более ранних форм программа lint выдает сообще-
ния и таких старомодных операциях.

Подобные проблемы возникают и при инициализации. В
ранней версии языка допустима конструкция

int x 1;

для инициализации x значением 1. Это также порождает син-
таксические трудности: например, объявление

int x (-1);

может показываться похожим на начало объявления функции:

int x (y) {....

и транслятор должен анализировать большой фрагмент входного


- 11 -










текста за x для того, чтобы убедиться, что действительно
представляет собой это объявление. В данном случае, как и в
предыдущем, проблема усложняется, если инициализирующая
конструкция включает некоторый макрос. в настоящее время
синтаксис требует введения знака равенства между переменной
и инициализатором:

int x= -1;

этим полностью исключается возникновение какой-либо синтак-
сической двусмысленности.

12. Выравнивание указателей

Некоторые присваивания для указателей могут быть пра-
вильными на одних машинах, но неверными на других, что цели-
ком обусловлено требованиями выравнивания. Например, на
СМ-1420 допускается присваивание целых указателей указателям
"на тип double", т.к. значения двойной точности могут начи-
наться с границы целого числа. На машине Honeywell 6000
значения двойной точности должны начинаться с границы четных
слов; таким образом, не все присваивания такого типа будут
иметь смысл. Программа lint старается обнаружить случаи
присваивания указателей другим указателям и выделить случаи
возможного возникновения проблемы выравнивания. В таких
ситуациях, если указаны флаги -p или -h, выдается сообщение
"possible pointer alignment problem" ("возможна ошибка
выравнивания указателей").

13. Многократные использования и побочные эффекты

В сложных выражениях наилучший порядок вычисления под-
выражений может быть в значительной степени машинно-
зависимым. Например, на машинах (типа СМ-1420), в которых
стек обрабатывается в обратном направлении, аргументы функ-
ции, вероятно, лучше вычислить справа налево. В машинах с
прямой обработкой стека более привлекательным является поря-
док слева направо. Вызовы функций, используемые в качестве
аргументов других функций, могут трактоваться подобно обыч-
ным аргументом, а могут обрабатываться и другим образом.
Подобные моменты возникают и для других операций, обладающих
побочными эффектами, например, для операций присваивания и
для операций увеличения и уменьшения.

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


- 12 -










не будет точно определен.

Программа lint проверяет специальный случай, когда
изменяется простая скалярная переменная. Например, оператор

a[i]=b[i++] ;

вызовет появление сообщения

warning: i evaluation order undefined
(предостережение: неопределен порядок вычисления i).


14. Реализацияч

Программа lint реализуется с помощью двух программ и
драйвера. Первой программой является версия переносимого
Си-компилятора[4,5] которая служит основой для компиляторов
языка Си для машин IBM-370, IHoneywell 6000 и Interdata
8/32. Данный компилятор выполняет лексический и синтакси-
ческий анализ входного текста, создает и поддерживает таб-
лицу символов и строит деревья выражений.

Вместо записи некоторого промежуточного файла, переда-
ваемого генератору кода, как это делается другими компилято-
рами, программа lint вырабатывает промежуточный файл, кото-
рый состоит из строк текста в коде ASCII. Каждая строка
содержит имя внешней переменной, кодировку контекста, в
котором эта переменная появляется (использование, определе-
ние, объявление и т.п.), спецификатор типа, имя исходного
файла и номер строки. Информация о переменных, локальных по
отношению к функции или файлу, собирается с помощью доступа
к таблице символов и исследования деревьев выражений.

Комментарии об обнаруженных "локальных" сомнительных
местах выдаются по мере обнаружения. Информация о внешних
именах накапливается в промежуточном файле. После того, как