`

СПЕЦІАЛЬНІ
ПАРТНЕРИ
ПРОЕКТУ

Чи використовує ваша компанія ChatGPT в роботі?

BEST CIO

Определение наиболее профессиональных ИТ-управленцев, лидеров и экспертов в своих отраслях

Человек года

Кто внес наибольший вклад в развитие украинского ИТ-рынка.

Продукт года

Награды «Продукт года» еженедельника «Компьютерное обозрение» за наиболее выдающиеся ИТ-товары

 

Чтобы сделать лучше...

0 
 

У трех самых массовых вычислительных платформ современности – конечно, у Microsoft Windows всех семейств, у всех Unix-подобных систем и у Mac OS X – есть нечто общее. Причем не в назначении, а в технологии их реализации. Если уточнить это утверждение, то общность проявляется в одном используемом для реализации трех столь разных систем инструменте.

Естественно, речь идет о великолепном и одновременно ужасном изобретении программистского гения – языке C. Именно на этом языке написаны ключевые, системообразующие компоненты всех трех упомянутых в преамбуле классов ОС. Большинство ключевых подсистем – и Windows XP, и Sun Solaris, и Linux, и любой ОС из BSD-семейства, и, наконец, Mac OS X реализованы на C, и никто этого не скрывает. Заметьте, все перечисленные системы ни в коем случае не являются представителями «компьютерной археологии» – они активно развиваются, совершенствуются и не собираются покидать арену современного ПО. Если добавить к этому, самому по себе немаленькому, перечню реализованных на C программ еще и сотни наименований операционных систем-«невидимок», а также безымянных «прошивок»-firmware, присутствующих в сотнях миллионов микрокомпьютеров, упрятанных в цифровые фотоаппараты, плееры, игрушки, автомобили, измерительные приборы, то картина получится одновременно удивительная и удручающая. Полный потенциальных ловушек для неопытного и невнимательного программиста низкоуровневый, «почти ассемблер», язык C, постоянно критикуемый за массу врожденных недостатков, встречается на каждом шагу. И это в 2005 г., когда имеются сотни почти совершенных языков программирования для самых разных областей применения, с колоссальными возможностями и минимумом потенциальных опасностей, как семантических, так и синтаксических. Но это еще не все. Оказывается, для многих классов задач убогих низкоуровневых возможностей C даже... слишком много, о чем свидетельствуют «усеченные стандарты» этого языка, например признанный в автомобильной промышленности стандарт MISRA C (MISRA – Motor Industry Software Reliability Association).

Все сказанное преследовало одну-единственную цель – показать, что C вовсе не мертвый сегодня язык, и не собирается становиться таковым завтра. Не будем пытаться дать объяснение этому феномену жизнеспособности столь несовершенного C наряду с совершенными Ada и Eiffel. Феномен феноменом, но пока C-программы не выброшены на свалку, пока такие производители кросс-компиляторов, как Keil, HI-TECH и IAR Systems не собираются закрывать свой бизнес, C будет оставаться тем самым пресловутым C – как востребованным, так и великолепным и ужасным. А от профессиональных программистов будет требоваться блестящее владение этим инструментом, подразумевающее не только отличное знание связанных с ним опасностей, но и умение выжимать из него максимум возможностей. В конце концов молотком можно отбить себе пальцы, но разве это означает, что забивать гвозди удобнее подушкой?

Сделать лучше? Что?

Если бы автор ставил перед собой задачу быстренько слепить статейку, этот ее раздел начинался бы непременно фразой «существует множество технологических приемов, позволяющих улучшить показатели качества C-программ». Самое смешное в этой невинной и вполне разумной на первый взгляд фразе то, что она... бессмысленна. И дело в том, что конкретных «показателей качества» для программы на уровне исходных текстов, т. е. для изделия, реализованного с помощью какого-либо языка программирования как инструмента, по сути, не существует. Есть размытые определения пригодности программы к прочтению людьми (читабельности, readability), есть понятия стиля программирования, есть разнообразные соображения о взаимосвязи смутных оценок качества исходных текстов с не менее смутными оценками программы как исполняемой сущности. Такое страшное откровение подтверждается большинством источников информации и мнением авторов самых популярных учебников для программистов. Например, известная всем «Википедия» утверждает, что «в действительности не существует концепции хорошо написанного кода». В книге «Практика программирования» (The Practice of Programming) Б. Керниган и Р. Пайк вообще не употребляют понятия «хороший код» и говорят так: «Подход, которого мы придерживаемся... основывается на базовых взаимосвязанных принципах, применяемых ко всем уровням компьютинга. Эти принципы – простота... понятность, ясность стиля... универсальность». В общем, достаточно попробовать однозначно перевести термин «clarity», используемый по отношению к программе, имеющей две сущности (исходные тексты и исполняемый процесс), чтобы понять, что концепции хорошо написанного кода действительно не существует. И все-таки все понимают: есть хорошо написанные программы, которые хорошо работают, и есть так себе написанные программы, которые весьма неплохо работают. Но вот чего точно нет, так это ужасно написанных программ, которые прекрасно работают.

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

В решении такой задачи помогают средства, условно именуемые «дисциплинирующими». Конечно, не только они, и конечно, речь идет не о примитивных розгах. Мы ведь говорим о дисциплине как о составляющей процесса проектирования ПО. И весьма странно, что такой мощный, прошедший проверку временем и опытом нескольких поколений программистов, свободно доступный дисциплинирующий инструмент, как Splint столь непопулярен среди разработчиков. О его предшественнице – программе статической проверки исходных текстов Lint – написано достаточно много, и она хорошо известна программистам, имеющим опыт работы в Unix-подобных ОС. Именно из-за Unix-происхождения и настоящей древности (по компьютерным меркам, естественно, – Lint написана в начале 70-х годов прошлого века), описанная во множестве статей и книг Lint стала очень популярной программой, оставившей в тени более передовой Splint. И очень жаль, хотя бы потому, что Lint и Splint – инструменты, по большому счету, совершенно разные.

Миф о серебряной пуле «лучших практик»

Не следует думать о любых рекомендациях, в том числе и о рекомендациях этой статьи, как о неоспоримом опыте, о «лучших практиках», неизменно приводящих к отличным результатам. Майкл Эванс, Корин Сигур и Фрэнк Доэрти – специалисты, к мнению которых стоит прислушаться хотя бы потому, что они представляют «оборонку» – пусть далекую, американскую, но «оборонку», подчиненную жесточайшим требованиям бюджета, времени и показателей качества. В своей статье о мифе «лучших практик» они пишут так:

«В 2001 году доктор Ричард Тарнер возглавил исследование в интересах Министерства обороны с целью выявления заслуживающих доверия лучших практик [разработки ПО. – Прим. автора], способных улучшить возможности, предсказуемость, качество и эффективность использования при одновременном снижении рисков, сокращении сроков и стоимости разработки [речь идет о ПО]. В результате проведенной работы Тарнер вынужден был констатировать, что так как термином «лучшие практики» долго, регулярно и последовательно злоупотребляли, сейчас он просто вводит в заблуждение, определяя область, в которой содержится все – от хороших идей до полностью бесполезных в наихудшем случае. Термин стал расхожей фразой... используемой некоторыми для узаконивания практик, инструментов и процессов, не прошедших серьезной проверки».

C полной версией статьи «Миф о серебряной пуле...» вы можете ознакомиться в сентябрьском номере pdf-версии журнала военных программистов CrossTalk.

Итак, Splint

Начнем мы знакомство с нею с... истории одной маленькой ошибки в C-программе. Стоимость этой ошибки оказалась неожиданно высокой – ее устранением определялась судьба контракта на 20 миллионов долларов. Эта история невыдуманная, и случилась она в 1993 г., в подразделении операционных систем известной компании SunSoft. Ошибку отыскали – ею была обыкновенная опечатка в исходном тексте программы, приводящая к формально правильной, но исключительно трудно обнаруживаемой на этапе компиляции и выполнения ошибочной конструкции. Смоделировать «ошибку на 20 миллионов» позволяет следующая макетная C-программа:

int main()
int Vel_01, Vel_02;

Vel_02 = 31;
Vel_01 = Vel_02;


Vel_01 == 36;

printf("Vel_01 = 0", Vel_01);

В восьмой ее строке программист случайно нажал лишний раз кнопку с символом равенства, и вместо присваивания значения переменной Vel_01 получилось абсолютно правильное C-выражение, значения Vel_01 никак не изменяющее. Если «натравить» на исходные тексты этой макетной программы Splint, можно получить такое предупреждение:

bug20m.c(8,3): Statement has no effect: Vel_01 == 36
Statement has no visible effect --- no values are modified. 
(Use –no effect to inhibit warning) 

Оператор, не приводящий к видимому эффекту, не модифицирующий никаких значений, в C – большая опасность. А тем более опасны такие операторы, схожие с операцией присваивания. Зная это простое эмпирическое правило, можно было бы отыскать «ошибку на 20 миллионов» в исходных текстах, не прибегая к компиляции и запуску программы. Для этого достаточно было прогнать через Splint (или его предшественницу – Lint) исходные тексты и в результатах ее работы отыскать все строки, совпадающие с шаблоном «Statement has no effect:x20[^=]+ ==» (в терминах регулярных выражений). Согласитесь, запуск одной программы и затем поиск одной командой в результатах ее выполнения – не такая уж и сложная процедура, зато эффект от ее применения может быть более чем ощутимым.

Игрушечный пример позволил продемонстрировать не столько возможности Splint, сколько идею статического анализатора исходных текстов. Этот инструмент предназначен для проверки различных аспектов корректности кода с точки зрения формальных правил языка программирования и обширного набора мощных эвристик. При этом анализ производится без участия компилятора и порождения исполняемого кода – отсюда «статический» и в названии. Перечень отыскиваемых потенциальных опасностей весьма велик и включает в себя такие ужасы, как попытки использования неопределенных областей памяти и разыменования указателей с «неправильным» значением (почему это ужасы, знает любой C-программист). Более мягкие, но не менее опасные для работоспособности программы нюансы также анализатором учитываются. Например, маленькая как будто проблемка несовпадения типов в арифметических операциях, приводящая иногда к появлению совершенно неработоспособного, но абсолютно синтаксически правильного кода. Следующая программа-макет позволяет понять, какой подводный камень имеет бесконтрольное использование красивой C-идиомы, определяющей число элементов массива:

int main()

int a[100];
int b;

b = -1 ;
if (sizeof(a)/sizeof(a[0]) > b)
printf("It's really true");

В этой программе макетируется ситуация, в которой заведомо положительное число – количество элементов массива сравнивается с неким иным числом (значением переменной b), в ходе вычислений принявшим отрицательное значение. Казалось бы, положительное число всегда больше отрицательного, но вот сообщения «It's really true» от этого макета вам не добиться. Splint объясняет причину подобной несуразности так:

bug_types_incomp.c(7,19): Operands of > have incompatible types 
(arbitrary unsigned integral type, int): sizeof((a)) / 
sizeof((a[0])) > b

Стандарт языка C определяет в такой ситуации четкое поведение компилятора при генерации кода – он приводит знаковое число b к беззнаковому. После этого невинная отрицательная единица превращается в число с установленными в единицу всеми битами, и результат выполнения операции сравнения можно считать предрешенным.

Пока мы говорили о возможностях Splint, ограничиваясь только обзором «классического» статического анализа, способностями к которому обладает и старая программа Lint. На самом деле и этих способностей Splint уже немало для повышения самодисциплины программиста. Но наиболее мощь Splint проявляется в способствовании повышению уровня... программиста, использующего язык C при разработке программ. Речь идет о том, что такие модные техники, как объектно-ориентированное программирование, по сути, не являются прерогативами программистов, применяющих сугубо объектно-ориентированные языки. Никто не запрещает создавать объектно-ориентированные программные системы на чистом C – более того, примеры успешной реализации всех перечисленных в начале статьи ОС полностью подтверждают истинность данного высказывания. Вот в этой области Splint является действительно отличным дисциплинирующим инструментом, дополняя низкоуровневые возможности C высокоуровневой поддержкой ряда необходимых для высококлассного программирования концепций. Теперь Splint становится уже не инструментом пассивной обработки исходных текстов, написанных сугубо на языке C, а активной составляющей процесса проектирования, работающей с гибридными исходными текстами – созданными на гибридном языке. Последний задуман создателями так, чтобы не усложнять сущности без необходимости и не требовать каких-либо дополнительных программ, кроме Splint и C-компилятора. Специализированные директивы этого гибридного языка называются аннотациями и реализованы с помощью игнорируемых компилятором C комментариев и стилизации. Достаточно добавить к традиционной последовательности, начинающей C-комментарий, специальный символ – и Splint будет считать этот комментарий своей директивой. По умолчанию в программе применяется специальный символ «@», но, в принципе, можно выбрать любой другой, указав его при запуске Splint с параметром -commentchar символ. Шаблон директивы Splint при использовании символа, принятого по умолчанию, выглядит так:

/*@ ...... @*/ 

Гибридный язык Splint позволяет, например, использовать в C-программах абстрактные типы. Для этого достаточно определить имя нового типа вот так:

typedef /*@abstract@*/ старый_тип имя_нового_типа ;

Если передать Splint на анализ исходные тексты программы, применяющей объекты такого абстрактного типа, анализатор выяснит все фрагменты кода, зависящие от знания программиста о реализации типа, и оповестит о них предупреждениями. «Очистка» от них программы позволит добиться максимальной независимости между именем типа и его реализацией, что, несомненно, полезно при разработке больших программ.

Для абстрактных типов Splint позволяет четко определять правила доступа к их реализациям на уровнях модулей (образуемых парой файлов с именами, например, Module.h и Module.c), имен отдельных файлов, отдельных функций. Более того, Splint позволяет объявлять абстрактные типы неизменными (immutable), что дает возможность контролировать все случаи попыток доступа к объектам такого типа из используемых их подпрограмм (функций).

К не менее мощным и полезным аннотациям Splint следует отнести используемые при проектировании интерфейсов (то есть спецификаций вызовов C-функций). Так, объявление следующего интерфейса заставит Splint «надавать по рукам» нерадивому программисту, пишущему его реализацию и пытающемуся изменить в теле программы значение, на которое ссылается переменная Val_02:

int Calc_Offset ( int * Val_01, char * Val_02) 
/*@modifies *Val_01@*/

Всего Splint поддерживает несколько сотен аннотаций, позволяющих «улучшить» C-программиста при решении почти двадцати классов типовых задач с подводными камнями, а также при формировании высокоуровневой архитектуры C-программы. Splint может быть как безумно строгим, так и совершенно индифферентным – строгость проверок также подчинена программисту. Высшим пилотажем использования этого анализатора является создание собственных правил аннотирования программы – это также возможно. Кроме того, Splint отлично согласуется с самыми разными программными инструментами и оболочками. Единственный недостаток, он же достоинство, – изобилие проверок и возможностей Splint – компенсируется идеальной пригодностью этой программы к поэтапному освоению: сначала вы можете относиться к ней как к пассивному статическому анализатору, а затем осваивать гибридный язык Splint-C (он даже имеет уже ставшее историей имя – Larch C Interface Language).

Не только для С

Естественно, статические анализаторы исходных текстов доступны не только C-программистам. Вот краткий перечень легально бесплатных программ этого класса.

Разработчики, применяющие язык Java, могут использовать анализатор PMD (pmd.sourceforge.net). Этой программной системе более трех лет, над ней активно работают свыше ста программистов. В репозитории открытого ПО SourceForge ей присвоен статус «пригодной к применению/стабильной». Что, впрочем, подтверждается большим числом статей о ней и даже выходом в свет посвященной ей книги. PMD привлекательна и высокой степенью интеграции с популярными интегрированными средами разработки, в первую очередь с Eclipse.

Для программистов на языке C# перечень доступных статических анализаторов можно найти на сайте SharpToolbox (sharptoolbox.com) в разделе «Анализ кода – стандартные верификаторы».

Примечательно, что техника статического анализа исходных текстов может распространяться не на «программы вообще», а только на отдельные, четко определенные классы программ. В частности, «статический верификатор драйверов» SDV компании Microsoft позволяет программисту избежать более 60 подводных камней, трудно обнаруживаемых на этапе отладки драйвера.

Ready, set, buy! Посібник для початківців - як придбати Copilot для Microsoft 365

0 
 

Напечатать Отправить другу

Читайте также

 

Ukraine

 

  •  Home  •  Ринок  •  IТ-директор  •  CloudComputing  •  Hard  •  Soft  •  Мережі  •  Безпека  •  Наука  •  IoT