Средства отладки

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

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

Программа st rings полезна и для нахождения текста в других двоичных файлах. Файлы с изображениями часто содержат ASCII-строки, сообщающие, какая программа создала этот файл, а сжатые файлы и архивы (например, zip-файлы) могут содержать имена файлов: strings обнаружит и их.

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

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

 % strings *.ехе *.dll | grep 'mystery message'

Функция st rings читает файл и печатает каждую последовательность из как минимум MINLEN = 6 печатных символов.


Форматная строка %. *s в функции printf берет длину строки из следующего аргумента (i), потому что buf не завершается нулем.

Цикл do-whi|le находит и печатает каждую строку, заканчивая работу при обнаружении EOF. Проверка конца файла после тела цикла позволяет функции getc и циклу по строке иметь одинаковое условие завершения, а также с\ помощью единственного обращения к printf обрабатывать конец строки, конец файла и слишком длинные строки.

Стандартный внешний цикл с проверкой при входе или единственный цикл с getc и более сложным телом заставил бы использовать printf дважды. Эта функция сначала так и работала, но потом мы нашли ошибку в операторе printf. Исправив в одном месте, мы забыли исправить ее в двух других. ("А не делал ли я ту же самую ошибку где-нибудь еще?") Здесь нам стало ясно, что программу нужно переписать, чтобы дублирующегося кода было меньше; так появился цикл do-while.

Основная процедура программы strings вызывает функцию strings для каждого файла- аргумента:


Вы, наверное, удивлены, что strings не читает стандартный ввод, если не было дано ни одного имени файла. Сначала именно так и было. Для того чтобы объяснить, почему теперь это изменилось, требуется рассказать историю об отладке.

Очевидный тест программы st rings — пропустить ее через саму себя. Это сработало отлично под Unix, но под Windows 95 команда

 С:\> strings <strings.exe

выдала ровно пять строк:

IThis program cannot be run in DOS mode.
'. rdata
@.data
.idata
.reloc

Первая строка "!Эта программа не может исполняться под DOS" выглядела как сообщение об ошибке, и мы потеряли некоторое время, пока не I поняли, что это на самом деле строка из файла с программой, так что результат был правилен, по крайней мере до какого-то момента. Не секрет, что некоторые отладочные сессии терпели крушение из-за неверного понимания источника сообщения.

Но в любом случае должны быть еще строки! Где они? Однажды поздно ночью наконец забрезжил свет. ("Я где-то уже видел это!") Это — проблема с переносимостью, описанная подробнее в восьмой главе. Изначально мы написали программу так, чтобы она читала только из стандартного ввода, используя функцию getchar. Под Windows, однако, getchar возвращает EOF, когда она встречает определенный байт (0x1 А или Control-Z) в текстовом режиме ввода,4 и именно это и приводило к преждевременному завершению.

Это абсолютно законное поведение, но совсем не то, что ожидали мы, с нашим опытом работы с Unix. Было решено открывать файл в двоичном режиме, используя "rb". Но stdin уже открыт, а стандартного способа изменить режим его работы не существует. (Можно использовать функции fdopen или setmode, но они не являются частью стандарта.) Таким образом, мы столкнулись с набором неприятных альтернатив: заставить пользователя всегда задавать имя файла, чтобы программа работала под Windows за счет неудобства для пользователей Unix; без пред-] упреждения выдавать неправильный ответ, если пользователь Windows пытается задействовать стандартный ввод; использовать условную компиляцию, чтобы адаптировать поведение к различным системам ценой пониженной переносимости. Мы выбрали первый вариант, чтобы программа везде работала одинаково.

Упражнение 5-2

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


Упражнение 5-3

Напишите программу vis, которая копирует стандартный ввод на стандартный вывод, отображая непечатаемые символы типа "забоя", контрольных символов и не-АЗСП-символов в виде\Хпп, где hh — шест-надцатеричное представление непечатаемого байта. В отличие от st ri ngs программа vis полезна при обработке файлов, содержащих лишь несколько непечатаемых символов.

Упражнение 5-4

Что выдает vis, если во входном потоке попадается строка \ХОА? Можете ли вы устранить двусмысленность результатов работы этой программы?

Упражнение 5-5

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