Данному образовательному сайту пришлось несколько раз менять свое имя. С 2022 года доступ к нему обеспечивается по URL
emc.km.ru (2001-2007) ==> educomp.org.ru (2007-2011) ==> educomp.runnet.ru (2011-2021) ==> emc.orgfree.com (2022-...)
Более подробно об истории сайта можно прочитать здесь.
|
Об образовательных возможностях Debug(переход к другим статьям из этой серии) 1. Как работает DebugВ одном из номеров нашей газеты была опубликована статья О.Ю. Заславской [1] по простейшим приемам работы с отладчиком Debug, некогда разработанным еще для операционной системы MS-DOS, но по-прежнему входящим в состав ОС MS Windows. Статья была посвящена экспериментам с регистрами центрального процессора; в ней было продемонстрировано, как с помощью весьма несложных манипуляций можно понаблюдать за выполнением простейшей программы внутри современного компьютера IBM PC. Хочется подчеркнуть, что возможности старой доброй программы Debug для образовательной демонстрации фундаментальных принципов работы компьютера значительно шире. И хотя существуют отладчики гораздо мощнее и с более современным интерфейсом, но простота использования и присутствие Debug в любой версии Windows позволяет считать его полезным в тех случаях, когда речь идет не о профессиональной отладке больших программ, а об изучении простейших закономерностей функционирования компьютера. В свете сформулированной позиции, редакция планирует опубликовать серию небольших статей, посвященных демонстрации тех или иных фундаментальных основ информатики и вычислительной техники с помощью данного отладчика. Предполагается, что материалы могут быть использованы как «пытливыми учениками», которые интересуются указанной проблематикой, так и преподавателями различных учебных заведений. По мнению автора, некоторые наиболее простые эксперименты могут быть продемонстрированы даже на уроках базового курса информатики. Сегодня мы публикуем первую статью этого цикла. Согласно традициям классического образования, начинать следует с теории. Как говаривал мудрый литературный персонаж Козьма Прутков, «бросая в воду камешки, смотри на круги, ими образуемые; иначе такое бросание будет пустою забавою». Поэтому первый эпизод знакомства с отладчиком будет посвящен ему самому. Если вы не верите, что «такое старье» заслуживает внимания, то вот вам «вопрос на засыпку»: как, по-вашему, должна работать программа Debug, отображая в столь подробном виде (см.[1]) содержимое многочисленных регистров микропроцессора, чтобы при этом не испортить значение ни в одном из них? Или еще: как отладчик умудряется останавливаться после каждой команды программы при ее пошаговом исполнении? 1.1. Немного историиНеобходимость в отладочных программах существует с тех пор, как люди начали программировать. Не случайно поэтому уже в первой операционной системе для 8-разрядных микрокомпьютеров CP/M (автор Г. Килдэл, 1975 год) существовало специальное отладочное средство, которое называлось DDT (Dynamic Debugging Tool). В одной из классических книг [2] по этой операционной системе роль отладчика охарактеризована весьма удачно. «Программа "DDT" позволяет пользователю загружать в оперативную память, просматривать, тестировать и изменять и отлаживать любую программу на языке ассемблера, представленную в машинном коде. ... Программа "DDT" работает в динамическом режиме, что позволяет пользователю запускать находящуюся в памяти отлаживаемую программу и отслеживать каждый шаг ее выполнения. С ее помощью можно также вносить небольшие изменения в существующие программы (такие, как текстовой процессор "WordStar") или реассемблировать их с тем, чтобы понять, как они работают. … Программа "DDT" представляет собой важнейшее инструментальное средство для программистов, использующих язык ассемблера. Для эффективного использования этой программы необходимо понимание принципов программирования на языке ассемблера.» Кроме DDT существовал аналогичный отладчик SID (Symbolic Instruction Debugger) и его расширенная версия для процессора Z80 [1] под названием ZSID. С переходом к 16-разрядным процессорам (начиная с Intel 8086 в 1978 году) возникла необходимость в новой операционной системе. Исторически сложилось так, что ею стала не усовершенствованная CP/M, а совсем иная ОС, которую разработала фирма Microsoft, возглавляемая Биллом Гейтсом. И хотя в настоящее время MS-DOS практически вытеснена более новыми системами с графическим интерфейсом, ее поддержка по-прежнему сохраняется. Необходимо подчеркнуть, что при написании MS-DOS программисты стремились достичь максимально возможной совместимости с предшествующей ей CP/M [2]. Нас сейчас данное свойство интересует в связи с тем, что в MS-DOS был написан функциональный аналог DDT – отладчик Debug. Таким образом, познакомившись с программой Debug, мы тем самым увидим, как выглядела работа с компьютером 2-3 десятилетия назад, когда никаких окон не было и в помине, а все действия инициировались не мышью, а набором текста в командной строке. Возможно, это не прибавит полезных навыков эксплуатации компьютера, но, согласитесь, весьма любопытно лишний раз почувствовать, как далеко ушел прогресс в интерфейсе общения человека и машины. 1.2. Особенности программы DebugDebug представляет собой исполняемый файл, который запускается операционной системой обычным образом. Тем не менее, будучи запущенным, он имеет некоторые особенности, которых нет у обычной прикладной программы. Обсудим их, воспользовавшись схемами, которые представлены на рис. 1. Рис. 1. Особенности функционирования программы Debug На рис. 1a показана ситуация с обычной прикладной программой. Пользователь дает операционной системе команду запустить программу [3] и ОС запускает ее. Важно подчеркнуть, что система сохраняет контроль над прохождением приложения (иначе, в частности, вам не удалось бы закрыть его окно!) При своем функционировании программа активно опирается на сервисы операционной системы: обменивается через ее посредство с внешними устройствами, запрашивает необходимую дополнительную оперативную память и т.д. Когда приложение завершает работу, оно сообщает об этом операционной системе и последняя выгружает его из памяти, освобождая все использовавшиеся ресурсы компьютера. Для программы Debug ситуация несколько сложнее. Дело в том, что отладчик по своему предназначению должен работать с программой пользователя, которая либо набирается с клавиатуры, либо читается из файла. В любом случае теперь мы имеем дело уже не с двумя, а с тремя программами (рис. 1b), причем Debug должен полностью контролировать отлаживаемую им программу. Контроль этот весьма разносторонний; здесь приведем только простой пример. Пусть Debug запустил программу пользователя, та нормально проработала и, завершаясь, собирается стандартным образом сообщить об этом операционной системе. Debug должен зафиксировать эту ситуацию, завершить работу отлаживаемой программы, перехватив управление и не давая операционной системе завершить сеанс работы отладчика. Аналогичные действия он должен проделать и в той ситуации, если в процессе работы программы пользователя возникла ошибка, иначе после любой самой пустяковой ошибки вам придется заново перезапускать Debug (особенно печальные последствия это бы имело в случаях, когда исправления в отлаживаемой программе не сохранялись на диск). Таким образом, главная особенность программы Debug по сравнению с остальными приложениями состоит в том, что отладчик полностью [4] контролирует работу другой отлаживаемой) программы. 1.3. Что умеет DebugКонкретные функции программы-отладчика подробно перечислены в уже упоминавшейся ранее статье [1], а также в п.1.1 при описании отладчика DDT (напомним, что функции DDT и Debug практически одинаковы). Для дальнейшего обсуждения выделим наиболее важные группы действий отладчика:
Рассмотрим подробнее, как реализует Debug некоторые из перечисленных действий. 1.3.1. Вывод и изменение содержимого памятиКак следует из п.1.2, Debug обязан выделить определенный объем памяти под отлаживаемую программу. Здесь, казалось бы, нет проблем, поскольку любая программа вправе запросить у операционной системы необходимую память, и она ее обязательно получит. Трудность однако состоит в том, что данная память по смыслу имеет особый статус – это память прикладной программы. В связи с этим немедленно возникает целый ряд вопросов. Должен ли отладчик отображать и особенно изменять только эту память прикладной программы (скажем, разрешить ли пользователю работать с областями памяти, в которых находится сам отладчик или даже операционная система)? Как поступить, если работающая программа пользователя «злонамеренно» или по ошибке захочет изменить «чужой» участок памяти (допустим, «подправить» саму программу Debug)? Рис.2. К пояснению принципов работы Debug Ответ на все эти вопросы такой. Тот, кто пытается работать с программой на языке процессора предполагается грамотным, поэтому Debug не содержит «защиты от дурака» и не накладывает особых ограничений на выбор адресов памяти. Стоит, правда, добавить, что в многозадачной среде Windows, в отличие от MS-DOS, для которой изначально предназначался Debug, контроль памяти более жесткий. Так что чрезмерно грубые попытки вторжения в «чужую» область памяти будут пресечены на уровне ОС. Из сказанного выше следует важное правило. Работа с программой под отладчиком Debug в случае грубых ошибок может приводить к непредсказуемым ситуациям, поэтому вы должны быть очень внимательны на всех этапах – от написания программы до ее набора с клавиатуры. В случае большого ввода с клавиатуры дополнительно рекомендуем сохранить программу и данные на диск прежде чем предпринимать дальнейшие манипуляции с отладчиком. К счастью, для наших несложных экспериментов последняя мера вряд ли потребуется. 1.3.2. Вывод и изменение содержимого регистров микропроцессораНесмотря на отдельные трудности, распределение памяти между программой Debug и отлаживаемой программой все-таки можно произвести. С регистрами микропроцессора ситуация принципиально иная – их крайне мало и они постоянно необходимы работающей программе. Так что не существует иной возможности сохранять значения регистров после остановки пользовательской программы кроме как копировать эти значения в ОЗУ (см. рис. 2). Отсюда первое, что делает отладчик при остановке работы программы пользователя, это немедленно копирует содержимое всех регистров микропроцессора в ОЗУ. Когда потребуется продолжить выполнение отлаживаемой программы, Debug заботливо восстановит предусмотрительно сохраненные значения и только потом запустит программу. Заметим, что Debug фактически создает в памяти своеобразную виртуальную машину, которая для исполнения программы использует центральный процессор. 1.3.3. Запуск отлаживаемой программыДанная операция очень проста – достаточно выполнить переход на требуемый адрес и немедленно начнется выполнение нужной части программы (не забывайте о мерах, изложенных в п.1.3.2). 1.3.4. Возврат управления отладчикуНасколько проста предыдущая операция, настолько сложна данная! В самом деле, программа-отладчик запустила абсолютно неизвестную программу и та начала работать. Как заставить ее автоматически вернуть управление отладчику? При запуске отлаживаемой программы существует три принципиально разных ситуации:
Интересно, что для каждой из них возврат из исполняемой пользовательской программы в отладчик происходит своим собственным путем. Рассмотрим эти пути подробнее. Если пользователь уверен в том, что в его программе нет серьезных ошибок, грозящих непредсказуемыми последствиями, он может запустить программу на полное выполнение. Согласно принятым в MS-DOS соглашениям,программы завершаются специальной инструкцией INT 20h, которую отладчик и перехватывает. Таким способом после завершения программы отладчик снова получает управление и сеанс отладки продолжается. Примечание. В некоторых источниках рекомендуется завершать программу командой RET (return – возврат). По мнению автора, это менее надежный вариант. Второй сценарий отладки заключается в том, что программа запускается на исполнение, но, в отличие от первого случая, в ней предварительно устанавливаются так называемые контрольные точки (или точки останова – по-английски breakpoint). Если выполнение доходит до одной из них, то программа прерывается, и управление вновь переходит к отладчику. Описанным способом происходит выполнение определенной части программы (несколько точек приостановки требуется при ветвлении программы). При реализации контрольных точек перед запуском отладчик запоминает данные из байтов, находящихся по их адресам, а затем заносит туда код специальной инструкции INT 3, которая обеспечивает выход в Debug. После получения управления отладчик удаляет из программы все инструкции прерывания, восстанавливая первоначальное содержимое контрольных байтов. Наконец, третий, пошаговый режим запуска реализуется уже не программным, а аппаратным путем. В регистре флагов (см. [1]) имеется для этого специальный управляющий бит TF. «TF (Trap Flag) – флаг трассировки (пошагового режима). При его установке после выполнения каждой команды вызывается внутреннее прерывание типа 1 (INT 1)» [3]. В классических книгах по ассемблеру прямо указывается, что «программа DOS DEBUG устанавливает данный флаг так, что возможно пошаговое выполнение каждой команды для проверки изменения содержимого регистров и памяти» [4]. 1.4. ЭкспериментыА сейчас для более детального знакомство с методами работы отладчика проведем следующие эксперименты. 1.4.1. Как обеспечивается возврат в DebugПонаблюдаем, как работает механизм возврата из точки останова. Из описания п.1.3.4 следует, что в момент выполнения программы в эту точку записывается особая команда INT 3. Казалось бы, ее невозможно увидеть, поскольку вывести на экран содержимое памяти можно только после остановки программы, а тогда Debug уже успеет восстановить первоначальное значение в контрольных точках. Но все же способ «проверить теорию» есть, и он несложен: надо в процессе работы программы скопировать содержимое контрольной точки в любую свободную ячейку памяти. После завершения программы изучить «копию» не составит труда. Перейдем непосредственно к эксперименту. Стандартным способом запустим Debug (см. [1]) и проведем с отладчиком диалог, приведенный в нижеследующем протоколе.
Здесь жирным шрифтом показаны символы, которые нам пришлось набирать; все остальные выведены на экран отладчиком. Причем текст, выделенный при редактировании курсивом, может отличаться от того, который получился на компьютере автора, а многоточие заменяет выводимые на экран строки, содержимое которых для нашей задачи абсолютно несущественно. Итак, по команде a (адрес отсутствует; при старте отладчика по традициям, восходящим к CP/M, он устанавливается равным 100h) Debug переходит в режим приема программы в мнемониках ассемблера. Набираемая нами программа копирует содержимое точки останова из байта с номером 106 в свободный байт 109 через регистр микропроцессора al [5]. В «подопытном» байте памяти 106 поместим однобайтовую инструкцию nop (нет операции), которая отличается тем, что ничего не делает. Наконец, завершается программа командой INT 20, которая, как мы уже знаем, организует по завершении программы выход в операционную систему. Заметим, что для прекращения набора нажатием <Enter> вводится пустая строка (см. строку с адресом 109). Далее по команде u (аргумент снова пропускается) для контроля выводится результат набора. Обязательно обратите внимание на то, каково будет в вашем случае содержимое ячейки 109, поскольку именно туда позднее попадет копия точки останова. Запустим программу командой g. Появившийся на экране текст свидетельствует о том, что программа проработала и завершилась нормально. Проверим результат, набрав u100: байт с номером 109 приобрел значение 90 – не что иное, как код команды nop. Мы не увидели ничего неожиданного – произошло обычное копирование байта. Самое интересное начинается дальше. Введем команду g=100 106, которая расшифровывается так: запустить программу с адреса 100, установив в качестве контрольной точки адрес 106. Программа выполнится, и остановка произойдет в контрольной точке 106: это видно из последней выведенной строки, где всегда отображается команда, которая будет исполняться следующей. Но зато содержимое адреса 109 теперь уже не 90, а CC, что является именно кодом команды INT 3. Предлагаем читателям самостоятельно найти в распечатке содержимого всех регистров значение al, через который копировалось содержимое контрольного байта. Проверьте также, что ситуация не изменится при выборе другой точки останова (при условии, что она устанавливается на первый байт команды). 1.4.2. Как зафиксировать пошаговое исполнениеИз рассмотренной в п.1.3.4 теории следует, что пошаговое исполнение отлаживаемой программы реализуется процессором аппаратно после установки в единицу флага трассировки TF. Казалось бы, достаточно проанализировать значение флага, и наличие или отсутствие пошагового исполнения будет легко распознано. К сожалению, дело обстоит не так просто, поэтому рассмотрим проблему подробнее. Флаг TF является 8-м битом (самый младший имеет номер 0) в регистре флагов [3,4]. Это означает, что он попадает в старший байт, причем там он оказывается самым младшим битом. Единственной подходящей для получения флагов, которые находятся в старшем байте, является команда pushf, которая сохраняет весь 16-битный флаговый регистр в стек. Примечание. Стек – это особая форма организации памяти. Она имеет в работе компьютера большое значение и заслуживает самостоятельного обсуждения, поэтому мы посвятим ей отдельный выпуск. Сейчас достаточно знать, что по команде push данные заносятся в стек, а по инструкции pop – извлекается оттуда. Важно подчеркнуть, что push и pop вполне могут использовать различные регистры, т.е. можно записать данные из одного регистра, а считать в другой. Итак, регистр флагов находится в стеке. Прочитаем его содержимое с помощью команды pop ax. Удобно, хотя и не обязательно, сбросить все биты старшей «половинки» регистра командой and ah,1 [6]. Остается просто сохранить ah в память аналогично тому, как мы делали это в предыдущем эксперименте. Предложенная простая и понятная схема анализа для флага TF, увы, оказалась неработоспособной: она всегда давала нулевое значение данного флага. Процессор почему-то тщательно «скрывал» от программы истинное значение TF. Честно говоря, автор уже хотел было отступится от дальнейших экспериментов, мысленно поругивая внутренние аппаратные особенности отладки у процессоров Intel. Но не давало покоя одно обстоятельство: задача определения факта работы под отладчиком является любимой хакерской проблемой! Не может быть, что не найдено ее простого решения. И вот после продолжительных и старательных поисков в Интернете удалось узнать, что всем процессорам Intel помимо стремления скрывать состояние бита TF свойственна еще одна странная особенность: этот бит, оказывается, некорректно обрабатывается при изменении некоторых регистров, в частности, регистра ss. В результате этой странности отладчик «проскакивает» ту команду, которая следует сразу после инструкции, меняющей значение в ss, и этот фокус позволяет узнать столь интересующее нас состояние флага. Дальше все уже просто. Ставим перед pushf две «компенсирующие» друг друга инструкции push ss и pop ss, назначение которых имеет целью исключительно «сбить с толку» могучий интеллект процессора и – о чудо! – все начинает великолепно работать!!! Теперь, когда проблему удалось решить, можно пригласить к проведению эксперимента читателей. Протокол диалога с Debug приводится ниже; при его редактировании приняты те же соглашения, что и в п.1.4.1.
Запускаем отладчик, набираем программу и проверяем ее так же, как в предыдущем эксперименте. Далее производим трассировку первых 6 инструкций программы. Поскольку согласно «хакерским теориям» одну из команд отладчик не заметит, набираем t 5, т.е. на одну инструкцию меньше. В ответ Debug выдает данные о состоянии процессора после каждой трассируемой инструкции. Особый интерес для нас представляет последняя строка, отображающая команду, которая будет выполняться следующей. Внимательно присмотревшись к выдаче, можно заметить, что Debug действительно «не заметил» команду pushf – ее нет в распечатке; и хотя на экране только 5 блоков информации, судя по адресам, выполнено 6 инструкций. А каков результат? Просмотр содержимого памяти по директиве u 100 показывает, что он наконец-то равен единице (в протоколе для удобства нахождения значение подчеркнуто), т.е. нашей программе удалось зафиксировать установку отладочного флага TF. Остается проверить случай, когда пошаговый режим отсутствует. Командой g =100 выполним программу с начала целиком. Видно, что теперь в память был сохранен нулевой байт, т.е., как и следует из теоретических представлений, TF = 0. Читателям предлагается самостоятельно проверить роль регистра ss в успешности программы. Для этого в первых двух инструкциях замените ss на другой регистр, например, ax, и повторите эксперимент. Таким образом, проведенные эксперименты показали, что механизмы взаимодействия Debug c отлаживаемой программой в среде Windows остаются такими же, как описано в классических книгах. Литература
1 - ОС CP/M написана для процессора Intel 8080; Z80 был более совершенным процессором, причем система его команд включала все инструкции процессора Intel 8080 в качестве подмножества 2 - маленький пример: в MS-DOS кроме штатной команды удаления del имеется абсолютно эквивалентная ей команда era – именно такое имя использовалось в CP/M 3 - обычно запуск инициируется двойным щелчком мыши, но для совместимости с MS-DOS можно использовать командную строку 4 - полностью, конечно, только в идеале; реально вполне возможна ситуация, когда вследствие грубых ошибок в неотлаженной программе нормальное функционирование сеанса отладки нарушится 5 - каким бы мощным не казался современный процессор Intel, он не в состоянии переписать данные из одного байта памяти в другой за одну операцию! 6 при выполнении логического И с константой, те биты, которые в ней сброшены в 0, стираются; вспомните таблицу истинности и вы убедитесь, что это так! © Е.А.Еремин, 2006-2007 Публикация: Еремин Е.А. Еще раз о программе Debug. "Информатика", 2006, N 23, с.37-40; 2007, N 1, с.39-42. |