Библиотека Интернет Индустрии I2R.ru |
|||
|
Введение в машинный кодСодержание
Вы читали "Хроники Амбера" Роджера Желязны? Там есть такой эпизод: 1.1. Система счисления #1. Наверняка среди ваших знакомых есть "крутые" программисты, или
люди, таковыми себя считающие ;). Попробуйте как-нибудь проверить их "на вшивость". Предложите им в уме
перевести число 12 из шестнадцатеричной в двоичную систему счисления. Если над подобным вопросом "крутой
программист" будет думать дольше 10 секунд - значит он вовсе не так крут, как говорит... 35672 = 30000 + 5000 + 600 + 70 + 2 35672 = 3*10000 + 5*1000 + 6*100 + 7*10 + 2*1 35672 = 3*104 + 5*103 + 6*102 + 7*101 + 2*100 (1) #4. Очень наглядно это отображают обыкновенные счеты.
Набранное на них число 35672 будет выглядеть... см. рисунок слева в общем... Чтобы набрать число 35672 мы должны передвинуть влево две "костяшки" на первом "прутике",
7 на втором, 6 на третьем, 5 на четвертом и 3 на пятом. (У нас ведь 1 "костяшка" на втором - это то
же самое, что и 10 "костяшек" - на первом, а одна на третьем равна десяти на втором - и так далее...)
Пронумеруем наши "прутики" снизу вверх - да так, чтобы номером первого был "0"... И снова посмотрим на
наши выражения: 35672 = 3*104 + 5*103 + 6*102 + 7*101 + 2*100 Это (если сверху вниз считать) сколько на каждом "прутике" "костяшек" влево отодвинуто. 35672 = 3*104 + 5*103 + 6*102 + 7*101 + 2*100 Это номер прутика (самый нижний - 0), на котором отодвинуто определенное число костяшек. 35672 = 3*104 + 5*103 + 6*102 + 7*101 + 2*100 Это на каждом прутике - по 10 костяшек нанизано, не все влево отодвинуты, но всего-то их - 10! #5. Пальцев на руках у человека 10, поэтому и считать мы привыкли в
системе счисления с основанием 10, то есть в десятичной. Если вы хорошо представляете себе счеты и
немного поупражнялись в разложении чисел аналогично выражению 1, то перейти на систему счисления с
основанием, отличным от привычной, особого труда для вас не составит. Нужно всего лишь представить
себе счеты, на каждый прут которых нанизано не привычные 10 костяшек, а... скажем, 9 или 8, или 16 или
32 или 2 и... попробовать мысленно считать на них. radix 9 - 0, 1, 2, 3, 4, 5, 6, 7, 8; radix 8 - 0, 1, 2, 3, 4, 5, 6, 7; radix 2 - 0, 1 и т. д. Если же основание системы счисления больше десяти, то есть больше, чем десять привычных нам чисел, то начинают использоваться буквы английского алфавита. Например, для обозначения чисел в системе счисления с основанием 11 "как цифра" будет использоваться буква А: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A В системе счисления с основанием 16 - буквы от A до F: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F И так далее... 1 раз 100 (10 раз по 10) + 2 раза 10 + 3 раза 1 Если же мы используем символы 123 для предоставления, например, шестнадцатеричного числа, то подразумеваем следующее: 1 раз 256 (16 раз по 16) + 2 раза 16 + 3 раза 1 Короче - полный беспредел. Говорим одно, а подразумеваем другое. И последнее - не для красного
словца сказано. А потому, что так оно и есть...
Следуя этой таблице, число 5BC в шестнадцатеричном формате "строится" так: 5 раз 256 (16 раз по 16) + 11 раз 16 (10 - потому что по таблице B как бы равно 11) + 12 раз 1 А теперь, если пораскинуть мозгами, с легкостью переведем 5BC из шестнадцатеричной в десятичную систему счисления: 5*256 + 11*16 + 12 = 1468 Вот и объединили цифры с буквами. Пространство со временем поучимся объединять немного позже -
если не испугаетесь сложностей низкоуровневого программирования. 42936/16 = 2683(8) 8 - младшая цифра 2683/16 = 167(11) B (11d=Bh по таблице) 167/16 = 10(7) 7 10/16 = 0(10) A - старшая цифра -------------------------------------- 42936d=A7B8h А вот и обратный процесс - перевод из HEX в DEC числа A7B8h: 10*16=160 160+7=167 (10 - потому что Ah=10d) 167*16=2672 2672+11=2683 2683*16=42928 42928+8=42936 -------------------------------------------- A7B8h=42936d Преобразования чисел в системы счисления с другим основанием проводятся аналогично... Счеты! Обыкновенные счеты, только с "плавающим" числом "костяшек" на каждом "прутике"... #11. Если чесна, то конкретный "рисунок" цифр - единица там палкой обозначается, двойка - лебледем - это все лишь историческая случайность. Мы запросто можем считать в "троичной" системе счисления с цифрами %, *, _ (где запятая - это знак препинания, а вовсе не число): %, *, _, *%, **, *_, _%, _*, __, *%%, *%*, *%_, **%... Или использовать родные цифры в десятичной системе счисления, но по другому "вектору упорядоченных цифр" - 1324890576: 1, 3, 2, 4, 8, 9, 0, 5, 7, 6, 31, 33,34, 34,38, 39, 30, 35, 37... Правда, этим немножко затрудняется понимание происходящего? А ведь тоже десятичная система!
И рисунок цифр как бы знакомый :-))) Ah=1010b 2h=0010b 3h=0011b Fh=1111b A23Fh = 1010 0010 0011 1111b Каждой шестнадцатеричной цифре соответствует тетрада (4 штуки) ноликов и единичек. Все, что потом
нужно сделать - "состыковать" эти тетрады. Круто? Вас еще не то ждет! 00000123 = 123, но!! 123 <> 12300000 ... но это так... кстати... 1.2. Регистры #1. Наверняка вы имеете представление о том, что такое переменная. Наиболее продвинутые даже знают, что переменная имеет тип. Кажется вполне естественным, что любой высокоуровневый язык программирования позволяет создавать любое количество переменных того или иного типа... Так вот, господа - при программировании на ассемблере вас ждет большая неожиданность. Потому что для всех ваших навороченных вычислений разрешается использовать только несколько переменных с фиксированными "именами собственными" и имеющих фиксированную "длину". Эти "предопределенные переменные" называются регистрами и каждая из них имеет свою специализацию. О специализации нам пока что говорить рано, описание наподобие "регистр-указатель базы кадра стека" вам вряд ли о чем-то скажет. Поэтому для начала познакомимся только с так называемыми регистрами общего назначения (РОН), и то не со всеми, а только с четырьмя основными, которые являются своего рода "рабочими лошадками" микропроцессора. Вот их "имена собственные" - AX, CX, DX, BX (именно в такой последовательности они "упорядочены" в Intel'овских микропроцессорах). А сейчас мы поближе посмотрим на эти "рабочие лошадки" микропроцессора. 1. Запустите программу DEBUG.EXE1. 2. Когда появится приглашение в виде "минусика", введите букву "R" (можно и "r" - регистр символов значения не имеет) и нажмите на "Enter". Не правда ли, весьма похоже на то, что показывают в художественных фильмах про хакеров? AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=18B2 ES=18B2 SS=18B2 CS=18B2 IP=0100 NV UP EI PL NZ NA PO NC 18B2:0100 6A DB 6A - Ну и что это такое? - скептически спросите вы. #2. То, что у вас должно появиться - это список доступных регистров и текущее значение каждого из них. Как видите, значения регистров AX, BX, CX, DX равны 0. Не правда ли, создается впечатление, что они просто-напросто ждут того, чтобы в них внесли какое-либо значение? Природа не терпит пустоты. Писателей приводит в ужас чистый лист бумаги... Весьма скоро и вы при виде "пустых" регистров будете испытывать непреодолимое наркотическое желание чем-нибудь их заполнить... Однако прежде чем мы сделаем это в первый раз, давайте уточним тип этих "переменных". А он очень простой, этот тип - шестнадцатеричное число в диапазоне 0...FFFF. Или, если в BIN, то - от 0 до 1111 1111 1111 1111. Маловато будет? А вот создатели первых "IBM-совместимых" :) компьютеров посчитали, что и этого много2! 16-битная переменная еще и на две части дробится - для совместимости с языками ас-семблера для предыдущих моделей процессора Intel, работавших только с 8-битными регистрами; да и просто - ради удобства... В общем, в умных книжках рисуют вот такую вот "нездоровую" схемку3:
А означает она следующее. Физически существует один регистр - AX, а вот логически он делится на два - на старшую (AH) и младшую (AL) части (от английского - high и low). Очевидно, что присвоить AX значение, например, 72F9h, мы можем следующими способами: 1. AX = 72F9h (одной командой); 2. AH = 72h; AL = F9h (двумя командами). Точно так же присвоить значение 78h регистру AH можно двумя способами: 1. AH = 78h; 2. AX = 7800h. То же самое, но для регистра AL: 1. AL = 78h; 2. AX = 0078h . Тех, кого смущают числа с буквами, мы со зловредной ухмылкой отсылаем к 1.1. Система счисления :-] #3. Если рассматривать регистр "целиком", то каждый из них имеет "длину" 16 бит, которые приня-то нумеровать справа налево4 . Так для числа 2F4Dh, внесенного, например, в регистр AX, мы можем нарисовать такую вот "навороченную" табличку:
Внимательно смотрим на таблицу: одной шестнадцатеричной цифре соответствует тетрада двоичных цифр (4 шт., они же - 4 бита). "Емкость" регистров AH и AL - две тетрады, т. е. 8 бит. Точно такую "длину" имеют: коды символов, скан-коды клавиш, номера функций прерываний и куча всего прочего, чего вы пока еще не знаете. Емкость AX (состоящего из двух половинок) - 4 тетрады, т. е. 16 бит; они же (эти 16 бит) иначе еще называются "словом"... #4. "Принудительно" присвоить регистру значение можно при помощи той же команды "R", только с параметром "имя собственное регистра". Например, команда - R AX [Enter] выбросит вам на монитор : Введите после двоеточия, например, число 123 и снова нажмите на Enter: :123 [Enter] На дисплее опять появится приглашение "-", на которое мы отвечаем командой "R" без параметров и таким образом вновь просматриваем значения наших регистров: AX=0123 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=18B2 ES=18B2 SS=18B2 CS=18B2 IP=0100 NV UP EI PL NZ NA PO NC 18B2:0100 6A DB 6A Смотрим внимательно - AX=0123, что и требовалось доказать... Примечания 1). В W9X она находится в папке WINDOWS\COMMAND\. В Y2K и XP - WINDOWS\SYSTEM32\. В обоих случаях достаточно набрать в командной строке "debug", чтобы она запустилась. 2). Не забудьте, что при тогдашней технологической базе и это было большим прорывом. А экстен-сивное расширение, например, разрядности, во-первых, нужно правильно предвидеть (вспомните, сколько в том же MS-DOS закладок на будущее, которые никуда не пошли за ненадобностью), а во-вторых, правильно оценить (в буквальном смысле). Неужели вы думаете, что, например, производи-тели памяти не могут легким мановением руки увеличить ширину шины, соединяющую память с процессором? Могут, но во-первых - это резко повысит стоимость памяти, а во-вторых - не гаран-тирует повышения производительности. 3). Впоследствии мы немного усложним эту схемку - регистры современных процессоров 32-разрядные и называются немного иначе ;) 4). "Первый справа" бит мы будем называть "нулевым". Однако нам попадались руководства, в которых это же бит обозван как "первый". Можно долго обсуждать тонкости русского языка (которые, к сожалению, не всегда понимает переводчик), однако это выходит за рамки данной книги. Просто имейте это ввиду, что можете с этим столкнуться, и будьте бдительнее, читая документацию. 1.3. Память #1. Первым видом памяти, с которым мы войдем (придется!) в тесный физический контакт, будет оперативная, она же - RAM (от английского - Random Access Memory). Оперативная память - это своего рода "рабочая площадка", по которой суетится этакий шустрый многорукий дядька-процессор - чего-то там собирает, от кучи к куче бегает, всех ругает... :) Оперативная память - это ряд пронумерованых ячеек размером в байт. Мы можем получить доступ к первому байту памяти, ко второму, к третьему и т.д. Короче - пришло время испробовать еще одну команду из скромного арсенала DEBUG'a! Запустите debug и введите команду D (от английского - DUMP). "Картинка", которую вы увидели, называется "дамп памяти" (что в переводе с английского означает "свалка") и она насыщена не только важной информацией, но и специальной низкоуровневой энергетикой. Да чего уж там греха таить - каждый ассемблерщик знает, что рассматривание дампа памяти поднимает настроение, жизненный тонус и другие, не менее важные вещи ;) 18B2:0100 6A 00 68 4B 01 66 83 7E-E0 00 74 05 B8 4C 01 EB j.hK.f.~..t..L.. 18B2:0110 03 B8 4A 01 2B D2 52 50-57 FF 36 C4 34 00 A1 18 ..J.+.RPW.6.4... 18B2:0120 F7 7F 83 C4 12 56 9A 16-44 F7 7F FF 76 FE 9A 59 .....V..D...v..Y 18B2:0130 04 8F 17 B8 FE FF 1F 5E-5F C9 CA 06 00 90 C8 54 .......^_......T 18B2:0140 04 00 57 56 8B 76 04 33-C0 89 46 D6 B9 0B 00 8D ..WV.v.3..F..... 18B2:0150 7E D8 16 07 F3 AB 89 46-BC B9 0C 00 8D 7E BE F3 ~......F.....~.. 18B2:0160 AB 9A 21 9C 8F 17 89 46-FA A1 08 01 8E 46 06 26 ..!....F.....F.& 18B2:0170 39 44 02 0F 84 55 01 C7-46 BC 1A 00 C4 5E 0C 26 9D...U..F....^.& Слева - это адрес памяти. В центре - 16 столбцов из спареных цифр... А здесь и повториться лишний раз не грех. Каждая пара шестнадцатеричных цифр - это байт. Смотрите внимательно на дамп! Байт по адресу 100 имеет значение 6A, байт по адресу 101 - 00, байт по адресу 102 - 68... Эти "сладкие парочки" - и есть неделимая "единица адресации" оперативной памяти. Тех, кого смущает наличие буковок в адресе, в очередной раз отсылаем ознакомиться с шестнадцатеричной системой счисления, так как все числа, отображаемые программой debug - именно шестнадцатеричные. И, наконец, столбец справа - это символы, соответствующие шестнадцатеричным кодам центрального столбца (например, коду 6A соответствует символ J). Большинству кодов не соответствует никакой из "печатных символов" - таким в колонке справа соответствуют точки. #2. А теперь потренируем наши пальчики дампировать память - пройдемся по некоторым "историческим местам" нашей оперативной памяти... Для этого мы будем вводить команду D с параметром. Например, команда (параметр L8 означает "вывести 8 байтов"): - D FFFF:5 L8 [Enter] покажет вам системную дату в правом столбце дампа. Короче, искателям приключений выдаем "простыню" самых интересных адресов (большинство слов в описании вам пока должны быть непонятны, но вы не пугайтесь - понимание придет!).
Ну и хватит для первого раза. Кому мало - ищите дополнительную документацию :-p " 1.4. Программа #1. Любая программа выполняется последовательно (мы ведь пока обсуждаем "простой" IBM PC, а не какой-нибудь крутой векторный параллельный суперкомпьютер). То есть пока не выполнилась текущая "строка" (инструкция) программы, следующая не выполнится. Совсем другой вопрос, какая "строка" будет выполнена после "текущей" (здесь мы имеем дело со всевозможными логическими "ветвлениями", "циклами" и т. д.), или же строчку из какой программы процессор выполнит следующей, а какая - будет ждать своей очереди (так называемая "многозадачность", которую пока трогать не будем - в большинстве случаев мы можем прекрасно прожить и без нее, поскольку все заботы об этом все равно берут на себя операционные системы). Итак, у нас есть оперативная память, в которую загружается программа перед ее выполнением (сразу же по нажатию на Enter из Norton Commander). Операционная система, которая, собственно, и загружает программу, сообщает процессору, что надо начать обрабатывать команды, которые в памяти начинаются с такого-то адреса. И здесь первый подводный камень, вернее скала, которую трудно не заметить. Начало программы в памяти процессор различает легко - ему указывает на это командный интерпретатор, а вот конец программы программист должен указывать сам! Каким образом? А очень легко! Компьютер "распознает" как выход из программы специальную последовательность байтов. Например, для исполнимых файлов типа com (именно с этим типом файлов мы будем работать на начальном этапе) достаточно последовательности CD и 20. Пробуем-проверяем? Ну конечно же! Только для этого вам понадобится какой-нибудь шестнадцатеричный редактор, например, HexWorkshop. Все очень просто - создаем новый файл, единственным содержимым которого является последовательность CD 20, и сохраняем его как, например, myprg_1.com. Если вы позаботились о том, чтобы после CD 20 не было никаких прочих символов, то исполнимая программа будет "весить" только 2 байта. Запускать это ваше первое творение лучше из Norton или Volcov Commander (все же это пока что DOS'овская программулька). Что же она делает, эта 2-байтовая малышка? А ничего, просто этот файл обладает двумя важными свойствами:
Последнее и является единственным, что она пока что может делать (корректно выгружаться из памяти)... Еще к вопросу о выгружаемости - если после CD 20 вы напишите еще что-нибудь (чепуху), она все равно будет проигнорирована. Дело до ее выполнения просто-напросто не дойдет. Другое дело - если вы напишите чепуху до... #2. Честно говоря, опасно при низкоуровневом программировании чепуху писать. Можно невзначай и винт отформатировать :))). Поэтому лабуду писать не будем, вернее - будем, но не лабуду... Итак, продолжим наше извращение. Познакомимся еще с некоторыми "машинными командами" (в нашем случае - последовательностями шестнадцатеричных циферек).
Вот и давайте создадим еще одну программу типа com со следующим "шестнадцатеричным содержимым": B8-23-01-05-25-00-8B-D8-03-D8-8B-CB-31-C0-CD-20 Если вы все ввели правильно, то прога у вас без проблем запустится, а операционная система не будет ругаться... Правда, визуально (в смысле на мониторе) вы ее работу так и не заметите, но поверьте на слово - она работает! В этом вы еще убедитесь, когда посмотрите на ее работу изнутри - не различающими цветов глазами компьютера... Только сначала еще немного теории... #3. Теперь поговорим о втором подводном камне :). Один из принципов фон Неймана звучит приблизительно так: машине безразлично целевое назначение данных... Одна и та же цепочка битов может быть и машинными командами, и данными (например, символами, выраженными в виде кодов - есть такая "таблица символов ASCII", наверняка вы ее знаете). Что из этого следует? А то, что компьютеру нужно указывать, что подразумевается под той или иной "простыней" из битов - данные или код. На высоком уровне это делает операционная система. Например, она не пытается загрузить в память для выполнения файлы с расширениями, отличными от COM, EXE и BAT (последний вообще не из этой оперы, но принцип сохраняется). Хотя..., вы всегда можете поэкспериментировать! Смените, например, у какого-нибудь текстового файла тип с TXT на COM и попробуйте его запустить на выполнение (хотя мы это делать настоятельно не рекомендуем!). В большинстве случаев ваш компьютер зависнет! Потому, что:
Почти такой же эффект, но с потенциально большей разрушительной силой может получиться, если управление получит ИСПОРЧЕННЫЙ код, который вроде бы "в основном" правильный, но часть его вместо инициализации переменных и прочих подготовительных действий в лучшем случае ничего не делает, а в худшем портит другой код и данные... Как вам тяжело "въехать" в смысл повествования, состоящего из кусков различных книг, так и компьютеру тяжело понять подобную "мешанину". С той лишь разницей, что любую "неинтересную книгу" вы можете использовать в качестве туалетной бумаги, а вот "компутер" подобного права выбора лишен - он должен в это "въезжать", его процессор начинает перегреваться, а мозги кипят и вытекают через низкоуровневые порты ввода-вывода (командами IN и OUT соответственно). #4. Еще немного идеологии. О программе, которая выполняется в памяти... Сколько бы ни было "мозгов" в вашей навороченной тачке, любая программа выполняется в 640 килобайтах "нижней" (или основной) памяти. Если отнять от этой цифры "резидентную часть" операционной системы, многочисленные драйвера и т.д., то оставшееся и есть объем памяти, в котором выполняется ваша программа. А остальные мегабайты - это место для кэширования диска, хранения промежуточных данных и т.п. Страшно? Медитируйте! #5. Как уже говорилось в #3, одна и та же последовательность битов в памяти может быть:
Соответственно, и программа состоит из трех частей (сегментов): сегмента данных (data), сегмента кода (code) и сегмента стека (stack)... Оставим пока что "гнилой базар" про смысл словосочетаний "реентерабельный/рекурсивный код" и "адрес возврата". Чтобы не затруднять себе понимание происходящего, мы попытаемся абстрагироваться от всех этих ужасающих вещей и для начала заняться только кодом. #6. Помните, как в конце фильма "Matrix" Нео в конце концов увидел ее - черно-зеленую "матрицу"? Сейчас с вами произойдет нечто подобное! ;) Посмотрите на машинные коды, и "что они делают" в #2. Немножко дополним эту "простыню". Например, командой "внести значение" 1234 последовательно в каждый из "регистров общего пользования": B83412 - AX=1234 BB3412 - BX=1234 B93412 - CX=1234 BA3412 - DX=1234 Наиболее наблюдательные должны для себя отметить, что первый байт - это команда "переместить в регистр", а второй и третий - само число, только байты почему-то "наоборот". Однако никто не пишет программы в шестнадцатеричных редакторах! Никто! Это большая глупость! Единственное, зачем мы вам про это рассказываем - это чтобы вы поняли, что могут означать загадочные пары шестнадцатеричных цифр в дампе... Нет необходимости заучивать, что B8 - это "переместить в регистр AX", BB - "переместить в регистр BX" и так далее... Когда-нибудь это может пригодиться тому, кто будет писать компилятор, умеющий генерировать исполняемый код, упаковщики исполняемых файлов, самомодифицирующийся код или, на худой конец, конструкторы полиморфных самошифрующихся вирусов. Но это мы оставим на будущее... Все намного проще! В этом вы можете убедиться, загрузив вашу программу myprg_1.com в debug (например, командной строкой debug myprg_1.com и введя команду "u". А вот дальше начинается самое интересное :))) #7. Вот что вы должны увидеть: 11B7:0100 B82301 MOV AX,0123 ; Внести значение 0123h в AX 11B7:0103 052500 ADD AX,0025 ; Прибавить значение 0025h к AX 11B7:0106 8BD8 MOV BX,AX ; Переслать содержимое AX в BX 11B7:0108 03D8 ADD BX,AX ; Прибавить содержимое AX к BX 11B7:010A 8BCB MOV CX,BX ; Переслать содержимое BX в CX 11B7:010C 31C0 XOR AX,AX ; Очистка AX 11B7:010E CD20 INT 20 ; Конец программы Возвратившись к #2, перенесем сюда "описание" машинных команд. Эти mov, add, xor, int - так называемые "мнемонические команды" (более или менее понятные человеку), на основе которых формируется (это debug делает) "машинный код". Не правда ли, так намного легче? Соответственно, вместо шестнадцатеричных кодов мы легко могли вводить эти команды при помощи команды "A" (однако этим мы займемся позже). #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)
Далее представим входные параметры в виде таблички: (_7)
Плюс подробнейшее толкование, что подразумевается под словом "атрибут" (регистр BH): | 0000 черный | | 0001 синий | | 0010 зеленый | | 0011 голубой | Цвет фона | 0100 красный |(разряды 654) | 0101 пурпурный | Цвет символа | 0110 коричневый | (разряды 3210) | 0111 белый | | | 1000 серый | | 1001 светло-синий | | 1010 светло-зеленый | Повышенная | 1011 светло-голубой | яркость | 1100 светло-красный | (разряд 3) | 1101 светло-пурпурный | | 1110 желтый | | 1111 яркий белый | Мерцание (разряд 7) ВЫХОДНЫЕ ПАРАМЕТРЫ: отсутствуют (т.е. ни один регистр не меняется). (_8) Входные строки гасятся в нижней части окна. (_9) Нормальное значение байта атрибута - 07h. (_10) Совсем недавно, если бы вам показали подобное "описание", вы бы ничего в нем не поняли и ужаснулись. Теперь же, после прочтения предыдущих глав курса, в эти "таблицы" вы, более или менее, но "въехать" должны! Тем более, что сейчас я сделаю комментарии для... хм... "отстающих" учеников (внимательно смотрим на циферки в скобках): _1. Черным по белому, в толстом талмуде, описывающем функции прерываний, написано: "Драйвер видео вызывается по команде INT 10h и выполняет все функции, относящиеся к управлению дисплеем". И далее. ":ДЕЙСТВИЕ: после входа управление передается одной из 18 программ в соответствии с кодом функции в регистре AH. При использовании запрещенного кода функции, управление возвращается вызывающей программе. НАЗНАЧЕНИЕ: прикладная программа может использовать 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, то он просто побездельничает некоторое очень короткое время. ПРОБУЙТЕ!! В конце концов, ваша прога должна принять такой вот вид: :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 MOV BH,20 ;окошко второе :0112 MOV CH,06 :0114 MOV CL,11 :0116 MOV DH,0F :0118 MOV DL,3D :011A INT 10 :011C MOV BH,30 ;окошко третее :011E MOV CH,07 :0120 MOV CL,12 :0122 MOV DH,0E :0124 MOV DL,3C :0126 INT 10 :0128 MOV BH,40 ;окошко четвертое :012A MOV CH,08 :012C MOV CL,13 :012E MOV DH,0D :0130 MOV DL,3B :0132 INT 10 :0134 MOV BH,50 ;окошко пятое :0136 MOV CH,09 :0138 MOV CL,14 :013A MOV DH,0C :013C MOV DL,3A :013E INT 10 :0140 INT 20 ;конец проги... О, да! Получившаяся у вас программа написана долго и бездарно! Имейте это в виду :)). Мы же торжественно обещаем, что в последующих главах обязательно ее усовершенствуем. Да, вот еще что - особенно извращенные могут попытаться заменить 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 Ну а как делать то же самое с остальными регистрами вы, наверняка, уже и сами догадались. Очень важно помнить, каким "нездоровым"образом в стеке реализована ОЧЕРЕДЬ -поместить/извлечь. Помните, мы вас предупреждали, что нам нельзя верить на слово? Не верьте! А посему - обязательно убедитесь в истинности/ложности нашего голословного утверждения при помощи следующей программульки: :0100 MOV AX,0001 ;AX = 1 :0103 PUSH AX ;В стек записана 1 :0104 MOV AX,0002 :0107 PUSH AX ;В стек записана 2 :0108 MOV AX,0003 :010B PUSH AX ;В стек записана 3 :010C MOV AX,0004 :010F PUSH AX ;В стек записана 4 :0110 MOV AX,0005 :0113 PUSH AX ;В стек записана 5 :0114 POP AX :0115 POP AX :0116 POP AX :0117 POP AX :0118 POP AX :0119 INT 20 С очередностью заполнения стека, наверное, все понятно :). Я много про абстрактные "блины" загружал. А вот с адреса 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. А теперь вопрос на засыпку ;). Сколько раз выполнится следующий цикл: :0102 MOV CX,0000 :0105 ADD AX,0001 :0108 LOOP 0105 Очевидный ответ - 0 раз. В CX же у нас занесен 0. Так вот - ответ неправильный. Менее очевидный ответ - 1 раз! Ведь перед LOOP'ом сложение один раз все-таки выполнится. Так вот, этот ответ тоже неправильный. Самые подозрительные могут сразу же посмотреть на этот цикл под отладчиком, и с удивлением обнаружат, что LOOP сначала уменьшает значение CX (0-1=FFFF), а потом уже проверяет, не равен ли он нулю. И с гордостью за задний ум своей головы воскликнут: FFFFh раз! Так вот: этот ответ близок к истине, но тоже неправильный ;) Правильный ответ - цикл выполнится 10000h (65536d) раз. Но только вы и мне не верьте! Истинно только то утверждение, которое вы сами проверили на практике. Медитируйте! 1.9. Немножко оптимизации Как мы уже говорили, линейное программирование - это плохо, но не всегда. Сравните размеры ваших линейной и нелинейной программ. Не знаю, как у вас, но у нас линейная "весит" 27, а нелинейная - 19 байтов. Как по-вашему, какая быстрее работать будет? - Ну, естественно, нелинейная, потому что она меньше! - скажете вы и будете неправы. Попытайтесь оттрассировать "зацикленную". Не правда ли, она трассируется намного дольше своего линейного аналога? Угу, всё поняли? Сам знаю, что ни черта. :( Объясняю - в "зацикленной" программе "компутеру" приходится выполнять БОЛЬШЕ команд, нежели в "незацикленной". Аргументирую это голословное утверждение следующей таблицей (построенной на основе трассировки):
Ну и как по-вашему, какую из двух простыней процессор быстрее обработает? Сказать вам по сек-рету? А вот ничего я вам не скажу! Сами думайте! :] Как сейчас помню, был в моем Турбо-Си в преференсах к компилятору такой радиобуттон: "опти-майзить" по размеру или по скорости выполнения. Угадайте, на чем основан принцип этой оптимиза-ции? Только не вздумайте писать линейные проги! Пишите "нелинейные"! Нелинейную в линейную "пе-реоптимайзить" - как два пальца намочить! А вот наоборот - :(( Резюме - бесплатный сыр бывает только в мышеловке, и за все надо платить. Компактность и ско-рость - обычно параметры конфликтующие, поэтому в каждом конкретном случае нужно выбирать, что предпочтительнее. Исследования, проведенные в свое время еще Кнутом, показали, что 80% вре-мени затрачивается на выполнение 20% программы. Соответственно, рекомендуется тратить время на оптимизацию скорости именно тех 20% программы, а остальные можно оптимизировать по размеру (в частности, за счет циклов). Так и получается баланс между компактностью и скоростью программы ;). 1.10. Разборка с процедурами #1. В разделе 1.7. #4 мы сделали глупую линейную программульку, выводящую окошки. Обещали, что в следующей главе сделаем ее менее "тупой", да отвлеклись почему-то на циклы и стек. То есть я-то знаю, ПОЧЕМУ, но вот вам об этом - не скажу! Догадайтесь сами. Итак, поехали... Шаг первый. Внимательно посмотрев на "линейную" прогу из 1.7. #4 и прочитав "условие задачи" из главы 1.7. #1, вы обязаны возмутиться - зачем мы использовали команду MOV, если и ежу понятно, что отличия следующего окошка от предыдущего можно выразить более лаконично: BH=BH+10, CH=CH+1, CL=CL+1, DH=DH-1, DL=DL-1? И не нужно напрягать мозги, подсчитывая новое значение регистра вручную. Если вы так подумали, то оказались совершенно правы! Программу из #4 запросто можно было представить в таком вот виде: :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 ADD BH,10 ;окошко второе :0113 ADD CH,01 :0116 ADD CL,01 :0119 SUB DH,01 :011C SUB DL,01 :011F INT 10 :0121 ADD BH,10 ;окошко третье :0124 ADD CH,01 :0127 ADD CL,01 :012A SUB DH,01 :012D SUB DL,01 :0130 INT 10 :0132 ADD BH,10 ;окошко четвертое :0135 ADD CH,01 :0138 ADD CL,01 :013B SUB DH,01 :013E SUB DL,01 :0141 INT 10 :0143 ADD BH,10 ;окошко пятое :0146 ADD CH,01 :0149 ADD CL,01 :014C SUB DH,01 :014F SUB DL,01 :0152 INT 10 :0154 INT 20 ;конец программы И несмотря на то что размер ее оказался несколько большим, она тоже будет работать правильно :). Но тут любой более или менее наблюдательный программер возмутится повторно - да что это за программа такая? В ней целых четыре раза повторяется один и тот же кусок: :0143 ADD BH,10 :0146 ADD CH,01 :0149 ADD CL,01 :014C SUB DH,01 :014F SUB DL,01 :0152 INT 10 И знаете что? Этот наблюдательный программер будет прав! А если он еще и нехорошо выразится по поводу такого "неправильного" стиля программирования - будет прав... или почти прав... Есть такой процесс - оптимизация, одной из особенностей которой является уменьшение параметризации (параметризация - вставка процедур в место вызова или фиксация значения отдельных параметров) и развертка циклов. То, что обычно такими вещами должен заниматься компилятор, суть меняет не сильно - тем более, что ассемблер оптимизацией сам не занимается :). Но до ассемблера мы с вами еще не добрались, поэтому говорить об этом пока еще рано. Внимательно всмотритесь в полный текст программы и в этот выделенный кусок. И помедитируйте над ним до полного просветления текущей "обстановки"... #2. Итак, у нас есть ПОВТОРЯЮЩАЯСЯ ЧАСТЬ программы. А еще у нас есть пальцы, которым, как правило, лень набивать длинные "простыни" программного кода. Это одна из многочисленных причин, по которым и придумали такого "зверя" как ПОДПРОГРАММУ (она же - ПРОЦЕДУРА, она же - ФУНКЦИЯ). Остальные причины мы рассмотрим попозже, а вот на счет "лени" поговорим прямо сейчас: Если мы возьмем наш "часто повторяющийся" кусок программы и допишем в конец команду RET, то получится у нас именно ПРОЦЕДУРА - во всей своей красе: :011E ADD BH,10 ;"точка входа"; она же - начало "тела". :0121 ADD CH,01 :0124 ADD CL,01 :0127 SUB DH,01 :012A SUB DL,01 :012D INT 10 ;конец "тела" :012F RET Красота ее вот в чем заключается - процедуру можно "вызвать" командой CALL :))) Все более чем просто. Когда в программе встречается CALL с указанием АДРЕСА-НАЧАЛА-ПРОЦЕДУРЫ (в нашем случае это 011E), то компьютер "идет" по этому адресу и выполняет все команды, расположенные между "точкой входа" (включительно) и командой RET, то есть так называемое "тело" процедуры. RET - это тоже команда, но к "телу" (адреса 11E... 12D) она не относится. Она является "ОРГАНИЗАТОРОМ" этого "тела". Процессор, встретив команду RET, возвращает управление обратно после последнего CALL (т.е. "перепрыгивает" на строчку ниже "вызвавшего" данную процедуру CALL'а)... Короче, CALL XXXX означает - "выполнить процедуру, начинающуюся по адресу XXXX". А RET означает - "конец процедуры" и, соответственно, переход на строчку ниже вызвавшего его CALL'а. Если же говорить более формально и строго, то процедура - это средство обобщения, когда некоторая общая последовательность действий получает "имя", и потом при необходимости ПОВТОРНОГО ИСПОЛЬЗОВАНИЯ данного кода к нему просто идет обращение по "имени" (напомним, что в отличие от языков высокого уровня и даже ассемблера, в машинном коде от имен остаются одни только адреса, а язык, принимаемый DEBUG, является промежуточным между машинным кодом и ассемблером). Более того, следующим логическим шагом после обобщения является параметризация, когда некоторые части общего кода зависят от передаваемой извне информации (параметров), с чем очень хорошо знакомы программисты на языках высокого уровня. Но о параметризации и ее применении в ассемблере мы поговорим в другой раз. Не ругайтесь. Мы знаем, что вы ни черта не поняли. А по сему набьем в debug'е эту прогу и посмотрим, что она делает. Те, кто читал внимательно, могут отметить, что инструкция CALL по своему действию очень похожа на инструкцию генерации прерывания INT, с той лишь разницей, что аргументом CALL является адрес процедуры, а не индекс в таблице "векторов прерываний", где и хранится адрес обработчика прерывания (той же процедуры). А для особо продвинутых отметим, что в ранних моделях процессоров от Intel при подаче запроса на обработку от внешнего устройства контроллер прерываний, помимо собственно сигнала прерывания, посылал в процессор инструкцию CALL. #3. Кстати, вы уже поняли, почему мы называем debug "до боли любимой программой"? Нет? Неу-жели вы еще не полюбили это произведение программерского гения всеми фибрами своей души? Еще нет? М-да... мы в вас разочаровались. А по сему: - НАБИВАЕМ! - злобно кричим, брызгая слюной на эргономичный коврик: :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 CALL 011E ;четыре раза вызываем подпрограмму, :0113 CALL 011E ;начинающуюся по адресу 011E :0116 CALL 011E :0119 CALL 011E :011C INT 20 ;выход из программы... :011E ADD BH,10 ;начало процедуры :0121 ADD CH,01 :0124 ADD CL,01 :0127 SUB DH,01 :012A SUB DL,01 :012D INT 10 :012F RET ;конец процедуры Не правда ли, красиво получилось? Первое, что вас может смутить - это то, что команда выхода (INT 20) расположена не там, где вы привыкли, то есть не в конце программы. Ну что я вам могу на это ответить? Концы - они-то разные бывают! Последняя строчка в листинге вовсе не означает, что последней будет выполняться именно она. И это не должно вас смущать! А если все же смущает - смотрим, как работает эта прога из-под отладчика. Итак, до адреса 0110 вам все должно быть понятно, мы это рассматривали. Трассируем дальше... - Что значит "трассируем"? - попросите вы напомнить. Мысленно мы ругаем вас нехорошими словами (ну сколько раз повторять-то можно!), а вслух скажем: Команда "T" и Enter. Команда "T" и Enter. Команда "T" и Enter: Команда CALL 011E по адресу 0110 говорит процессору: "Дальше мы не пойдем, пока не выполним простыню, начинающуюся по адресу 011E". И далее, естественно, следует переход на этот адрес. Входим в тело процедуры, начиная с 011E, и выполняем команды до 012D включительно... А теперь внимательно смотрим, на какой адрес нас "перекинет" команда RET. На 113-й? И это правильно! По 113-му адресу у нас какая команда? Да вот опять CALL 011E! Опять процедура с адреса 011E, опять RET[URN] на строку ниже, то есть на 116... Так и далее, до того момента, пока следующей строчкой не окажется INT 20 - собственно, на этом и программе конец. Ну оно и ежу понятно, что, несмотря на то что INT 20 - не в конце программы, последним выполнится именно он. Короче, куда бы вас не посылали всяческие "столбы с указателями", конец вашего пути только один... А плутать вокруг да около этого конца вы можете сколько вам заблагорассудится... Кстати, именно это и является одной из многочисленных тайн программинга. Кто после этого скажет, что программисты - не дZенствующие люди? #4. Те, кто внимательно ознакомились с циклами, они и на этом не остановятся! Посмотрев на адреса 110...119, они вообще возьмут и возомнят себя воистину крутыми парнями! Знаете, что они напи-шут? А вот что (предвидим!): :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 MOV CX,0004 :0113 CALL 011A :0116 LOOP 0113 :0118 INT 20 :011A ADD BH,10 :011D ADD CH,01 :0120 ADD CL,01 :0123 SUB DH,01 :0126 SUB DL,01 :0129 INT 10 :012B RET То бишь еще и CALL в цикл при помощи MOV CX,4 и LOOP'а "закрутят". И что? А попробуйте! Что, не "пашет"? А что надо делать, если "не пашет, а должно бы"? Правильно! Смотреть из-под отладчика! Смотрим? Если посмотрите, то сразу же и "загвоздку" увидите - CX, использованный в качестве "счетчика" циклов, "перебивает" тот же CX, но используемый как "координаты верхнего левого угла окна". И что с этим делать прикажете? Вот, вы и столкнулись с одной из самых больших проблем. В процессорах фирмы Intel есть только 4 регистра общего назначения (и то в большинстве случаев - специализированных). Помните, мы вам говорили об этом? А теперь попробуйте выкрутиться из этой нехорошей ситуации с использованием стека :). Кстати, весьма "мозгопрочищающая" задачка :). 1.11. Переходы #1. "Переходы" бывают разные. Если вы пришли в гости, а вас просто послали к черту - такой переход называется "безусловный". А нежели вам сказали: "Если без пива - то иди к черту, а если с пивом - тогда проходи", - то это уже "условный" переход. Соответствено, для успешного перехода необходимо указать: ПРИ КАКОМ УСЛОВИИ выполнить переход, КУДА ПЕРЕЙТИ, ну и, наконец, сам пинок под зад нужно СДЕЛАТЬ, чтобы переход "гостя" в заданном направлении все-таки "состоялся". Безусловный переход у нас "делает" мнемоническая команда JMP, после которой следует указать адрес, на который "компьютер" должен пойти "на" ;). В данном случае УСЛОВИЕМ у нас будет "при любых обстоятельствах": хоть пустой, хоть с пивом, хоть с ... все равно. Когда рисовали окошки, вы уже использовали эту команду для создания "спецдефекта". Если кто еще не понял, что делает эта команда - к нему (см. 1.7 #4) и отсылаю. Сделайте "спецдефект" и посмотрите на него под отладчиком. Когда до вас дойдет, почему мы там не предусмотрели выхода (INT 20h) - можете переходить к п.2 текущей главы. #2. Условный переход у нас организуется в два шага. На первом шаге мы вычисляем условие ("принес ли пиво?"), на втором шаге "посылаем" или не "посылаем" - в зависимости от результатов вычислений. Можно привести такую аналогию - на первом шаге два груза кладутся на весы, сравнивающие их массу. Соответственно, возможны только три их положения: наклон влево (груз в левой чашке тяжелее), наклон вправо (груз в правой чашке тяжелее) и равновесие. На втором шаге мы предпринимаем действия в зависимости от положения весов. Например, на первом шаге можно использовать как "аптекарские весы" инструкцию CMP, которой обязательно нужно указать, ЧТО и С ЧЕМ она будет сравнивать. Пишем, например, CMP AX,BX В зависимости от значений регистров у нас возможны следующие состояния: "наклон влево" (AX &tt; BX), "наклон вправо" (AX > BX) и "равновесие" (AX = BX). Таким образом ВЫЧИСЛЕНИЕ УСЛОВИЯ у нас уже организовано :). Только условие не бинарное, а есть еще и "серединный вари-ант" (и даже несколько других!). Это нормально. Это для того сделано, чтобы мы могли выражения типа "больше-или-равно", "меньше-или-равно" да и просто "равно" в своих программах использовать... Итак, УСЛОВИЕ есть. Теперь решаем, что нам делать при том или ином условии. Вот далеко не полный список возможных "прыг-скоков":
Естественно, что после мнемоники ("прыгнуть, если") должен стоять АДРЕС, куда нужно "прыгнуть", если условие соблюдено. Если же условие не соблюдено, то прыжок не происходит, и выполняется нижеследующая строка программы. Задание на медитирование - зрительно представьте себе "весы правосудия". И побросайте на их чашки разную шестнадцатеричную дрянь в различных "пропорциях" и "комбинациях". Просветлиться вы должны следующим образом - в какую бы сторону эти ваши "весы" ни склонялись, вы все равно заставите систему работать так, как ЭТО вам угодно! "Весы" - они только констатируют факт. А вот "приговор" выносят судьи. Хе... и пусть после этого только кто-нибудь скажет, что программерам чужда политика - дело, как известно, весьма грязное А о чем это мы? Ах да, переходы... #3. Продолжим программировать, что ли? Напишем что-нибудь красивое и неизменно тупое? С использованием условных и безусловных переходов? Поехали! Слабаем мы сейчас что-то наподобие графического редактора :)). Не верите? У-у-у... Сложная задачка! Если въедете, что да как - значит, молодцы! Значит, разобрались-таки с дzебагом! Значит, подключились-таки к программерскому эгрегору и более или менее привели в порядок свои мозги :)... А это сложная штука, мы вам скажем - мозги в порядок приводить! Особенно когда есть куча инструментов, которые "порядок в коде" сами как бы наводят :). Думаете, вы по нашим текстам программировать учитесь? По-настоящему программировать мы еще не начали! Все, чем мы пока занимаемся - это приводим в порядок свои мозги и тренируемся на кнопки клавиатуры нажимать :)). А вот ско-о-оро НАЧНЕМ... тогда "прощай, здоровье" будет настоящее! Итак, сначала рассмотрим прерывания, которые в нашем "графическом редакторе" будут использоваться. Их три штуки, и все - BIOS'вские: mov AH,00 ;функция 0 прерывания 10h устанавливает "режим видео" mov AL,04 ;"на входе" (в регистре AL) - номер режима . int 10h ;если AL=4, то устанавливается цветной 320х200 графический режим mov CX,64h ; CX и DX - координаты точки mov DX,64h mov AH,0Ch; функция 0Ch (в регистре AH!) прерывания 10h рисует точку. mov AL,1Bh ;в AL - "код цвета" этой точки. int 10h Опять-таки - подробности о координатах и "кодах" цвета ищите сами!! Благо, знаете, где искать. mov AH,00 ;функция 0 прерывания 16h int 16h ;"читать код нажатой клавиши" Эта функция считывает код сканирования и код символа (клавиши на клавиатуре и соответствующий ей ASCII-код) из буфера клавиатуры (есть такой). Если в буфере ничего нет - она ждет, пока там что-нибуть появится. То есть ЖДЕТ, чтобы вы нажали на какую-нибудь клавишу, код которой будет занесен в регистр AX. Причем в AL - "символ", а вот в AH - так называемый "код сканирования"... Кодами вы пока голову не забивайте. Достаточно знать, что после нажатия клавиши Up в AH "попадет" значение 48h, Down - 50h, Left - 4Bh, Right - 4Dh. Как работает последний кусок кода, обязательно проверьте под отладчиком, это полезно :). #4. И лезем, лезем в наш горячо любимый DZEBUG, дабы набить там драгоценные строчки машинного мнемонического никому-кроме-вас-непонятного кода! -a :0100 MOV AH,00 ;устанавливаем графический режим :0102 MOV AL,04 :0104 INT 10 :0106 MOV CX,0064 ;координаты Первой Точки :0109 MOV DX,0064 :010C MOV AH,0C ;рисуем точку! :010E MOV AL,1B :0110 INT 10 :0112 MOV AH,00 ;ждем нажатия на клавишу :0114 INT 16 :0116 CMP AH,4B ;а не нажат ли у нас Left? :0119 JE 012A ;если да - то "прыг"! ;если нет - то следующая строчка :011B CMP AH,4D ;а не нажат ли у нас Right? :011E JE 012D :0120 CMP AH,48 ;а не нажат ли у нас Up? :0123 JE 0130 :0125 CMP AH,50 ;а не нажат ли у нас Down? :0128 JE 0133 :012A DEC CX ;задаем новые координаты, в зависимости :012B JMP 010C ;от нажатой клавиши - и скок в начало! :012D INC CX :012E JMP 010C :0130 DEC DX :0131 JMP 010C :0133 INC DX :0134 JMP 010C Тут один из автору вставили шпильку: "Не хватает проверки и выхода (со сбросом видеорежима!) по Esc - такие действия должны быть обязательным атрибутом, а не домашним заданием." Совершенно верная шпилька, товарищи! Но все равно - пусть это будет домашним заданием. Если вы все ввели правильно - должно заработать! Полюбуйтесь плодами своей медитации... Красиво? #5. А сейчас мы это все дело прокомментируем:
1.12. Данные #1. Работать с кодом мы с вами научились. Сейчас поучимся заставить наш код обрабатывать данные... Итак, запускаем DZEBUG и вводим следующую команду: -e cs:115 Которая означает: "набиваем память всяким дерьмом начиная со смещения 115". В ответ он вам выплюнет: 17B3:0115 00. Что означает: байт по смещению 115 равно 00. И точка. Но это не простая точка - это приглашение ввести НОВОЕ ЗНАЧЕНИЕ этого байта. Когда вы его ввели, нужно нажать на пробел. Если вы вознамеритесь последовательно ввести 1,2,3,4,5, то это будет выглядеть приблизительно так: 17B3:0115 00.1 75.2 AD.3 17B3:0118 66.4 FF.5 [Enter] А теперь делаем дамп памяти и смотрим, что за дрянь у нас получилась... А ведь получилoсь же!! #2. Мы запросто умеем "присваивать" регистру любое значение (mov AL,1C какой-нить), запросто можем "копировать" содержимое одного регистра в другой (mov AL,BL например)... А сейчас мы с вами научимся при помощи той же команды MOV еще и с данными из памати работать. Все проще пареной репы... Если мы напишем MOV AL,[115] то в результате выполнения этой команды в регистр AL "внесутся" две шестнадцатеричные циферки (байт), которые по адресу 115 находятся. То есть в нашем случае AL станет равным 1. А теперь посмотрите, что делает "обратная" команда: mov AL,55 mov [115],AL В первой строчке мы присвоили AL значение 55, а второй строчкой "скопировали" значения регистра в байт по адресу 115. Правда, проще некуда? Обязательно посмотрите на этот процесс под отладчиком! #3. А еще вот какой изврат с этим можно делать: mov BX,115 mov AL,[BX] Сие присваивает регистру AL значение байта по адресу 115 :). Ну... через посредника "BX" присваивает! Который у нас "переменная", как известно :). mov AL,1C mov BX,115 mov [BX],AL А этот кусок кода у нас "записал" 1C в сегмент данных по адресу 115 :). Ну, и извращения наподобие: mov AL,[BX+1] и mov [BX+1],AL Тоже весьма и весьма полезны в программерском деле :). Короче: все, что в квадратных скобках, - это адрес в памяти, с которым вы собираетесь "работать". Другой вопрос, что этот адрес может быть "составным"... #4. Низкоуровневый Paint мы с вами уже писали. Сегодня напишем низкоуровневый дZенский EXCEL. Задание простое... Есть у нас табличка типа: 1 8 ? 2 9 ? 3 1 ? 4 2 ? 5 2 ? в которой данные в формате HEX. И все, что нам нужно с ними сделать - это просумировать каждую "строчку", а сумму занести в третий "столбец"... В EXCEL'е это делается элементарно... А на машинном уровне, в общем-то, не намного сложней!! Для начала мы наберем "исходные данные" и зарезервируем место (например, забьем нулями) под третий столбец, в который собираемся помещать результат... Набиваем блок данных, начиная с адреса, например, 115: -e ds:115 17EA:0115 01.1 08.8 02.0 17EA:0118 09.2 02.9 00.0 03.3 03.1 00.0 04.4 04.2 17EA:0120 00.0 05.5 05.2 00.0 Вот так это у меня в DZEBUG'е выглядело :). Только я еще дамп посмотрел, правильно ли я ввел: 17EA:0110 03 E2 F3 CD 20 01 08 00-02 09 00 03 01 00 04 02 .... ........... 17EA:0120 00 05 02 00 6A 87 04 FF-76 FE 57 57 9A 5C 6C 87 ....j...v.WW.\l. Вроде правильно :)). Ну а программу я вот какую придумал: 17EA:0100 BB1501 MOV BX,0115 17EA:0103 B90500 MOV CX,0005 17EA:0106 8A07 MOV AL,[BX] 17EA:0108 024701 ADD AL,[BX+01] 17EA:010B 884702 MOV [BX+02],AL 17EA:010E 83C303 ADD BX,3 17EA:0111 E2F3 LOOP 0106 17EA:0113 CD20 INT 20 В BX я занес адрес начала блока данных (он же - верхний левый угол нашей таблицы). В CX внес 5, чтобы столько раз цикл выполнился (LOOP по адресу 111). А тело цикла вообще простое: Команда по адресу 106 забирает в AL цифирь из первого столбца. 108 - суммирует "цифирь из первого столбца с цифирью из второго столбца" (сумма, само собой, в AL'е остается). 10B - записывает сумму в третий столбец :). Ну и ADD BX,3 для перехода на следующую строчку :). И все на этом... Сделайте трассировку (внутрь INT 20 залезать не надо) и посмотрите на дамп нашего блока данных :) Я и говорю: ПРОЩЕ ПАРЕНОЙ РЕПЫ!! ;) #5. Видите? В качестве переменных "в компьютере" можно использовать не только регистры, но и "куски" памяти! А уж там вы можете клепать свои переменные в почти неограниченном количестве! Единственное, что нужно иметь ввиду: с переменными-регистрами компьютер работает намного быстрее, чем с переменными-в-памяти :). Кстати, если вы хотите сохранить плод своих сегодняшних трудов на веник, то имейте ввиду, что вы и сегмент данных тоже должны сохранить! То есть: вам нужно сохранить весь "диапазон" от адреса 100 до 123 включительно :). Ну и, само собой, при попытке дизассемблирования с адреса 115 у вас абракадабра пойдет... мы об этом уже говорили и упоминали один из принципов фон Неймана. Полагаю, вы уже поняли, что значит "выучить язык ассемблера" :) и теперь с удовольствием кинете грязью в того, кто скажет вам, что это сложно ;) Что значит "выучить язык", и что значит "программировать"? А проводите сами границы между этими понятиями! Только имейте в виду, кто скажет "выучить - значит все команды запомнить - тот дурак :((. Слова и понятия извращать можно по-всякому. Переопределите собственный тип и носите свежеобмоченные пеленки в полной уверенности, что это носки... В моем понимании "знать ассемблер" и "изучить на ассемблере" - синонимы (хотя лингвисты могут придраться, но мне это пофиг). Согласно границам, которые провели для себя (гы... ну и для вас немножко) автор курса, ассемблер вы уже знаете, а вот программировать на нем пока еще не умеете... Да о чем это я, в общем-то? (Утомлен кофием, поэтому речь несвязна)? Просто хотел сообщить вам, что первая часть курса закончилась. Вооружившись справочником команд и прерываний, вы уже можете программировать под дос. Если вы внимательно штудировали предыдущие главы, то идеология этого дела (под дос) вам уже должна быть понятна как 2х2=100b. |
|
2000-2008 г. Все авторские права соблюдены. |
|