Дневники чайника. Чтива 0, виток0

Циклы, ветвления и регистр флагов

В предыдущей программе 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 бита).
Eflags
 Flags

А вот назначение - специальное. В этот регистр нельзя просто записать значение командой MOV. У него даже нет имени обращения. Он изменяется по-другому. И он значительно отличается от первого специального регистра - EIP (указатель текущей инструкции).

EIP имеет имя обращения, и он цельный по содержанию регистр, в этом его сходство с регистрами общего назначения.

Я понимаю это так. Содержимое всех РОН программист может воспринимать как хранилище:

Регистр EIP (или IP) имеет смысл воспринимать только как хранинилище числа, и оно всегда означает адрес.

А вот регистр флагов - хранилище битовой информации!

Что я имею в виду под "битовой информацией"?

Разумеется, что с точки зрения компьютера любая информация битовая. Однако с точки зрения программиста, даже низкоуровневого, есть числа, которые имеет смысл делить, умножать и производить с ними сложные вычисления. В этих числах важнее целое, чем каждый разряд по отдельности (примечание).

И есть такие байты, каждый бит которых означает самостоятельное явление. Состояние процессора, результат сравнения и многое другое. Конечно, эти байты тоже могут выглядеть как hex число, но оно, наоборот, в целом виде не имеет явного смысла.

Регистр флагов - это как раз 32 бита, важных по отдельности.

Матрос, ты знаешь, каким флагом обозначают занятую посадочную полосу на орбитальной станции?

Стыдно такого не знать. Учи космический формат, иначе всю жизнь будешь драить потолки в столовой.

За исключением одного парного флага каждый бит обозвали флагом. Просто от балды. Могли бы обозвать светофором или знаком.

У всех флагов есть имена. Радует то, что для нас интересны далеко не все флаги. Я буду рассказывать о них по одному.

Сам когда-то прочёл сразу обо всех и ничего не запомнил, а вот при написании программ всё быстро усвоилось. 5-6 дней смотрел в справочник, а потом как-то само запомнилось (то, что было нужно, а точнее 3 флага :)).

Сегодня я расскажу о самом главном для программиста бит-флаге.

ZF (Zero Flag - флаг нуля)

Причём рассказывать о нём, перечисляя все команды, которые его меняют или смотрят, - бесполезно, так как это будет половина команд Ассемблера. Флаг нуля включается, когда в результате действия команды переменная (часто мнимая) обнуляется, и выключается, когда переменная становится не ноль. Но так, естественно, делают не все команды (mov не меняет флаги, арифметические и некоторые другие команды меняют флаги...).

Самое интересное, что флаг нуля переключается при сравнении переменных.

Я расскажу, что с ZF делает команда CMP. Команду cmp мы не будем сразу же описывать полностью, ведь она может менять 6 флагов. А нам на первых порах все они совершенно не нужны.

Сегодня мы столкнулись с тем случаем, когда для нас важен лишь флаг ZF.

Адрес имя            операнды
011D: CMP   byte ptr [176h],19h ; Сверяется значение байта в памяти с числом 25d

Как и у большинства команд, при cmp в квадратных скобках указывается адрес памяти, по которому будет производиться действие.
Команда 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-го байта
Команда условного перехода JNZ (она же JNE)
ПроисхождениеОт англ. слов 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, и вы сами всё поймёте.
Команда 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

<<предыдущая глава     следующая глава>>

Вернуться на главную

Hosted by uCoz