03.04.2014 Views

Май - Xakep Online

Май - Xakep Online

Май - Xakep Online

SHOW MORE
SHOW LESS

Create successful ePaper yourself

Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.

coding<br />

обстоятельствах приводящая к потере нуля<br />

в конце ASCIIZ-строки. Но поскольку за ней<br />

следовало двойное слово, заброшенное на<br />

стек командой PUSH reg16, и я отлаживал<br />

программу на процессоре, очищающем<br />

старший разряд, то все работало более или<br />

менее нормально (2 байта мусора, появляющихся<br />

в конце строки, никому не мешали). Но<br />

вот при запуске на другом процессоре, где<br />

завершающего нуля не оказывалось, возникала<br />

критическая ситуация, завершающаяся<br />

исключением.<br />

Или вот: незначительные различия в реализации<br />

«плавающих» команд на различных<br />

процессорах могут привести к странному<br />

поведению программы, которое будет<br />

невозможно воспроизвести на любом другом<br />

процессоре!<br />

Про разгон, дефекты памяти и т.д. я вообще<br />

молчу! Никогда нельзя быть уверенным в<br />

том, что после выполнения «a=6; a=a+3;»<br />

в переменной а окажется именно 9, а не<br />

83737382. И виноват тут может быть не<br />

только процессор, но и «удар по памяти»,<br />

когда совершенно посторонняя функция,<br />

обратившись по неинициализированному<br />

указателю, запишет что-то в чужую область<br />

данных.<br />

Естественно, самотестирование занимает<br />

некоторое время, и для достижения максимальной<br />

производительности я использую<br />

его в том случае, если предыдущий запуск<br />

программы завершился в аварийном режиме.<br />

Кроме того, на всякий непредвиденный<br />

случай присутствует недокументированный<br />

флаг, форсирующий самотестирование, даже<br />

если предыдущий запуск был завершен<br />

нормально.<br />

Функции самотестирования не раз выручали<br />

меня и помогли мне сэкономить колоссальное<br />

количество времени, поскольку ряд ошибок<br />

был связан с особенностями конкретного<br />

оборудования, то есть причина крылась вне<br />

исходного кода программы.<br />

03<br />

секреты<br />

отладочной печати<br />

Отладочная печать — великолепное изобретение,<br />

появившееся еще в те времена, когда<br />

интерактивных отладчиков не существовало<br />

и в помине, а отлаживать было надо.<br />

Большинство программистов использует<br />

тривиальную запись в текстовый log-файл<br />

или API-функцию OutputDebugString. Первый<br />

метод, естественно, лучше, поскольку он<br />

не требует наличия отладчика или специальной<br />

утилиты для перехвата отладочной<br />

печати, которую конечному пользователю<br />

нужно устанавливать на свой компьютер. Мы<br />

же ведь не собираемся исключать отладочную<br />

печать из финальной версии, верно?<br />

Естественно, не собираемся! Достаточно добавить<br />

специальный ключ командой строки,<br />

секретную комбинацию клавиш или вполне<br />

честную опцию в настройках программы.<br />

Лог лучше всего вести в текстовой форме.<br />

Так пользователю будет проще пересылать<br />

его нам по почте и он сможет убедиться, что<br />

там нет ничего такого, чего бы он не хотел<br />

разглашать.<br />

Вот только… при возникновении критической<br />

ошибки система завершает работу<br />

приложения еще до того, как будут сброшены<br />

дисковые буферы. Даже использование функции<br />

fflush ничего не решает (а вот скорость<br />

программы замедляет весьма существенно).<br />

Как же быть?! Да очень просто: создать в<br />

shared-memory кольцевой буфер заданного<br />

размера и весь отладочный вывод направлять<br />

туда, читая его с помощью дочернего<br />

процесса. Тогда, при аварийном завершении<br />

материнского процесса, shared-memory не<br />

будет освобождена системой и дочерний<br />

процесс успеет принять последнее отладочное<br />

сообщение, отправленное упавшей<br />

программой. К тому же этот метод работает<br />

намного быстрее прямой записи на диск.<br />

А почему буфер должен быть именно кольцевым?!<br />

В общем, это не требование, а так,<br />

простое пожелание. Обычно нас интересует<br />

не весь отладочный вывод целиком, а<br />

события, непосредственно предшествующие<br />

падению. Но при интенсивном отладочном<br />

выводе полный размер лога может достигать<br />

десятков мегабайт, большая часть которых<br />

не несет никакой полезной нагрузки, так что<br />

лучше заранее исключить ее, замкнув буфер<br />

в кольцо.<br />

04<br />

автоматический трассировщик<br />

— это просто<br />

В самых ответственных случаях программу,<br />

поставляемую заказчику, имеет смысл<br />

снабдить простейшим автоматическим<br />

трассировщиком, на создание которого уйдет<br />

не больше одного вечера. Просто взводим<br />

флаг трассировки (TF) и отлавливаем<br />

отладочные исключения штатными средствами<br />

операционной системы (через SEH),<br />

записывая: а) адрес машинной команды;<br />

б) содержимое регистров; в) адрес ячейки<br />

памяти, к которой она обращается.<br />

Когда трассировка из прикладного уровня<br />

дойдет до ядра, процессор самостоятельно<br />

опустит флаг трассировки на время<br />

прохождения нулевого кольца и потом<br />

поднимет его при возвращении на прикладной<br />

уровень, так что предусматривать<br />

специальную обработку для исключения<br />

системных вызов из списка трассируемых<br />

функций не надо.<br />

Естественно, трассировка на несколько<br />

порядков (!) замедляет скорость работы<br />

программы и потому должна включаться<br />

специальной комбинацией клавиш, которую<br />

пользователь нажимает в тот момент,<br />

когда программа приближается к месту<br />

сбоя на максимально близкое расстояние.<br />

А для этого пользователю придется<br />

воспроизвести ситуацию, при которой<br />

возникает ошибка. Если же ему это сделать<br />

не удастся, что ж! Включаем трассировщик<br />

при старте программы специальным<br />

ключом командой строки и пишет результат<br />

трассировки в кольцевой буфер, который<br />

внимательно изучаем.<br />

Располагая информацией о ходе<br />

выполнения программы, содержимом<br />

регистров и ячеек памяти, мы сможем<br />

поймать любую ошибку, какой бы заковыристой<br />

она ни была, ведь фактически<br />

мы отлаживаем программу на<br />

клиентской стороне в неинтерактивном<br />

режиме. При желании (если жаба душит)<br />

трассировщик можно реализовать в<br />

виде отдельной динамической библиотеки,<br />

высылаемой клиенту только при<br />

возникновении серьезных проблем. То<br />

же самое, кстати, относится к функциям<br />

самодиагностики.<br />

Понятное дело, программа, защищенная<br />

протекторами, содержащими антиотладочные<br />

приемы, с автоматическим трассировщиком<br />

работать не будет. Так что придется<br />

отказаться или от протекторов, или от<br />

трассировки, либо же надо писать «умный»<br />

трассировщик, обходящий антиотладочные<br />

приемы, но это уже серьезная задача,<br />

решение которой может затянуться не на<br />

одну неделю. z<br />

xàêåð 05 /101/ 07<br />

/ 133

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!