Отладка приложений

         

Адреса загрузки DLL


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

Первым важным указателем для аварийных сбоев является базовый адрес ваших динамических библиотек (DLL) и элементов управления ActiveX (OCX), который указывает, с какой ячейки памяти начинается отведенное им адресное пространство. Когда заказчик сообщает адрес аварийного завершения, необходимо быстро сузить его до первых двух или трех цифр адреса DLL, из которого он пришел. Конечно, трудно запомнить адреса всех системных DLL, но надо знать, по крайней мере, базовые адреса DLL своего проекта.

Если все ваши DLL загружены в уникальные адреса, то имеется несколько хороших указателей, помогающих вести поиск аварийных остановов. Но что, вы думаете, случилось бы, если бы все DLL имели один и тот же адрес загрузки? Очевидно, что операционная система не отображает все DLL в одно и то же место памяти. Она должна перемещать любую входящую DLL, которая хочет занять уже заполненную память, в другое место. Тогда возникают проблемы, связанные с попытками вычислить, где какая DLL загружена. К сожалению, нет никакого способа узнать, что операционная система будет делать на различных машинах. Следовательно, программист понятия не имеет, откуда пришел аварийный останов, и ему придется потратить уйму времени на его поиск через отладчик.

По умолчанию, для проектов, созданных с помощью соответствующего мастера, Visual Basic загружает DLL-библиотеки по адресу 0x11000000, a Visual C++ — по адресу 0x10000000. Держу пари, что сегодня по крайней мере половина DLL-библиотек в мире пытается загрузиться по одному из этих адресов. Изменение базового адреса для DLL называется перебазированием (rebasing), и это — простая операция, в которой указывается адрес загрузки, отличающийся от умалчиваемого.

Прежде чем перейти к перебазированию, рассмотрим более легкий способ выяснить, имеются ли конфликты загрузки в ваших DLL. Получив следующее уведомление в окне Output отладчика Visual C++, следует немедленно остановиться и исправить адреса загрузки конфликтующих DLL.
Удостоверьтесь, что вы исправляете адреса загрузки как для выпускных (финальных), так и для отладочных конфигураций.

LDR: Dll xxx base 10000000 relocated due to collision with yyy 

(LDR: Dll xxx база 10000000 перемещена из-за конфликта с yyy)

xxx и yyy в этом утверждении — имена DLL-библиотек, которые находятся в конфликте друг с другом.

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

Существует два способа перебазирования DLL-библиотек в приложении. Первый метод использует утилиту REBASE.EXE, которая поставляется с набором разработчика Platform SDK. Утилита REBASE.EXE имеет много различных возможностей (опций), но лучший выбор состоит в ее вызове через командную строку с ключом /b, со стартовым базовым адресом и указанием в командной строке имен соответствующих DLL-файлов.



Данные, представленные в табл. 2.1, взяты из документации Platform SDK и могут быть применены для перебазирования пользовательских DLL. Как видите, рекомендованный формат достаточно прост. Динамические библиотеки операционной системы загружаются в адреса от 0x70000000 до 0x78000000, поэтому следование рекомендациям табл 2.1 предохранит вас от конфликта с операционной системой.

Таблица 2.1. Схема перебазирования DLL

Первая буква имени DLL-файла

Стартовый адрес

А-С

0x60000000

D-F

0x61000000

G-I

0x62000000

J-L

0x63000000

М-О

0x64000000

P-R

0x65000000

S-U

0x66000000

V-X

0x67000000

Y-Z

0x68000000

Если в приложении имеются четыре DLL-файла: APPLE.DLL, DUMPLING.DLL, GINGER.DLL и GOOSEBERRIES.DLL, то, чтобы перебазировать все эти DLL-файлы, нужно запустить REBASE.EXE три раза.


Следующие три команды показывают, как нужно запускать REBASE.EXE с этими DLL:

REBASE /b 0x60000000 APPLE.DLL

REBASE /b 0x61000000 DUMPLING.DLL

REBASE /b 0x62000000 GINGER.DLL GOOSEBERRIES.DLL

Если несколько DLL-файлов передаются в REBASE.EXE в командной строке, как здесь показано для файлов GINGER.DLL и GOOSEBERRIES.DLL, то REBASE.EXE перебазирует их так, чтобы они были загружены друг за другом, начиная с указанного стартового адреса.

Другой метод перебазирования DLL состоит в спецификации адреса загрузки при компоновке DLL. В IDE Visual Basic установите адрес в поле DLL Base Address на вкладке Compile диалогового окна Project Properties. В Visual C++ укажите адрес в редактируемое поле Base Address, перейдя на вкладку Link диалогового окна Project Settings и выбрав там элемент Output в комбинированном списке Category. Visual C++ транслирует адрес, который вы вводите в поле Base Address в ключ /BASE компоновщика LINK.EXE.

