В предыдущей программе 25 раз повторяется вывод строки текста на экран. Можно было бы написать 25 раз один и тот же код. Но что если нужно повторить программу 10 000 раз? А если нужно повторять её до тех пор, пока не получишь правильного решения? Для этого мы заставляем нужный участок программы повторяться несколько раз. В данном примере повторяется почти вся программа, но это совсем не обязательно. Сейчас я приведу упрощённую схему этой программы, это будет практически алгоритм.
01 Очистить экран и задать размерность 80x25 02 Переменные X,Y принять за позицию вывода текста 03 Вывести текст в указанное место 04 Увеличить Y на 1, X на 3 05 Если Y ещё не стал 25d, то перейти к шагу 2. Если стал - программа выполнена |
Очень важно, чтоб вы поняли, что 1 шаг алгоритма - это вовсе не одна строка в коде программы.
Обязательно соотнесите каждую строку реальной программы с представленным алгоритмом.
Когда вы поймёте все отличия такой записи от кода Ассемблера, вы поймёте, как писать программы.
Это самый простой способ начать думать на Асме.
Шаг 5 предполагает выбор между выполнением выхода из программы или возвратом к шагу 2.
Чтобы такой выбор предложить процессору, лучше всего использовать специальный регистр флагов.
И действительно, я разъяснил действие двух команд так, как будто они что-то друг другу говорят:
Адреса имена операнды 011D: CMP byte ptr [176h],19h ;сверяется значение байта в памяти с числом 25d 0122: JNE 0105h ;и если эти значения не равны, ;то прыг на выполнение адреса 105
У команд нет волшебных свойств, и они никак не связаны друг с другом!
Всё, что они могут, это менять переменные в памяти и регистры процессора (есть, конечно, ещё специальные команды ввода/вывода).
Команда CMP сравнивает два числа путём вычитания и в зависимости от результата меняет биты в регистре флагов.
Команда JNZ, второе написание JNE (пишите как нравится). Эта команда осуществляет прыжок, если выключен флаг нуля. Про эти две команды я подробно напишу после того, как расскажу об устройстве регистра флагов.
Если я ничего не забыл, то регистр флагов будет завершать мой рассказ о регистрах процессора. При отладке обычных программ другие регистры процессора изучать не обязательно. В отладчиках они даже не отображаются постоянно. Если интересно, посмотрите в учебнике.
Физическое устройство - такое же, как и у других регистров (32 бита).
  | Flags |
