#8. А теперь мы выполним нашу программу пошагово - произведем так называемую "трассировку"
при помощи команды "T".
Итак, вводим "T" и жмем на Enter!
Вот что мы видим:
AX=0123 BX=0000 CX=0010 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0103 NV UP EI PL NZ NA PO NC 11B7:0103 052500 ADD AX,0025
Смотрим на значение AX и вспоминаем предыдущую инструкцию - "внести значение 0123h в AX".
Внесли? И правда! А в самом низу - код и мнемоника команды, которая будет выполняться следующей...
Вводим команду "T" снова:.
AX=0148 BX=0000 CX=0010 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0106 NV UP EI PL NZ NA PE NC 11B7:0106 8BD8 MOV BX,AX
AX=0148 - "прибавить значение 0025h к AX". Сделали? Сделали!!
Вводим команду "T" снова:.
AX=0148 BX=0148 CX=0010 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0108 NV UP EI PL NZ NA PE NC 11B7:0108 03D8 ADD BX,AX
AX=0148=BX - "переслать содержимое AX в BX". Сделали? Сделали!!
Вводим команду "T" снова:
AX=0148 BX=0290 CX=0010 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=010A NV UP EI PL NZ AC PE NC 11B7:010A 8BCB MOV CX,BX
"Прибавить содержимое AX к BX". Оно? А то!
Вводим команду "T" снова:
AX=0148 BX=0290 CX=0290 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=010C NV UP EI PL NZ AC PE NC 11B7:010C 31C0 XOR AX,AX
"Переслать содержимое BX в CX". Сделано!
Вводим команду "T" снова:
AX=0000 BX=0290 CX=0290 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=010E NV UP EI PL ZR NA PE NC 11B7:010E CD20 INT 20
"Очистка AX"? И точно: AX=0000!
Вводим команду "T" снова... И ГРОМКО РУГАЕМСЯ!!
Потому что, по идее, сейчас наша программа должна была завершиться - у нас же там код выхода
прописан, а она куда лезет? NOPы какие-то (если продолжать команду "T" вводить), CALL 1085 (да вы
продолжайте "трассировку", продолжайте!)
Для тех, кому лень продолжать жать на букву "T", введите для разнообразия команду "G" (от
английского GO). На монитор должна вывалиться надпись "Нормальное завершение работы программы".
- Уф, - должны сказать вы - Работает!
А то!
#9. Только непонятно вот, почему вдруг между int 20 (CD 20) и надписью "Нормальное
завершение работы программы" куча всяких "левых" непонятных команд (в том случае, если вы и дальше
производили тарассировку, а не воспользовались "халявной" командой "G")?
А потому, дорогие наши, что вы имели счастье нарваться на прерывание (interrupt)!
Понимаете ли, завершить программу - дело непростое :). Нужно восстановить первоначальное
значение регистров, восстановить переменные среды и кучу всего другого! Знаете, как это сложно?
Однако эта процедура насколько сложная, настолько и типичная
для исполняемых программ. А по сему разработчики операционной системы
решили избавить программистов от необходимости делать это вручную, и
включили эту стандартную процедуру в ядро операционной системы. И
сказали: "да будешь ты (процедура обработки прерывания) вызываться как
int 20, и будешь ты обеспечивать корректную передачу управления из
выполняемой программы - назад в ядро". И стало так...
Ну, посудите сами, должна же операционная система ну хоть что-нибудь делать!!
1.5. Прерывания
#1. Прерывание - это СИГНАЛ процессору, что одно из устройств в компьютере НУЖДАЕТСЯ в
обслуживании со стороны программного обеспечения. В развитие этой же идеи, программам позволили самим
посылать запросы на обслуживание через механизм прерываний. Получив этот сигнал, процессор временно
переключается на выполнение другой программы ("обработчика прерывания") с последующим ВОЗОБНОВЛЕНИЕМ
выполнения ПРЕРВАННОЙ программы.
Когда же и "кем" генерируются эти "сигналы" (в смысле "прерывания")?
Многочисленными "схемами" компьютера, его устройствами. Например, соответствующее прерывание
генерируется при нажатии клавиши на клавиатуре.
Также прерывания генерируются как "побочный продукт" при некоторых "необычных" ситуациях (например,
при делении на "букву О"), из которых компьютеру хочешь, не хочешь, но приходится как-то выкручиваться...
Наконец, прерывания могут преднамеренно генерироваться программой, для того чтобы произвести то или
иное "низкоуровневое" действие.
Когда процессор получает сигнал прерывания, он останавливает работу приложения и активизирует
"программу обработки прерывания", соответствующую "номеру прерывания" (т.е. разных сигналов прерываний
больше одного - точнее, их 256). После того как обработчик свое отработает, снова продолжает выполняться
основная программа.
Для тех, кто не понял. Представьте себе, что вы сидите за компом и выполняете какую-либо работу.
И вдруг ловите себя на мысли, что вам СРОЧНО НУЖНО сходить в туалет (терпеть вы больше уже не можете).
Вот это СРОЧНО НУЖНО и есть сигнал-прерывание, по которому вы начинаете выполнять определенную
СТАНДАРТНУЮ последовательность инструкций (программу обработки прерывания), как-то: встать, пойти туда-то,
включить свет ... вернуться, сесть за комп и ПРОДОЛЖИТЬ РАБОТУ с того же самого места, на котором вы
остановились перед выполнением программы "поход в туалет". В данном случае наш мозг выполняет роль
процессора, наши внутренние органы сигнализируют мозгу о потребности в обслуживании, а само обслуживание
проводится "программой-навыком", заложенным в процессе нашего развития и (хм!) воспитания.
#2. Программы обработки прерывания располагаются в оперативной памяти (ну а где же еще
им располагаться?!) и, следовательно, имеют свой АДРЕС. Однако генератору прерывания этот адрес знать не
обязательно :). Есть такая замечательная штука (спросите у тех, кто пишет вирусы) - таблица векторов
прерываний. Это таблица соответствия номеров и адресов памяти, по которым находятся программы их обработки.
Почему "спросите у вирмейкеров?". А потому, что поменять адрес "программы обработки прерывания"
на другой - проще пареной репы (мы этим еще займемся), в результате чего при запуске классической
программы "HELLO, WORLD" может получиться еще более классический format c:...
Программы обработки прерывания автоматически сохраняют значения регистра флагов, регистра
кодового сегмента CS и указателя инструкции IP, чтобы по завершении "обработки прерывания", к нашей
безумной радости, снова возвратиться к выполняемой программе (просто программе)... Остальные регистры,
содержимое которых меняется в обработчике, должен сохранять сам обработчик - и если он этого делать не
будет, то нарушится выполнение основной программы. Ведь она даже не знает, что ее "ненадолго" прервали!
Однако на самом деле все намного сложнее :)). Но ведь это только "первое погружение" в прерывания,
верно? А посему - пока что без особых "наворотов"...
#3. Одно прерывание мы с вами уже знаем. Это 20-е прерывание, обеспечившее "выход" из
нашей COM-программы. Сегодня мы пойдем немножко дальше - помимо "выхода" попробуем поработать еще с
одним прерыванием.
Итак, я достаю свой толстый талмуд с описанием прерываний и выбираю, каким бы это прерыванием
вас занять на ближайшие 1/2 часа ;)...
Ну, например, вот одно симпатичное, под названием "прокрутить вверх активную страницу".
Внимательно читаем описание (и наши комментарии):
INT 10h, AH=06h (_1) - прокручивает вверх произвольное окно на дисплее на указанное количество
строк.
ВХОДНЫЕ ПАРАМЕТРЫ: (_2)
AH=06h; (_3)
AL - число строк прокрутки (0…25) (AL=0 означает гашение всего окна); (_4)
BH - атрибут, использованный в пустых строках (00h…FFh); (_5)
CH - строка прокрутки - верхний левый угол; (_6)
CL - столбец прокрутки - верхний левый угол;
DH - строка прокрутки - нижний правый угол;
DL - столбец прокрутки - нижний правый угол;
Далее представим входные параметры в виде таблички: (_7)
AH
06h
AL
Число строк
BH
Атрибут
BL
Не имеет значения
CH
Строка (верх)
CL
Столбец (верх)
DH
Строка (низ)
DL
Столбец (низ)
Плюс подробнейшее толкование, что подразумевается под словом "атрибут" (регистр BH):
ВЫХОДНЫЕ ПАРАМЕТРЫ: отсутствуют (т.е. ни один регистр не меняется). (_8)
Входные строки гасятся в нижней части окна. (_9)
Нормальное значение байта атрибута - 07h. (_10)
Совсем недавно, если бы вам показали подобное "описание", вы бы ничего в нем не поняли и
ужаснулись. Теперь же, после прочтения предыдущих глав курса, в эти "таблицы" вы, более или менее, но
"въехать" должны! Тем более, что сейчас я сделаю комментарии для... хм... "отстающих" учеников
(внимательно смотрим на циферки в скобках):
_1. Черным по белому, в толстом талмуде, описывающем функции прерываний, написано: "Драйвер
видео вызывается по команде INT 10h и выполняет все функции, относящиеся к управлению дисплеем".
И далее. "…ДЕЙСТВИЕ: после входа управление передается одной из 18 программ в соответствии
с кодом функции в регистре AH. При использовании запрещенного кода функции, управление возвращается
вызывающей программе.
НАЗНАЧЕНИЕ: прикладная программа может использовать INT 10h для прямого выполнения функций
видео..."
Вот что из этого следует:
выполнив команду INT 10h, мы ВЫПОЛНЯЕМ одну из "функций видео";
так как функций видео - много, необходимо УКАЗАТЬ, КАКУЮ именно ФУНКЦИЮ из МНОЖЕСТВА мы хотим ВЫПОЛНИТЬ.
Дело в том, что "прерывание номер десять" - это не только
"прокрутка окна", но и, например, "установка режима видео", "установка
типа курсора", "установка палитры" и многое другое. Нас же интересует
именно первое, поэтому из списка возможных значений (он приведен ниже)
мы выбираем именно AH=06h.
Нижеследующая табличка называется "Функции, реализуемые драйвером видео":
AH=00h Установить режим видео AH=01h Установить тип курсора AH=02h Установить позицию курсора AH=03h Прочитать позицию курсора AH=04h Прочитать позицию светового пера AH=05h Выбрать активную страницу видеопамяти AH=06h Прокрутить вверх активную страницу AH=07h Прокрутить вниз активную страницу AH=08h Прочитать атрибут/символ AH=09h Записать символ/атрибут AH=0Ah Записать только символ AH=0Bh Установить палитру AH=0Ch Записать точку AH=0Dh Прочитать точку AH=0Eh Записать TTY AH=0Fh Прочитать текущее состояние видео AH=13h Записать строку
Соответственно, если перед выполнением INT 10 в регистре AH будет значение 06h, то выполнится
именно "прокрутить вверх активную страницу", а не что-то другое из "простыни" функций десятого прерывания...
Теперь читаем описание дальше (смотрим на циферки в скобках):
_2. Входные параметры? Что тут может быть непонятного? Даже запуск ракеты с атомной боеголовкой
требует прежде всего указать координаты цели... Чего уж тут говорить об обыкновенной функции?
_3. То, о чем мы уже говорили - номер функции из "простыни".
_4. Т.е. на СКОЛЬКО строчек прокручивать. Вспомните так называемый "скроллинг" в любой
прикладной программе. На кнопки Up, Down подвешен скроллинг на одну строчку (не путать с координатами
курсора), а вот на PgUp и PgDown - штук на 18 строк (AL=01h и AL=12h соответственно). А вот AL=0, вместо
того чтобы вообще не скроллировать (по идее), поступает наоборот - "скроллирует" все, что может.
_5. Скажем так - какого цвета будет окно и символы в нем после скроллирования.
_6. Как известно из школьного курса геометрии, прямоугольник можно построить по двум точкам.
Это утверждение справедливо и для окна, в котором мы желаем проскроллировать наш текст.
_7. Резюме того, что было написано выше.
_8. К примеру, попала ли наша ракета в цель или нет ;).
_9. Если бы мы использовали функцию 07h, то было бы глубокомысленно написано, что "строки гасятся
в верхней части окна".
_10. Это то самое, которое в DOS по умолчанию. Т.е. белыми буквами на черном фоне. Правда, это
07h лучше все же рассматривать как 00000111b :) но это уже совсем другая проблема...
#4. А теперь мы напишем программу. Ручками, без использования компилятора. Запускаем наш
любимый debug.exe, вводим команду "а" и судорожно стучим по клавиатуре:
-a 119A:0100 xor al,al ;гашение всего окна 119A:0102 mov bh,70 ;белое окно 119A:0104 mov ch,10 ;четыре координаты прямоугольника 119A:0106 mov cl,10 119A:0108 mov dh,20 119A:010A mov dl,20 119A:010C mov ah,06 ;такая-то функция прерывания 119A:010E int 10 ;Go!! 119A:0110 int 20 ;выход... 119A:0112 -r cx ;в CX - сколько байтов программы ;писать 112h-100h=12h CX 0000 :12 -n @int10.com -w Запись: 00012 байтов
Сначала запускаем из-под Norton Commander. Затем запускаем из-под debug. Трассируем. Открываем в
HEX-редакторе. Смотрим на "бессмыслицу" шестнадцатеричных циферек. Медитируем, медитируем и еще раз медитируем...
1.6. Немножко программируем и немножко отлаживаем
#1. Тем, кто не в курсе - НАСТОЯТЕЛЬНО рекомендую проштудировать предыдущие части курса,
иначе "въехать" будет сложно. Тем же, кто внимательно читал предыдущие главы, нижеследующие упражнения
для ума и пальцев покажутся детским лепетом...
Давайте немножко видоизменим программу, которую мы писали в прошлый раз. Сделаем так, чтобы
наше "окошко скроллинга" располагалось более или менее посередине экрана.
:0100 XOR AL,AL ;ПРИМЕЧАНИЕ: с целью экономии пространства и :0102 MOV BH,10 ;времени мы немножко сократили наш DEBUG-й :0104 MOV CH,05 ;листинг, т.е. убрали адрес сегмента и :0106 MOV CL,10 ;машинные коды, соответствующие мнемоническим :0108 MOV DH,10 ;командам :)) :010A MOV DL,3E ;А так, конечно, в оригинале первая строка вот :010C MOV AH,06 ;как должна выглядеть: :010E INT 10 ;11B7:0100 30C0 XOR AL,AL :0110 INT 20
Теперь наша задача - написать программу, которая последовательно выводит пять таких окошек,
причем каждое последующее окно "вложено" в предыдущее, а значение атрибута в шестнадцатеричной нотации на
10 больше предыдущего.
Если мы будем программировать ЭТО линейно (именно так для начала), то очевидно, что все, что
мы должны сделать - это заданное количество раз (5) заставить машину выполнить вышеуказанные операции,
изменяя перед "запуском прерывания" значения регистров AL, BH, CX, DX (полное описание 6-й функции 10-го
прерывания ищите в прошлых главах).
#2. Вот к каким умозаключениям вы должны были придти, пораскинув мозгами. За атрибут
(то бишь цвет) у нас отвечает регистр BH. Он был равен 10h, а нужно на 10h больше... это, значит, 20h будет...
Ладно... CX (он же CH и CL, как известно) - это ТОЖЕ ШЕСТНАДЦАТЕРИЧНЫЕ координаты левого верхнего
угла нашего окошка. Чтобы "окно в окне" получилось, все это нужно на строчку больше сделать и на колонку
больше тоже, и все считать в HEX'e. Получается, что в регистр СH нужно вместо значения 05h внести 06h,
а в регистр CL вместо 10h - 11h.
А еще можно одним махом в CX записать число 0510h той же командой mov.
Ладно... DX (DH и DL соответственно) - это координаты правого нижнего угла прямоугольника.
DH=10h-1h=Fh и DL=3Eh-1h=3Dh.
Ну, а AL=0 и AH=6 - это уже и ежу понятно из описания данной функции (mov AH,6) данного
прерывания (INT 10h).
Все, что осталось - это набить в debug'е после команды "a" эти мнемоники энное количество раз.
(кажется 5). Набиваем!!
:0100 XOR AL,AL ;первый раз :0102 MOV BH,10 :0104 MOV CH,05 :0106 MOV CL,10 :0108 MOV DH,10 :010A MOV DL,3E :010C MOV AH,06 :010E INT 10 :0110 INT 20 :0112 XOR AL,AL ;второй раз :0114 MOV BH,10 :0116 MOV CH,06 :0118 MOV CL,11 :011A MOV DH,0F :011C MOV DL,3D :011E MOV AH,06 :0120 INT 10 :0122 INT 20 ;третий раз :0124 XOR AL,AL : и т.д
Правда, красивые циферки-буковки? Набиваем-набиваем! Если сейчас к вам подойдут недZенствующие
приятели/коллеги и посмотрят, что вы тут колупаете, то ни черта не поймут и покрутят пальцем у виска.
Привыкайте к этому. Только не говорите им, что пытаетесь сейчас получить Матрицу, ПОТОМУ ЧТО это неправда.
А неправда это потому, что сейчас Матрица в очередной раз обманула вас!
#3. ВСЕ ПОТОМУ, ЧТО МОЗГАМИ ДУМАТЬ НАДО, А НЕ ТОЛЬКО СЛЕПО СЛЕДОВАТЬ РУКОВОДСТВУ!
У вас только первое окно прорисуется, сразу же после чего программа натолкнется на INT 20h и
благополучно завершится! А следовательно, и все, что после первого CD 20 написано будет - останется
проигнорированным! Исправляйте! (Т.е. уберите все INT 20 КРОМЕ ПОСЛЕДНЕГО).
Второй момент. ВОЗВРАЩАЕТ ЛИ это прерывание ЧТО-НИБУДЬ В РЕГИСТР AX? Смотрите описание. Ничего?
Ну так какого черта тогда по новой вводить XOR AL,AL и MOV AH,06 и переприсваивать AH значение 6h, если и
без того AH = 6h? Один раз ввести - более чем достаточно!
Скажите, какая мелочь- байтом больше, байтом меньше! А я скажу вот что - на то он и assembler,
чтобы "байтом меньше".
Исправляйте!
#4. - Исправляйте? - возмутитесь вы - Да это же по-новому все вводить нужно!
- По-новому? - возмутимся мы в свою очередь! - Зачем по-новому? Вы что, с ума сошли?
1. Что вам мешает после команды "a" указать адрес, который вы желаете переассемблировать? И
благополучно заменить старую команду на новую!
- А что делать, если не переассемблировать нужно, а вообще удалить?
2. Существует куча способов, что вы в самом-то деле! Например, в HEX Workshop с блоками
шестнадцатеричных цифр запросто можно работать. Да и в других программах это можно делать - например, в
HIEW или даже в Volcov Commander.
Кстати, если процессор встретит команду NOP, то он просто побездельничает некоторое очень
короткое время.
ПРОБУЙТЕ!! В конце концов, ваша прога должна принять такой вот вид:
О, да! Получившаяся у вас программа написана долго и бездарно! Имейте это в виду :)). Мы же
торжественно обещаем, что в последующих главах обязательно ее усовершенствуем. Да, вот еще что -
особенно извращенные могут попытаться заменить INT 20 на JMP 100. Получится, конечно, не ахти, но все же
- "анимация" ;)
#5. А теперь мы попробуем ОТЛАДОЧНЫЙ прием! Все кракеры его знают и пользуются им для
взлома софта. Имейте в виду, пока вы будете использовать его для своих исполняемых программ - вы программер,
исправляющий ошибки, а как только попытаетесь использовать это для отвязки чужой программы от какого-нибудь
серийного номера - ваша деятельность станет считаться неэтичной или незаконной. Так что думайте сами, что
лучше - флаг в руки или барабан вместе с петлей на шею.
Итак, вводим приблизительно такую командную строку - debug имя_проги.com или же
подгружаем прогу в отладчик командой "l" (от слова load) и трассируем, как вы уже неоднократно это делали.
Цель - "на лету" (без изменения кода) заставить первое окошко "рисоваться" не синим (BH=10h), а
красным (BH=40h) цветом.
Мы просто приведем вам последовательность действий, а вывод "зачем это нужно" и прочие
возможные выводы вы уже сами делать будете. Ок?
-t AX=0000 BX=0000 CX=0043 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0102 NV UP EI PL ZR NA PE NC 11B7:0102 B710 MOV BH,10 -
Состояние: обнулился регистр AX (первую команду MOV AL,AL мы не видим). Процессор готовится
выполнить команду MOV BH,10. Дадим ему это сделать!
-t AX=0000 BX=1000 CX=0043 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0104 NV UP EI PL ZR NA PE NC 11B7:0104 B505 MOV CH,05 -
Состояние - в BX уже внесен код синего цвета, который нам по условию необходимо заменить на
красный (т. е. заменить значение регистра BX с 1000h на 4000h).
Вот теперь-то мы и делаем это "на лету":
-r bx BX 1000 :4000 -
А действительно ли сделали? Проверим!
-r AX=0000 BX=4000 CX=0043 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0104 NV UP EI PL ZR NA PE NC 11B7:0104 B505 MOV CH,05 -
Состояние? BH теперь равно 40h! Мы "вклинились" между строчками:
:0102 MOV BH,10 :0104 MOV CH,05
И изменили текущую цепь событий, заставив программу делать ТО, ЧТО НАМ НУЖНО! Поздравляю!
А дальше - вводим команду "g" и даем нашей тупорылой программе исполниться. 1:0 не в пользу
Матрицы!
Медитируйте!
1.7. Стек
#1. В средней школе, где когда-то учился автор, учителем физкультуры был настоящий зверь.
Помимо того, что он заставлял нас школьников бегать-прыгать-подтягиваться, у него еще любимое наказание
было - проходило оно в так называемом "зале тяжелой атлетики".
Что такое тяжелая атлетика вы наверняка знаете. Видели по телевизору, когда выходит на помост
этакий здоровяк, и рвет собственное здоровье, поднимая штангу.
Штанга - это такая "палка", по бокам которой навешиваются так называемые "блины" - круглые плоские
с дыркой посередине диски невероятной тяжести.
Хранятся же эти диски на штырях, которые представляют собой те же "палки", но вторкнутые
вертикально в пол. На них и хранились диски - один на другой положенные.
"Наказание" заключалось вот в чем - тот "педагог" заставлял нерадивого ученика комплектовать
штангу! Это элементарно сделать, если диски просто валяются на полу. Но когда они аккуратно сложены на
штырь - это намного сложнее :(.
Садист! Чтобы достать со штыря диск заданной тяжести (который обычно находился внизу) необходимо
было снять со штыря все "вышележащие" диски, достать самый нижний, а остальные снова надеть на штырь. А
потом точно так же достать диск другой "тяжести" со второго штыря. А он как бы случайно тоже в самом низу.
И так далее - до полной победы идиотизма над здравым рассудком. Не правда ли, изощренная пытка?
К чему это лирическое бредисловие? А к тому, что стек - это тоже своего рода штырь с блинами.
И уж поверьте, упражняться вы с ним будете намного чаще, чем мы делали это на уроках физкультуры. С той
лишь несущественной разницей, что у нас на следующий день болела спина, а у вас на следующий день будут
болеть мозги.
Так вот, о стеке: "штырь" для блинов находится в оперативной памяти (где же еще?). А роль
блинов выполняют хорошо знакомые нам всем регистры, вернее - их "значения".
Правила работы с ним те же - вы можете снять только верхний "блин". Чтобы получить самый нижний
"блин" - вам нужно прежде снять все те, которые НАД ним.
Очевидно, что из десяти "блинов", которые вы надели на "штырь", первым будет сниматься последний
из надетых (верхний), а последним - первый, то есть самый нижний.
Все очень просто: "первый пришел - последним уйдешь" и наоборот "пришел последним - уйдешь
первым".
Это вам не очередь времен социализма... Это очередь "загрузки-разгрузки" стека!
#2. Для работы со стеком вам пока что необходимо знать только две команды: push и pop. Так
как в качестве "блинов" у нас регистры, то, соответственно, необходимо после этих команд указывать и
"имена собственные" помещаемых в стек значений регистров.
Соответственно:
push AX ;ПОМЕЩАЕТ В СТЕК значение регистра AX pop AX ;ИЗВЛЕКАЕТ ИЗ СТЕКА значение регистра AX
Ну а как делать то же самое с остальными регистрами вы, наверняка, уже и сами догадались.
Очень важно помнить, каким "нездоровым" образом в стеке реализована ОЧЕРЕДЬ -поместить/извлечь.
Помните, мы вас предупреждали, что нам нельзя верить на слово? Не верьте! А посему - обязательно убедитесь в
истинности/ложности нашего голословного утверждения при помощи следующей программульки:
С очередностью заполнения стека, наверное, все понятно :). Я много про абстрактные "блины"
загружал. А вот с адреса 114 начинается извлечение из стека. В какой последовательности это делается,
вы можете увидеть сами, произведя трассировку этой небольшой проги.
-r AX=0000 BX=0000 CX=001B DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0100 NV UP EI PL NZ NA PO NC 14DC:0100 B80100 MOV AX,0001 -
Анализируем. Прога еще не начала работать, готовится выполниться команда по адресу 100.
Делаем ШАГ!
-t AX=0001 BX=0000 CX=001B DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0103 NV UP EI PL NZ NA PO NC 14DC:0103 50 PUSH AX -
Анализируем. AX=0001 - значит, команда выполнилась правильно :). Следующая команда, по идее,
должна поместить 1 в стек.
-t AX=0001 BX=0000 CX=001B DX=0000 SP=FFFC BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0104 NV UP EI PL NZ NA PO NC 14DC:0104 B80200 MOV AX,0002 -
И что? Команда выполнилась, но где мы можем увидеть, что в стек
действительно "ушла" единица? Увы, но здесь это не отображается :).
Проверим потом. Ведь логично предположить, что если эти значения
действительно сохранились в стеке, то мы их потом без проблем оттуда
извлечем, т.е. если найдем "там" наши 1, 2, 3, 4, 5 - значит все Ок.
А поэтому - дадим программе работать дальше до адреса 114 (не включительно), не вдаваясь в
подробный анализ. Что тут анализировать? Если значение регистра AX последовательно меняется от 1 до 5 -
значит, команда mov работает. А стек (команда push) проверим потом, как и договорились.
Проехали до адреса 114.
-g 114 AX=0005 BX=0000 CX=001B DX=0000 SP=FFF4 BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0114 NV UP EI PL NZ NA PO NC 14DC:0114 58 POP AX -
А вот теперь снова анализируем :). При следующем шаге выполнится команда, извлекающая некогда
"запомненное" значение AX из стека.
Обратите внимание, регистр IP указывает на адрес (114) выполняемой команды. Мы с вами это уже
проходили, не так ли?
Поехали дальше!!
-t AX=0005 BX=0000 CX=001B DX=0000 SP=FFF6 BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0115 NV UP EI PL NZ NA PO NC 14DC:0115 58 POP AX -
Выполнился первый POP. Готовится выполниться второй. AX=5. Т.е., по сравнению с предыдущим шагом,
вроде ничего не изменилось... Но на самом деле это не так. AX=5 - эта пятерка "загрузилась" из стека :)).
В этом вы легко убедитесь, сделав следующий шаг трассировки.
-t AX=0004 BX=0000 CX=001B DX=0000 SP=FFF8 BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0116 NV UP EI PL NZ NA PO NC 14DC:0116 58 POP AX -
Ууупс... AX=4 :). А команда, вроде, та же :) - POP AX :)
-t AX=0003 BX=0000 CX=001B DX=0000 SP=FFFA BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0117 NV UP EI PL NZ NA PO NC 14DC:0117 58 POP AX -
AX=3 :)
-t AX=0002 BX=0000 CX=001B DX=0000 SP=FFFC BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0118 NV UP EI PL NZ NA PO NC 14DC:0118 58 POP AX -
AX=2 :)
-t AX=0001 BX=0000 CX=001B DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0119 NV UP EI PL NZ NA PO NC 14DC:0119 CD20 INT 20 -
AX=1 :) То есть нашлись-таки наши 1, 2, 3, 4, 5 :). Восстановились из стека. Теперь поверили? А то!
Еще раз обращаю ваше внимание на то, что последовательность записи (четыре PUSH'а)
была - 1, 2, 3, 4, 5, а вот последовательность извлечения (четыре POP'а) - 5, 4, 3, 2, 1. Т.е. "последний
пришел - первый ушел". Зарубите это себе на носу! (Как сделал это на своем перебитом носе наш школьный
учитель физкультуры).
Медитируйте над этой темой до полного просветления! Иначе потом придется туго!
1.8. Цикл
#1. Наша программа для работы со стеком линейна. А линейное программирование - это плохо.
Хотя и не всегда :)
Итак, давайте еще раз посмотрим на нашу программу для работы со стеком. С 100-го до 113-го
адреса у нас имеется пять почти идентичных блоков. Изменяется только значение AX, но на одно и то же
число - на единицу в большую сторону. То есть AX = предыдущее значение + 1. Это очевидно.
Еще более очевидно, что простая команда POP AX (с 114 по 119) повторяется у нас тоже 5 раз.
Мне почему-то сразу вспомнился анекдот о том, как два мента едут в машине, и один спрашивает у
другого: "Глянь, работает ли у нас мигалка на крыше". Тот высунул в голову в форточку и говорит:
"Работает-не работает-работает-не работает-работает-не работает..."
Так вот, не будем уподобляться этим нехорошим людям и сделаем нашу прогу более нормальной.
Добьемся мы этого с помощью так называемого "цикла". Цикл - это... Не буду давать общепринятые
определения; кто хочет - поищите в книжках, благо, их навалом.
Скажу только: "сесть-встать, сесть-встать, сесть-встать" - это не цикл, а вот "сесть-встать и
так три раза" - уже можно считать циклом.
Реализуется же он (цикл), например, при помощи регистра CX и команды LOOP следующим образом.
Число циклов заносится в регистр CX. После этого следует "простыня" из команд, которые вы хотите
"зациклить", т. е. выполнить энное количество раз. Заканчиваться все это должно LOOP'ом с указанием адреса
"строки", с которой необходимо начать цикл (обычно это "строка", следующая сразу же после mov СХ.
Давайте мы сначала "набьем" нелинейный вариант нашей проги, а потом разберемся, что там к
чему. Набиваем:
:0100 ХOR AX,AX ; AX=0 :0102 MOV CX,0005 ; нижеследующий до команды LOOP кусок повторить CX раз :0105 ADD AX,0001 ; AX=AX+1 (у нас же значение AX на 1 увеличивается...) :0108 PUSH AX ; помещаем в стек :0109 LOOP 0105 ; конец цикла; инициируем повторение; CX уменьшается на 1 :010B MOV CX,0005 ; второй цикл повторить тоже 5 раз :010E POP AX ; достаем из стека :010F LOOP 010E ; конец цикла; повторить! ; CX=CX-1 :0111 INT 20 ; выход из нашей "правильной" проги...
Наверное, вы уже поняли, что цикл повторяется до тех пор, пока CX не станет равен 0. Несмотря
на то что CX - он как бы регистр общего назначения, для "зацикливания" используется именно он :). С
остальными такой фокус не проходит. Это и есть так называемая "специализация регистров", о которой мы уже
вскользь упоминали.
Протрассируйте эту программу! Искренне надеюсь, что вы поняли, чем это я вас тут загружал.
Медитируйте!
#2. А теперь вопрос на засыпку ;). Сколько раз выполнится следующий цикл:
Очевидный ответ - 0 раз. В CX же у нас занесен 0. Так вот - ответ неправильный.
Менее очевидный ответ - 1 раз! Ведь перед LOOP'ом сложение один раз все-таки выполнится. Так
вот, этот ответ тоже неправильный.
Самые подозрительные могут сразу же посмотреть на этот цикл под отладчиком, и с удивлением
обнаружат, что LOOP сначала уменьшает значение CX (0-1=FFFF), а потом уже проверяет, не равен ли он нулю.
И с гордостью за задний ум своей головы воскликнут: FFFFh раз!
Так вот: этот ответ близок к истине, но тоже неправильный ;)
Правильный ответ - цикл выполнится 10000h (65536d) раз.
Но только вы и мне не верьте! Истинно только то утверждение, которое вы сами проверили на
практике. Медитируйте!
1.9. Немножко оптимизации
Как мы уже говорили, линейное программирование - это плохо, но не всегда. Сравните размеры
ваших линейной и нелинейной программ. Не знаю, как у вас, но у нас линейная "весит" 27, а нелинейная - 19
байтов. Как по-вашему, какая быстрее работать будет?
- Ну, естественно, нелинейная, потому что она меньше! - скажете вы и будете неправы.
Попытайтесь оттрассировать "зацикленную". Не правда ли, она трассируется намного дольше своего
линейного аналога?
Угу, всё поняли? Сам знаю, что ни черта. :(
Объясняю: в "зацикленной" программе "компутеру" приходится выполнять БОЛЬШЕ команд, нежели в
"незацикленной".
Аргументирую это голословное утверждение следующей таблицей (построенной на основе трассировки):
Что делает линейная
Что делает нелинейная
AX=1
AX=0
Помещаем в стек 1
CX=5
AX=2
AX=AX+1=1
Помещаем в стек 2
Помещаем в стек 1
AX=3
Конец цикла - переход
Помещаем в стек 3
AX=AX+1=2
AX=4
Помещаем в стек 2
Помещаем в стек 4
Конец цикла - переход
AX=5
AX=AX+1=3
Помещаем в стек 5
Помещаем в стек 3
Достаем из стека 5
Конец цикла - переход
Достаем из стека 4
AX=AX+1=4
Достаем из стека 3
Помещаем в стек 4
Достаем из стека 2
AX=AX+1=5
Достаем из стека 1
Помещаем в стек 5
Выход
CX=5
Достаем из стека 5
Конец цикла - переход
Достаем из стека 4
Конец цикла - переход
Достаем из стека 3
Конец цикла - переход
Достаем из стека 1
Выход
Ну и как по-вашему, какую из двух простыней процессор быстрее
обработает? Сказать вам по секрету? А вот ничего я вам не скажу! Сами
думайте! :]
Как сейчас помню, был в моем Турбо-Си в преференсах к
компилятору такой радиобуттон: "оптимайзить" по размеру или по скорости
выполнения. Угадайте, на чем основан принцип этой оптимизации?
Только не вздумайте писать линейные проги! Пишите
"нелинейные"! Нелинейную в линейную "переоптимайзить" - как два пальца
намочить! А вот наоборот - :((
Резюме - бесплатный сыр бывает только в мышеловке, и за все
надо платить. Компактность и скорость - обычно параметры конфликтующие,
поэтому в каждом конкретном случае нужно выбирать, что
предпочтительнее. Исследования, проведенные в свое время еще Кнутом,
показали, что 80% времени затрачивается на выполнение 20% программы.
Соответственно, рекомендуется тратить время на оптимизацию скорости
именно тех 20% программы, а остальные можно оптимизировать по размеру
(в частности, за счет циклов). Так и получается баланс между
компактностью и скоростью программы ;).