С помощью утилиты REBASE.EXE можно автоматически обрабатывать одновременную установку множественных адресов загрузки DLL. Однако при установке адреса загрузки во время компоновки следует быть немного осторожнее. Если адреса загрузки нескольких DLL-файлов установлены слишком близко друг к другу, то в окне Ouput появляется сообщение перераспределения загрузчика. Фокус в том, чтобы установить загрузочные адреса достаточно далеко друг от друга (чтобы не беспокоиться о них после того, как вы их установили).

Для тех же DLL-файлов, что и в примере с REBASE.EXE, загрузочные адреса устанавливаются так:

APPLE.DLL           0x60000000

DUMPLING.DLL.       0x61000000

GINGER.DLL          0x62000000

GOOSEBERRIES.DLL    0x62100000

Два файла — GINGER.DLL и GOOSEBERRIES.DLL - интересны потому, что их имена начинаются с одного и того же знака. Когда это случается, адреса загрузки дифференцируются по третьей старшей цифре.


В случае добавления еще одного DLL-файла, имя которого начиналось бы с символа "G", его адрес загрузки был бы 0x62200000.

Для того чтобы увидеть проект, в котором адреса загрузки установлены вручную, посмотрите на проект WDBG в разделе "WDBG: реальный отладчик" главы 4. Ключ /BASE позволяет также указать текстовый файл, содержащий адреса загрузки для каждого DLL в приложении (как это сделано в проекте WDBG).

Хотя перебазировать DLL- и OCX-файлы может как метод, использующий REBASE.EXE, так и ручное перебазирование, но лучше следовать второму способу и перебазировать DLL-файлы вручную. Именно вручную были перебазированы DLL-файлы всех примеров на сопровождающем компакт-диске данной книги. Главное достоинство этого метода заключается в том, что устанавливаемый специфический адрес будет содержаться в МАР- файле. МАР- файл — это текстовый файл, который указывает, куда компоновщик помещает все символы и исходные строки программы. В выпускной конфигурации всегда следует создавать МАР- файлы, потому что они — единственное прямое текстовое представление символов, которое можно получить.

МАР- файл содержит карту распределения глобальных символических имен (символов) конкретного приложения в памяти компьютера. Файл необязательный, он создается компоновщиком конкретной системы программирования (по запросу разработчика, через специальные ключи /MAP компоновщика) и имеет расширение .MAP. Описание состава, структуры и методики использования МАР-файлов для отладки приложений приводятся в главе 8. — Пер

 МАР- файлы окажутся особенно удобны в будущем, когда потребуется найти положение точки аварийного останова, а текущая версия отладчика не сможет прочитать старые символы. Если вместо ручного перебазирования DLL используется REBASE.EXE, то МАР -файл, созданный компоновщиком, будет содержать первоначальный базовый адрес, и нужно будет сделать некоторые арифметические вычисления, чтобы преобразовать адрес в МАР- файле в перебазированный адрес. В главе 8 МАР- файлы рассматриваются более подробно.



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

Общий вопрос отладки

Какие дополнительные параметры компилятора и компоновщика помогут мне с упреждающей отладкой?

Параметры (ключи) компилятора и компоновщика помогают управлять выполнением и облегчают отладку приложений. Установки по умолчанию, которые обеспечивают мастера проектов Visual C++ для компилятора и компоновщика, годятся не на все случаи жизни. Поэтому некоторые из них приходится изменять.

Параметры (ключи) компилятора CL.EXE

Все ключи компилятора можно ввести с клавиатуры непосредственно в поле редактирования Project Options в нижней части вкладки C/C++ диалогового окна Project Settings.

IP (препроцессорная обработка файла)

Если у вас неприятности с макросами, ключ /р будет предварительно обрабатывать ваш исходный файл, расширяя все макросы, включая все include-файлы и посылая вывод в файл с тем же именем, но с расширением .1. Чтобы увидеть, как расширен ваш макрос, вы можете заглянуть в М-файл. Удостоверьтесь, что на диске имеете достаточно места, потому что М-файлы могут иметь объем в несколько мегабайт каждый. Если они слишком велики, то можно использовать ключ /ЕР (совместно с /р), чтобы подавить директивы #line, выводимые препроцессором. Директивы #line используются препроцессором для координации номеров строк и имен исходных файлов в файле препроцессора, так что компилятор может сообщать о расположении ошибок компиляции.

