Верификатор программ на языке Си LINT
Производственно-внедренческий кооператив
"И Н Т Е Р Ф Е Й С"
Диалоговая Единая Мобильная
Операционная Система
Демос/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. Каждая строка
содержит имя внешней переменной, кодировку контекста, в
котором эта переменная появляется (использование, определе-
ние, объявление и т.п.), спецификатор типа, имя исходного
файла и номер строки. Информация о переменных, локальных по
отношению к функции или файлу, собирается с помощью доступа
к таблице символов и исследования деревьев выражений.
Комментарии об обнаруженных "локальных" сомнительных
местах выдаются по мере обнаружения. Информация о внешних
именах накапливается в промежуточном файле. После того, как
будут собраны все описания из исходных файлов и библиотек,
промежуточный файл сортируется для объединения вместе всей
собранной информации о каждой конкретной внешней переменной.
Вторая, довольно маленькая, программа читает затем строки из
промежуточного файла и сравнивает все определения, объявле-
ния и использования на совместимость.
Программа-драйвер управляет указанным процессом, а
также несет ответственность за то, чтобы заданные ключи были
известны обоим проходам программы lint.
15. Переносимость
В различных реализациях языка Си допускались отступле-
ния от стандарта языка системы UNIX. Тем не менее многие
Си-программы были с небольшими усилиями успешно перенесены
- 13 -
на различные установки. В данном разделе описываются неко-
торые различия реализаций Си, а также обсуждаются полезные
свойства программы lint.
Неинициализированные внешние переменные обрабатываются
по-разному в различных реализациях языка Си. Предположим,
что имеются два файла, каждый из которых содержит вне
какой-либо функции объявление без инициализации (например,
int a;). Редактор свзей ДЕМОС при обработке этих объявлений
выделит для a только одно слово памяти. В рамках некоторых
реализаций это невозможно (по различным причинам, которые
трудно счесть разумными!), и каждое такое объявление влечет
заведение своего внешнего имени. В момент загрузки это при-
ведет к фатальному конфликту. Если программа lint будет
вызвана с флагом -c, то такие многократные объявления будут
выявлены.
Аналогичные трудности возникают при представлении внеш-
них имен в объектном файле. В системе ДЕМОС внешние имена
имеют семь значащих символов, причем учитывается различие
верхнего и нижнего регистров. В системах ЕС ЭВМ число зна-
чащих символов равно восьми, однако различие регистров игно-
рируется. Данное обстоятельство может порождать ситуации,
когда программа, работающая в ДЕМОС не собирается в других
системах. Использование флага -p вызывает преобразование
всех имен внешних переменных к одному регистру и усекает их
до шести символов, предусматривая при этом анализ "худшего
случая".
Некоторые трудности возникают и при обработке символов:
символы в ДЕМОС представляются восемью битами в коде КОИ-8,
в ОС ЕС используется восьмибитных код ДКОИ. Кроме того,
строки символов располагаются от старшей к младшей битовой
позиции ("слева направо") в системах ЕС, и от младшей к
старшей позиции ("справа налево") на СМ-1420. Это означает,
что фрагмент программы, выполняющий построение строк не из
символьных констант, или использующий символы в качестве
индексов массивов, должен проверяться очень внимательно.
lint не может существенно помочь в данной ситуации, за иск-
лючением указания символьных констант, состоящих из несколь-
ких символов.
Естественно, размеры слова в разных машинах могут раз-
личаться. Однако, это вызывает меньше затруднений, чем
можно было бы ожидать, по крайней мере, при переносе с 16-
битной машины на 32-битную. По всей вероятности, основные
проблемы возникают при сдвигах и маскировании. В настоящее
время язык Си обеспечивает обработку битовых полей, что поз-
воляет писать значительную часть таких кодов с высокой сте-
пенью переносимости. Часто переносимость таких программ
может быть расширена путем небольшой перестройки стиля прог-
раммирования. Многие из несовместимостей являются, по-
видимому, следствием записей типа
- 14 -
x &= 0177700
для обнуления шести младших битов x. Будучи удовлетвори-
тельной для СМ-1420, такая запись приведет к тяжелой ошибке
на ЕС ЭВМ. Желаемый эффект может быть получен при использо-
вании контсрукции:
x &= 077;
которая выполняется правильно на всех рассматриваемых маши-
нах.
Операция сдвига вправо является арифметическим сдвигом
на СМ-1420 и логическим сдвигом на большинстве других машин.
Для получения логического сдвига для всех машин, левый опе-
ранд может задаваться в виде целого без знака. Символы счи-
таются целыми со знаком в СМ-1420, но целыми без знака в
других машинах. Такое "поведение" знакового бита может быть
справедливо расценено как ошибка аппаратурой PDP-11, харак-
теристики которой оказали определенное влияние на язык Си.
Если бы имелся приемлемый способ обнаружения программ, в
которых встречается указанная трудность, язык Си мог бы быть
изменен. Во всяком случае, программа lint здесь бессильна.
Приведенные выше примеры могут создать впечатление, что
проблема переносимости представляет больше трудностей, чем
это есть в действительности. Возникающие здесь вопросы
редко бывают неуловимыми или загадочными, по крайней мере
для разработчика программы, хотя и могут иногда потребовать
определенной работы по их устранению. Значительно более
серьезным препятствием для переносимости утилит системы
ДЕМОС является невозможность имитировать существенные функ-
ции системы ДЕМОС на других системах. Невозможность доступа
к символу, находящемуся в произвольной позиции в текстовом
файле, или невозможность установления межпрограммного интер-
фейса требует значительно большей переработки и отладки, чем
любые различия в компиляторах языка Си. С другой стороны,
программа lint является очень полезной при переносе операци-
онной системы ДЕМОС и связанных с ней программ-утилит на
другие машины.
16. Управление выдачей сообщений
Бывают случаи, когда программист умнее программы lint.
Так, могут быть веские причины для использования "недопусти-
мых" изменений типа, функций с переменным числом аргументов
и т.д. Более того, как указано выше, информация о потоке
управления, порождаемая lint, часто имеет "слепые пятна",
что может вызвать случайные ложные сообщения о вполне разум-
ных программах. Таким образом, желательно иметь некоторые
способы связи с lint, обычно для подавления таких ложных
сообщений.
- 15 -
Форма, которую должен иметь такой механизм, не вполне
ясна. Дело в том, что введение, например, новых ключевых
слов потребовало бы от современных и старых компиляторов
обеспечения распознавания этих слов или по крайней мере их
игнорирования. При этом возникают как философские, так и
практические трудности, в частности потребует изменения син-
таксис нового препроцессора.
В конце было решено, что ряд слов, находящихся внутри
комментариев, должен распознаваться программой lint. Это
потребовало минимальных изменений в препроцессоре: препро-
цессор пропускает комментарии в свой выход, а не удаляет их,
как это делалось раньше. Таким образом, директивы lint
являются невидимыми для компиляторов; эффект этих изменений
для систем со старыми препроцессорами заключается только в
том, что эти директивы программы lint на этих системах не
работают.
Первая директива относится к информации о потоке управ-
ления: если какой-то участок программы не может быть достиг-
нут, а программе lint это не очевидно, то его можно указать
программе lint с помощью директивы /* NOTREACHED */, поме-
щенной в подходящей точке программы. Подобным образом, если
желательно отключить строгую проверку типов в последующим
выражении, то может быть использована директива
/* NOSTRICT */
После обработки этого выражения lint возвратится в прежнее
(принимаемое по умолчанию) состояние. Флаг -v может быть
включен для одной функции с помощью директивы
/* ARGUSED */
Сообщения о различном числе аргументов при обращениях к
некоторой функции могут быть подавлены директивой
/* VARARGS */
помещенной перед определением данной функции. В некоторых
случаях желательно проверять несколько первых аргументов,
оставляя остальные аргументы непроверенными. Это может быть
указано ключевым словом VARARGS, за которым следует цифра,
задающая число аргументов, которые должны быть проверены;
так,
/* VARARGS2 */
вызывает проверку лишь первых двух аргументов. Наконец,
директива
/* LINTLIBRARY */
- 16 -
помещенная в заголовке файла, указывает, что данный файл
является файлом объявления библиотеки. Этой теме посвящен
следующий раздел.
17. Файлы объявления библиотек
Программа lint допускает несколько библиотечных дирек-
тив, подобных -ly, и проверяет исходные файлы на совмести-
мость с этими библиотеками. Это осуществляется путем дос-
тупа в файлы описания библиотек, имена которых (файлов)
строятся из библиотечных директив. Все такие файлы начина-
ются директивой
/* LINTLIBRARY */
за которой следует серия пустых определений функций. Крити-
ческими частями этих определений являются: определение типа
возвращаемого функцией значения, вопрос - не возвращает ли
пустая функция некоторого значения, а также число и типы
аргументов функции. Для указания особенностей данных библи-
отечных функций могут быть использованы директивы VARARGS и
ARGUSED.
Библиотечные файлы lint обрабатываются почти в точности
так же, что и обычные исходные файлы. Единственное различие
заключается в том, что о функциях, определенных в библиотеч-
ном файле, но не использованных в исходном файле, не выда-
ется никаких сообщений. Программа lint не моделирует полный
алгоритм поиска в библиотеке, и выдает предостерегающее
сообщение в случае, если исходные файлы содержат переопреде-
ление некоторой библиотечной функции (это-особенность!).
По умолчанию программа lint сверяет указанные ей прог-
раммы со стандартным библиотечным файлом, содержащим описа-
ние программ, которые обычно загружаются при работе программ
на языке Си. При задании флага -p проверяется другой файл,
содержащий описание стандартных программ библиотеки
ввода/вывода, которые, как ожидается, обладают переноси-
мостью на различные машины. Для подавления всех проверок
библиотек можно использовать флаг -n.
18. Ошибки и т.д.
При создании программы lint разработчики встретились со
значительными трудностями, отчасти обусловленными тесной
связью с вопросами стиля программирования, а отчасти тем,
что пользователи обычно не уведомляют о ситуациях, в которых
программа пропускает ошибки, о которых она должна бы была
информировать. (Интересно, что в случаях, когда lint оши-
бочно сообщает о несуществующих на самом деле ошибках, прог-
раммисты откликаются немедленно).
- 17 -
Дальнейшее усовершенствование программы lint можно
вести в нескольких направлениях. Так, недостаточно адек-
ватна проверка структур и массивов, никаких попыток проверки
соответствия объявлений структур и объединений в различных
файлах. Ясно, что желательно несколько более строгая про-
верка использования определения типов typedef; однако вопрос
о том, какой уровень контроля будет достаточен и как доби-
ваться этого - еще требует определения.
Программа lint делит препроцессор с Си-компилятором.
Это может оказаться благоприятным обстоятельством для созда-
ния специальной версии препроцессора, охватывающего ряд не
выполняемых в настоящее время проверок, таких как неисполь-
зуемые макроопределения, макроаргументы которых имеют побоч-
ные эффекты, или которые не расширяются вообще, или расширя-
ются более чем один раз и т.д.
Центральной проблемой, связанной с lint, является упа-
ковка собираемой информации. Имеется много ключей, служащих
единственно для выключения или небольшого изменения тех или
иных особенностей обработки. Имеется тенденция включения
еще большего числа таких ключей.
В заключение отметим, что складывается впечатление, что
основная идея создания двух программ оказалась удачной.
Компилятор концентрируется на быстром и точном переводе
программного текста в исполняемый код, а программа lint
занимается главным образом вопросами переносимости, стиля и
эффективности. Программа lint может позволить себе оши-
баться, поскольку некорректность и излишний консерватизм,
хотя и раздражают, но не губительны. Компилятор может рабо-
тать быстро, поскольку он "знает", что программа lint в
сложной ситуации "прикроет его фланги". Наконец, програм-
мист может сосредоточиться исключительно на таких аспектах
процесса программирования, как описание алгоритмов и струк-
тур данных и корректности программы, а затем обеспечить с
помощью lint желаемые свойства универсальности т переноси-
мости.
ПРИЛОЖЕНИЕ. Список имеющихся ключей команды lint
В настоящее время команда lint имеет следующий формат:
lint [-ключи] файлы ... дескрипторы-библиотеки...
Ключами являются:
h Осуществлять эвристический контроль
p Осуществлять контроль переносимости
- 18 -
v Не выдавать сообщений о неиспользуемых аргументах
u Не выдавать сообщений о неиспользуемых или неопределен-
ных внешних переменных
b Выдавать сообщения о недостижимых операторах break
x Выдавать сообщения о неиспользованных объявлениях внеш-
них переменных
a Выдавать сообщения о присваиваниях значений типа long
значениям типа int