А вот назначение - специальное. В этот регистр нельзя просто записать значение командой MOV. У него даже нет имени обращения. Он изменяется по-другому. И он значительно отличается от первого специального регистра - EIP (указатель текущей инструкции).
EIP имеет имя обращения, и он цельный по содержанию регистр, в этом его сходство с регистрами общего назначения.
Я понимаю это так. Содержимое всех РОН программист может воспринимать как хранилище:
Регистр EIP (или IP) имеет смысл воспринимать только как хранинилище числа, и оно всегда означает адрес.
А вот регистр флагов - хранилище битовой информации!
Что я имею в виду под "битовой информацией"?
Разумеется, что с точки зрения компьютера любая информация битовая. Однако с точки зрения программиста, даже низкоуровневого, есть числа, которые имеет смысл делить, умножать и производить с ними сложные вычисления. В этих числах важнее целое, чем каждый разряд по отдельности (примечание).
И есть такие байты, каждый бит которых означает самостоятельное явление. Состояние процессора, результат сравнения и многое другое. Конечно, эти байты тоже могут выглядеть как hex число, но оно, наоборот, в целом виде не имеет явного смысла.
Регистр флагов - это как раз 32 бита, важных по отдельности.
Матрос, ты знаешь, каким флагом обозначают занятую посадочную полосу на орбитальной станции?
Стыдно такого не знать. Учи космический формат, иначе всю жизнь будешь драить потолки в столовой.
За исключением одного парного флага каждый бит обозвали флагом. Просто от балды. Могли бы обозвать светофором или знаком.
У всех флагов есть имена. Радует то, что для нас интересны далеко не все флаги. Я буду рассказывать о них по одному.
Сам когда-то прочёл сразу обо всех и ничего не запомнил, а вот при написании программ всё быстро усвоилось. 5-6 дней смотрел в справочник, а потом как-то само запомнилось (то, что было нужно, а точнее 3 флага :)).
Сегодня я расскажу о самом главном для программиста бит-флаге.
Причём рассказывать о нём, перечисляя все команды, которые его меняют или смотрят, - бесполезно, так как это будет половина команд Ассемблера. Флаг нуля включается, когда в результате действия команды переменная (часто мнимая) обнуляется, и выключается, когда переменная становится не ноль. Но так, естественно, делают не все команды (mov не меняет флаги, арифметические и некоторые другие команды меняют флаги...).
Самое интересное, что флаг нуля переключается при сравнении переменных.
Я расскажу, что с ZF делает команда CMP. Команду cmp мы не будем сразу же описывать полностью, ведь она может менять 6 флагов. А нам на первых порах все они совершенно не нужны.
Сегодня мы столкнулись с тем случаем, когда для нас важен лишь флаг ZF.
Адрес имя операнды 011D: CMP byte ptr [176h],19h ; Сверяется значение байта в памяти с числом 25d
Как и у большинства команд, при cmp в квадратных скобках указывается адрес памяти, по которому будет производиться действие.
Происхождение | От англ. слова compare - сравнивать |
Формат | CMP |
Действие | X=операнд1 - операнд2 в соответствии с X-результатом измененяет 6 флагов |
Примечание | Операнды не меняются (x - мнимый) |
На самом деле всё элементарно. Например, относительно флага нуля (ZF) действие команды выглядит так:
MOV AH, 10h    ; для наглядности присвоим AH значение 10h CMP AH, 8      ; ZF = 0 CMP AH, 10h    ; ZF = 1 CMP AH, 0A0h   ; ZF = 0 |
И всё. Про флаг нуля практически вся теория. Теперь надо потренироваться на практике. Запустите в отладчике prax03a.com.
CodeView показывает флаги по состоянию.
NZ - означает, что ZF=0, ZR - означает, что ZF=1, подсветка означает изменение состояния.
prax03a.com:
00000000: FEC0 inc al 00000002: 3D0400 cmp ax,00004 00000005: 40 inc ax 00000006: 3D0400 cmp ax,00004 00000009: FEC0 inc al 0000000B: 3D0400 cmp ax,00004 0000000E: 40 inc ax 0000000F: 3D0400 cmp ax,00004 00000012: FEC4 inc ah 00000014: 3D0400 cmp ax,00004 00000017: 3D0401 cmp ax,00104 0000001A: 3D0401 cmp ax,00104 0000001D: 3D0100 cmp ax,00001 00000020: FECC dec ah 00000022: 48 dec ax 00000023: 48 dec ax 00000024: 48 dec ax 00000025: 48 dec ax 00000026: 40 inc ax 00000027: 66B805000000 mov eax,000000005 0000002D: 66B800000000 mov eax,000000000 00000033: 6683F800 cmp eax,000 00000037: 6683C009 add eax,009 0000003B: 6683E809 sub eax,009 0000003F: 66B807000000 mov eax,000000007 00000045: 6633C0 xor eax,eax 00000048: 6683F801 cmp eax,001 0000004C: C606610101 mov b,[0161],001 00000051: 803E610101 cmp b,[0161],001 00000056: 803E610100 cmp b,[0161],000 0000005B: FE0E6101 dec b,[0161] 0000005F: CD20 int 020
Обязательно смотрите в отладчике подобные примеры, не ленитесь. Потому что если вы увидите своими глазами, что происходит в регистрах, знания будут сохранены в полезную область памяти. Так уж устроен человеческий мозг. Пощупал - значит реально, значит запомню. Вам надо понять, когда будет меняться флаг ZF и почему это будет происходить. Это очень важный урок!
Напишите на листке бумаги, при каких обстоятельствах флаг нуля выключается и при каких включается. Я не случайно прошу вас это сделать. Если вы собираетесь заняться исследованием программ, самая важная вещь для вас - это флаг нуля. Подобная выписка поможет быстро разобраться с принятием решений в коде программ.
Вернёмся к предыдущему примеру (prax03.com). Мы разобрались, что CMP меняет флаг нуля. Теперь следующая строка.
00000022: 75E1 jne 000000005 ;если значения не равны, прыг на выполнение 5-го байта
Происхождение | От англ. слов Jmp if Not Zero - Прыгнуть если не ноль |
Формат | JNZ метка |
Действие | Если ZF=0, то действие похоже на JMP (смена EIP на указанный адрес, то есть прыг куда сказали). Если ZF=1, то действие как у NOP (смена EIP на адрес следующей команды, то есть ничего не происходит). |
Примечание | Команда противоположного действия JZ (JE). |
Вообще, надо сказать, что у большинства прыжковых команд Ассемблера два написания,
кроме того, разные диалекты (MASM'а,FASM'а,Heiw'а и т.п.) вносят своё написание других команд,
но особой путаницы это пока не создаёт.
Теперь, когда вы знаете все команды prax03.com, можно его как следует разобрать.
00000000: B80300 mov ax,00003 00000003: CD10 int 010
В строке 0 происходит присвоение регистру AX значения 0003, причём если EAX был бы не ноль, изменения выглядели бы так: ????0003. В строке 3 вызывается BIOS-прерывание, которое при AL=3 переключает видеорежим в 80x25 текстовых ячеек и чистит экран.
После выполнения этих строк: EAX=00000003 Появляется экран ДОС (текстовый режим 80x25) |
00000005: B402 mov ah,002 00000007: 8B167501 mov dx,[0175] 0000000B: CD10 int 010
В строке 5 меняется значение регистра AH.
В строке 7 происходит загрузка регистра DX значениями из памяти. Значения будут размером в регистр (16 бит). Два этих байта при старте программы равны 00 00. А дальше программа будет их менять.
В строке 0Bh происходит вызов прерывания, которое установит курсор в положение DH - колонка, DL - строка. В первый раз DH=0, DL=0, значит строка будет печататься от левой верхней ячейки на экране.
После выполнения этих строк: EAX=00000203 Курсор меняет положение |
0000000D: FEC6 inc dh 0000000F: 80C203 add dl,003 00000012: 89167501 mov [0175],dx
В строке 0Dh происходит увеличение на 1 значения регистра DH.
В строке 0Fh происходит увеличение на 3 значения регистра DL.
В строке 12h DH и DL сохраняются в память, причём как целое. А это значит, что младший байт регистра DX будет в памяти раньше (по адресу 175h), соответственно, старший байт будет следующим (по адресу 176h).
После выполнения этих строк: EDX=0000????          + +          1 3 Память: 0175h здесь будет число из DL (будущее значение колонки курсора) 0176h здесь будет число из DH (будущее значение строки курсора и по совместительству счетчик повторений цикла) |
00000016: B409 mov ah,009 00000018: BA5001 mov dx,00150 0000001B: CD21 int 021
В строке 16h снова происходит присвоение регистру AH значения 09.
В строке 18h меняется регистр DX. Теперь там будет адрес строки текста.
В строке 1Bh вызывается прерывание ДОС-функций. При AH=9 это прерывание выполняет функцию вывода на экран строки текста (указатель на строку в DX).
0000001D: 803E760119 cmp b,[0176],019 00000022: 75E1 jne 000000005
Вот самое интересное в этой программе.
В строке 1Dh происходит сравнение содержимого в памяти по адресу 0176h со значением 19h. В памяти по адресу 175h находится word (2 байта), адрес 176h (старший байт слова) сейчас содержит число, которое при следующем проходе цикла должно стать номером строки для вывода текста. 19h = 25d - ровно столько строк в нашем видеорежиме. Однако начали мы с нулевой строки, значит 25d - это уже двадцать шестая строка. Она оказалась бы за пределами видимости, а этого мы не хотим. Именно поэтому мы проверяем на значение 25 - будущее значение строки. Когда в памяти будет 19h, то есть 25d, на экране все строки уже будут заполнены.
Итак, если значение в памяти равно 19h, то флаг нуля (ZF) будет включен командой cmp, или, как иногда говорят - флаг поднят.
В строке 22h происходит выяснение, поднят ли флаг нуля. Первые 24d раза флаг нуля опущен (ZF=0). И соответственно следующая команда, которая будет выполняться, находится в памяти по адресу 0105 (а в файле 05). Но когда ZF включится, вместо прыжка будет "пустое действие" - просто EIP станет указывать на следующую команду. То есть в любом случае действие команд перехода заключается в изменении EIP.
Значит, в строке 22h происходит условный переход, а условие перехода ZF=0.
После выполнения этих строк: Ничего не меняется, кроме ZF=? и EIP= 124h или 105. |
00000024: B410 mov ah,010 00000026: CD16 int 016
Это код, вызывающий паузу до нажатия клавиши.
00000028: CD20 int 020
Подпрограмма завершения.
Видите, организовать цикл на Ассемблере совсем не сложно.
Хотя этот цикл я сначала построил неправильно и только в ходе отладки обнаружил ошибку.
Что ещё раз доказывает: отладчик - главный инструмент программиста.
Насчёт ветвлений. Можно их делать точно так же, как и циклы, только если веток очень много, получится неэффективно. Пример: вы ждёте конкретное сообщение от пользователя и вам нужно обработать по-разному 200 или 500 действий юзера. Ну, не перебирать же всё подряд на "да/нет"... Хотя большинство программистов и языков программирования именно так и делают :). Поэтому и вам начать можно вот с такого варианта.
prax04.com:
00000000: CD16 int 016 00000002: B409 mov ah,009 00000004: 3C20 cmp al,020 00000006: 7411 je 000000019 00000008: 3C61 cmp al,061 0000000A: 7415 je 000000021 0000000C: 3C0D cmp al,00D 0000000E: 7419 je 000000029 00000010: 3C1B cmp al,01B 00000012: 741D je 000000031 00000014: 33C0 xor ax,ax 00000016: E9E7FF jmp 000000000 00000019: BA3801 mov dx,00138 0000001C: CD21 int 021 0000001E: E9DFFF jmp 000000000 00000021: BA6701 mov dx,00167 00000024: CD21 int 021 00000026: E9D7FF jmp 000000000 00000029: BAB701 mov dx,001B7 0000002C: CD21 int 021 0000002E: E9CFFF jmp 000000000 00000031: BACF01 mov dx,001CF 00000034: CD21 int 021 00000036: CD20 int 020
Код программы, как всегда, связан с данными. Их вполне можно набирать в текстовом редакторе Фара в кодировке DOS.
Закорючки - это код.
Заметьте, каждая строка текста отбита Enter'ами.
_?_?_?_?_?_?_?_? _?_?_?_?_?_?_?_?_?_?_?_?_?_?_?_?_?_?_?_?_?_?_?_?Если б я была игра - пулемёт бы застрелял... $А-а-а в Африке горы вот такой вышины, а-а-а в Африке реки вот такой ширины... $Enter кнопка о-го-го! $Очень жалко мне прощаться :(.$
Вы можете набрать текст с каким-нибудь сдвигом, тогда программа будет отображать неправильные строки.
Точно убедиться в правильности набора можно, сверив ваш файл с моим.
Каждый раз, как вы печатаете команду, совершаете ошибку или находите её у меня, вы совершенствуетесь в геометрической прогрессии. Поэтому и prax04 тоже лучше набрать самому. Практика - это главное в данный момент обучения.
Я уверен, что вам захочется переделать эту программу, например, в текстовый редактор. Что ж, это очень похвальное желание. Но для того, чтоб этот маленький пример стал текстовым редактором, нужно о нём кое-что знать.
Во-первых, прерывание 16 при AH=0 ждёт нажатия клавиши и возвращает в AX сообщение:
AH - скан-код клавиши (в таком коде различаются все кнопки на клаве),
AL - ASCII-код символа.
Во-вторых, в этой программе есть одна новая для вас команда XOR. Команда логического действия. Для компьютера такие команды всё равно что сложение и вычитание - арифметические действия. Есть даже задачи, которые можно решать в столбик с использованием логических команд. Основных логических действий всего 5. Сейчас я расскажу о XOR, и вы сами всё поймёте.
Происхождение | от англ. слов eXclusive OR - исключающее ИЛИ |
Формат | xor приёмник , источник |
Действие | xor - "побитовое исключающее или" (притивоположность включающему). Если один из сравниваемых битов равен 0, а другой равен 1, то результат равен 1. Если сравниваемые биты одинаковы (оба - 0 или оба - 1), то результат = 0. приёмник = приёмник xor источник. |
Примечание | Команда XOR обратима. Это значит, что поXORив её результат с одним из операндов, мы получим второй операнд. |
Знаю, в первый раз сбивает с толку, но если вы решите пару примеров, всё станет понятно. Вот все 4 возможных варианта битов в операндах.
0101 XOR 1001 ---- 1100
Поняли? Если нет, потренируйтесь на листочке.
Вывод: если операнды одинаковые, то на выходе в любом случае будет 0. Самый быстрый для процессора способ обнулить регистр EAX - написать:
XOR EAX,EAX
Так же можно обнулять и другие регистры общего назначения. Через пару примеров мы будем использовать команду XOR не только для обнуления.
Ещё я не говорил отдельно о команде jmp. Но думаю, вы и сами поняли, что это команда перехода без всяких условий.
Теперь вы можете экспериментировать. Напишите сами подробный разбор действий в этом примере.
Только из Hiew'a, как мы это делаем сейчас, будет очень неудобно вносить поправки в код.
Вам придется переправлять все прыжки, кроме того, они в этом примере короткие.
То есть в коде операций пишется, сколько нужно прибавить к адресу следующей команды или сколько нужно от него отнять.
Несмотря на то, что дизассемблер показывает
команду и адрес в памяти, куда будет совершен прыжок. Допустим, вы написали:
jmp 00000000
Теперь вы вставили 3 байта выше и не тронули саму команду, строка станет такой:
jmp 00000003
И ещё адреса текстовых данных в памяти "уедут", придётся править команды с указателями.
Так писать серьёзную программу можно годами.
В следующей главе мы познакомимся с MASM'ом, и тогда дело пойдёт значительно быстрее.
Возможно, к этому времени у моих читателей накопились скептические мысли. Если это всё, на что способен Ассемблер, то зачем тогда мучиться его изучать? Признаюсь, когда я выполнял подобные примеры, тоже сомневался, что на Ассемблере можно программировать полезные программы.
Однако сейчас знаю, что, используя знания Асма и машинного языка, можно "попросить" свой компьютер работать в нужном вам направлении, а ответ его почти наверняка будет положительным. И в Windows Ассемблер даёт возможность легко и красиво писать инструменты, получающие доступ к глубинам форточек, драйверы и системные программы. Но это далеко не единственное применение Ассемблера. Вы думаете, что самые навороченные игрушки пишут на Delphi или C++? Нет, это вовсе не обязательно. Качественный 3D-движок для игры, написанный на Ассемблере, куда как круче, чем любой подобный движок, построенный с использованием только высокоуровневых языков. И поверьте, такие вещи писать на Ассемблере реально!
Вот замечательный пример (!!!97Kb!!!).
Скачайте его и посмотрите (если ваш комп потянет). Сайт программы: www.theprodukkt.com
Причём совершенно не обязательно писать всю программу на Ассемблере.
Вы можете писать базу данных на Delphi или С++ и, не выходя из проекта, вставлять куски кода на Ассемблере в специальных скобках.
Есть много задач, которые требуют многократного повторения. Если вы их красиво напишете на Асме,
программа может обрести возможности, которых не будет у конкурентов. Я уже не говорю о программах перебора различных значений.
Такие программы не имеет смысл писать на других языках, когда есть Ассемблер.
На завтра у нас запланирован переход к программированию под Windows.
- Любое число, представленное в двоичной системе, можно воспринимать как битовую информацию,
и очень часто в этом есть математический смысл. Например, нулевой бит определяет нечётность числа.
Также старший бит числа может трактоваться как знаковый бит. Если он включен, число можно воспринимать как отрицательное.
Обо всём этом мы поговорим в витке1 и следующих Чтивах.
Bitfry