/X (игнорировать стандартные пути)

Получение правильной конфигурации приложения может иногда быть затруднено, если на машине разработчика установлено несколько компиляторов и SDK. Если этот ключ не указывается, то компилятор при вызове из МАК-файла будет использовать переменную среды INCLUDE. Для того чтобы точно управлять включением конкретных файлов заголовков, применяется ключ /х, заставляющий компилятор игнорировать переменную среды INCLUDE и искать файлы заголовков только в тех местах, которые явно указаны в ключе /I.



/Zp (выравнивать члены структур)

Разработчик не должен использовать этот флаг. Вместо того чтобы указывать в командной строке, как члены структуры должны быть выровнены в памяти, это следует сделать при помощи директивы ttpragma pack внутри конкретного файла заголовка. Источником ошибок является то, что команда разработчиков выполняла построение приложений, изначально установив ключ /Zp. Требуется много времени, чтобы найти такие ошибки.

/GZ (отлавливать ошибки конфигурации версии в отладочной конфигурации)

В Visual C++ 6 введено выдающееся отладочное свойство, при включении которого компилятор после вызовов функций автоматически инициализирует их локальные переменные и проверяет стек вызовов. Этот флаг включен по умолчанию для отладочных конфигураций, но можно также использовать его в конфигурациях версии. Если возникают неприятности с чтением неинициализированной памяти (wild reads), записью неинициализированной памяти (wild writes) или перезаписью памяти, создайте новую проектную конфигурацию, которая основана на конфигурации версии и добавьте данный ключ к параметрам компиляции. Просматривая локальные переменные, заполненные во время их создания значениями ОхСС, можно попытаться понять, что изменило их исходные значения в неподходящий момент.

Кроме того, ключ /GZ будет генерировать код, который сохраняет текущий указатель стека перед косвенным вызовом функции (таким как вызов DLL-функции) и подтверждает, что указатель стека остается неизменным после вызова. Подтверждение правильности указателя стека предохраняет от одной из наиболее коварных ошибок описания, противоречащего соглашениям о вызовах. Эта ошибка происходит, когда вызываемая функция, специфицированная как _stdcall, неправильно объявлена со спецификатором _cdecl. Эти два спецификатора по-разному чистят стек, что позже приводит программу к аварийному сбою, если программист нарушает данное соглашение о вызовах.

/О1 (минимизировать размер)

По умолчанию проект, созданный с помощью мастера AppWizard библиотеки классов Microsoft Foundation Class (MFC), использует ключ /02 (максимизировать скорость) для построения конфигураций версии.


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

Страничные ошибки происходят, когда выполняющийся код переходит с одной страницы памяти (4 Кбайт для процессоров Intel x86) на следующую. Чтобы исправить страничную ошибку, операционная система должна прекратить выполнение вашей программы и разместить новую страницу в CPU. Если страничная ошибка — "мягкая" (т. е. страница уже находится в памяти), то издержки не слишком ужасны, но это, тем не менее, дополнительные издержки. 

 Страничная ошибка (page fault) — ошибка из-за отсутствия страницы (ошибка, которая возникает в случае, когда процесс указывает на страницу виртуальной памяти, отсутствующую в рабочем наборе в главной памяти). — Пер.

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

В дополнение к использованию ключа /01, следует обратить внимание на применение утилиты Working Set Tuner (WST) из Platform SDK. Утилита WST поможет упорядочить наиболее часто вызываемые функции в начале двоичного файла так, чтобы минимизировать рабочий набор (число страниц, хранящихся в памяти). С общими функциями в начале операционная система сможет выполнять свопинг ненужных страниц. Таким образом, приложение будет выполняться быстрее. Подробнее об использовании WST, см. февральскую колонку "Bugslayer" (1999) в Microsoft Systems Journal на MSDN.



Ключи (параметры) компоновщика LINK.EXE

Эти ключи можно ввести с клавиатуры прямо в поле редактирования Project

Options в нижней части вкладки Link диалогового окна Project Settings.

/MAP (генерировать МАР-файл)

/MAPINFO:LINES (включать строчную информацию в МАР-файл)

/MAPINFO:EXPORTS (включать экспортную информацию в МАР-файл)

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

/NODEFAULTLIB (ignore libraries)

Многие системные файлы заголовков включают записи #pragma comment (lib#, xxx), указывающие, с каким библиотечным файлом они связаны, где ххх — имя библиотеки. Ключ /NODEFAULTLIB сообщает, что компоновщик игнорирует директивы pragma. Этот ключ позволяет указать, с какими библиотеками надо держать связь и в каком порядке. Чтобы приложение имело связь с библиотеками, нужно указать каждую необходимую библиотеку в командной строке компоновщика, но по крайней мере нужно точно знать, какие библиотеки вы получаете и в каком порядке. Управление порядком, в котором связаны библиотеки, может быть достаточно важным, если один и тот же символ включен более чем в одну библиотеку, что может привести к ошибкам, которые очень трудно найти.

