Системы программирования

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

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

#include " имя файла "

а включаемые файлы обычно имеют расширение.h. Включаемые файлы чаще всего содержат описание используемых структур данных и функций. Формирование подлежащих компиляции файлов из текстовых заготовок выполняет препроцессор. Получаемые на выходе файлы уже не содержат директив препроцессора и имеют расширение.i.

Далее полученные.i файлы поступают на вход собственно транслятору, который для каждого из них получает объектные модули. Объектный модуль – это фрагмент программы на машинном языке, в котором могут использоваться ссылки на элементы (например, функции), определённые в других модулях. Кроме того, для того, чтобы другие модули могли ссылаться на данный модуль, он должен содержать описание тех его элементов, на которые можно сослаться.

Далее осталось собрать все объектные модули воедино, подключая при необходимости библиотеки стандартных объектных модулей, обычно имеющих расширение.lib. При этом возможно, что части объектных модулей перегруппируются, например, так, чтобы данные из всех модулей собрать в одном месте, а команды – в другом. Таким образом, на этом этапе инеобходимо определить для каждого фрагмента объектного модуля его место в готовой программе и заменить ссылки на реальные машинные адреса. Всю эту работу выполняет редактор связей или ассемблер.

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

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

Редактор связей
Транслятор
Транслятор
person.h
io.h
graph.h
person.c
read.c
main.c
person.i
read.i
main.i
person.obj
read.obj
main.obj
io.lib
graph.lib
prog.exe

 


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

Как видно, порождение исполняемой программы должно учитывать множество взаимосвязей: что из чего и в какой последовательности получается. Кроме этого, можно заметить, что раздельная трансляция даёт возможность при изменении отдельных исходных файлов не повторять весь процесс построения полностью. Например, если изменения коснулись только файла read.c, то достаточно транслировать его в read.obj и после этого собрать готовую программу из старых объектных модулей. Если же был затронут включаемый файл io.h, то заново транслировать нужно read.c и main.c, но не person.с.

Таким образом, построение становится достаточно сложным, чтобы потребовать от системы программирования поддержки этого процесса. Система построения (build, make) может извлекать зависимости либо автоматически, например, путём просмотра исходного текста на языке C и обнаружение директив препроцессора, либо доверить пользователю описать все зависимости и порядок построения в специальном файле, либо использовать какой-нибудь комбинированный подход, когда пользователь описывает лишь нестандартные зависимости и процессоры.

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

В принципе этим функции системы программирования можно и ограничить, поскольку она уже предоставляет необходимые инструменты для исполнения программы. Однако деятельность, связанная с программами, далеко не ограничивается их исполнением. Оставив за рамками рассмотрения вопросы анализа предметной области, требований заказчика и проектирования (которые в идеальном случае тоже должны быть поддержаны соответствующими инструменами), будем считать, что программа исходно представляется своим текстом, который может быть создан любым текстовым редактором. Однако, если редактор используется именно для работы с программами на определённом входном языке, то естественно ожидать от него автоматического форматирования, раскраски текста и т.п. Кроме того, пользователь заглядывает в текст программы не только в момент её написания, но и, например, когда транслятор обнаружил ошибку, либо когда программа была приостановлена в процессе исполнения. При этом хотелось бы, чтобы редактор сам указал нам нужную точку в программе. Естественно, что для этого требуется более тесная интеграция редактора с другими компонентами системы программирования: он должен «понимать» сообщения транслятора, а для объектных модулей и готовой программы должна поддерживаться их связь с исходным кодом, причём пронизывающая весь путь преобразования программы. Таким образом, система программирования чаще всего включает в себя собственный языково-ориентированный текстовый редактор, а мы можем говорить о интегрированной среде разработки, для которой редактор является основым интерфейсом, через который осуществляется не только изменение тескта программы, но и все другие действия.

Знание редактора о том, что он обрабатывает именно текст программы на некотором языке программирования, позволяет существенно расширить его возможности. Например, он может включать в себя справочную систему, которая может выдать информацию об указанной языковой конструкции, стандартной функции или типе данных. Если же система поддерживает документирование программ, то справочная система может предоставить информацию не только  о предопределённых конструкциях и объектах, но и об определённых программистом, если для них были обеспечены соответствующие аннотации, оформляемые чаще всего как комментарии специального вида. Система документирования может извлечь из текста программы все эти аннотации и собрать их в отдельный документ или совокупность взаимосвязанных документов. Документация программы должна освещать по крайней мере следующие аспекты:

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