/ORDER (разместить функции по порядку)

После того как выполнена утилита WST, ключ /ORDER позволяет указать файл, который содержит упорядоченный список функций. Ключ /ORDER отменит инкре-ментную компоновку, так что указывайте его только в конфигурациях версии.

/PDBTYPE:CON (Объедините PDB-файлы)

Всегда указывайте ключ /PDBTYPEICON для всех конфигураций (как для выпускных, так и для отладочных). Для проектов Visual C++ этот параметр по

умолчанию не включен. Ключ /PDBTYPE:CON объединяет всю отладочную информацию модуля в единый PDB-файл. Наличие единственного PDB-файла существенно облегчает отладку одного и того же двоичного кода несколькими пользователями, а также упрощает архивирование отладочной информации.



/VERBOSE (печатать сообщения о ходе процесса)

/VERBOSE:LIB (печатать только сообщения о найденных библиотеках)

Если возникают затруднения с компоновкой, то эти сообщения могут показать, какие символы компоновщик ищет и где он их находит. Вывод может оказаться довольно громоздким, но покажет, где имеются проблемы, связанные с построением приложения. Я использовал ключи /VERBOSE и /VERBOSE : LIB, получив случайный сбой из-за того, что вызываемая функция выглядела (на уровне языка ассемблера) как-то не так, как должна была выглядеть, по моему представлению. Оказалось, что я имел две функции с идентичными сигнатурами, но различными реализациями в двух разных библиотеках, и компоновщик находил не ту, которая была нужна.

/WARN:3

Вообще-то этот ключ не нужен все время, но пару раз в течение жизни проекта программисту надо посмотреть, на какие библиотеки он фактически ссылается. Включив параметр /WARN:3, вы будете получать сообщения о том, имеются ли ссылки на библиотеки, переданные компоновщику LINK.EXE. Лично мне нравится точно знать, с какими библиотеками я связан, и я удаляю из списка компоновщика те библиотеки, на которые нет ссылок.



Частые построения


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

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

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

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

Ваша система построения продукта будет извлекать самые последние главные исходные файлы (master sources) из системы управления версией каждый раз, когда будет выполняться процесс построения (в идеале, разработчики должны также получать эти данные каждый день).
Нет ничего хуже, чем проводить время, пытаясь решить неприятную проблему только для того, чтобы выяснить, что она связана со старшей версией файла на машине разработчика. Другое преимущество частого получения исходных данных разработчиками состоит в том, что оно позволяет осуществлять непрерывное построение. Благодаря частому извлечению, любая проблема с главным построением (master build) продукта автоматически становится проблемой для локального построения (local build) каждого разработчика. Когда прерываются ежедневные построения, то раздражаются менеджеры, а разработчики не любят, когда прерывается их локальное построение. Зная, что прерывание главного построения означает прерывание построения всех индивидуальных разработчиков, каждый вынужден ограничиться чистым кодом в главном источнике.

Общий вопрос отладки

Когда следует заморозить модернизацию (upgrade) компилятора и других инструментов?

Как только завершена разработка ведущих свойств продукта (что также известно как разработка версии beta 1), вы определенно не должны модернизировать никакие инструменты. Изменяя код, нельзя позволить себе риск новой схемы оптимизации компилятора, независимо от того, насколько хорошо она обдумана. Ко времени, когда получена версия beta 1, уже выполнено некоторое существенное тестирование, и в случае изменения инструментов вы будете вынуждены повторно начать тестирование — с нуля.





Частые построения и интенсивные тесты обязательны


Две наиболее важных части инфраструктуры — это система построения продукта (build system) и набор интенсивных тестов (smoke-tests) Система построения — это то, что выполняет и компонует продукт, а набор smoke-тестов включает тесты, которые выполняют программу и подтверждают, что она работает.



Планируйте время для построения систем отладки


Начиная планировать проект, предусмотрите в нем время для построения систем отладки. С самого начала следует решить, как вы будете реализовывать аварийные обработчики (этой теме посвящена глава 9), разгрузчики файлов данных и другие инструменты, которые понадобятся, чтобы помочь воспроизвести проблемы, сообщенные в полях отчетов об ошибках. Целесообразно обращаться с системами обработки ошибок как со свойством продукта — это позволяет коллегам в компании видеть, как вы подходите к профилактике обработки ошибок.

При планировании системы отладки нужно определить превентивную политику отладки. Первое и наиболее трудное — определить то, как возвращать состояния ошибок в проект. Следует выбрать один способ отладки и его придерживаться. Однажды я столкнулся с проектом (к счастью, я в нем не участвовал), в котором было три различных способа возврата ошибок: возврат значений, исключения setjmp/longjmp и через глобальную переменную ошибки, подобную переменной errno из исполнительной библиотеки языка С. Разработчики этого проекта сталкивались с очень большими трудностями при прослеживании ошибок через границы подсистем.

К сожалению, я не могу рекомендовать какой-либо конкретный способ возврата ошибок как общий для всех ситуаций, потому что разработка в Windows слишком зависит от технологий и чужих компонентов. Такие технологии, как Component Object Model (COM — модель компонентного объекта), предписывают стандарт возврата ошибок. В общем случае, я предпочитаю СОМ-подход, при котором проверяется возвращаемое значение, а не выбрасывается объект типа С++- исключений. Некоторые С++ - разработчики могут со мной и не согласиться, но мои симпатии всегда на стороне простоты и ясности (что очевидно присуще СОМ-подходу).



Постоянно отслеживайте изменения


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

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

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

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

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



Проектируйте облегченную диагностическую систему для выпускных конфигураций


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

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

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

Вообще необходимо регистрировать все (ну почти все), что связано с проектами, но, как минимум, следует точно регистрировать отказы и аварийные ситуации. Нужно также попытаться ухватить логический смысл действий программы. Например, если программа выполняет файловые операции, то не стоит регистрировать такие мелкие детали, как "переход к смещению 23 в файле...", но желательно регистрировать открытие и закрытие файла. Тогда, если последний вход в журнале оказался: "Preparing to open D:\Foo BAR.DAT" (Подготовка к открытию D:\Foo\BAR.DAT), то почти наверняка файл BAR.DAT испорчен.


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

Для регистрации в коде C++ я применяю макрос следующего типа (обратите внимание, что G_isLogging — глобальная переменная, которую могут видеть все модули). Наличие глобальной переменной позволяет сэкономить на вызове функции.

//Visual C++ макрос для регистрации событий

#define LOGGING(x)                     \ 

if ( TRUE == G_IsLogging)               \ 

{                                       \ 

Logginglnfo ( x);                       \

 }

Для Visual Basic, поскольку в этом языке нет макросов, я только проверяю глобальную переменную вручную. Читателю предоставляется возможность написать простое встраиваемое дополнение к IDE Visual Basic, которое позволяло бы с помощью кнопки добавлять все, кроме строк, для передачи в функцию регистрации.

 Visual Basic. Пример вызова функции регистрации 

i£ ( 1 = G_IsLogging) Then

Logginglnfo ( "Подготовка к открытию" & sFile)

End If

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



Рассматривайте предупреждения как возможные ошибки


Поскольку Visual Basic намного более чувствителен к ошибкам компиляции, чем C++, то все, что компилятор сообщает вам, является ошибками. Как знает каждый, кто компилировал какую-нибудь программу, более сложную чем "Hello, World!", C++ — значительно более свободный язык, который позволяет избежать многих неприятностей. Подобно Visual Basic, Visual C++ имеет некоторые трудные ошибки, аварийно завершающие компиляцию. Такие ошибки, как С2037 ("left of 'operator' specifies undefined struct/union 'identifier1— "слева от 'операция' указан неопределенный 'идентификатор' struct/union"), означают, что компилятор не может продолжать выполнение. Visual C++ отличается от Visual Basic в первую очередь тем, что первый может (кроме ошибок) выдавать также предупреждения.

Эти предупреждения, в общем случае, означают, что некоторая часть кода неоднозначна, но компилятор сделает обоснованное предположение о правильном значении. Превосходным примером является предупреждение типа С4244 ("'conversion' conversion from 'typel' to 'type2', possible loss of data" — '"преобразование1преобразование из 'тип Г в 'тип2', возможная потеря данных"), которое всегда выводится при преобразованиях типов со знаком и без знака. Многие думают, что предупреждения являются только предупреждениями, но я считаю, что любое предупреждение — это то же самое, что ошибка, и что нужно трактовать его как ошибку— увидев предупреждение компилятора, следует остановиться и исправить текст программы, сделав его однозначным для компилятора.

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


Проекты по умолчанию, создаваемые мастерами Visual C++, выдают предупреждения 3-го уровня, которые соответствуют ключу /из компилятора CL.EXE. Следующий уровень предупреждений — 4 (ключ /W4), и можно даже заставить компилятор трактовать все предупреждения как ошибки (ключ /их). Все эти уровни легко установить в диалоговом окне Project Settings интегрированной среды разработки (Integrated Development Environment — IDE) Visual C++. Уровень предупреждений устанавливается в комбинированном поле Warning Level на вкладке C/C++, General Category, а ключ /wx — с помощью флажка Warnings As Errors, расположенного ниже поля Warning Level.

Можно почти доказать утверждение: "Все проектные конструкции должны компилироваться с предупреждениями уровня 4, и все предупреждения следует трактовать как ошибки", но действительность заставляет смягчить это требование. Во-первых, некоторые общие файлы заголовков не будут компилироваться с ключами /wх и /w4. Сам компилятор имеет пару ненужных информационных предупреждений, которые он трактует как реальные предупреждения, поэтому ключ /wх остановит компиляцию. Стандартная библиотека шаблонов (Standard Template Library — STL), поставляемая вместе с Visual C++, генерирует много предупреждений 4-го уровня. Некоторые проблемы порождает также применение шаблонов с компилятором. К счастью, программист может работать, не думая о большинстве этих проблем и предупреждений.

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


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

Хороший пример понижения уровня ошибок — включение файлов заголовков, которые не компилируются с предупреждениями 4-го уровня. Расширенная директива #pragma warning, первоначально предложенная в Visual C++ 6, может понизить уровень предупреждений. В следующем отрывке кода устанавливается уровень предупреждений перед включением подозрительного файла заголовков и повторно устанавливается так, чтобы код компилировался с предупреждениями уровня 4:

#pragma warning ( push, 3)

#include "IDoNotCompileAtWarning4.h"

#pragma warning ( pop)

С помощью директивы #pragma warning также можно выключать индивидуальные предупреждения. Эта директива оказывается полезной, если при использовании неименованной структуры или объединения получена ошибка С4201 ("nonstandard extension used: nameless struct/union" — "использовано нестандартное расширение: неименованная структура/объединение") с предупреждениями уровня 4. Чтобы выключить эти предупреждения, используется директива #pragma warning, как показано в следующем коде. Заметьте, что в нем прокомментировано выключение предупреждения и приведено соответствующее предупреждение. Выключая индивидуальные предупреждения, убедитесь, что ограничили область действия директивы #pragma warning определенной секцией кода. Если разместить директиву слишком высоко, то можно замаскировать другие проблемы в программе.

// Выключить предупреждение

// "nonstandard extension used: nameless struct/union"

// потому что я не пишу переносимый код

tpragma warning ( disable : 4201)

struct S

{

float y;

struct

 Это окно открывает команда Build из меню Build интегрированной среды разработки. — Пер.

{

int a;

int b;

int c;

};.

} *p_s;



// Включить предупреждение обратно.

 #pragma warning ( default : 4201)

Если STL не используется, то данная схема работает хорошо, а если да, то она может работать, а может и не работать. Прежде чем понижать уровень ошибок с помощью директивы #pragma warning ( push, 3), обязательно пытайтесь получить STL-заголовки, чтобы компилировать на 4-ом уровне предупреждений. При этом, может быть, придется выключить некоторые дополнительные индивидуальные предупреждения, но надо стремиться сохранять 4-й уровень предупреждений, если это возможно. Для пары проектов я так и не получил компилированного кода без предупреждений, как бы ни "вылизывал" программу. В этих случаях я понизил глобальный уровень предупреждений до 3. Даже тогда, однако, я сохранил включенным режим "Warnings As Errors" (ключ /wx).

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



В этой главе описаны важные



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

Системы отслеживания ошибок


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

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

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

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

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





Системы управления версией


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

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



Smoke-тесты


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

Smoke-тест — это просто контрольный список элементов, которыми программа может управлять. Начните с малого: установите приложение, запустите и затем закройте его. По мере продвижения через цикл разработки smoke-тест должен дорасти до проверки новых свойств продукта. В лучшем случае smoke-тест должен содержать по крайней мере один тест для каждого свойства и главного компонента изделия. В магазине информационных технологий (ITshop) это означает тестирование каждого из главных свойств, которые разработчик обещал менеджеру по информатизации (СЮ1) и клиенту. Имейте в виду, что с помощью smoke-теста не нужно исчерпывающе проверять каждую ветвь кода в программе, но его следует использовать, чтобы судить, работоспособно ли приложение в целом. Как только программа пройдет smoke-тест, инженеры по качеству могут начать трудную работу, пытаясь прервать (или даже разрушить) программу.

Важный компонент smoke-теста — некоторый вид эталонного теста производительности2. Многие забывают включать его в тест и расплачиваются позже, в цикле разработки. Если эталонный тест установлен для определения, например, длительности выполнения последней версии продукта, то можно определить как отказ, если текущее выполнение на 10 или более процентов превышает значение, полученное по эталонному тесту. Просто удивительно, какое вредное воздействие на производительность может оказать маленькое изменение в безобидном, на первый взгляд, месте программы.
Контролируя производительность по всему циклу разработки, вы можете решать проблемы производительности прежде, чем они выйдут из-под контроля.

Идеальная ситуация для smoke-теста — та, в которой программа автоматизирована так, что она может выполняться, не требуя какого-либо взаимодействия с пользователем. Инструмент, который применяется для автоматизации ввода и операций в приложении, называется инструментом регрессивного тестирования (regression-testing tool). К сожалению, не всегда можно автоматизировать каждое свойство. На рынке много хороших инструментов регрессивного тестирования, и если вы работаете с большим, сложным приложением и можете назначить кого-то для поддержки smoke-тестов, следует рассмотреть покупку такого инструмента. Некоторые из этих инструментов перечислены в приложении 2. 

СIO — сокр. от Chief Information Officer (менеджер по информатизации — руководитель, отвечающий за развитие информационных технологий в рамках фирмы). — Пер.

Тест, определяющий сравнительные характеристики производительности продукта. — Пер.

Если вас устраивает просто посылка нескольких клавишных команд приложению, то можете взглянуть на тестирующую систему, которая рассмотрена в главе 13.

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



Создайте программу установки


Начинайте разработку программы установки сразу после того, как вы приступили к разработке проекта. Программа установки — первая часть продукта, которую видят пользователи. Слишком много изделий производят слабое первое впечатление, показывая, что программа установки была оставлена на последнюю минуту. Начиная разработку программы установки как можно раньше, вы оставляете достаточное время для ее тестирования и отладки. Если программа установки сделана преждевременно, можно также включить ее в smoke-тест.

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

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



Специалисты по качеству должны тестировать отладочные конфигурации


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

В начальных стадиях цикла разработки продукта инженеры по качеству должны чередовать работу с отладочной и выпускной конфигурациями. По мере развития продукта они должны постепенно все больше концентрироваться на выпускной конфигурации. Пока не достигнута отметка alpha-версии продукта, в которой достаточно реализованных свойств для демонстрации продукта заказчикам, инженеры по качеству должны использовать отладочную конфигурацию от двух до трех дней в неделю. Когда вы подойдете к работе с версией beta 1, они должны ограничиться (в работе с отладочной конфигурацией) двумя днями в неделю. После перехода к версии beta 2, когда все свойства и главные ошибки установлены, они должны ограничить работу с отладочной конфигурацией одним днем в неделю. После перехода к работе с версией-кандидатом (release candidate) они должны сосредоточиться исключительно на выпускной конфигурации.



Стройте все конфигурации продукта с символами отладки


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

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

Включать символы отладки в выпускную (выходную) конфигурацию нетрудно. В Microsoft Visual Basic на вкладке Compile диалогового окна Project Properties включите флажок Create Symbolic Debug Info. Для проектов Microsoft Visual C++ потребуется два шага. Первый шаг — установка для компилятора (CL.EXE) режима размещения символов отладки в OBJ-фай-лах. В диалоговом окне Project Settings выберите элемент Win32 Release комбинированного поля Settings For, чтобы изменять только выпускные конфигурации. На вкладке C/C++ General Category комбинированного поля Debug Info выберите элемент Program Database. Эта установка добавит переключатель /zi к вашим компиляциям. Удостоверьтесь, что не выбран элемент Program Database For Edit And Continue (/ZI) — это привело бы к добавлению всех разновидностей наполнителей и другой информации в двоичные файлы так, чтобы во время отладки вы могли редактировать исходный код.


Второй шаг для проектов Visual C++ — генерация символов отладки компоновщиком LINK.EXE. Для этого выберите элемент Release Win32 в комбинированном поле Settings For, и на вкладке Link General Category установите флажок Generate Debug Info. Эта установка включает ключ отладки (/DEBUG) компоновщика, который необходим для конструкций отладки. Вы также должны ввести с клавиатуры строку /OPT:REF в редактируемое поле Project Options на вкладке Link. Использование ключа отладки /DEBUG компоновщика автоматически предписывает ему вводить все функции (независимо от того, будут ли на них ссылаться или нет), которые необходимы для отладочных конфигураций. Ключ /OPT:REF предписывает компоновщику вводить только функции, которые программа вызывает непосредственно. Если этот ключ не добавлен, то версия приложения будет также содержать функции, которые никогда не вызываются, что намного увеличит ее размер.

Хотя можно подумать, что включение отладочных символов сделает обратную разработку (reverse engineering) вашего приложения более легкой задачей, но, на самом деле, это не так. Если в проекте выбрана установка Program Database (PDB), то все символы отладки сохраняются в отдельных PDB-файлах, которые генерирует программа. Поскольку разработчики" не отправляют эти файлы заказчикам, дополнительные символы отладки нисколько не облегчат обратную разработку приложения.

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



Управление изменениями


Учет изменений жизненно необходим, однако наличие хорошей системы трассировки ошибок не означает, что разработчики могут произвольно изменять главные исходные файлы. Такой карт-бланш сделал бы весь учет бессмысленным. Идея состоит в том, чтобы управлять изменениями периода разработки, ограничиваясь лишь определенными типами изменений в некоторых стадиях проектирования, так чтобы можно было ежедневно оценивать состояние главных исходных файлов. Лучшая идея управления изменениями, о которой я когда-либо слышал, принадлежит моему другу Стиву Маньяну (Steve Munyan) и заключается в том, что вся разработка разделяется на три периода. В первом, который Стив назвал зеленым, допускаются любые изменения в главных исходных файлах ("горит зеленый свет"). Самые ранние части проекта обычно создаются именно в этот период, когда команда работает над новыми свойствами.

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

На последнем этапе загорается красный свет, начинается этап замораживания кода и для любого изменения кода требуется одобрение менеджера разработки. Будучи таким менеджером, я даже ужесточил ограничение, разрешив доступ "только для чтения" (read-only) к файлам, составляющим проект. На этом этапе разработчики думают: "Это всего лишь маленькое изменение, которое исправляет ошибку и ничему не повредит". Благое намерение может заставить целую команду возобновить тестирование с самого начала.

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

Сколько людей затрагивает эта проблема?  Вносятся изменения в ядро или в периферийную часть продукта?  Какие части приложения потребуется повторно протестировать?



Важность использования меток


Одной из наиболее важных команд, применяемых в системе управления версией, является команда метки (label command). В системе Microsoft Visual SourceSafe метку называют "label" (метка), в MKS Source Integrity — контрольной точкой (checkpoint), а в PVCS Version Manager — меткой версии (version label). Различные системы управления версиями, как мы только что видели, могут ссылаться на команду метки различными способами, но независимо от того, как она называется, метка отмечает некоторый набор главных источников. Метка позволяет отыскать и извлечь отмеченную ею версию главных исходных файлов в будущем. Если при отметке вы сделаете ошибку, то можете навсегда потерять возможность идентифицировать главные исходные файлы, используемые индивидуальной версией, а следовательно, обнаружить, почему специфическая версия завершается аварийно.

Решая вопрос о том, что нужно помечать, я всегда следовал следующим трем правилам:

1. Помечайте все внутренние опорные точки.

2. Помечайте любую конфигурацию продукта, которая посылается кому-то вне команды.

3. Помечайте любые ежедневные версии проекта Во всех случаях я использую следующую схему:

<имя-проекта>  <Отметка/Причина>  <Дата>

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

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

Общий вопрос отладки

Что делать, если возникают трудности с воспроизведением конфигураций для программистов, не входящих в команду?

Компилируя проект для кого-то, кто не входит в команду, необходимо сделать полную копию каталога сборки проекта на компакт-диск или ленту. Эта копия должна включать все исходные файлы, промежуточные файлы, файлы символов и окончательный вывод (final output). Кроме того, в нее также следует включить комплект установки, который был послан заказчику.

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



Выбор подходящих систем


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

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

При выборе системы отслеживания ошибок применяются те же критерии, что и при выборе системы управления версией. Однажды, будучи менеджером продукта, я остановился на системе прослеживания ошибок, не уделив достаточного внимания просмотру самой важной части — сообщениям об ошибках. Продукт было достаточно легко установить и использовать, но его возможности в составлении отчетов были так ограничены, что пришлось передать все существующие ошибки другому продукту сразу после того, как мы натолкнулись на первую внешнюю метку кода. Как уже упоминалось в этой главе, следует рассматривать те системы отслеживания ошибок, которые обеспечивают интеграцию с системами управления версией. На рынке Windows большинство систем управления версией поддерживает интерфейс фирмы Microsoft — Source-Code Control Interface (SCCI). Если система прослеживания ошибок также поддерживает SCCI, то можно координировать исправления ошибок со специфическими версиями файла.

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