· системная документация предназначена для администраторов и описывает, где и как должна быть установлена программа, какие ресурсы она использует и т.п.

· пользовательская документация описывает, с какими параметрами можно запустить программу, реализуемые ею сценарии, пользовательские интерфейсы и т.п.

Включённость в систему программирования позволяет редактору осуществлять и более сложные реадактирующие действия. Например, помимо простого текстового поиска редактор может использовать информацию, полученную от транслятора и загрузчика, для отображения перекрёстных ссылок: перехода от использующего вхождения переменной или функции к соответствующему определению и наоборот – поиска всех использований объекта, причём не только в редактируемом файле, но и во всей системе. Редактирующие действия могут быть расширены на языковые конструкции. Простейшим примером такого действия является переименование объекта, что, естественно, должно приводить к поиску и переименованию всех использующих вхождений. Можно реализовать и более сложные трансформации: открытую подстановку функций/макросов (т.е. подстановку определения вместо использования с соответствующей заменой параметров), изменение формы циклов, и т.п. Такого рода операции служат для так называемого рефакторинга программ, т.е. изменения их текста или структуры без изменения реализуемой функциональности с целью улучшения понимаемости, простоты дальнейшего сопровождения или развития и т.п. 

Естественным расширением понятия исполнения является отладка программы. Целью отладки является поиск и исправление обнаруженных ошибок поведения программы. По-видимому, самый простой способ отладки состоит во вставке в текст программы дополнительных операторов, выводящих текущую информацию о состоянии исполнения (исполняемой инструкции, значении переменных и т.п.) или проверяющих условия, которые обязаны в данной точке выполняться. Вставка подобных дополнительных действий, которые (в идеале) не меняют поведение программы, а предназначены для анализа программы, называется инструментированием. Программист может после выполнения такой расширенной программы проанализировать её вывод и, если повезёт, понять причину ошибки. Интерактивная отладка дополнительно позволяет задавать точки останова, достигая которые, программа в отладочном режиме приостанавливается, указывает текстовому редактору позицию в исходном файле, позволяет пользователю проинспектировать текущее состояние вычислений, после чего продолжить или прервать исполнение. Современные системы предоставляют и более продвинутые средства отладки. Например, для точки останова можно указать дополнительное условие вида: "каждый 100-ый раз, и только в том случае, когда x<eps". Бывает возможным "откатить" исполнение на несколько шагов назад, "пропустить" часть инструкций или даже изменить программу в процессе выполнения.

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

Как уже было сказано, потребность в отладке возникает, когда обнаруживается, что программа работает неправильно. Однако, невозможно предсказать, когда ошибка проявит себя. Может пройти много лет эксплуатации до того момента, когда вдруг программа «сломалась», и нет никаких гарантий, что в программе не осталось других ошибок, либо что исправление найденной ошибки не привело к новой. Альтернативой отладки в этом смысле является тестирование, цель которого состоит нахождении такого набора входных данных, правильная работа программы на которых, если и не доказывала, то убеждала бы в её правильности. Система программирования может предоставлять средства для автоматического построения набора тестов как для отдельных компонент программы (например, функций), так и программы в целом. Постороение полного набора тестов для любой программы является неразрешимой задачей. Поэтому ограничиваются лишь выполнением некоторого критерия тестирования, такого, например, как гарантия того, что каждая точка тестируемого компонента будет выполнена, либо того, что любой цикл выполнится не менее двух раз, и т.п. Система тестов (построенная либо автоматически, либо вручную) даёт возможность для регрессионного тестирования: проверки того, что новая версия программы работает так же, как предыдущая. Ввиду того, что системы тестов оказываются весьма большими и, следовательно, требуют больших вычислительных ресурсов для проверки, отдельной задачей является минимизация системы тестов без существенной потери в достоверности.

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

· любой переменной присваивается некоторое значение до момента её использования (это относительно легко сделать для локальных переменных функций и существенно сложнее для глобальных переменных);

· никогда не разыменуется пустой указатель NULL;

· открытие файла всегда предшествует чтению из него;

· индекс массива всегда находится в его границах и т.п.

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

Самым трудоёмким в жизненном цикле программного обеспечения является его сопровождение. Это объясняется следующими факторами:

· длительное время использования - иногда десятилетия, что особенно важно, учитывая, что за такое время может кардинально поменяться как вычислительные средства, так и технология программирования;

· большое количество пользователей, от которых могут поступать разные запросы по развитию функциональности программы, причем запросы от разных пользователей могут быть несовместимы;

· большой коллектив разработчиков, который может со временем полностью поменяться.

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

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



Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: