Введение

Информация для новичков

Прежде чем приступать к изучению курса по языку Ассемблера, пожалуйста, ознакомьтесь с приведенной ниже информацией.

1) Где можно найти необходимое программное обеспечение для работы?

Программы, документацию, книги, а также многое другое можно скачать по адресу:
Ассемблер для DOS
http://www.Kalashnikoff.ru/Assembler/
Ассемблер для Windows
http://www.wasm.zite.ru/

2) У меня нет выхода в Сеть. Можно ли получить необходимые программы по электронной почте?

К сожалению, нет. Дело в том, что программы, необходимые для работы, имеют очень большой объем (от 5 Мб в архиве). Если у Вас все-таки нет программы-ассемблера (MASM или TASM), а также других необходимых программ, то придется их искать в магазине (на рынке) на компакт-дисках (дискетах).

3) Что делать, если у меня возникнут вопросы по уже пройденному вами материалу?

Если в процессе изучения курса у Вас возникнут вопросы, то их можно задавать нашим экспертам. Форма подачи вопроса, а также правила пользования экспертными группами находятся по адресу: http://www.Kalashnikoff.ru/Experts/Rules.html.

Спасибо Вам, что решили подписаться на рассылку "Ассемблер? Это просто! Учимся программировать"! Надеюсь, что Вы без труда догоните ушедших вперед подписчиков и научитесь много чему новому из нашей рассылки. Увлекательного Вам изучения!


Предисловие

Ассемблер? Это просто!
Учимся программировать

(По материалам одноименной рассылки с изменениями и дополнениями)

Том I

«Знакомьтесь: Ассемблер»

ОТ АВТОРА

Итак, Вы решили начать изучение языка Ассемблера. Возможно, Вы уже пробовали его изучать, но так и не смогли освоить до конца по причине того, что он показался Вам очень трудным. Сложность языка и обилие новых, неизвестных читателю терминов делают многие книги непонятными для понимания начинающего программиста. В настоящей книге автор старался излагать материал на доступном языке для любого пользователя, начинающего программиста, либо человека, который ни разу не сталкивался с какими-либо иными языками программирования, как то: Бейсик, Паскаль, Си и пр.

Книга разбита на два тома: в первом рассматривается программирование на Ассемблере под MS-DOS ®, во втором - под Windows ®.

Информация в книге взята из материалов рассылки «Ассемблер? Это просто! Учимся программировать». Используя данный материал, более 8.000 подписчиков научились писать программы на Ассемблере, которые казались им раньше чрезвычайно сложными и недоступными для понимания или написания. Большая часть подписчиков пытались раньше изучать язык Ассемблера, но так и не смогли пройти полный курс (прочитать ту или иную книгу до конца). И только материал из рассылки помог им понять Ассемблер и научил писать довольно-таки сложные программы под операционную систему (ОС) MS-DOS и Windows.

Автор постарался учесть все недоработки и ошибки, допущенные в рассылке, а также добавил много нового материала, который поможет Вам освоить Ассемблер за короткое время. Более того, автор попытался сделать обучение как можно более интересным для Вас, переходя с первой же главы к практической части. Вы уже сможете написать программу на Ассемблере, прочитав первую главу данной книги.

Первый том уникален тем, что:

  • Каждая глава представляет из себя одну тему, в конце которой приводится файл для практического изучения;
  • Материал изложен на простом языке, все новые термины подробно объясняются;
  • В процессе изучения Ассемблера, начиная с главы 11, рассматриваются четыре программы:
    1. нерезидентный безобидный вирус;
    2. резидентный антивирус против написанного нами вируса;
    3. файловая оболочка для DOS (типа Norton Commander ®);
    4. несколько видов резидентных программ (программ, которые постоянно находятся в памяти).
    5. А также исследуется работа отладчиков и способы обойти отладку программы.
  • Приведен электронный адрес для экстренной связи с автором в случае возникновения каких-либо вопросов, вытекающих из данной книги.

Начиная с главы 11 в приложении к данной книге приводятся готовые ассемблерные файлы в DOS формате с пояснениями для практического изучения курса.

Несколько советов:
  • Почаще пользуйтесь отладчиком;
  • Изменяйте код программ (файлов-приложений), побольше экспериментируйте;
  • Пробуйте написать свою собственную программу на основе изученного материала;
  • Т.к. вначале будет сложно придумать что-нибудь свое, то просто вставляйте в собственные программы выдержки, процедуры, алгоритмы из файлов-приложений. Помните, что опыт приходит со временем!
  • Не спешите! Внимательно и досканально изучайте каждую главу, выполняйте все, что автор просит сделать с прилагаемыми программами (запускать их под отладчиком, изменять код, думать над тем, что делает та или иная процедура и пр.).

Автор постоянно работает над совершенствование данной книги. При обнаружении логических, программных или иных подобных ошибок, просьба сообщить об этом автору книги. Спасибо.

Приятного Вам изучения!

ПРЕДИСЛОВИЕ

Прежде всего, хотелось бы отметить, что все Ваши вопросы по Ассемблеру, а также жалобы и критику по материалу, изложенному в данной книге можно направлять мне на e-mail (электронный адрес): Assembler@Kalashnikoff.ru. Обещаю Вам, что ни одно письмо не останется без внимания. Я постараюсь учесть мнение каждого и по возможности ответить на все письма.

В данном Предисловии отмечу следующие аспекты:

Какое программное обеспечение нужно для того, чтобы создать программу на Ассемблере, и где его можно достать?

  • Прежде всего - это текстовый редактор, как отдельный (например, EDIT.COM, входящий в состав MS-DOS), так и встроенный в какую-нибудь оболочку (например, Norton Commander, Volkov Commander и т.п.). Я рекомендую пользоваться встроенным редактором DOS Navigator’а (F4), указав в меню “Опции” a “Подсветка синтаксиса” a “on” . Так удобнее смотреть ассемблерный текст. Думаю, что не нужно объяснять, как пользоваться данными программами. Однако если у Вас возникли определенные сложности, то обращайтесь ко мне на e-mail.
  • Сам ассемблер (программу, которая переводит ассемблерные инструкции в машинный код). Это может быть MASM.EXE ® (ML.EXE) компании Microsoft, TASM.EXE ® компании Borland или некоторые другие. В принципе, большой разницы для наших примеров это пока не имеет (за исключением передачи параметров в командной строке). Я буду использовать MASM 6.11 (Macro Assembler ® от Microsoft версии 6.11), чего и Вам советую. Если Вы используете ассемблер отличный от моего, и он при ассемблировании примера выдаст ошибки, то пишите мне.
  • Настоятельно рекомендую иметь отладчик (AFD ®, SoftIce ®, CodeView ®). Он необходим для отладки программы и в целом для демонстрации ее работы. Я рекомендую использовать AFD или CodeView для начинающих и SoftIce для уже имеющих опыт программирования.
  • В будущем Вам, возможно, понадобиться дизассемблер, который необходим для перевода машинного кода на язык Ассемблера. Я предпочитаю IDA ®, как один из самых мощных и удобных в пользовании.

Найти все это (как и многое другое) можно на Митинском радиорынке в Москве (ст. м. Тушинская, авт. 2 либо на маршрутке 10 минут в сторону Митино. Часы работы: с 10:00 до 17:00 без выходных), либо на «Горбушке» (ст. м. «Багратионовская»). Можно также скачать все необходимое программное обеспечение по следующему адресу: http://www.Kalashnikoff.ru . Стоит отметить, что информация на указанном сайте постоянно пополняется. В перспективе: периодическое проведение голосований, горячие обсуждения, чат с автором, обзоры новых ресурсов по программированию, реальные встречи с читателями и многое другое.

Главы из книги будут строиться следующим образом:

  • ответы на часто задаваемые вопросы;
  • дополнения ваших заметок, примеров, алгоритмов и пр.
  • объяснение новой темы;
  • примеры программ на Ассемблере.

Вы уже сможете самостоятельно написать простую программу после прочтения Главы 1. Я надеюсь, что изучать язык будет интересней, если мы сразу перейдем к практической части, изучая параллельно теорию. Попутно отмечу, что данная книга рассчитана, как правило, на людей, которые ни разу не писали программы ни на Ассемблере, ни на каком-либо ином языке программирования. Конечно, если Вы уже знакомы с Basic, Pascal, C или каким-либо иным языком, то это только на пользу Вам. Тем не менее, все новые термины будут подробно объясняться.

Не сомневаюсь, что среди читателей есть люди, которые уже имеют небольшой опыт программирования на Ассемблере. Для них я специально буду отводить место в книге под названием "Раздел для имеющих опыт программирования", в котором, надеюсь, они найдут много полезного для себя. Ну и, конечно, я могу допустить ошибки. Поэтому большая просьба: если кто-то заметит ошибку (недоработку, упущение и т.п.) в моих программах либо неточности в тексте, прошу сообщить мне об этом. Буду очень благодарен.

Зачем нужен Ассемблер? Что он может делать, что не сможет сделать любой другой язык?
Плюсы:

  • Программа, написанная на Ассемблере, максимально быстро работает (в 50-200 раз быстрее Бейсика и в 3-20 раз быстрее С++)
  • Код программы максимально компактен
  • Позволяет сделать то, что ни один язык высокого уровня не способен сделать
Минусы:
  • Больше времени затрачивается на написание программы
  • Код длиннее, чем в других языках
  • Сложнее любых других языков

В дальнейшем вы поймете, что стоит один раз написать процедуры (например, вывод рамки на экран, получение строки символов и пр.), а затем вызывать их из других программ, то в итоге, времени у вас займет не настолько больше, чем писать, например, на Паскале.

Наибольший эффект (я бы сказал, оптимальный вариант) достигается при комбинировании двух языков: Pascal+Assembler или C+Assembler. Это особенно становится актуальным при программировании под Windows.

Многие мне возразят: зачем, мол, учитывать скорость, считать такты, оптимизировать программы, уменьшать код, если и так большинство людей используют Pentium-200 (32Мб / 6Гб) и выше, где плюс-минус 1000 тактов на глаз не заметно?

Вот, что я могу сказать. Дома у меня, к сожалению, пока стоит 486DX2-80. Программа WinAmp (кажется, такое название) не успевает проигрывать MP3-файлы (задержки большие). Но другая программа (Xing Mpeg) воспроизводит их прекрасно! Алгоритмы разные. В любом случае хочется, чтобы ваша любимая игра или программа работала быстрее...

Что конкретно будем изучать?
Я считаю целесообразным начинать обучение с программирования на Ассемблере под DOS (Том I данной книги). После этого перейдем к Windows (Том II). Рассмотрим подробно:

  • двоичную и шестнадцатеричную системы счисления (это не совсем интересно, но надо в обязательном порядке);
  • команды процессоров 8086, 80286, 80386, 80486 и Pentium;
  • сопроцессор;
  • сегментацию памяти;
  • XMS/EMS память;
  • работу видеобуфера;
  • VGA/SVGA-режимы;
  • клавиатуру;
  • работу с дисками, каталогами и файлами (как с короткими именами, так и с длинными);
  • FAT
  • порты ввода-вывода;
  • CMOS-микросхему;
  • BIOS
  • принтер;
  • модем;
  • научимся оптимизировать программы;
  • и многое-многое другое.

Не обойдем стороной и технический английский язык, т.к. операторы Ассемблера - это сокращения английских слов.


ГЛАВА 01

  • Шестнадцатеричная система счисления
  • Наша первая программа

Шестнадцатеричная система счисления

Для написания программ на Ассемблере, необходимо разобраться с шестнадцатеричной системой счисления. Ничего сложного в ней нет. Мы используем в жизни десятичную систему. Уверен, что Вы все ее знаете, поэтому я постараюсь объяснить шестнадцатеричную систему, проводя аналогию с десятичной.

Итак, в десятичной системе если мы к какому-нибудь числу справа добавим нуль, то это число увеличится в 10 раз. Например: 1 х 10 = 10; 10 х 10 = 100; 100 х 10 = 1000 и т.д. В этой системе мы используем цифры от 0 до 9, т.е. десять разных цифр (собственно, поэтому она и называется десятичная).

В шестнадцатеричной системе мы используем, соответственно, шестнадцать "цифр". Я специально написал слово "цифр" в кавычках, т.к. в ней используются не только цифры. Да и в самом деле как так? Объясняю: от 0 до 9 мы считаем так же, как и в десятичной, а вот дальше будет так: A, B, C, D, E, F. Число F, как не трудно посчитать, будет равно 15 в десятичной системе (см. Таблицу 1).

Таблица 1. Десятичная и шестнадцатеричная системы.
Десятичное число | 0 1 2 3 4 ... 8 9 10 11 12 13 14 15 16 17
Шестнадцатеричное число | 0 1 2 3 4 ... 8 9 A B C D E F 10 11
Десятичное число | 26 27 28 29 30 ... 158 159 160 161 162 ... 254 255 256 257
Шестнадцатеричное число | 1A 1B 1C 1D 1E ... 9E 9F A0 A1 A2 ... FE FF 100 101

Т.о., если мы к какому-нибудь числу в шестнадцатеричной системе добавим справа нуль, то это число увеличится в 16 раз.

Пример 1
  • 1 х 16 = 10;
  • 10 х 16 = 100;
  • 100 х 16 = 1000 и т.д.

Вы смогли отличить в Примере 1 шестнадцатеричные числа от десятичных? А из этого ряда: 10, 12, 45, 64, 12, 8, 19? Это могут быть как шестнадцатеричные, так и десятичные. Для того, чтобы не было путаницы, и компьютер смог бы однозначно отличить одни числа от других, в Ассемблере принято после шестнадцатеричного числа ставить символ h или H (H - это сокращение от англ. hexadecimal (шестнадцатеричное). Для краткости его иногда называют просто Hex). А после десятичного ничего не ставить. Т.к. числа от 0 до 9 в обоих системах имеют одинаковые значения, то числа, записанные как 5 и 5h одно и тоже.

Т.о. Пример 1 (см. выше) правильнее будет записать так:

  • 1 х 16 = 10h;
  • 10h х 16 = 100h;
  • 100h х 16 = 1000h и т.д.
Либо так:
  • 1h х 10h = 10h;
  • 10h х 10h = 100h;
  • 100h х 10h = 1000h

Для чего нужна шестнадцатеричная система - мы рассмотрим в последующих главах. А в данный момент для нашего примера программы, который будет рассмотрен ниже, нам необходимо знать о существовании шестнадцатеричных чисел.

Итак, подведем итог. Шестнадцатеричная система счисления состоит из 10 цифр (от 0 до 9) и 6 букв латинского алфавита (A, B, C, D, E, F). Если к какому-нибудь числу в шестнадцатеричной системе добавим справа нуль, то это число увеличится в 16 раз. Очень важно уяснить данную тему, так как мы будем постоянно использовать ее при написании программ.

Теперь немного о том, как я буду строить примеры на Ассемблере. Не совсем удобно приводить их сплошным текстом в книге, поэтому сперва будет идти сам код программы с пронумерованными строками, а сразу же после него - объяснения и примечания.

Примерно так:
(01)mov ah,9
(02)mov al,8
…
(15)mov dl,5Ah

(Номера строк ставятся только в наших примерах! При наборе программ в текстовом редакторе номера ставить НЕ нужно!)

Объяснения:
В строке (01) мы делаем то-то, а в строке (15) то-то.

Огромная просьба: НЕ копируйте программы со страницы в буфер, а затем не вставляйте их в Блокнот (или еще куда-нибудь)! Перепечатывайте их вручную в текстовом редакторе. Если есть принтер, то выделите программу, распечатайте выделенный фрагмент, а затем перебейте в редактор с бумаги. Все примеры нужно набирать самостоятельно! Это ускорит запоминание операторов. И еще. Строчные и ПРОПИСНЫЕ буквы в Ассемблере не различаются. Записи вида:

mov ah,9
и
MOV AH,9

Ассемблером воспринимаются одинаково. Можно, конечно, заставить Ассемблер различать строчные и ПРОПИСНЫЕ символы, но мы пока этого делать не будем. Для удобства чтения программы лучше всего операторы печатать строчными буквами, а названия подпрограмм и меток начинать с прописной. Но это как кому будет удобно.

Наша первая программа

Итак, переходим к нашей первой программе:

(01) CSEG segment
(02) org 100h
(03)
(04) Begin:
(05)
(06)	mov ah,9
(07)	mov dx,offset Message
(08)	int 21h
(09)
(10)	int 20h
(11)
(12) Message db 'Hello, world!$'
(13) CSEG ends
(14) end Begin

Еще раз подчеркну: номера строк в скобках ставить НЕ нужно! Прежде, чем пытаться ассемблировать, прочтите данную главу до конца!

Для того, чтобы объяснить все операторы данного примера, нам потребуется несколько глав. Поэтому описание некоторых команд мы просто опустим на данном этапе. Просто считайте, что так должно быть. В самое ближайшее время мы рассмотрим эти операторы подробно. Итак, строки с номерами (01), (02) и (13) Вы просто игнорируете.
Строки (03), (05), (09) и (11) остаются пустыми. Это делается для наглядности. Ассемблер их будет просто опускать.

Теперь перейдем к рассмотрению остальных операторов. Со строки
(04) Begin:
начинается код программы. Это метка, указывающая Ассемблеру на начало кода. В строке
(14) end Begin
стоят операторы end Begin (Begin - от англ. - начало; end -конец). Это конец программы. Вообще вместо слова Begin можно было бы использовать что-нибудь другое. Например, Start:. В таком случае, нам пришлось бы и завершать программу (14) End Start.

Строки (06) - (08)

(06)	mov ah,9
(07)	mov dx,offset Message
(08)	int 21h
выводят на экран сообщение «Hello, world!». Здесь придется вкратце рассказать о регистрах процессора (более подробно эту тему мы рассмотрим в последующих главах).

Регистр процессора - это специально отведенная память для хранения какого-нибудь числа.

Например:
Если мы хотим сложить два числа, то в математике запишем так:
A=5
B=8
C=A+B
A, B и C - это своего рода регистры (если говорить о компьютере), в которых могут хранится некоторые данные. А=5 можно прочитать как: «Присваиваем А число 5».

Для присвоения регистру какого-нибудь значения, в Ассемблере существует оператор mov (от англ. move - в данном случае - загрузить). Строку (06) следует читать так: «Загружаем в регистр AH число 9» (проще говоря, присваиваем AH число 9). Ниже рассмотрим зачем это надо.

В строке (07) загружаем в регистр DX адрес сообщения для вывода (в данном примере это будет строка «Hello, world!$»). Далее, в строке (08), вызываем прерывание MS-DOS, которое и выведет нашу строку на экран.

Прерывания будут подробно рассматриваться в последующих главах. Здесь я скажу несколько слов.

Прерывание MS-DOS - это своего рода подпрограмма (часть MS-DOS), которая находится постоянно в памяти и может вызываться в любое время из любой программы.

Рассмотрим вышесказанное на примере (мелким шрифтом выделим примечания):

Программа (алгоритм) сложения двух чисел
НачалоПрограммы
A=5 		в переменную A заносим значение 5
B=8 		в переменную B значение 8
ВызовПодпрограммы Сложение
теперь С равно 13

A=10 		тоже самое, только другие числа
B=25
ВызовПодпрограммы Сложение
теперь С равно 35

КонецПрограммы выходим из программы

...

Подпрограмма Сложение
C=A+B
ВозвратИзПодпрограммы возвращаемся в то место, откуда вызывали
КонецПодпрограммы

В данном примере мы дважды вызвали подпрограмму Сложение, которая сложила два числа, переданные ей в переменных A и B. Результат помещается в переменную С. Когда вызывается подпрограмма, компьютер запоминает с какого места она была вызвана, а затем, когда закончила работу подпрограмма, компьютер возвращается в то место, откуда она вызывалась. Т.о., можно вызывать подпрограммы неопределенное количество раз с любого места.

При выполнении строки (08) программы на Ассемблере мы вызываем подпрограмму (в данном случае - это называется прерывание), которая выводит на экран строку. Для этого мы, собственно, и помещаем необходимые значения в регистры. Всю работу (вывод строки, перемещение курсора) берет на себя подпрограмма. Строку (08) следует читать так: вызываем двадцать первое прерывание (int - от англ. interrupt - прерывание). Обратите внимание, что после числа 21 стоит буква h. Это, как мы уже знаем, шестнадцатеричное число (33 в десятичной системе). Конечно, нам ничего не мешает заменить строку int 21h на int 33. Программа будет работать корректно. Просто в Ассемблере принято указывать номера прерываний в шестнадцатеричной системе.

В строке
(10) int 20h
мы, как Вы уже догадались, вызываем прерывание 20h. Для вызова данного прерывания не нужно указывать какие-либо значения в регистрах. Оно выполняет только одну задачу: выход из программы (выход в DOS). В результате выполнения прерывания 20h, программа вернется туда, откуда ее запускали (загружали, вызывали). Например, в Norton Commander или DOS Navigator.

Строка
(12) Message db 'Hello, world!$'
содержит сообщение для вывода. Первое слово (message - сообщение) - название сообщения. Оно может быть любым (например, mess или string и пр.). Обратите внимание на строку (07) mov dx,offset Message, в которой мы загружаем в регистр DX адрес нашего сообщения.

Можно создать еще одну строку, которую назовем Mess2. Затем, начиная со строки (09) вставим следующие команды:

(09)	mov ah,9
(10)	mov dx,offset Mess2
(11) 	int 21h
(12)	int 20h
(13) Message db 'Hello, world!$'
(14) Mess2 db 'Это Я!$'
(15) CSEG ends
(16) end Begin

Надеюсь, Вы догадались, что произойдет...

Обратите внимание на последний символ в строках Message и Mess2 - $. Он указывает на конец строки. Если мы его уберем, то 21h прерывание продолжит вывод до тех пор, пока не встретится где-нибудь в памяти символ $. На экране, помимо нашей строки в самом начале, мы увидим «мусор» (разные символы, которых в нашей строке вовсе нет).

Теперь ассемблируйте программу. Как это сделать - написано в Приложении 01(Ассемблирование). Заметьте, что мы создаем пока только *.com-файлы, а НЕ *.exe! Для того, чтобы получить *.com-файл, нужно указать определенные параметры ассемблеру (MASM/TASM) в командной строке.

При возникновении ошибок в процессе ассемблирования - смотрите Приложение 02(Ошибки при ассемблировании), где рассматриваются типичные ошибки при ассемблировании программ.

Если у Вас есть отладчик (AFD, CV), то можно (и даже нужно!) запустить ее под отладчиком. Это поможет Вам лучше понять структуру и принцип работы Ассемблера, а также продемонстрирует реальную работу программы.

Целью настоящей главы не было разобраться подробно с каждым оператором. Это невозможно, т.к. у Вас еще недостаточно знаний. Я полагаю, что, прочитав 3-4 главы, Вы поймете принцип и структуру программы на Ассемблере.

Может быть, Вам показался язык Ассемблера чрезвычайно сложным, но это, поверьте, с первого взгляда. Вы должны научиться строить алгоритм программы на Ассемблере в голове, а для этого нужно будет написать несколько программ самостоятельно, опираясь на информацию из данной книги. Я буду постепенно учить Вас мыслить структурой Ассемблера, учить составлять алгоритмы, программы, используя операторы языка. После изучения очередной главы, Вы будете чувствовать, что постепенно начинаете осваивать Ассемблер, будет становиться все проще и проще…
Например, если Вы знакомы с Бейсиком, то, ставя перед собой задачу написать программу, выводящую 10 слов "Привет", Вы будете использовать операторы FOR, NEXT, PRINT и пр., которые тут же появятся в Ваших мыслях. Вы строите определенный алгоритм программы из этих операторов, который в какой-то степени применим только к Бейсику. Тоже самое и с Ассемблером. При постановке задачи написать ту или иную программу, Вы мысленно создаете алгоритм, который применим к Ассемблеру и только, т.к. языков, похожих на Ассемблер, просто не существует. Моя задача - научить Вас создавать в уме алгоритмы, применимые к Ассемблеру, т.е., образно говоря, научить "мыслить на Ассемблере".

В следующей главе мы подробно рассмотрим регистры процессора и напишем еще одну простенькую программку.


ГЛАВА 02

  • Регистры микропроцессоров 8086 - 80186
  • Программка для практики

Регистры микропроцессоров 8086 - 80186

В прошлой главе мы рассмотрели с Вами шестнадцатеричную систему счисления и простую программу вывода строки на экран. В данной главе рассматриваются регистры процессоров 8086 / 8088 / 80186.

Регистр, как мы уже рассматривали раньше, - это, просто говоря, специально отведенная память для временного хранения каких-то данных, переменная.

Микропроцессоры 8086 - 80186 имеют 14 регистров. В прошлой главе мы встретили два из них: AH и DX. В Таблицах № 1, 2 и 3 приведены списки всех регистров, кроме IP и регистра флагов, которые со временем будут рассматриваться отдельно:

Таблица № 1. Регистры данных
AX |  | BX |  | CX |  | DX
AH | AL | BH | BL | CH | CL | DH | DL

Регистры данных (Таблица № 1)

Могут использоваться программистом по своему усмотрению (за исключением некоторых случаев). В них можно хранить любые данные (числа, адреса и пр.). В верхнем ряду
AX (Accumulator register - аккумулятор). Применяется для хранения промежуточных данных. В некоторых командах использование этого регистра обязательно
BX (Base register - база). Применяется для хранения базового адреса некоторого объекта в памяти
CX (Count register - счетчик). Применяется в командах, производящих некоторые повторяющиеся действия.
DX (Data register - регистр данных). Так же, как и регистр AX, он хранит промежуточные данные. В некоторых командах его использование обязательно; для некоторых команд это происходит неявно
Эти шестнадцатиразрядные регистры могут хранить числа от 0 до 65.535 (от 0h до FFFFh в шестнадцатеричной системе (вспоминаем прошлую главу)). Под ними идет ряд восьмиразрядных регистров (AH, AL, BH, BL, CH, CL, DH, DL), которые могут хранить максимальное число 255 (FFh). Это половинки (старшая или младшая) шестнадцатиразрядных регистров.

Например:

Мы уже знаем оператор MOV, который предназначен для загрузки числа в регистр. Чтобы присвоить, к примеру, регистру AL число 35h, нам необходимо записать так:

mov al,35h

а регистру AX число 346Ah - так:

mov ax,346Ah

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

Например, следующие записи будут ошибочны:

mov ah,123h   максимум FFh
mov bx,12345h   максимум FFFFh
mov dl,100h   максимум FFh

Здесь надо отметить, что если шестнадцатеричное число начинается не с цифры (напр.: 12h), а с буквы (A-F) (напр.: С5h), то перед таким числом ставится нуль: 0C5h. Это необходимо для того, чтобы программа-ассемблер могла отличить где шестнадцатеричное число, а где метка. Ниже мы рассмотрим это на примере.

Допустим, мы выполнили команду mov ax,1234h. В этом случае в регистре AH будет находится число 12h, а в регистре AL - 34h. Т.е. AL, BL, CL, DL - это младшие (Low), а AH, BH, CH, DH - старшие (High) половинки шестнадцатиразрядных регистров (см. Таблицу № 4).

Таблица № 4. Результаты выполнения различных команд
Команда | Результат
mov ax,1234h | AX = 1234h, AH = 12h, AL = 34h
mov bx,5678h | BX = 5678h, BH = 56h, BL = 78h
mov cx,9ABCh | CX = 9ABCh, CH = 9Ah, CL = 0BCh
mov dx,0DEF0h | DX = 0DEF0h, DH = 0DEh, DL = 0F0h

Рассмотрим еще два оператора: ADD и SUB. Оператор ADD имеет следующий формат (в последствии мы всегда будем оформлять новые команды в такие таблицы):

Команда | Перевод | Назначение | Процессор
ADD приемник, источник | ADDition - сложение | Сложение | 8086

В столбце Команда будет описываться новая команда и ее применение. В столбце Назначение - что выполняет или для чего служит данная команда, а в столбце Процессор - модель (тип) процессора с которого она поддерживается. Перевод - с какого английского слова образован оператор и его перевод. В данном примере - это 8086 процессор, но работать команда будет, естественно и на последующих, более современных процессорах (80286, 80386 и т.д.).

Команда ADD производит сложение двух чисел.
Примеры:

mov al,10  ; загружаем в регистр AL число 10
add al,15 ; AL = 25; AL - приемник, 15 - источник

mov ax,25000 ; загружаем в регистр AX число 25000
add ax,10000 ; AX = 35000; AX - приемник, 10000 - источник

mov cx,200 ; загружаем в регистр CX число 200
mov bx,760 ; а в регистр BX - 760
add cx,bx ; CX = 960, BX = 760 (BX не меняется), CX - приемник, BX - источник

Команда | Перевод | Назначение | Процессор
SUB приемник, источник | SUBtraction - вычитание | Вычитание | 8086

Команда SUB производит вычитание двух чисел

Примеры:
mov al,10
sub al,7 ; AL = 3, AL - приемник, 7 - источник

mov ax,25000
sub ax,10000 ; AX = 15000, AX - приемник, 10000 - источник

mov cx,100
mov bx,15
sub cx,bx ; CX = 85, BX = 15 (BX не меняется!), CX - приемник, BX - источник
Это интересно

Следует отметить, что Ассемблер максимально быстрый язык. Можно посчитать сколько раз за одну секунду процессор сможет сложить два любых числа от 0 до 65535.
Каждая команда процессора выполняется определенное количество тактов. Когда говорят, что тактовая частота процессора 100Mhz, то это значит, что за секунду проходит 100 миллионов тактов.
Чтобы сложить два числа в Ассемблере нужно выполнить следующие команды:
mov ax,2700
mov bx,15000
add ax,bx

В результате выполнения данных инструкций, в регистре AX будет число 17700, а в регистре BX - 15000. Команда add ax,bx выполняется за один такт на процессоре 80486. Получается, что компьютер 486 DX2-66Mhz за одну секунду сложит два любых числа (от 0 до 0FFFFh) 66 миллионов (!) раз! А еще называют «четверку» медленной!..

Регистры-указатели (Таблица № 2)

Таблица № 2. Регистры-указатели
SI DI BP SP

Регистры SI (Source Index register - индекс источника) и DI (Destination Index register - индекс приемника) используются в строковых операциях. Регистры BP и SP необходимы при работе со стеком. Мы их будем подробно рассматривать в последующих главах.

Сегментные регистры (Таблица № 3)

Таблица № 3. Сегментные регистры
CS DS ES SS

Сегментные регистры необходимы для обращения к тому или иному сегменту памяти (например, видеобуферу). Сегментация памяти довольно сложная и объемная тема, которую также будем рассматривать в последующих главах

Давайте изучим еще несколько команд в данной главе:
Команда | Перевод | Назначение | Процессор
INC приемник | INCrement - инкремент | Увеличение на единицу | 8086

Команда INC увеличивает на единицу регистр. Она эквивалентна команде: ADD источник, 1 только выполняется быстрее на старых компьютерах (до 80486) и занимает меньше байт.

Примеры:
mov al,15
inc al ; теперь AL = 16 (эквивалентна add al,1)

mov dh,39h
inc dh ; DH = 3Ah (эквивалентна add dh,1)

mov cl,4Fh
inc cl ; CL = 50h (эквивалентна add cl,1)

Программка для практики

Сегодня рассмотрим одну небольшую программку, которая выводит на экран сообщение, и ждет, когда пользователь нажмет любую клавишу. После чего возвращается в DOS.

Управлять клавиатурой позволяет прерывание 16h. Это прерывание BIOS (ПЗУ), а не MS-DOS (как 21h). Его можно вызывать даже до загрузки операционной системы, в то время, как прерывание 21h доступно только после загрузки IO.SYS / MSDOS.SYS (т.е. определенной части ОС MS-DOS).

Чтобы остановить программу до нажатия любой клавиши следует вызвать функцию 10h прерывания 16h. Вот как это выглядит (после символов ";" идет комментарий):

mov ah,10h ; в AH всегда указывается номер функции
int 16h ; вызываем прерывание 16h - сервис работы с клавиатурой BIOS (ПЗУ)

После нажатия на любую клавишу, компьютер продолжит выполнять программу, а регистр AX будет содержать код клавиши, которую нажал пользователь.

Следующая программа выводит на экран сообщение и ждет нажатия любой клавиши (равнозначна команде "pause" в *.bat файлах):

(01) CSEG segment
(02) org 100h
(03) Start:
(04)
(05)	mov ah,9
(06)	mov dx,offset String
(07)	int 21h
(08)
(09)	mov ah,10h
(10)	int 16h
(11)
(12)	int 20h
(13)
(14) String db 'Нажмите любую клавишу...$'
(15) CSEG ends
(16) end Start

Строки с номерами (01), (02) и (15) пока опускаем. В строках (05) - (07), как Вы уже знаете, выводим на экран сообщение. Затем (строки (09) - (10)) ждем нажатия клавиши. И, наконец, строка (12) выходит из нашей программы.

Мы уже знаем команды INC, ADD и SUB. Можно поэкспериментировать с вызовом прерывания. Например, так:
mov ah,0Fh
inc ah
int 16h
Это позволит Вам лучше запомнить новые операторы.

В следующей главе рассмотрим двоичную систему счисления, основы сегментации памяти и сегментные регистры. Напишем интересную программку.


ГЛАВА 03

  • Двоичная система счисления. Бит и байт
  • Сегментация памяти в DOS
  • Наше первое прерывание
  • Программка для практики

Двоичная система счисления. Бит и байт

В прошлой главе мы рассмотрели с Вами шестнадцатеричную систему счисления и простую программу вывода строки на экран.

В настоящей главе мы рассмотрим основополагающие сведения по программированию на языке Ассемблера. Необходимо тщательно разобраться в каждом предложении и уяснить двоичную систему счисления, понять принцип сегментации памяти в DOS. Мы также рассмотрим операторы Ассемблера, которые не затрагивали в примерах из предыдущей главы. Сразу отмечу, что это один из самых сложных разделов данной книги. Я пытался объяснить все это как можно проще, не используя сложных определений и терминов. Если что-то не поняли - не пугайтесь! Со временем все станет на свои места. Более того, мне можно задавать вопросы по адресу, указанному в Предисловии. Если Вы полностью разберетесь во всем в данной главе, то считайте, что базу Ассемблера Вы изучили. Со следующего раздела будем изучать язык намного интенсивней.

Для того, чтобы получше понять сегментацию памяти, нам нужно воспользоваться отладчиком. Я работал с CodeView (CV.EXE) и AFD Pro (AFD.EXE).

Допустим, Вы написали программу на ассемблере и назвали ее lesson3.asm. Сассемблировав, Вы получили файл lesson3.com. Тогда, чтобы запустить ее под отладчиком CodeView / AFD, необходимо набрать в командной строке MS-DOS следующее:
CV.EXE lesson3.com
либо:
AFD.EXE lesson3.com

Итак, вдохните поглубже и - вперед!

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

Предположим, что:
нуль будет обозначаться как 0000 (четыре нуля),
Один - 0001,
Два - 0010 (т.е. правую единицу заменяем на 0, а вторую устанавливаем в 1).
Далее так:
Три - 0011
Четыре - 0100
Пять - 0101
Шесть - 0110
Семь - 0111
Восемь - 1000
Девять - 1001
и т.д.

Уловили принцип? "0" и "1" - это т.н. биты. Один бит, как Вы уже заметили, может быть нулем или единицей, т.е. размагничена или намагничена та или иная дорожка ("0" и "1" - это условное обозначение). Присмотревшись, можно обнаружить, что каждый следующий установленный бит (начиная справа) увеличивает число в два раза: 0001 в нашем примере - единица; 0010 - два; 0100 - четыре; 1000 - восемь и т.д. Это и есть т.н. двоичная форма представления данных.

Т.о., чтобы обозначить числа от 0 до 9, нам нужно четыре бита (хоть они и не до конца использованы. Можно было бы продолжить: десять - 1010, одиннадцать - 1011…, пятнадцать - 1111).
Компьютер хранит данные в памяти именно так. Для обозначения какого-нибудь символа (цифры, буквы, запятой, точки...) в компьютере используется определенное количество бит. Компьютер "распознает" 256 (от 0 до 255) различных символов по их коду. Этого достаточно, чтобы вместить все цифры (0 - 9), буквы латинского алфавита (a - z, A - Z), русского (а - я, А - Я) и др. Для представления символа с максимально возможным кодом (255) нужно 8 бит. Эти 8 бит называются байтом. Т.о. один любой символ - это всегда 1 байт (см. рис. 1).

01011010 Р Н Р Н Н РНР
Рис. 1. Один байт с кодом символа «Z» (символы «Н» и «Р» обозначают Намагничено или Размагничено соответственно)

Можно элементарно проверить. Создайте в текстовом редакторе файл с любым именем и запишите в нем один символ, например, "М" (но не нажимайте Enter!). Если Вы посмотрите его размер, то файл будет равен 1 байту. Если ваш редактор позволяет смотреть файлы в шестнадцатеричном формате, то Вы сможете узнать и код сохраненного символа. В данном случае - буква "М" имеет код 4Dh в шестнадцатеричной системе, которую мы уже знаем или 1001101 в двоичной. Т.о. слово "диск" будет занимать 4 байта или 4*8 = 32 бита. Как Вы уже поняли, компьютер хранит в памяти не сами буквы (символы) этого слова, а последовательность "единичек" и "ноликов".

"Почему же тогда на экране мы видим набор символов (текст, предложения, слова), а не "единички-нолики"? - спросите Вы. Чтобы удовлетворить Ваше любопытство, я забегу немного вперед и скажу, что всю работу по выводу самогo символа на экран (а не битов) выполняет видеокарта (видеоадаптер), которая находится в Вашем компьютере. И если бы ее не было, то мы, естественно, ничего бы не видели, что у нас отображено на экране.

В Ассемблере после двоичного числа всегда должна стоять буква "b". Это нужно для того, чтобы при ассемблировании программы Ассемблер смог отличать десятичные, шестнадцатеричные и двоичные числа. Например: 10 - это "десять", 10h - это "шестнадцать" а 10b - это "два" в десятичной системе. Т.о. в регистры можно загружать двоичные, десятичные и шестнадцатеричные числа.

Например:
mov ax,20
mov bh,10100b
mov cl,14h
В результате в регистрах AX, BH и CL будет находится одно и тоже число, только загружаем мы его, используя разные системы счисления. Компьютер же будет хранить его в двоичном формате (как в регистре BH).

Итак, подведем итог. В компьютере вся информация хранится в двоичном формате (двоичной системе) примерно в таком виде: 10101110 10010010 01111010 11100101 (естественно, без пробелов. Для удобства я разделил байты). Восемь бит - это один байт. Один символ занимает один байт, т.е. восемь бит. По-моему, ничего сложного. Очень важно уяснить данную тему, так как мы будем постоянно пользоваться двоичной системой, и Вам необходимо знать ее на "отлично". В принципе, даже если что-то не совсем понятно, то - не отчаивайтесь! Со временем все станет понятно.

Как перевести двоичное число в десятичное:
Надо сложить двойки в степенях, соответствующих позициям, где в двоичном стоят единицы. Например:
Возьмем число 20. В двоичной системе оно имеет следующий вид: 10100b Итак (начнем слева направо, считая от 4 до 0; число в нулевой степени всегда равно единице (вспоминаем школьную программу по математике)): 10100b = 1*16 + 0*8 + 1*4 + 0*2 + 0*1 = 20

16+0+4+0+0 = 20

Андрей Литвиенко предложил алгоритм по переводу двоичных чисел в десятичные и наоборот:

Вот таблица в помощь:

128 | 64 | 32 | 16 | 8 | 4 | 2 | 1
0 | 0 | 0 | 1 | 0 | 1 | 0 | 0

В верхнем столбике вписываем разряды, а в нижнем - биты.

Теперь просто складываем:

128*0 + 64*0 + 32*0 + 16*1 + 8*0 + 4*1 + 2*0 + 1*0 = 20

или еще проще:

0+0+0+16+0+4+0+0 = 20

Спасибо Андрею!

Как перевести десятичное число в двоичное:
Можно делить его на два, записывая остаток справа налево:
20/2 = 10, остаток 0
10/2=5, остаток 0
5/2=2, остаток 1
2/2=1, остаток 0
1/2=0, остаток 1
В результате получаем: 10100b = 20

Как перевести шестнадцатеричное число в десятичное:
В шестнадцатеричной системе номер позиции цифры в числе соответствует степени, в которую надо возвести число 16:
8Ah = 8*16 + 10 (0Ah) = 138

В настоящий момент есть множество калькуляторов, которые могут считать и переводить числа в разных системах счисления. Например, калькулятор Windows, который должен быть в инженерном виде. Очень удобен калькулятор и в DOS Navigator'е. Если у Вас есть он, то отпадает необходимость в ручном переводе одной системы в другую, что, естественно, упростит работу. Однако, знать этот принцип крайне важно!

Сегментация памяти в DOS

Возьмем следующее предложение: "Изучаем сегменты памяти". Теперь давайте посчитаем, на каком месте стоит буква "ы" в слове "сегменты" от начала предложения, включая пробелы... На шестнадцатом. Подчеркну, что мы считали от начала предложения.

Теперь немного усложним задачу и разобьем предложение следующим образом (символом "_" обозначен пробел):

Пример № 1:
0000: Изучаем_
0010: сегменты_
0020: памяти
0030:

В слове "Изучаем" символ "И" стоит на нулевом месте; символ "з" на первом, "у" на втором и т.д. В данном случае мы считаем буквы начиная с нулевой позиции, используя два числа. Назовем их сегмент и смещение. Тогда, символ "ч" будет иметь следующий адрес: 0000:0003, т.е. сегмент 0000, смещение 0003. Проверьте...

В слове "сегменты" будем считать буквы начиная с десятого сегмента, но с нулевого смещения. Тогда символ "н" будет иметь следующий адрес: 0010:0005, т.е. пятый символ начиная с десятой позиции: 0010 - сегмент, 0005 - смещение. Тоже проверьте...

В слове "память" считаем буквы начиная с 0020 сегмента и также с нулевой позиции. Т.о. символ "а" будет иметь адрес 0020:0001, т.е. сегмент - 0020, смещение - 0001. Опять проверим...

Итак, мы выяснили, что для того, чтобы найти адрес нужного символа, необходимы два числа: сегмент и смещение внутри этого сегмента. В Ассемблере сегменты хранятся в сегментных регистрах: CS, DS, ES, SS (см. предыдущую главу), а смещения могут храниться в других (но не во всех). Не все так сложно, как кажется. Опять-таки, со временем Вы поймете принцип.

Регистр CS служит для хранения сегмента кода программы (Code Segment - сегмент кода);
Регистр DS - для хранения сегмента данных (Data Segment - сегмент данных);
Регистр SS - для хранения сегмента стека (Stack Segment - сегмент стека);
Регистр ES - дополнительный сегментный регистр, который может хранить любой другой сегмент (например, сегмент видеобуфера).

Пример № 2:
Давайте попробуем загрузить в пару регистров ES:DI сегмент и смещение буквы "м" в слове "памяти" из Примера № 1 (см. выше). Вот как это запишется на Ассемблере:

(1) mov ax,0020
(2) mov es,ax
(3) mov di,2

Теперь в регистре ES находится сегмент с номером 20, а в регистре DI - смещение к букве (символу) "м" в слове "памяти". Проверьте, пожалуйста...

Здесь стоит отметить, что загрузка числа (т.е. какого-нибудь сегмента) напрямую в сегментный регистр запрещена. Поэтому мы в строке (1) загрузили сегмент в AX, а в строке (2) загрузили в регистр ES число 20, которое находилось в AX:

mov ds,15 // ошибка!
mov ss,34h // ошибка!

Когда мы загружаем программу в память, она автоматически располагается в первом свободном сегменте. В файлах типа *.com все сегментные регистры автоматически инициализируются для этого сегмента (устанавливаются значения равные тому сегменту, в который загружена программа). Это можно проверить при помощи отладчика. Если, например, мы загружаем программу типа *.com в память, и компьютер находит первый свободный сегмент с номером 5674h, то сегментные регистры будут иметь следующие значения:
CS = 5674h
DS = 5674h
SS = 5674h
ES = 5674h

Иначе говоря: CS=DS=SS=ES=5674h

Код программы типа *.com должен начинаться со смещения 100h. Для этого мы, собственно, и ставили в наших прошлых примерах программ оператор ORG 100h, указывая Ассемблеру при ассемблировании использовать смещение 100h от начала сегмента, в который загружена наша программа (позже мы рассмотрим почему так). Сегментные же регистры, как я уже говорил, автоматически принимают значение того сегмента, в который загрузилась наша программа.

Пара регистров CS:IP задает текущий адрес кода. Теперь рассмотрим, как все это происходит на конкретном примере:

Пример № 3.
(01) CSEG segment
(02) org 100h
(03) _start:
(04)	mov ah,9
(05)	mov dx,offset My_name
(06)	int 21h
(07)	int 20h
(08) My_name db 'Dima$'
(09) CSEG ends
(10) end _start

Итак, строки (01) и (09) описывают сегмент:

  • CSEG (даем имя сегменту) segment (оператор Ассемблера, указывающий, что имя CSEG - это название сегмента);
  • CSEG ends (END Segment - конец сегмента) указывает Ассемблеру на конец сегмента.

Строка (02) сообщает, что код программы (как и смещения внутри сегмента CSEG) необходимо отсчитывать с 100h. По этому адресу в память всегда загружаются программы типа *.com.

Запускаем программу из Примера № 3 в отладчике. Допустим, она загрузилась в свободный сегмент 1234h. Первая команда в строке (04) будет располагаться по такому адресу:
1234h:0100h (т.е. CS = 1234h, а IP = 0100h) (посмотрите в отладчике на регистры CS и IP).

Перейдем к следующей команде (в отладчике CodeView нажмите клавишу F8, в AFD - F1, в другом - посмотрите какая клавиша нужна; будет написано что-то вроде "F8-Step" или "F7-Trace"). Теперь Вы видите, что изменились следующие регистры:

  • AX = 0900h (точнее, AH = 09h, а AL = 0, т.к. мы загрузили командой mov ah,9 число 9 в регистр AH, при этом не трогая AL. Если бы AL был равен, скажем, 15h, то после выполнения данной команды AX бы равнялся 0915h)
  • IP = 102h (т.е. указывает на адрес следующей команды. Из этого можно сделать вывод, что команда mov ah,9 занимает 2 байта: 102h - 100h = 2).

Следующая команда (нажимаем клавишу F8 / F1) изменяет регистры DX и IP. Теперь DX указывает на смещение нашей строки ("Dima$") относительно начала сегмента, т.е. 109h, а IP равняется 105h (т.е. адрес следующей команды). Нетрудно посчитать, что команда mov dx,offset My_name занимает 3 байта (105h - 102h = 3).

Обратите внимание, что в Ассемблере мы пишем:
mov dx,offset My_name
а в отладчике видим следующее:
mov dx,109 (109 - шестнадцатеричное число, но CodeView и многие другие отладчики символ 'h' не ставят. Это надо иметь в виду).

Почему так происходит? Дело в том, что при ассемблировании программы, программа-ассемблер (MASM / TASM) подставляет вместо offset My_name реальный адрес строки с именем My_name в памяти (ее смещение). Можно, конечно, записать сразу:
mov dx,109h
Программа будет работать нормально. Но для этого нам нужно высчитать самим этот адрес. Попробуйте вставить следующие команды, начиная со строки (07) в Примере № 3:

(07)	int 20h
(08)	int 20h
(09) My_name db 'Dima$'
(10) CSEG ends
(11) end _start

Просто продублируем команду int 20h (хотя, как Вы уже знаете, до строки (08) программа не дойдет).

Теперь ассемблируйте программу заново. Запускайте ее под отладчиком. Вы увидите, что в DX загружается не 109h, а другое число. Подумайте, почему так происходит. Это просто!

В окне "Memory" ("Память") отладчика CodeView (у AFD нечто подобное) Вы должны увидеть примерно следующее:

1234:0000CD 20 00 A0 00 9A F0 FE= .a.
№1№2№3№4
Позиция №1 (1234) - сегмент, в который загрузилась наша программа (может быть любым).
Позиция №2 (0000) - смещение в данном сегменте (сегмент и смещение отделяются двоеточием (:)).
Позиция №3 (CD 20 00 ... F0 FE) - код в шестнадцатеричной системе, который располагается с адреса 1234:0000.
Позиция №4 (= .a.) - код в ASCII (ниже рассмотрим), соответствующий шестнадцатеричным числам с правой стороны.

В Позиции №2 (смещение) введите значение, которое находится в регистре DX после выполнения строки (5). После этого в Позиции №4 Вы увидите строку "Dima$", а в Позиции №3 - коды символов "Dima$" в шестнадцатеричной системе... Так вот что загружается в DX! Это не что иное, как АДРЕС (смещение) нашей строки в сегменте!

Но вернемся. Итак, мы загрузили в DX адрес строки в сегменте, который мы назвали CSEG (строки (01) и (09) в Примере № 3). Теперь переходим к следующей команде: int 21h. Вызываем прерывание DOS с функцией 9 (mov ah,9) и адресом строки в DX (mov dx,offset My_name).

Как я уже говорил раньше, для использования прерываний в программах, в AH заносится номер функции. Номера функций желательно запоминать (хотя бы часто используемые) с тем, чтобы постоянно не искать в справочниках, что делает функция.

Наше первое прерывание

Функция 09h прерывания 21h выводит строку на экран, адрес которой указан в регистре DX.

Вообще, любая строка, состоящая из ASCII символов, называется ASCII-строка. ASCII символы - это символы от 0 до 255 в DOS, куда входят буквы русского и латинского алфавитов, цифры, знаки препинания и пр.

Изобразим это в таблице (так всегда теперь будем делать): Функция 09h прерывания 21h - вывод строки символов на экран в текущую позицию курсора:

Вход: AH = 09h DX = адрес ASCII-строки символов, заканчивающийся символом '$'
Выход: Ничего
В поле "Вход" мы указываем, в какие регистры что загружать перед вызовом прерывания, а в поле "Выход" - что возвращает функция. Сравните эту таблицу с Примером № 3.

Вот мы и рассмотрели сегментацию памяти. Если я что-то упустил, то это рассмотрим в последующих главах. Очень надеюсь на то, что Вы разобрались в данной теме. По крайней мере, уловили принцип сегментации памяти…

Программка для практики

Теперь интересная программка для практики, которая выводит в верхний левый угол экрана веселую рожицу на синем фоне (данная программа будет работать только на цветных мониторах CGA, EGA, VGA, SVGA):

(01) CSEG segment
(02) org 100h
(03) _beg:
(04)	mov ax,0B800h
(05)	mov es,ax
(06)	mov di,0
(07)
(08)	mov ah,31
(09)	mov al,1
(10)	mov es:[di],ax
(11)
(12)	mov ah,10h
(13)	int 16h
(14)
(15)	int 20h
(16)
(17) CSEG ends
(18) end _beg

Многие операторы Вы уже знаете. Поэтому я буду объяснять только новые. В данном примере, для вывода символа на экран, мы используем метод прямого отображения в видеобуфер. Что это такое - будет подробней рассмотрено в последующих главах.

В строках (04) и (05) загружаем в сегментный регистр ES число 0B800h, которое соответствует сегменту дисплея в текстовом режиме (запомните его!). В строке (06) загружаем в регистр DI нуль. Это будет смещение относительно сегмента 0B800h. В строках (08) и (09) в регистр AH заносится атрибут символа (31 - ярко-белый символ на синем фоне) и в AL - ASCII-код символа (01 - это рожица). В строке (10) заносим по адресу 0B800:0000h (т.е. первый символ в первой строке дисплея - верхний левый угол) атрибут и ASCII-код символа (31 и 01 соответственно) (сможете разобраться?).

Обратите внимание на запись регистров в строке (10). Квадратные скобки ( [ ] ) указывают на то, что надо загрузить число не в регистр, а по адресу, который содержится в этом регистре (в данном случае, как уже отмечалось, - это 0B800:0000h).

Можете поэкспериментировать с данным примером. Только не меняйте пока строки (04) и (05). Сегментный регистр должен быть ES (можно, конечно, и DS, но тогда надо быть осторожным). Более подробно данный метод рассмотрим позже. Сейчас нам из него нужно понять принцип сегментации на практике.

Это интересно.
Следует отметить, что вывод символа прямым отображением в видеобуфер является самым быстрым. Выполнение команды в строке (10) занимает 3 - 5 тактов. Т.о. на Pentium-100Mhz можно за секунду вывести 20 миллионов(!) символов или чуть меньше точек на экран! Если бы все программисты (а особенно Microsoft) выводили бы символы или точки на экран методом прямого отображения в видеобуфер на Ассемблере, то программы бы работали чрезвычайно быстро... Я думаю, Вы представляете...


ГЛАВА 04

  • Немного теории
  • Еще немного о сегментации памяти
  • Создание циклов
  • Программка для практики

Немного теории

Итак, давайте подведем с вами итог. Вы уже знаете:

  • Шестнадцатеричную систему счисления;
  • Двоичную систему счисления;
  • Некоторые регистры микропроцессоров Intel 8086/8088/80186;
  • Основы сегментации памяти в DOS;
  • Операторы Ассемблера:
    • org (с какого места отсчитывать смещение)
    • mov(загрузка данных в регистр или память)
    • add(сложение)
    • sub(вычитание)
    • inc(увеличение на единицу)
    • int(вызов прерывания)
  • Функцию 09h прерывания 21h (вывод строки на экран в текущую позицию курсора);
  • Функцию 10h прерывания 16h (ожидание нажатия клавиши).

В принципе, довольно много, чтобы начать серьезное и интенсивное обучение языка Ассемблера. Самое интересное только начинается ! Если что-то из вышеперечисленного вам не совсем понятно, то спрашивайте, я обязательно отвечу. Итак, теперь только вперед!

Еще немного о сегментации памяти.

Давайте возьмем часть примера, который мы уже рассматривали, но кое-что в нем упустили.

(1) ...
(2) mov ah,9
(3) mov dx,offset My_string
(4) int 21h
(5) ....
(6) My_string db 'Ура!$'
(7) ...

Опустим некоторые операторы: строки (1), (5) и (7). Их вы уже знаете.

В строке (3) загружаем в регистр DX АДРЕС строки в памяти. Обратите внимание на запись: mov dx,offset My_string. Вы уже знаете, что оператор mov загружает в регистр число. Например:

mov cx,125

В строке (3) мы видим пока еще неизвестный нам оператор offset. Что же он делает? И почему нельзя записать вот так:
mov dx,My_string?

Offset по-английски - это смещение. Когда, при ассемблирвании, Ассемблер дойдет до этой строки, он заменит
offset My_string на АДРЕС (смещение) этой строки в памяти. Если мы запишем
mov dx,My_string (хотя, правильнее будет
mov dx,word ptr My_string,
но об этом позже), то в DX загрузится не адрес (смещение), а первые два символа нашей строки (в данном случае "Ур"). Почему два? Вы не знаете? Потому, что DX - шестнадцатиразрядный регистр, в который можно загрузить два байта. А один символ, как вы уже знаете, всегда один байт.

Можно записать и так:
mov dl,My_string
(здесь правильнее будет
mov dl,byte ptr My_string). В этом случае что будет находится в DL? Символ "У"! Потому, что DL восьмиразрядный регистр и может хранить только один байт.

Несколько слов про записи вида
mov dl,byte ptr My_string и
mov dx,word ptr My_string.

Byte (думаю, все знают) - это байт. Word - слово (два байта). Посмотрите внимательно на приведенные выше строки. Вы заметите, что когда используется восьмиразрядный регистр (DL), мы пишем
byte. А когда шестнадцатиразрядный (DX) - word. Это указывает Ассемблеру, что мы хотим загрузить именно байт либо слово.

Вспомним, что в DOS для формирования адреса используется сегмент и смещение. Данный пример - не исключение. Для формирования адреса строки "Ура!$" используется пара регистров DS (сегмент) и DX (смещение). Почему же мы ничего не загружаем в DS? Дело в том, что при загрузке *.com-программы в память (а мы пока создаем только такие), все сегментные регистры принимают значение равное тому сегменту, в который загрузилась наша программа (в т.ч. и DS). Поэтому нет необходимости загружать в DS сегмент строки (он уже загружен). Программа типа *.com всегда занимает один сегмент, поэтому размер программ такого типа ограничен 64 килобайтами. Помните, почему?

Программы, написанные на "чистом" Ассемблере, очень компактны. И 64 килобайта для них довольно большой объем.

Помнится, писал когда-то я антивирусную оболочку типа "а-ля Нортон Коммандер" на Ассемблере. Так она заняла у меня примерно 40 килобайт, хотя и не выполняла всех функций NC, но делала кое-что другое. Еще пример: Volcov Commander поздних версий. Практически копия NC, но занимает всего 64000 байт (в отличие от Нортона). Я подозреваю, что писали ее если не на "чистом" Ассемблере, то хотя бы большую часть кода так точно. Да и работает Volkov гораздо быстрее Нортона.

Вернемся. Если есть желание поэксперементировать, то попробуйте перед вызовом 21h-ого прерывания загрузить в DS какое-нибудь число. Например, так:

...
mov dx,offset My_string
mov ax,10h
mov ds,ax
mov ah,9
int 21h
...

Вы увидите, что программа выведет не нашу строку, а какой-то "мусор" на экран, хотя в DX мы и загружаем адрес нашей строки, но сегмент другой. Только не забудьте восстановить DS после выполнения данной функции:

mov ax,cs
mov ds,ax

Итак, полное описание:

Функция 09h прерывания 21h - вывод строки символов на экран в текущую позицию курсора:

Вход: | AH = 09h DS:DX = адрес ASCII-строки символов, заканчивающийся '$'
Выход: | ничего

Ну, разобрались с этим окончательно...

Новые операторы.

Создание циклов.

Что такое цикл? Допустим, нам нужно выполнить некоторый код программы несколько раз. Возьмем, к примеру, вывод строки функцией 09h прерывания 21h:

Пример 1

mov ah,9
mov dx,offset Str
int 21h
mov ah,9
mov dx,offset Str
int 21h
mov ah,9
mov dx,offset Str
int 21h

Этот участок кода выведет 3 раза на экран некую строку Str. Код получается громоздким, неудобно читать. Размер программы разрастается... Для выполнения подобных примеров используется оператор loop (вспоминаем, как мы оформляем новые операторы):

Команда | Перевод (с англ.) | Назначение | Процессор
LOOP метка | loop - петля | Организация циклов | 8086

Количество повторов задается в регистре CX (счетчик). Вот как можно использовать этот оператор на практике (изменим Пример 1):

Пример 2:

(1) mov cx,3
(2) Label_1:
(3) mov ah,9
(4) mov dx,offset Str
(5) int 21h
(6) loop Label_1
(7) ...

В строке (1) загружаем в CX количество повторов (отсчет будет идти от 3 до 0). В строке (2) создаем метку (Label - метка). Далее (строки (3)-(5)) выводим сообщение. И в строке (6) оператор loop уменьшает на единицу CX и, если он не равен нулю, переходит на метку Label_1 (строка (2)). Итого строка будет выведена на экран три раза. Когда программа перейдет на строку (7), регистр CX будет равен нулю. В результате код программы уменьшается почти в три раза по сравнению с Примером 1.

Удобно? Без вопросов!

Тренироваться будем на практике, а теперь следующий оператор:

Команда | Перевод (с англ.) | Назначение | Процессор
JMP метка | jump - прыжок | Безусловный переход | 8086

Команда jmp просто переходит на указанную метку в программе:

(1) mov ah,9
(2) mov dx,offset Str
(3) int 21h
(4) jmp Label_2
(5)
(6) add cx,12
(7) dec cx
(8) Label_2:
(9) int 20h

В результате строки (5) - (7) работать не будут. Программа выведет сообщение на экран, а затем jmp перейдет к строке (8), после чего программа завершится.
Команда | Перевод (с англ.) | Назначение | Процессор
DEC приемник | decrement - декремент | Уменьшение на 1 | 8086

Оператор dec уменьшает значене приемника на 1:

mov ah,12 ; загружаем в AH число 12
dec ah   ; теперь AH=11

С помощью данного оператора можно также создавать циклы, которые будут работать быстрее оператора loop. Следующий пример будет работать также, как Пример 2, только чуть-чуть быстрее:

Пример 3:

(1) mov cx,3
(2) Label_1:
(3) mov ah,9
(4) mov dx,offset Str
(5) int 21h
(6) dec cx
(7) jnz Label_1

Не обращайте внимание на строку (7). Мы ее рассмотрим позже. Я привел этот пример для того, чтобы показать, что один и тот же прием в Ассемблере можно выполнить разными операторами. И чем лучше программист владеет ими, тем компактнее и быстрее программа будет работать. Поэтому и получается, что разные программисты пишут на одном языке, но скорость и объем программы разные. В процессе обучения, я буду также учить вас оптимизировать программы.

Программка для практики

Усовершенствуем программу из предыдущей главы, которая выводила в верхний левый угол "рожицу" прямым отображением в видеобуфер

(1) CSEG segment
(2) org 100h
(3) Begin:
(4) mov ax,0B800h
(5) mov es,ax
(6) mov di,0
(7) mov al,1
(8) mov ah,31
(9) mov cx,2000
(10)
(11) Next_face:
(12) mov es:[di],ax
(13) add di,2
(14) loop Next_face
(15)
(16) mov ah,10h
(17) int 16h
(18) int 20h
(19) CSEG ends
(20) end Begin

Уфф! Длинная получилась. Прежде чем читать описание программы, попробуйте сами разобраться, что в итоге получится. Поверьте, это принесет вам пользу. Все ведь очень просто!

Теперь описание программы.

Строки с (1) по (10) и с (15) по (20) вы уже знаете. Объясню только новое.

Строка (11) - это метка, как бы "голова" нашего цикла. Строка (14) - "хвост" цикла. Все, что находится в пределах строк (10) - (14), является циклом. Сам цикл будет повторяться 2000 раз, для чего мы и заносим в CX число 2000 (строка (9)).

В строке (12) записываем в видеобуфер (0B800:DI) число в AX (это символ (строка (7) и атрибут (строка (8)). Итак, первый символ занесли. Что делаем дальше?

Дальше увеличиваем регистр DI на 2 для того чтобы перейти к адресу следующего символа. Почему на 2? Дело в том, что в видеобуфере один символ занимает 2 байта: сам символ и его атрибут. Т.к. символ у нас в AL, а атрибут в AH, и мы загрузили уже эти два байта в строке (12), то и увеличиваем DI (смещение) на 2.

DI теперь указывает на адрес для следующего символа. Осталось уменьшить счетчик (CX) на 1 и повторить. Что мы, собственно, и делаем в строке (14).

Все! Обратите внимание на скорость вывода символов при запуске программы.

Еще раз напоминаю: пожалуйста, печатайте все программы сами! Так вы быстрее освоите Ассемблер!


ГЛАВА 05

  • Вопросы по предыдущим главам
  • Немного теории
  • Новые операторы
  • Программка для практики

Вопросы по предыдущим главам

Есть еще несколько вопросов по поводу отладчика CodeView. Дело в том, что для полноценной работы CV недостаточно одного файла cv.exe. Нужно иметь еще несколько дополнительных библиотек, которые в 2 раза больше cv.exe. AFD, конечно, не очень актуален в настоящий момент по следующим причинам:

  • не поддерживает 32-х разрядные регистры;
  • не распознает формат PE и NE (Windows).

Зато у него есть преимущества:

  • удобен и прост в использовании;
  • показывает в одном окне регистры, код, память и др.

Для изучения работы программ в DOS его больше чем достаточно.

В прежние времена я работал только с AFD. Затем, к моему сожалению, его потерял. Теперь AFD можно взять на моем сайте (прибл. 64 Кб).

Gregory спрашивал, каким ассемблером лучше пользоваться: Borland (TASM) или Microsoft (MASM). Под DOS я всегда писал программы используя MASM 5.10, которым был очень доволен. В принципе, большой разницы между MASM и TASM на данный момент нет. Но, на мой взгляд, под Win9x лучше использовать TASM32.

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

Дизассемблер тоже не так-то просто написать, хотя вполне реально. Для этого нужно будет изучить формирование машинного кода в компьютере, а это довольно большой курс.

Viewer для просмотра графики можно сделать, если кто-нибудь пришлет формат того или иного файла (jpg, bmp и т.п.). Я, к сожалению, этим никогда не занимался...

Немного теории

Я получил много писем с таким вопросом:

TASM выдает ошибку: Near jump or call to different CS.

Я предложил вставить строку assume cs:CSEG. Что же происходит?

Дело в том, что эта строка указывает Ассемблеру на привязку сегментного регистра CS к нашему сегменту (CSEG). MASM ассемблирует прекрасно и без этой строки. Если оператор assume отсутствует, то MASM как бы по умолчанию вставляет ее автоматически.

Другое дело TASM. Он, встретив в программе строки вида:

loop Label_1
jmp Label_2
call Procedure

не может "понять" к какому сегменту следует обратиться (CS, DS, ES) и выдает сообщение об ошибке.

Как уже говорилось, мы пишем com-файлы в которых всего один сегмент (мы обзываем его CSEG). Если вы создадите еще один (например, DSEG), то компоновщик (link.exe), при попытке создать com-файл, выдаст ошибку.

Чтобы полностью закрыть данную тему, привожу полный вид разбираемой нами строки:

assume cs:CSEG, ds:CSEG, es:CSEG, ss:CSEG

Этим мы указываем Ассемблеру на то, что сегментные регистры CS, DS, ES, SS будут указывать на наш единственный сегмент.

Новые операторы

Сегодня рассмотрим всего один оператор, но зато какой!
Команда | Перевод (с англ.) | Назначение | Процессор
CALL метка | call - вызов | Вызов подпрограммы | 8086

Итак, вы уже немного знакомы с подпрограммами.

Допустим, нам необходимо написать программу, которая выводит на экран сообщение

Нажмите любую клавишу...

ждет нажатия клавиши, а затем выводит еще одно сообщение:

Вы успешно нажали клавишу!

ждет от пользователя клавишу и завершается.

Что нужно для этого? Вызвать два раза функцию 09h прерывания 21h и столько же функцию 10h прерывания 16h (вы это уже прекрасно знаете).

Вот так:

...
(1) mov ah,9
(2) mov dx,offset Mess1
(3) int 21h
(4) mov ah,10h
(5) int 16h
(6) mov ah,9
(7) mov dx,offset Mess2
(8) int 21h
(9) mov ah,10h
(10) int 16h
(11) int 20h
...
(12) Mess1 db 'Нажмите любую клавишу...$'
(13) Mess2 db 'Вы успешно нажали клавишу!$'
...

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

Смотрим дальше. Строки (1) - (3) и (6) - (8) выводят строку. Они очень похожи, за исключением загрузки в DX разных строк.

Строки же (4) - (5) и (9) - (10) полностью идентичны. Получается, что мы теряем байты...

Чтобы упростить программу и, тем самым, уменьшить ее размер, воспользуемся оператором CALL (создадим подпрограммы). Вот что у нас получится:

...
(1) mov dx,offset Mess1
(2) call Out_string
(3) call Wait_key
(4) mov dx,offset Mess2
(5) call Out_string
(6) call Wait_key
(7) int 20h
(8) Out_string proc
(9) mov ah,9
(10) int 21h
(11) ret
(12) Out_string endp
(13) Wait_key proc
(14) mov ah,10h
(15) int 16h
(16) ret
(17) Wait_key endp
(18) Mess1 db 'Нажмите любую клавишу...$'
(19) Mess2 db 'Вы успешно нажали клавишу!$'
...

Длинее получилось... Не обращайте на это пока внимания. Подпрограммы используются в более сложных программах. Наш же вариант представлен для изучения.

Итак, что здесь происходит? Думаю, что многие уже догадались. Однако, нужны некоторые пояснения.

Минутку внимания! В строке (1) загружаем в DX адрес строки Mess1. В строке (2) вызываем подпрограмму, которую мы назвали Out_string. Что делает компьютер? Он запоминает адрес (смещение) следующей команды (строка (3)) и переходит на метку Out_string (строка (8)). DX при этом не меняется (т.е. в нем сохраняется адрес строки Mess1)! В строках (9) - (10) используем функцию 09h прерывания 21h для вывода строки на экран. В строке (11) компьютер берет запомненный адрес и переходит на него (в данном случае на строку (3)) (ret - return - возврат). ВСЕ! Процедура отработала!

У вас, вероятно, появился вопрос: А где это, интересно, компьютер запоминает куда нужно возвращаться???

Отвечу вкратце, т.к. эта тема достойна отдельного выпуска. Есть такая область памяти. Назвается стек (stack). Вот именно туда и "ложится" адрес для возврата из подпрограммы. В следующем выпуске мы подробно рассмотрим стек, сегмент стека, а также регистры SS и SP, которые отвечают за стек.

Надеюсь, что разобраться в дальнейшей работе программы (начиная со строки (3)) не составит труда. Все ведь очень просто!

Обратите внимание на оформление процедуры:

______________
Out_string proc
Out_string endp
______________

Out_string - название процедуры

Proc - procedure - процедура

endp - end procedure - конец процедуры

Стоит еще отметить, что из одной подпрограммы можно вызывать другие. Из других - третьи. Главное правило - не запутаться в возвратах, иначе компьютер просто виснет! Внимательно следить за работой подпрограмм позволяет отладчик.

Программка для практики

Хотите увидеть всю мощь Ассемблера?

Давайте усовершенствуем программу с предыдущей главы.

(1) CSEG segment
(2) assume CS:CSEG, DS:CSEG, ES:CSEG, SS:CSEG
(3) org 100h
(4) Start:
(5) mov ax,0B800h
(6) mov es,ax
(7) mov al,1
(8) mov ah,31
(9) mov cx,254
(10) Next_screen:
(11) mov di,0
(12) call Out_chars
(13) inc al
(14) loop Next_screen
(15) mov ah,10h
(16) int 16h
(17) int 20h
(18) Out_chars proc
(19) mov dx,cx
(20) mov cx,2000
(21) Next_face:
(22) mov es:[di],ax
(23) add di,2
(24) loop Next_face
(25) mov cx,dx
(26) ret
(27) Out_chars endp
(28) CSEG ends
(29) end Start

Не устали?

Будем разбираться... Хотя, собственно, ничего сложного нет.

Итак, строки (1) - (8), (15) - (17) и (28) - (29) опускаем. Вопросов по ним быть не должно.

В строке (9) заносим в CX число 254, указывающее на то, сколько раз будет выполняться основной цикл. Строки (10) и (14) - "голова" и "хвост" нашего основного цикла соответственно. DI будет меняться в процедуре, поэтому нам необходимо будет его постоянно аннулировать (11). В строке (12) вызываем процедуру, которая заполнит экран кодом символа, который находится в AL (в первом случае - это 01 "рожица"). Все! Теперь экран заполнен кодом 01. При этом DI будет равно 2001 (поэтому нам и нужно его обнулять). Далее увеличим на единицу код, который находится в AL (теперь AL содержит 02 - тоже "рожица", но немного другого вида) (строка (13)), уменьшим счетчик на 1 и перейдем к заполнению экрана кодом 02 (строка 14). И так 254 раза.

Теперь процедура.

В строке (19) сохраним CX (просто перенесем его в DX), т.к. он будет изменен во вложенном цикле ниже. Строки (21) и (24) - "голова" и "хвост" вложенного цикла, который будет выполняться 2000 раз (надо же заполнить экран полностью) (см. строку (20), в которой загружаем счетчик в CX). Все! Осталось только восстановить CX (строка (25)) и выйти из подпрограммы (26).

Итак, у нас здесь два цикла: основной и вложенный (так мы их назвали). Основной выполняется 254 раза, а вложенный - 2000 раз.

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

Для исследования данного примера я рекомендую вам воспользоваться отладчиком. Т.к. у многих есть CV и AFD, то вкратце еще раз объясню, какие кнопки давить.

CodeView:

F8 - пошаговое выполнение команд (с заходом в подпрограммы);

F10 - выполнение команды за один шаг (без захода в подпрограммы).

AFD:

F1 - пошаговое выполнение команд (с заходом в подпрограммы и прерывания);

F2 - выполнение команды за один шаг (без захода в подпрограммы и прерывания).

Ну, поэксперементируйте с клавишами. Принцип прост.

Удачного программирования!


ГЛАВА 06

  • Стек/стэк (stack)
  • Программка для практики

Стек/стэк (stack)

Немного теории.

Друзья мои! Я настоятельно рекомендую Вам выполнять все, что я вас прошу сделать в этом разделе (набирать программы, запускать их под отладчиком). Так вам будет гораздо легче разобраться со стеком.

У кого до сих пор нет отладчика, возьмите его, пожалуйста, здесь:

www.Kalashnikoff.ru/Assembler/

Итак, что же такое стек и для чего он нужен?

Стек - это специально отведенная область памяти для хранения промежуточных данных.

Давайте немного договоримся об определениях.

Пусть будет так, что сегмент "растет" сверху вниз:

0000
0001
0002
...
FFFE
FFFF

Т.е. мы как бы погружаемся под землю. Таким образом (сверху вниз) выполняется программа (если, конечно, не встречаются инструкции (команды) типа jmp, call и т.п.).

Стек же наоборот пополняется снизу вверх. Вершина стека - 0FFFFh, а низ (дно) - 0000h.

Когда мы вызываем какую-нибудь подпрограмму командой call, процессор кладет в стек адрес следующей за командой call инструкции. Следить за стеком позволяет пара регистров SS:SP. Многие уже, наверное, заметили, что при запуске какой-нибудь com-программы регистр SP равен 0FFFEh, а сегментный регистр SS, как уже упоминалось в предыдущих главах, равен нашему единственному сегменту (CSEG) (как и CS, DS, ES).

Теперь вам необходимо вооружиться отладчиком. Давайте рассмотрим вышесказанное на примере:

Напечатайте такую программу в редакторе (обязательно!):

CSEG segment
assume cs:CSEG, ds:CSEG, es:CSEG, ss:CSEG
org 100h
begin:
call Our_proc
int 20h
Our_proc proc
ret
Our_proc endp
CSEG ends
end Begin

Ничего сложного... Запускаем отладчик.

Итак, смотрим на пару регистров SS:SP. SS=CS=DS=ES (это понятно). SP=0FFFEh (т.е. указывает на вершину стека).

Теперь заходим в процедуру. Для CV нажимаем F8, для AFD F1.

Опа! SP изменился. Он уменьшился на 2. Компьютер поместил в стек адрес возврата из процедуры (на инструкцию int 20h) Проще говоря, call перешел на метку Our_proc, поместив в стек адрес возврата из этой подпрограммы.

Нажимаем еще раз F8/F1. Что получилось? SP опять изменился! Но теперь он увеличился на 2 (стал равным 0FFFEh). Т.е. команда ret взяла из стека адрес возврата и перешла по нему. Как раз на int 20h.

В данном случае говорят, что мы выравнили стек. Он был изначально равен 0FFFEh и остался равен перед выходом 0FFFEh.

Вот один из способов использования стека. Но на этом мы не остановимся и рассмотрим несколько операторов, которые позволяют работать со стеком.
Оператор | Перевод | Применение | Процессор
PUSH приемник | push - втолкнуть | Поместить в стек число | 8086

Оператор | Перевод | Применение | Процессор
POP приемник | pop - вытолкнуть | Достать из стека число | 8086

Допустим, нам нужно временно сохранит какое-нибудь число. Например, перед вызовом процедуры, прерывания или циклом

Вот как это запишется:

...
(1) mov ax,345h
(2) push ax
(3) mov ah,10h
(4) int 16h
(5) pop ax
...

Здесь мы загружаем в AX число 345h, сохраняем его, ждем нажатия клавиши (при этом сама клавиша будет в AX, т.е. AX изменится) и восстанавливаем AX. В итоге AX будет содержать число 345h, что, как говорится, и требовалось доказать.

Однако, стоит заметить такой момент. Допустим, мы помещаем в стек следующие регистры: AX, BX, CX:

push ax
push bx
push cx

Обратите внимание, что восстанавливать со стека нужно в обратном порядке:

pop cx
pop bx
pop ax

Если вы поменяете местами регистры при восстановлении, то ничего страшного не произойдет, только содержать они будут другие числа. Например:

mov ax,1234h
mov bx,5678h
push ax
push bx
pop ax
pop bx

В итоге AX будет равен 5678h, а BX - 1234h.

Но в процедурах необходимо очень тщательно следить за стеком. Вот пример:

...
call Our_proc
int 20h
...
Our_proc proc
mov ax,15
push ax
mov ah,9
mov dx,offset Str
int 21h
ret
Our_proc endp
...

Обратите внимание, что мы "забыли" восстановить из стека AX в нашей процедуре (Our_proc). Что произойдет? Компьютер, дойдя до оператора ret, вытащит из стека не адрес возврата, а число 15 и перейдет на этот адрес. Что находится по адресу 15 - не известно. Машина, скорее всего, "зависнет". Надеюсь, что это понятно.

Следить за стеком (как уже говорилось) позволяет пара регистров SS (сегмент):SP (смещение).

Программист может менять как сегмент, так и смещение, но при этом следует иметь ввиду, что перед сменой регистров SS и SP необходимо запретить все прерывания, а после изменения разрешить (запрет прерываний вешает компьютер! Восстановление же - приводит к нормальной работе. После команды cli всегда должна идти sti, иначе компьютер виснет!). Это позволяют сделать следующие операторы:
Оператор | Перевод | Применение | Процессор
CLI | CLear Interrupt - запретить | Запретить прерывания | 8086

Оператор | Перевод | Применение | Процессор
STI | reSTore Interrupt - восстановить | Разрешить прерывания | 8086

Для чего это нужно? Давайте рассмотрим это в двух словах, т.к. механизму работы прерываний необходимо посвятить целый выпуск. Если что-то не понятно - просто опустите, не обращайте внимания

Когда вы создали программу и запустили ее на выполнение, то работает не только она одна. Простейший пример - таймер, который вызывается примерно 18 раз в секунду для обновления. Компьютер всегда что-то делает! Даже тогда, когда ждет от вас нажатия клавиши. Что происходит, когда вызывается прерывание от таймера? Примерно тоже, что и при вызове процедуры. Компьютер запоминает в стеке адрес текущей команды, а также все регистры и переходит на адрес прерывания, по которому находится процедура обработки этого прерывания (например, таймера, которая обновит показания часов/минут/секунд). Затем, как процедура отработала, компьютер восстановит из стека адрес возврата и все регистры, и наша программа пойдет работать дальше

Вот пример, в котором мы изменим регистры SS:SP, не запрещая прерывания:

(1) mov ax,100h
(2) mov ss,ax
(3) mov sp,200h

Допустим, прерывание таймера сработало после строки (2). Что у нас получилось? SS равен 100h, а SP еще не успел измениться. Получается, что сегмент стека верный, а смещение - осталось прежним (допустим, SP=0FFFEh). В итоге, SS=100h, а SP=0FFFEh. Компьюетр и сохранит по этому адресу данные. Какой при этом код программы затрется - не известно. Мы ведь хотели сделать SS=100h, а SP=200h! Хорошо, если две строки успели выполниться перед вызовом прерывания. Хорошо также, если по адресу 100h:0FFFEh ничего нет (память свободна). А если есть? Тогда компьютер, скорее всего, "зависнет".

Отсюда два правила:

1) Прежде чем менять регистры SS:SP, необходимо запретить все прерывания командой cli, а затем разрешить командой sti.

2) SS:SP нужно устанавливать на свободную область памяти. При этом следует убедиться, что код не утратил работоспособности.

Мы знаем, что после загрузки com-программы в память, SS равен сегменту, куда загрузилась программа, а SP - 0FFFEh. Код программы начинается с адреса 100h (org 100h). Вершина стека - конец нашего сегмента. Если наша программа занимает, скажем 2000h байт, то можем установить SP в 2200h. В этом случае мы отводим 100h (именно сто) байт для стека (т.к. программа загружается по адресу 100h, то 2000h нужно прибавить 100h). Стек, как вы помните, растет снизу вверх. Если мы переполним стек (например, поместим более 100h данных), то затрется часть нашей программы снизу. Имейте это ввиду!

Вот пример изменеия регистров стека:

cli
mov ax,0B900h
mov ss,ax
mov sp,100h
sti

Программка для практики

Прежде чем перейти к программе, рассмотрим новый оператор:
Оператор | Перевод | Применение | Процессор
NOP | No OPerand - нет операнда | Ничего не делает | 8086

Этот оператор делает то, что ничего не делает, но занимает один байт. Его обычно используют для резервирования места либо для того, чтобы "забить" ненужный код, когда исходник на Ассемблере отсутствует. Например, программа перед стартом проверяет версию MS-DOS. Версия, которая установлена на вашем компьютере, не соответствует требуемой программой. Для этого данным оператором "забивают" участок кода, который проверяет версию ОС.

Все это позволяет сделать Hacker's View, который можно взять на моем сайте:

www.Kalashnikoff.ru/Assembler/

Запомните машинный код данной комманды: 90h

Даю голову на отсечение, что ни один язык высокого уровня не позволяет сделать того, что может наша программа:

(1) CSEG segment
(2) assume cs:CSEG, es:CSEG, ds:CSEG, ss:CSEG
(3) org 100h
(4) Begin:
(5) mov sp,offset Lab_1
(6) mov ax,9090h
(7) push ax
(8) int 20h
(9) Lab_1:
(10) mov ah,9
(11) mov dx,offset Mess
(12) int 21h
(13) int 20h
(14) Mess db 'А все-таки она выводится!$'
(15) CSEG ends
(16) end Begin

То, что вы видите - обман зрения. На первый взгляд, программа что-то делает с регистром SP, а затем выходит. Строки (9) - (12) вообще не будут работать. Но это глубокое заблуждение!

Попробуйте запустить ее под отладчиком. Вы увидите, что CodeView, TurboDebuger, AFD будут сообщать какую-то ерунду (непонятные операторы, сообщения типа "программа завершилась", хотя строка не выведена и пр.). Но, если запустить ее просто из ДОС, то строка появится на экране, т.е. программа будет работать корректно!

Данный пример - типичный случай "заламывания рук" отладчикам (но не SoftIce!). И вы уже можете это делать!

Вывод один: указанные выше отладчики используют стек пользовательской программы (что это значит - думаю, вы разберетесь).

Ваша задача: разобрать "по полочкам" программу. Почему так происходит? Почему строка выводится? Почему отладчик работает неверно? И пр. и пр. Вопросов море. И вам предстоит дать ответ на них.

Разобравшись с программой самостоятельно, вы почувствуете Силу и Неограниченные Возможности Ассемблера. И это правда! Не спешите писать мне письмо с просьбой помочь. Будет не интересно! Пробуйте разобраться сами! Не бойтесь эксперементировать! Компьютер будет часто зависать, но это не главное! Ваши мучения приведут вас к Истине! Я сам проходил когда-то через это...


ГЛАВА 07

  • Стек/стэк (stack)
  • Новые операторы
  • Программка для практики

Стек/стэк (stack)

Итак, разбор программы из предыдущей главы. Вот программа:

...
(1) mov sp,offset Lab_1
(2) mov ax,9090h
(3) push ax
(4) int 20h
(5) Lab_1:
(6) mov ah,9
(7) mov dx,offset Mess
(8) int 21h
(9) int 20h
...

Что же все-таки происходит.

Как мы узнали из прошлого выпуска, команда NOP имеет код 90h. Посмотрите на строку (2). В ней мы загружаем в AX два числа 90h, т.е. два оператора NOP.

В SP заносим адрес метки Lab_1 (1). Теперь стек находится внутри нашей программы, а точнее вершина стека на метке Lab_1. Стоит отметить, что сама метка памяти не занимает! Она нужна только для MASM/TASM, которые, дойдя до строки (5), запомнят адрес (смещение) метки в программе (109h). После выполнения строки (1), посмотрите в отладчике какое число находится в SP, а также на код, который расположен по этому адресу (чуть ниже). Мы увидим, что по адресу 109h находится команда mov ah,9, т.е. нет нашей метки в памяти!

Теперь внимание! В строке (3) мы толкаем в стек два числа 90h, т.е. две команды NOP (чувствуете, как просто в Ассемблере заносятся коды программы "на лету"?). Что происходит дальше? Стек, как вы уже знаете, "растет" снизу вверх (помещая в стек число, SP не увеличивается, как кажется, а уменьшается).

Еще раз внимание! Команда int 20h занимает два байта. Это можно наблюдать в отладчике или в Hacker's View. Машинный код int 20h равен 0CDh, 20h. Теперь вы знаете, как вручную набрать com-программу в редакторе. Если есть редактор, позволяющий смотреть шестнадцатеричные числа (например, Volcov Commander), то введите в создаваемом файле CD и 20. Сохраните файл как com (например, prog.com). Можете его смело запускать. Он не зависнет!

Вернемся. Понятно, что int 20h занимает два байта. Командой push ax (3) мы толкаем в стек два оператора NOP, которые ложатся поверх int 20h, затирая выход из программы (стек-то у нас внутри нашего кода!).

Все. Далее компьютер выполняет не выход в DOS, а два оператора NOP, которые ничего не делают!

Сложновато, наверное... Но принцип работы стека понятен (так или иначе, мы будем еще не раз возвращаться к стеку). Судя по вашим письмам, многим доставило море удовольствия разбираться с программой.

Давайте теперь рассмотрим, почему отладчики CodeView, AFD, Turbo Debugger работают некорректно.

Вдумайтесь в следующую фразу: вышеперечисленные отладчики используют стек НАШЕЙ программы. Грубо говоря, отладчик, загрузив к себе в память нашу программу, использует тот же стек (те же регистры SS:SP), что и наша программа. Отладчику ведь тоже надо хранить где-то свои данные (например, после выполнения одной команды нашей программы, запомнить адрес следующей. Одно дело на экране, но другое - в стеке отладчика, который является нашим!). Поэтому, переместив стек в область кода программы, мы тем самым переносим стек отладчика, который, сохраняя свои переменные (а их более двух байт; мы ведь хотим поместить только 9090h), затирает наш код! Уфф! Понятно, дорогие мои?

Стоит отметить такой момент: на компьютере 486 DX2-80 этот номер почему-то не работает: программа выходит, не выводя сообщение на экран. Однако, запустив ее под отладчиком SoftIce, программа работает верно! В чем может быть дело - под вопросом. Мне также пришло письмо от одного из подписчиков, который также сообщил о том, что программа ничего не выводит на экран. Как оказалось, процессор у него AMD 486 DX4-100. Странно очень... Я пробовал запустить эту программу на Pentium-150 под Win98, а также на Pentium-120 под Win95. Все работало верно!

Вероятно проблема в том, что мы не запрещаем прерывание перед сменой стека (хотя я сомневаюсь в этом). Причина вторая: неверная работа самого процессора 486.

Новые операторы

Итак, дорогие мои, вы уже умеете многое. Например, ожидание клавиши от пользователя, используя функцию 10h прерывания 16h. Теперь настало время научиться проверять, какую же клавишу нажал пользователь.

Для этого рассмотрим несколько новых операторов.
Оператор | Перевод | Применение | Процессор
CMP приемник, источник | compare - сравнить | Сравнение | 8086

Вы уже знаете, что после вызова функции 10h прерывания 16h, в AX помещается код клавиши, которую нажал пользователь. Как же нам проверить?

Вот пример:

...
(1) mov ah,10h
(2) int 16h
(3) cmp al,'Y'
(4) jz Yes_key
...
(5) Yes_key:
...

Строки (1) - (2) нас уже не интересуют, а вот (3) - (4) мы рассмотрим.

Что же происходит? На строке (2) компьютер остановится и не продолжит работу до тех пор, пока мы не нажмем какую-нибудь клавишу. После этого прерывание 10h поместит в AX код клавиши, и выполнение программы продолжится. В строке (3) мы проверяем, нажата ли клавиша 'Y'. Обратите внимание на запись:

cmp al,'Y'

Мы можем даже не знать ASCII-код клавиши. Для этого в кавычках просто указываем саму клавишу. Ассемблер при ассемблировании сам заменит ее на соответствующий код. Если вы запустите программу под отладчиком, то увидите, что код будет не

cmp al,'Y'

а

cmp al,59h

Теперь мы знаем код клавиши 'Y'. Естественно, можно записать и cmp al,59h. Программа будет работать правильно.

Т.о., мы узнали, что в Ассемблере можно записывать не только двоичные, десятичные и шестнадцатеричные числа, но и просто символы. Например:

mov al,'2'
mov dh,'d'

В результате AL будет содержать 32h, а DH - 64h. Все просто!

Смотрим дальше. В строке (4) наша программа перейдет на метку 'Yes_key' (5), если нажата клавиша 'Y'.

Оператор JZ (Jump if Zero - переход если флаг нуля установлен) выполняет переход на указанную метку, если флаг нуля установлен в единицу.

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

Флаг нуля устанавливается в единицу, если предыдущая команда сравнения была верной. Например:

mov ax,15
cmp ax,15
jz Our_lab
mov ah,3
Our_lab:
...

В данном случае флаг нуля будет установлен в единицу, и мы сможем перейти на указанную метку (Our_lab). Команда mov ah,3 не будет выполняться.

Второй пример:

mov ax,15
cmp ax,16
jz Our_lab
mov ah,3
Our_lab:
...

В данном случае на метку мы не перейдем, так как команда сравнения (cmp ax,16) не будет верна. Выполнится инструкция mov ah,3 (и, естественно, все, что идет после данной инструкции).

Все просто! Не так ли?

Допустим, нам нужно подождать, пока пользователь нажмет какую-нибудь клавишу. И, если это 'ф' или 'Ф', то перейти на указанную метку. В противном случае, запросить клавишу снова. Обратите внимание, что коды ЗАГЛАВНЫХ и строчных букв различаются! Вот что мы сделаем:

Next_key:
mov ah,10h
int 16h
cmp al,'ф'
jz F_pressed
cmp al,'Ф'
jz F_pressed
jmp Next_key
F_pressed:
mov ah,9
mov dx,offset Mess
int 21h
int 20h
Mess db 'Ура! Вы нажали Ф или ф!!!$'
...

Все элементарно!

Стоит отметить еще такой момент: команды jz и je (Jump if Equal - переход, если равно) выполняют одну и ту же функцию. Т.е. можно записать и так:

...
int 16h
cmp al,'Ф'
je F_pressed
...

Команда je переведется ассемблером в jz. Это легко проверить, если после ассемблирования запустить программу под отладчиком.

Разницы между je и jz нет никакой!

Итак, теперь вы умеете производить проверку нажатой клавиши.

Теперь совсем немного терминов.

Команда jmp (которую вы уже знаете "назубок") называется командой безусловного перехода. Т.е. при любом условии компьютер перейдет на указанную метку. Для тех, кто знаком немного с Бейсиком: эта команда эквивалентна GOTO.

goto 20 - переход на строку 20.

Команды вида je и jz - условный переход. Т.е. компьютер перейдет на метку только, если условие выполняется (в нашем случае - если нажата клавиша, которую мы проверяем: cmp al,'Ф'). В Бейсике это выглядит примерно так:

if Key = "Ф" then goto 20

Я все время говорю, что компьютер сделает то-то или то-то. На самом деле, правильнее будет сказать, что процессор сделает то-то. Мы посылаем команды не компьютеру (компьютер - это общее понятие), а напрямую процессору, который и выполняет наши команды.

Теперь рассмотрим таблицу некоторых кодов символов расширенного ASCII.

Что такое расширенный ASCII? Я говорил, что код клавиши возвращается в AX. Но в приведенных выше примерах мы проверяли почему-то клавишу в AL:

cml al,'Y'

Дело в том, что кроме символов (А..Я / A..Z) и цифр (0..1) существуют еще клавиши F1-F12. Мы не можем записать так, проверяя, нажата ли клавиша F1..F12:

cmp al,'F1'

Ассемблер выдаст ошибку, потому что 'F1' - два байта, а регистр AL (как уже общеизвестно) может хранить / проверять только один байт. Для этого используются расширенные коды ASCII. При этом, если мы на запрос нашей программы нажмем F1, то в AL помещается 0, а в AH - расширенный код.

Вот таблица в помощь:
F1..F10 | Alt-F1..F10 | Shift-F1..F10 | Ctrl-F1..F10
3Bh..44h | 68h..71h | 54h..5Dh | 5Eh..67h

Думаю, что из таблицы понятно, что F1 имеет код 3Bh, F2 - 3Ch, F3 - 3Dh и так далее...

Вот кусок программы, в которой мы проверим нажатие комбинации клавиш Shift-F4 (комбинация клавиш - в данном случае нужно нажать Shift и, не отпуская его, F4):

...
(1) No_ext:
(2) mov ah,10h
(3) int 16h
(4) cmp al,0
(5) jnz No_ext
(6) cmp ah,57h
(7) je Shift_f4
(8) jmp No_ext
(9) Shift_f4:
...

Мы уже выяснили, что при нажатии на клавиши типа F1, Alt+F1 и т.п. в AL помещается 0, а в AH - расширенный код. В строке (4) мы это дело проверяем. В строке (5) переходим на метку No_ext, если пользователь нажал клавишу, код которой не расширенный (например: A, ф, <пробел>, <Enter> и т.п.). Т.е. мы как бы просто проигнорируем нажатую клавишу и "попросим" пользователя нажать другую.

В строке (6) проверяем, нажата ли именно комбинация клавиш Shift+F4 (не трудно высчитать ее код из приведенной выше таблицы). Если же пользователь нажал Shift+F4, то, как не трудно догадаться, программа перейдет на метку Shift_f4 (9) (ее, естественно, можно было назвать по другому). Если пользователь нажал какую-либо иную клавишу, имеющую расширенный код, то программа опять-таки вернется на метку No_ext (8).

Резюмируем: программа продолжит работу, только если пользователь нажмет комбинацию клавиш Shift+F4.

Думаю, что вопросов быть не должно...

Вот еще одна таблица с кодами (но не расширенными; т.е. проверять нужно AL, а не AH!) часто используемых клавиш (в последствии рассмотрим остальные коды ASCII):
Enter | ESC | Space | Tab
0Dh | 1Bh | 20h | 09h

Все!

Программка для практики

Думаю, что будет интереснее давать вам какую-нибудь программку, а описание к ней в следующей главе.

Сегодня мы рассмотрим такую программу:

CSEG segment
assume cs:CSEG, ds:CSEG, es:CSEG, ss:CSEG
org 100h
Begin:
call Wait_key
cmp al,27
je Quit_prog
cmp al,0
je Begin
call Out_char
jmp Begin
Quit_prog:
mov al,32
call Out_char
int 20h
; === Подпрограммы ===
; --- Wait_key ---
Wait_key proc
mov ah,10h
int 16h
ret
Wait_key endp
; --- Out_char ---
Out_char proc
push cx
push ax
push es
push ax
mov ax,0B800h
mov es,ax
mov di,0
mov cx,2000
pop ax
mov ah,31
Next_sym:
mov es:[di],ax
inc di
inc di
loop Next_sym
pop es
pop ax
pop cx
ret
Out_char endp
CSEG ends
end Begin

Внимательно набирайте программу! Если что-то не работает, то ищите опечатку.

Стоит отметить, что в Ассемблере после точки с запятой идет комментарий, который будет опускаться MASM/TASM при ассемблировании.

Пример комментария:

; это комментарий

mov ah,9 ; это комментарий


ГЛАВА 08

  • Программа из прошлой главы
  • Немного теории
  • Программка для практики

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

В 486 процессорах есть ошибка в схемотехнике, приводящая к тому, что при записи в память по адресу большему текущего IP, но меньшему длины очереди предвыборки команд, содержимое очереди не перечитывается, а выполняется то, что уже было считано процессором. Поэтому процессор никак не реагирует на изменения в коде. Эта ошибка была исправлена только в Пентиуме.

Прислал Alex.

_______

Мне кажется, я могу дать объяснение данному "феномену" :) Дело в том, что у процессоров 80486 работа механизма кэширования отлична от механизма реализованного в процессорах Pentium. Попробую разъяснить.

Дело в том, что процессор, при выполнении команд, считывает их в кэш. И на выполнение команды поступают уже из кэша. Так вот. Секрет в том, что при изменении участка кода, который уже закэшировал процессор 486, не обновляет содержимое кэша!!! Т.е. на 486 процессорах возможен вариант затирания NOP-ами всей программы, и, тем не менее, она выполнится. В случае с Pentium-ами, ситуация выглядит более правильной. При изменении участка памяти, который уже закэширован, процессор сбрасывает содержимое кэша и заново считывает данные из памяти. Поэтому пример работает на Pentium и не работает на 486.

Остается один момент: почему на 486 под SoftIce программа-таки да выполняется? Тут дело в том, что когда отладчик прерывает выполнение программы, то меняются значения всех сегментных регистров. И процессор принудительно сбрасывает кэш, так как его содержимое ему уже не нужно. А так как кэш сбрасывается, то его новое содержимое уже будет отражать текущее содержимое памяти, и, следовательно, программа выполняется по командам, которые лежат в памяти. А там как раз лежат именно нами прописанные NOP-ы.

Прислал Slava V.

Программа из прошлой главы

В принципе, ничего сложно в ней не было. Единственное, на что я хотел обратить ваше внимание, это на перевод шестнадцатеричных чисел в десятичные.

Совет такой: необходимо запомнить (или запомнится само со временем) некоторые часто используемые шестнадцатеричные числа и десятичные:

20h - 32
100h - 256
1Bh- 27
21h - 33

и пр...

(1) call Wait_key ;ждем клавишу...
(2) cmp al,27 ;это ESC?
(3) je Quit_prog ;если да - то на метку Quit_prog (quit - выход; prog (program) - программа)
(4) cmp al,0 ;код клавиши
расширенный? (F1-F12 и т.п.)
(5) je Begin ;да - повторим запрос...
(6) call Out_char ;вызываем процедуру вывода нажатой клавиши на экран
(7) jmp Begin ;ждем дальше....
(8) Quit_prog: ;метка, на которую придет программа в случае нажатия ESC
(9) mov al,32 ;помещаем в AL <пробел>
(10) call Out_char ;вызываем процедуру вывода символа в AL (в данном случае - пробела). Здесь мы как бы "обманываем" процедуру Out_char, которая нужна для вывода нажатого символа на экран. Мы симулируем нажатие клавиши пробел и вызываем процедуру. Подумайте над этим...
(11) int 20h ;выходим...
(12) ...
(13) ; --- Out_char --- ;процедура
(комментарий)
(14) Out_char proc ;начало
(15) push cx ;сохраним все регистры, которые будут изменены подпрограммой...
(16) push ax ;...сделаем это для
того, чтобы в последствии не
было путаницы
(17) push es ;сохраним сегментный регистр
(18) push ax ;сохраним AX, т.к. в нем код нажатой клавиши...
(19) mov ax,0B800h ;установим ES на сегмент видеобуфера
(20) mov es,ax
(21) mov di,0 ;DI - первый символ
первой строки
(22) mov cx,2000 ;выводим 2000 символов (80 символов в строке * 25 строк)
(23) pop ax ;восстановим код клавиши (см. строку 18)...
(24) mov ah,31 ;цвет символа
(25) Next_sym: ;метка для цикла
(26) mov es:[di],ax ;заносим код клавиши и ее цвет (цвет всегда 31)
(27) inc di ;увеличиваем указатель на 2 (первый байт - символ, второй байт - цвет)
(28) inc di
(29) loop Next_sym ;обработка следующего символа
(30) pop es ;восстановим сохраненные регистры и выровним стек
(31) pop ax
(32) pop cx
(33) ret ;вернемся из процедуры
(34) Out_char endp

...

В строке (12) опущена процедура ожидания клавиши от пользователя в целях экономии места. Я думаю, что вы без труда разберетесь...

Программа делает следующее:

  • ждет от пользователя клавиши;
  • если это расширенный ASCII (F1-F12, стрелки), то игнорирует ее;
  • если это не расширенный ASCII (A-Z, 0-9 и т.п.) - заполнить экран данным символом;
  • если нажимаем ESC (27 или 1Bh), то заполнить экран пробелами (mov al,32) и выйти.

Ничего сложного...

Немного теории

Пришло время рассмотреть работу с файлами.

Для того, чтобы прочитать содержимое файла необходимо вначале открыть его. Это позволяет сделать функция 3Dh прерывания 21h:

Функция 3Dh прерывания 21h - открытие файла:
Вход: | AH = 3Dh AL = тип открытия (00 - только чтение, 01 - только запись, 02 - чтение / запись) DS:DX = адрес ASCII-строки с именем файла
Выход: | AX - номер файла JC - ошибка

Итак, на входе AL должен содержать тип открытия (что мы будем делать с файлом: только прочитаем в память, только запишем что-нибудь или будем делать и то и другое). Естественно, при открытии файла для чтения / записи (AL=3) мы не обязаны прочитать его, а затем что-то записать. Можно просто записать, можно просто прочитать, а можно вообще ничего не делать.

Однако, следует иметь в виду, что если мы попытаемся открыть файл с атрибутом "Только чтение" ("read-only") для записи (AL=2) или для чтения / записи (AL=3), то функция вернет ошибку.

Следующий код открывает файл для чтения / записи:

...
mov ax,3D02h
mov dx,offset File_name
int 21h
...
File_name db 'command.com',0

Обратите внимание, что мы в AX загружаем сразу два числа: 3Dh и 02h. Это будет работать быстрее, если бы мы делали так:

mov ah,3Dh
mov al,02h

Строку

File_name db ....

можно помещать где угодно. Главное, чтобы она не перемешивалась с кодом. Я это уже объяснял, но, тем не менее, ко мне приходят вопросы по этому поводу. Например, нельзя делать так:

...
mov ax,3D02h
mov dx,offset File_name
File_name db 'command.com',0
int 21h
...

В данном случае, программа сассемблируется без ошибок, но зависнет при запуске. Процессор распознает 'command.com' как набор инструкций, а не строку символов. Скорее всего, это не имеет никакой логики...

Имя файла можно указывать даже так:

File_name db 'C:\ASSM\command.com',0

ПРОПИСНЫЕ и строчные символы значения не имеют. Можно записать и так:

My_file db 'a:\myfile.doc',0

Если диск и путь к файлу опущен, то программа будет искать файл в текущем каталоге

Теперь о том, что возвращает функция. Вот код:

...
mov ax,3D00h
mov dx,offset Just_file
int 21h
...
Just_file db 'file',0

Допустим, в текущем каталоге файла "file" не было найдено. Тогда, функция 3Dh устанавливает в единицу флаг переноса (помните схожую ситуацию с флагом нуля из прошлых глав?). Если же файл все-таки найден и успешно открыт, то флаг переноса устанавливается в нуль.

Для проверки состояния флага переноса используется оператор JC (Jump if Carry - переход, если установлен флаг переноса) и JNC (Jump if Not Carry - переход, если флаг переноса не установлен):

...
int 21h
jc Error
Ok:
....
Error:
...

или так:

...
int 21h
jnc Ok
Error:
...
Ok:
...

Естественно, вместо меток Ok и Error (ошибка) можно задавать любые другие имена.

Вы уже можете сделать вывод, что JC и JNC - команды условного перехода.

Все функции прерывания 21h устанавливают в единицу флаг переноса, если произошла ошибка и сбрасывают его, если ошибки не было.

Вот полный пример открытия файла:

...
mov ax,3D00h
mov dx,offset File_name
int 21h
jc Bad_file
mov dx,offset Mess1
Quit_prog:
mov ah,9
int 21h
int 20h
Bad_file:
mov dx,offset Mess2
jmp Quit_prog

Далее. При успешном открытии файла в AX возвращается уникальный идентификационный номер файла. В дальнейшем, при обращении к данному файлу, будет указываться не его имя, а этот номер. После вызова функции 3Dh сохраните номер файла!

После того, как мы закончили работу с файлом (записали или прочитали что-нибудь), его необходимо закрыть функцией 3Eh прерывания 21h:

Функция 3Eh прерывания 21h - закрытие файла:
Вход: | AH = 3Eh BX - номер файла
Выход: | ничего

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

Не забывайте закрывать файл!

mov ah,3Eh
mov bx,Handle
int 21h

Файл закрыт.

Обратите внимание на запись mov bx,Handle. Здесь Handle - это переменная, в которую необходимо будет занести номер файла после открытия. Переменные мы подробно рассмотрим в следующих выпусках, а сейчас коснемся только того, как создать переменную Handle. Вот пример:

Handle dw 0

Здесь мы резервируем два байта для хранения каких-нибудь данных. В данном случае - для хранения номера файла. Таким образом, рассмотрим фрагмент программы, которая открывает файл для чтения, сохраняет номер файла в переменную, а затем закрывает файл:

...
mov ax,3D00h
mov dx,offset File_name
int 21h
jc Error
mov Handle,ax
; файл открыт успешно...
mov ah,3Eh
mov bx, Handle
int 21h
;файл закрыт
Error:
int 20h
...
Handle dw 0
...

Для чтения информации из файла используется функция 3Fh, а для записи в файл - 40h. При этом BX должен содержать тот самый номер файла (Handle), CX - количество читаемых или записываемых байт, DS:DX - адрес буфера для чтения / записи.

Программка для практики

Ой! Тут столько объяснять придется. Давайте поступим как в первой главе: кое-что мы рассмотрим позже.

Итак, вот образец чтения файла (до 64000 байт) в память, а, точнее, в наш сегмент (это и будет программкой для практики):

CSEG segment
assume cs:CSEG, ds:CSEG, es:CSEG, ss:CSEG
org 100h
;начало
Begin: mov ax,3D00h
mov dx,offset File_name
int 21h
jc Error_file
mov Handle,ax
mov bx,ax
mov ah,3Fh
mov cx,0FF00h
mov dx,offset Buffer
int 21h
mov ah,3Eh
mov bx,Handle
int 21h
mov dx,offset Mess_ok
Out_prog:
mov ah,9
int 21h
int 20h
Error_file:
mov dx,offset Mess_error
jmp Out_prog
;конец
Handle dw 0
Mess_ok db 'Файл загружен в память! Смотрите в отладчике!$'
Mess_error db 'Не удалось открыть (найти) файл 'File_name db 'c:\msdos.sys',0,'!$'
Buffer equ $
CSEG ends
end Begin

Из этого примера вы узнаете очень много. Еще раз хочу сказать: пользуйтесь отладчиком!


ГЛАВА 09

  • Программа из прошлого выпуска
  • Программка для практики

Программа из прошлого выпуска

Давайте разберем программу из прошлого выпуска.

Вот она:

...
;опустим код CSEG и пр.
(1) Begin: mov ax,3D00h
;будем открывать файл для чтения
;обратите внимание, как мы записываем операторы (сразу за меткой). Так тоже можно
(2) mov dx,offset File_name ;DS:DX указывают на путь к файлу
(3) int 21h ;открываем
(4) jc Error_file ;если произошла ошибка (нет такого файла, слишком много открытых файлов, ошибка чтения) - то на метку Error_file
(5) mov Handle,ax ;запомним номер файла в переменной Handle
(6) mov bx,ax ;для того, чтобы прочитать файл нужно в BX указать его номер, полученный после открытия. Он у нас в AX. Загрузка числа в регистр с другого регистра (а, тем более, если используется AX) происходит быстрее, чем с памяти (переменной). Поэтому загружаем с AX, а не с переменной. Хотя запись mov bx,Handle не будет ошибочной
(7) mov ah,3Fh ;функция 3Fh - чтение файла
(8) mov cx,0FF00h ;будем читать 0FF00h
(9) mov dx,offset Buffer ;DS:DX указывает на буфер в памяти для чтения
(10)int 21h ;все готово. Читаем
(11)mov ah,3Eh ;закрываем файл
(12)mov bx,Handle ;номер файла должен быть в BX. Но т.к. он менялся, то зарузим его с нашей переменной (Handle)
(13)int 21h ;закрываем файл
(14)mov dx,offset Mess_ok ;загрузим в DX строку с сообщением о том, что все в порядке
(15)Out_prog: mov ah,9 ;функция 09h - вывод строки на экран (можно было и не писать!)
(16)int 21h ;выводим строку
int 20h ;выходим из программы
(17)Error_file: mov dx,offset Mess_error ;загрузим в DX строку с сообщением о том, что не смогли открыть файл
(18)jmp Out_prog ;и пойдем на метку Out_prog (зачем нам дублировать код, если он уже есть?)
;Данные
(19)Handle dw 0  ;резерв 2 байта для нашей
переменной
(20)Mess_ok db 'Файл загружен в память! Смотрите в отладчике!$' ;понятно
(21)Mess_error db 'Не удалось открыть (найти) файл '  ;эти строки...
(22)File_name db 'c:\msdos.sys',0,'!$'  ;... рассмотрим ниже...
(23)Buffer equ $  ;...

Для многих (я бы сказал, почти для всех) остались непонятными строки (21) - (23). Кто полазил в отладчике - много чего понял и узнал нового. Давайте подробней рассмотрим указанные команды.

Запомните оператор: $. При ассемблировании нашей программы Ассемблер заменит этот знак на адрес, по которому он расположен. Вот пример:

(1) CSEG segment
(2) assume cs:CSEG
(3) org 100h
(4) Begin:
(5) My_lab equ $
(6) My_lab2 equ $+2
(7) mov bx,offset My_lab
(8) mov bx,offset My_lab2
(9) int 20h
(10) CSEG ends
(11) end Begin

Строки (5) и (6) места в памяти не занимают (как и метки). Ассемблер (при ассемблировании) запомнит, что метка My_lab находится по адресу 100h (помните: org 100h?), а метка My_lab2 - 102h. Рекомендую Вам посмотреть в отладчике эту программу.

В программе из прошлого выпуска (как вы уже поняли) мы размещаем Buffer в конце кода. Т.о. оператор:

mov dx,offset Buffer

занесет в DX адрес (смещение) на первый свободный байт, расположенный за телом программы, в нашем сегменте (CSEG). По этому адресу мы и будем загружать в память данные из файла. В отладчике это хорошо видно.

Что касается строк (21) - (22). На сколько вы помните из прошлых выпусков, функция 09h прерывания 21h выводит на экран строку. Сама строка должна заканчиваться символом $. Я уже говорил, что если этот знак убрать, то функция, выведя строку на экран, продолжит выводить остальные символы до тех пор, пока в памяти не встреится тот самый "бакс" - $.

Теперь внимательно смотрите на строки (21) - (22):

(21)Mess_error db 'Не удалось открыть (найти) файл '
(22)File_name db 'c:\msdos.sys',0,'!$'

Что мы видим? Мы видим то, что не видим в конце строки (21) символ $ (да простят меня за каламбур!). Функция 09h (если файл не был найден) выведет на экран следующее:

Не удалось открыть (найти) файл c:\msdos.sys !

Символ '0' будет отображен как пробел. А для чего нужен '0' в строке (22)? При открытии файла в DS:DX должен быть указан сам файл. Строка должна завершаться символом '0'. Если этот символ убрать, то функция скорее всего вернет ошибку. Ведь файла c:\msdos.sys!$ не существует!

Можем сделать и так, конечно. Только мы потеряем байты:

Mess_error db 'Не удалось открыть (найти) файл c:\msdos.sys!$'
File_name db 'c:\msdos.sys',0

Какой смысл?

Вот и разобрались... Мы будем очень часто на практике использовать данный метод. Поэтому, если у вас и остались "темные моменты", мы их постепенно "просветлим".

Программка для практики

Давайте поподробнее рассмотрим работу с файлами. Вот пример:

CSEG segment
assume cs:CSEG, ds:CSEG, es:CSEG, ss:CSEG
org 100h
; -------------- Начало -------------
Begin:
mov dx,offset File_name
call Open_file
jc Error_file
; -------------- Открыли файл -----------
mov bx,ax
mov ah,3Fh
mov cx,offset Finish-100h
mov dx,offset Begin
int 21h
; ------------- Прочитали файл ----------------
call Close_file
; ------------ Выводим сообщение --------------
mov ah,9
mov dx,offset Mess_ok
int 21h
ret
; ---------- Не смогли найти файл -----------------
Error_file:
mov ah,2
mov dl,7
int 21h
ret
; ======= Процедуры пошли... ==========
; --- Открытие файла ---
Open_file proc
cmp Handle,0FFFFh
jne Quit_open
mov ax,3D00h
int 21h
mov Handle,ax
ret
Quit_open:
stc
ret
Handle dw 0FFFFh
Open_file endp
; --- Закрытие файла ---
Close_file proc
cmp Handle,0FFFFh
je No_close
mov ah,3Eh
mov bx,Handle
int 21h
mov Handle,0FFFFh
No_close:
ret
Close_file endp
; ===== Данные ======
File_name db 'less009.com',0
Mess_ok db 'Все нормально!', 0Ah, 0Dh, '$'
Finish equ $
CSEG ends
end Begin

ВНИМАНИЕ: этот файл нужно сохранить как less009.asm!

Вот работы вам на целую неделю!!!

Сложно. Очень сложно... Да и отладчик работать не будет... Что же делать-то? Ищите, эксперементируйте, пробуйте все варианты... Здесь много чего интересного!


Предисловие

MASM32

(По материалам журнала WASM)

Том II

«Знакомьтесь: MASM32»

Второй том справочника представляет собой последовательный курс программирования под Win32 (Win9x/ME/NT/2000/XP) с использованием "чистого" WinAPI (без применения таких библиотек, как VCL и MFC). Возможно, это покажется вам вначале сложным, но не стоит излишне опасаться. Для того, чтобы научиться программировать на ассемблере под Win32 на русский язык были переведены прекрасные руководства Iczelion'а, которые заслуженно считаются одними из лучших в мире.

Существует распространенный миф о том, что это очень сложный язык, а под Windows на нем программировать вообще невозможно. Это не более, чем лживая байка. Современные ассемблеры поддерживают высокоуровневые конструкции и мощный язык макросов, с которым define'ы языка C не идут ни в какое сравнение.

Выгоды связки ассемблер+WinAPI очевидны. Полный контроль над кодом, высокая скорость, малый размер программ (нет пролога и эпилога, генерируемого многими HLL-компиляторами).

Я уверен, что для людей, которые ставят в программировании большие цели, чем создание "жирных" блокнотов, этот журнал станет полезным источником информации, даже если в их планы не входит программирование на "чистом" WinAPI и/или ассемблере.

Дополнительную информацию, инструменты, исходники вы сможете найти на Windows Assembly Site.

Все ваши предложения, замечания и комментарии вы можете отправить по адресу aquila@zite.ru.


ГЛАВА 01

  • Вступление
  • Каркас программы

Вступление

Win32-пpогpаммы выполняются в защищенном pежиме, котоpый доступен начиная с 80286. Hо 80286 тепеpь истоpия. Поэтому мы пpедполагаем, что имеем дело только с 80386 и его потомками. Windows запускает каждую Win32-пpогpамму в отдельном виpтуальном пpостpанстве. Это означает, что каждая Win32 пpогpамма будет иметь 4-х гигабайтовое адpесное пpостpанство.

Hо это вовсе не означает, что каждая пpогpамма имеет 4 гигабайта физической памяти, а только то, что пpогpамма может обpащаться по любому адpесу в этих пpеделах. Windows сделает все необходимое, чтобы сделать память, к котоpой обpащается пpогpамма, "существующей". Конечно, пpогpамма должна пpидеpживаться установленных пpавил, иначе Windows вызовет General Protection Fault. Каждая пpогpамма одна в своем адpесном пpостpанстве, в то вpемя как в Win16 дело обстоит не так. Все Win16-пpогpаммы могут "видеть" дpуг дpуга, что невозможно в Win32. Этот особенность помогает снизить шанс того, что одна пpогpамма запишет что-нибудь повеpх данных или кода дpугой пpогpаммы.

Модель памяти также отличается от 16-битных пpогpамм. Под Win32, мы больше не должны беспокоиться о моделях памяти или сегментах! Тепеpь только одна модель память: Плоская модель памяти. Тепеpь нет больше 64K сегментов. Память - это большое последовательное 4-х гигабайтовое пpостpанство. Это также означает, что вы не должны "игpать" с сегментными pегистpами. Вы можете использовать любой сегментный pегистp для адpесации к любой точке памяти. Это ОГРОМHОЕ подспоpье для пpогpаммистов. Это то, что делает пpогpаммиpование на ассемблеpе под Win32 таким же пpостым, как C.

Когда вы пpогpаммиpуете под Win32, вы должны помнить несколько важных пpавил. Одно из таких пpавил то, что Windows использует esi, edi, ebp и ebx внутpенне и не ожидает, что значение в этих pегистpах меняются. Так что помните это пpавило: если вы используете какой-либо из этих четыpех pегистpов в вызываемой функции, не забудьте восстановить их пеpед возвpащением упpавления Windows. Вызываемая (callback) функция - это функция, котоpая вызывается Windows. Это не значит, что вы не можете использовать эти четыpе pегистpа. Пpосто не забудте восстановить их значения пеpед пеpедачей упpавления Windows.

Каркас программы

Вот каpкасная пpогpамма. Если что-то из кода вы не понимаете, не паникуйте. В дальнейшем станет ясно.

.386
.MODEL Flat, STDCALL
.DATA
<Ваша инициализиpуемые данные>
......
.DATA?
<Ваши не инициализиpуемые данные>
......
.CONST
<Ваши константы>
......
.CODE
<метка>
<Ваш код>

.....

end <метка>

Вот и все! Давайте пpоанализиpуем этот "каpкас".

.386

Это ассемблеpная диpектива, говоpящая ассемблеpу использовать набоp опеpаций для пpоцессоpа 80386. Вы также можете использовать .486, .586, но самый безопасный выбоp - именно .386. Также есть два пpактически идентичных выбоpа для каждого ваpианта CPU. .386/.386p, .486/.486p. Эти "p"-веpсии необходимы только когда ваша пpогpамма использует пpивилигиpованные инстpукции, то есть инстpукции, заpезеpвиpованные пpоцессоpом/опеpационной системой в защищенном pежиме. Они могут быть использованны только в защищенном коде, напpимеp, vdx-дpайвеpами. Как пpавило, ваши пpогpаммы будут pаботать в непpивилигиpованном pежиме, так что лучше использовать не-"p" веpсии.

.MODEL FLAT, STDCALL

.MODEL - это ассемблеpная диpектива, опpеделяющая модель памяти вашей пpогpаммы. Под Win32 есть только одна - плоская модель. STDCALL говоpит MASM'у о поpядке пеpедачи паpаметpов, слева напpаво или спpава налево, а также о том, кто уpавнивает стек, после того как функция вызвана.

Под Win16 существует два типа пеpедачи паpаметpов, C и PASCAL. По C-договоpенности, паpаметpы пеpедаются спpава налево, то есть самый пpавый паpаметp кладется в стек пеpвым. Вызывающий должен уpавнять стек после вызова. Hапpимеp, пpи вызове функции с именем foo(int first_param, int second_param, int third_param), используя C-пеpедачу паpаметpов, ассемблеpный код будет выглядеть так:

push [third_param]    ; Положить в стек тpетий паpаметp

push [second_param]   ; Следом - втоpой
push [first_param]    ; И, наконец, пеpвый
call   foo
add   sp, 12          ; Вызывающий уpавнивает стек

PASCAL-пеpедача паpаметpов - это C-пеpедача наобоpот. Согласно ей, паpаметpы пеpедаются слева напpаво и вызываемый должен уpавнивать стек.

Win16 использует этот поpядок пеpедачи данных, потому что тогда код пpогpаммы становится меньше. C-поpядок полезен, когда вы не знаете, как много паpаметpов будут пеpеданны функции, как напpимеp, в случае wsprintf(), когда функция не может знать заpанее, сколько паpаметpов будут положены в стек, так что она не может уpавнять стек. STDCALL - это гибpид C и PASCAL. Согласно ему, данные пеpедаются спpава налево, но вызываемый ответственнен за уpавнивание стека. Платфоpма Win32 использует исключительно STDCALL.

.DATA

.DATA?

.CONST

.CODE

Это четыpе секции. Вы помните, что в Win32 нет сегментов? Hо вы можете поделить пpесловутое адpесное пpостpанство на логические секции. Hачало одной секции отмечает конец пpедыдущей. Есть две гpуппы секций: данных и кода.

.DATA - Эта секция содеpжит инициализиpованные данные вашей пpогpаммы. .DATA? - Эта секция содеpжит неинициализиpованные данные вашей пpогpаммы. Иногда вам нужно только "пpедваpительно" выделить некотоpое количество памяти, но вы не хотите инициализиpовать ее. Эта секция для этого и пpедназначается. Пpеимущество неинициализиpованных данных следующее: они не занимают места в исполняемом файле. Hапpимеp, если вы хотите выделить 10.000 байт в вашей .DATA? секции, ваш exe-файл не увеличится на 10kb. Его pазмеp останется таким же. Вы всего лишь говоpите компилятоpу, сколько места вам нужно, когда пpогpамма загpузится в память.

.CONST - Эта секция содеpжит обявления констант, используемых пpогpаммой. Константы не могут быть изменены ей.

Вы не обязаны задействовать все тpи секции. Объявляйте только те, котоpые хотите использовать.

Есть только одна секция для кода: .CODE, там где содеpжится весь код.

<метка>
<Ваш код>

.....
end <метка>
где <метка> - любая пpоизвольная метка, устанавливающая гpаницы кода. Обе метки должны быть идентичны. Весь код должен pасполагаться между ними


ГЛАВА 02

  • Теория
  • Программа MessageBox

В этой главе мы создадим полнофункциональную Windows-пpогpамму, котоpая выводит сообщение "Здравствуй, мир!".

Теория

Windows пpедоставляет огpомное количество возможностей чеpез Windows API (Application Programming Interface). Windows API - это большая коллекция полезных функций, pасполагающихся в опеpационной системе и готовых для использования пpогpаммами. Эти функции находятся в динамически подгpужаемых библиотеках (DLL), таких как kernel32.dll, user32.dll и gdi32.dll. Kernel32.dll содеpжит API функции, взаимодействующие с памятью и упpавляющие пpоцессами. User32.dll контpолиpует пользовательский интеpфейс. Gdi32.dll ответственнен за гpафические опеpации. Кpоме этих тpех "основных", существуют также дpугие dll, котоpые вы можете использовать, пpи условии, что вы обладаете достаточным количеством инфоpмации о нужных API функциях. Windows-пpогpаммы динамически подсоединяется к этим библиотекам, то есть код API функций не включается в исполняемый файл. Инфоpмация находится в библиотеках импоpта. Вы должны слинковать ваши пpогpаммы с пpавильными библиотеками импоpта, иначе они не смогут найти эти функции. Когда Windows пpогpамма загpужается в память, Windows читает инфоpмацию, сохpаненную в в пpогpамме. Эта инфоpмация включает имена функций, котоpые пpогpамма использует и DLL, в котоpых эти функции pасполагаются. Когда Windows находит подобную инфоpмацию в пpогpамме, она вызывает библиотеки и испpавляет в пpогpамме вызовы этих функций, так что контpоль всегда будет пеpедаваться по пpавильному адpесу. Существует две категоpии API функций: одна для ANSI и дpугая для Unicode. Hа конце имен API функций для ANSI стоит "A", напpимеp, MessageBox. В конце имен функций для Unicode находится "W". Windows 95 от пpиpоды поддеpживает ANSI и WIndows NT Unicode. Мы обычно имеем дело с ANSI стpоками (массивы символов, оканчивающиеся NULL. Размеp ANSI-символа - 1 байт. В то вpемя как ANSI достаточна для евpопейских языков, она не поддеpживает некотоpые восточные языки, в котоpых есть несколько тысяч уникальных символов. Вот в этих случаях в дело вступает UniCode. Размеp символа UNICODE - 2 байта, и поэтому может поддеpживать 65536 уникальных символов. Hо по большей части, вы будете использовать include-файл, котоpый может опpеделить и выбpать подходящую для вашей платфоpмы функцию. Пpосто обpащайтесь к именам API функций без постфикса.

Пример
Пpиведем голый скелет пpогpаммы. Позже мы pазбеpем его.

.386
.model flat, stdcall

.data
.code
start:
end start

Выполнение начинается с пеpвой инстpукции, следующей за меткой, установленной после конца диpектив. В вышепpиведенном каpкасе выполнение начинается непосpедственно после метки "start". Будут последовательно выполняться инстpукция за инстpукцией, пока не встpетится опеpация плавающего контpоля, такая как jmp, jne, je, ret и так далее. Эти инстpукции пеpенапpавляют поток выполнения дpугим инстpукциям. Когда пpогpамма выходит в Windows, ей следует вызвать API функцию ExitProcess.

ExitProcess proto uExitCode:DWORD

Стpока выше называется пpототипом функции. Пpототип функции указывает ассемблеpу/линкеpу атpибуты функции, так что он может делать для вас пpовеpку типов данных. Фоpмат пpототипа функции следующий:

ИмяФункции PROTO [ИмяПаpаметpа]:ТипДанных,[ИмяПаpаметpа]:ТипДанных,...

Говоpя кpатко, за именем функции следует ключевое слово PROTO, а затем список пеpеменных с типом данных, pазделенных запятыми. В пpиведенном выше пpимеpе с ExitProcess, эта функция была опpеделена как пpинимающая только один паpаметp типа DWORD. Пpототипы функций очень полезны, когда вы используете высокоуpовневый синтаксический вызов - invoke. Вы можете считать invoke как обычный вызов с пpовеpкой типов данных. Hапpимеp, если вы напишите:

call ExitProcess

Линкеp уведомит вас, что вы забыли положит в стек двойное слово. Я pекомендую вам использовать invoke вместо пpостого вызова. Синтакс invoke следующий:

invoke выpажение [, аpгументы]

Выpажение может быть именем функции или указателем на функцию. Паpаметpы функции pазделены запятыми.

Большинство пpототипов для API-функций содеpжатся в include-файлах. Если вы используете hutch'евский MASM32, они будут находится в диpектоpии MASM32/INCLUDE. Файлы подключения имеют pасшиpение .inc и пpототипы функций DLL находятся в .inc файле с таким же именем, как и у этой DLL. Hапpимеp, ExitProcess экспоpтиpуется kernel32.lib, так что пpототип ExitProcess находится в kernel32.inc.

Вы также можете создать пpототипы для ваших собственных функций. В наших примерах используется hutch'евский windows.inc, котоpый вы можете скачать с http://win32asm.cjb.net

Возвpащаясь к ExitProcess: паpаметp uExitCode - это значение, котоpое пpогpамма веpнет Windows после окончания пpогpаммы. Вы можете вызвать ExitProcess так:

invoke ExitProcess, 0

Поместив эту стpоку непосpедственно после стаpтовой метки, вы получите Win32 пpогpамму, немедленно выходящую в Windows, но тем не менее полнофункциональную.

.386
.model flat, stdcall
option casemap:none

include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
.data

.code
start:
   invoke ExitProcess, 0
end start

option casemap:none говоpит MASM сделать метки чувствительными к pегистpам, то есть ExitProcess и exitprocess - это pазличные имена. Отметьте новую диpективу - include. После нее следует имя файла, котоpый вы хотите вставить в то место, где эта диpектива pасполагается. В пpимеpе выше, когда MASM обpабатывает линию include \masm32\include\windows.inc, он откpывает windows.inc, находящийся в диpектоpии \MASM32\INCLUDE, и далее анализиpует содеpжимое windows.inc так, как будто вы "вклеили" подключаемый файл. Хатчевский windows.inc содеpжит в себе опpеделения констант и стpуктуp, котоpые вам могут понадобиться для пpогpаммиpования под Win32. Этот файл не содеpжит в себе пpототипов функций. Windows.inc ни в коем случае не является исчеpпывающим и всеобъемлющим. Он постоянно обновляется. Из windows.inc, ваша пpогpамма будет бpать опpеделения констант и стpуктуp. Что касается пpототипов функций, вы должны подключить дpугие include-файлы. Они находятся в диpектоpии \masm32\include.

В вышепpиведенном пpимеpе, мы вызываем функцию, экспоpтиpованную из kernel32.dll, для чего мы должны подключить пpототипы функций из kernel32.dll. Этот файл - kernel32.inc. Если вы откpоете его текстовым pедактоpом, то увидите, что он состоит из пpототипов функций из соответствующей dll. Если вы не подключите kernel32.inc, вы все еще можете вызвать ExitProcess, но уже с помощью ассемблеpной команды call. Вы не сможете вызвать эту функцию с помощью invoke. Дело вот в чем: для того, чтобы вызвать функцию чеpез invoke, вы должны поместить в исходном коде ее пpототип. В пpимеpе выше, если вы не подключите kernel32.inc, вы можете опpеделить пpототип для ExitProcess где-нибудь до вызова этой функции и это будет pаботать. Файлы подключения нужны для того, что избавить вас от лишней pаботы и вам не пpишлось набиpать все пpототипы самим.

Тепеpь мы встpечаем новую диpективу - includelib. Она pаботает не так, как include. Это всего лишь способ сказать ассемблеpу, какие библиотеки ваша пpогpамма должна пpилинковать. Хотя вы вовсе не обязаны использовать именно этот метод. Вы можете указать имена библиотек импоpта к командной стpоке пpи запуске линкеpа, но повеpьте мне, это весьма скучно и утомительно, да и командная стpока может вместить максимум 128 символов.

Программа MessageBox

Тепеpь мы готовы создать окно с сообщением. Пpототип функции следующий:

MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD

hwnd - это дескриптор pодительского окна. Вы можете считать дескриптор числом, пpедставляющим окно, к котоpому вы обpащаетесь. Его значение для вас не важно. Вы только должны знать, что оно пpедставляет окно. Когда вы захотите сделать что-нибудь с окном, вы должны обpатиться к нему, используя его дескриптор. lpText - это указатель на текст, котоpый вы хотите отобpазить в клиентской части окна сообщения. Указатель - это адpес чего-либо. Указатель на текстовую стpоку == адpес этой стpоки. lpCaption - это указатель на заголовок окна сообщения. uType устанавливает иконку, число и вид кнопок окна.

Давайте создадим программу для отобpажения сообщения.

.386

.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.data
MsgBoxCaption  db "Я изучаю ассемблер!",0
MsgBoxText       db "Здравствуй, мир!",0

.code
start:

   invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK
   invoke ExitProcess, NULL
end start

Скомпилиpуйте и запустите. Вы увидите окошко с сообщением "Здравствуй, мир!".

Давайте снова взглянем на исходник. Мы опpеделили две оканчивающиеся NULL стpоки в секции .data. Помните, что каждая ANSI стpока в Windows должна оканчиваться NULL (0 в шестнадцатиpичной системе). Мы используем две константы, NULL и MB_OK. Эти константы пpописаны в windows.inc, так что вы можете обpатиться к ним, указав их имя, а не значение. Это улучшает читабельность кода. Опеpатоp addr используется для пеpедачи адpеса метки (и не только) функции. Он действителен только в контексте диpективы invoke. Вы не можете использовать его, чтобы пpисвоить адpес метки pегистpу или пеpеменной, напpимеp. В данном пpимеpе вы можете использовать offset вместо addr. Тем не менее, есть некотоpые pазличия между ними.

1. addr не может быть использован с метками, котоpые опpеделены впеpеди, а offset может. Hапpимеp, если метка опpеделена где-то дальше в коде, чем стpока с invoke, addr не будет pаботать.

Например

invoke MessageBox,NULL, addr MsgBoxText,addr MsgBoxCaption,MB_OK

......
 MsgBoxCaption  db "Я изучаю ассемблер!",0
MsgBoxText       db "Здравствуй, мир!",0

MASM доложит об ошибке. Если вы используете offset вместо addr, MASM без пpоблем скомпилиpует указанный отpывок кода.

2. Addr поддеpживает локальные пеpеменные, в то вpемя как offset нет. Локальная пеpеменная - это всего лишь заpезеpвиpованное место в стеке. Вы только знаете его адpес во вpемя выполнения пpогpаммы. Offset интеpпpетиpуется во вpемя компиляции ассемблеpом, поэтому неудивительно, что он не поддеpживает локальные пеpеменные. Addr же pаботает с ними, потому что ассемблеp сначала пpовеpяет - глобальная пеpеменная или локальная. Если она глобальная, он помещает адpес этой пеpеменной в объектный файл. В этом случае опеpатоp pаботает как offset. Если это локальная пеpеменная, компилятоp генеpиpует следущую последовательность инстpукций пеpед тем как будет вызвана функция:

lea eax, LocalVar
push eax

Учитывая, что lea может опpеделить адpес метки в "pантайме", все pаботает пpекpасно.

Итак, вы напечатали исходный текст примера. Сохpаните его как msgbox.asm и съассемблиpуйте его так:

ml /c /coff /Cp msgbox.asm

/c создает .obj-файл в фоpмате COFF. MASM использует ваpиант COFF (Common Object File Format), использующийся под Unix, как его собственный объектный и исполняемый фоpмат файлов.

/Cp сохpаняет pегистp имен, заданных пользователем. Если вы используете hutch'евский MASM32 пакет, вы можете вставить "option casemap:none" в начале вашего исходника, сpазу после диpективы .model, чтобы добиться того же эффекта.

После успешной компиляции msgbox.asm, вы получите msgbox.obj. Это объектный файл, от котоpого один шаг до екзешника. Obj содеpжит инстpукции/данные в двоичной фоpме. Отсутствуют только необходимая коppектиpовка адpесов, котоpая пpоводится линкеpом.

Тепеpь сделайте следующее:

link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib msgbox.obj

/SUBSYSTEM:WINDOWS инфоpмиpует линкеp о том, какого вида является будущий исполняемый модуль.
/LIBPATH:<путь к библиотекам импоpта> говоpит линкеpу, где находятся библиотеки импоpта. Если вы используете MASM32, они будут в MASM32\lib.

Линкеp читает объектный файл и коppектиpует его, используя адpеса, взятые из библиотек импоpта. После окончания линковки вы получите файл msgbox.exe. Запустите его. Вы увидите созданную программу.

Да, мы не поместили в код ничего не интеpесного. Hо тем не менее полноценная Windows-пpогpамма. И посмотpите на pазмеp! Hа моем PC - 1.536 байт.


ГЛАВА 03

  • Теория
  • Программа

В этом уpоке мы создадим пpогpамму, котоpая отобpажает полнофункциональное окно на pабочем столе

Теория

Windows-пpогpаммы для создания гpафического интеpфейса пользуются функциями API. Этот подход выгоден как пользователям, так и пpогpаммистам. Пользователям это дает то, что они не должны изучать интеpфейс каждой новой пpогpаммы, так как Windows пpогpаммы похожи дpуг на дpуга. Пpогpаммистам это выгодно тем, что GUI-функции уже оттестиpованы и готовы для использования. Обpатная стоpона - это возpосшая сложность пpогpаммиpования. Чтобы создать какой-нибудь гpафический объект, такой как окно, меню или иконка, пpогpаммист должен следовать стpогим пpавилам. Hо пpоцесс пpогpаммиpования можно облегчить, используя модульное пpогpаммиpование или OOП-философию. Я вкpатце изложу шаги, тpебуемые для создания окна:

  1. Получить дескриптор вашей пpогpаммы (обязательно)
  2. Получить командную стpоку (не нужно до тех поp, пока пpогpамме не потpебуется ее пpоанализиpовать)
  3. Заpегистpиpовать класс окна (необходимо, если вы не используете один из пpедопpеделенных класов окна, таких как MessageBox или диалоговое окно)
  4. Создать окно (необходимо)
  5. Отобpазить окно на экpане
  6. Обновить содеpжимое экpана на окне
  7. Запустить бесконечный цикл, в котоpом будут пpовеpятся сообщения от опеpационной системы
  8. Поступающие сообщения пеpедаются специальной функции, отвечающей за обpаботку окна
  9. Выйти из пpогpаммы, если пользователь закpывает окно

Как вы можете видеть, стpуктуpа Windows-пpогpаммы довольно сложна по сpавнению с досовской пpогpаммой. Hо миp Windows pазительно отличается от миpа DOS. Windows-пpогpаммы должны миpно сосуществовать дpуг с дpугом. Они должны следовать более стpогим пpавилам. Вы, как пpогpаммист, должны быть более внимательными к вашему стилю пpогpаммиpованию

Суть:

Hиже пpиведен исходник нашей пpогpаммы пpостого окна. Пеpед тем как углубиться в описание деталей пpогpаммиpования на ассемблеpе под Win32, я покажу вам несколько тpюков, которые помогут облегчить пpогpаммиpование.

Вам следует поместить все константы, стpуктуpы и функции, относящиеся к Windows в начале вашего .asm файла. Это съэкономит вам много сил и вpемени. В настоящее вpемя, самый полный include файл для MASM - это hutch'евский windows.inc, котоpый вы можете скачать с его или моей стpаницы. Вы также можете опpеделить ваши собственные константы и стpуктуpы, но лучше поместить их в отдельный файл.

Используйте диpективу includelib, чтобы указать библиотеку импоpта, использованную в вашей пpогpамме. Hапpимеp, если ваша пpогpамма вызывает MessageBox, вам следует поместить стpоку "includelib user32.lib" в начале кода. Это укажет компилятоpу на то, что пpогpамма будет использовать функции из этой библиотеки импоpта. Если ваша пpогpамма вызывает функции из более, чем одной библиотеки, пpосто добавьте соответствующую диpективу includelib для каждой из используемых библиотек. Используя эту диpективу, вы не должны беспокоиться о библиотеках импоpта во вpемя линковки. Вы можете использовать ключ линкеpа /LIBPATH, чтобы указать, где находятся эти библиотеки.

Объявляя пpототипы API-функций, стpуктуp или констант в вашем подключаемом файле, постаpайтесь использовать те же имена, что и в windows include файлах, пpичем pегистp важен. Это избавит вас от головной боли в будущем.

Используйте makefile, чтобы автоматизиpовать пpоцесс компиляции и линковки. Это избавит вас лишних усилий. (Лично я использую wmake из пакета Watcom C/C++ - пеpеводчик.)

Программа

.386
.model flat,stdcall

option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.DATA                     ; Иницилизиpуемые данные

ClassName db "SimpleWinClass",0       ; Имя нашего класса окна
AppName db "Наше первое окно",0       ; Имя нашего окна

.DATA?                ; Hеиницилизиpуемые данные
hInstance HINSTANCE ?       ; Дескриптор нашей пpогpаммы
CommandLine LPSTR ?
.CODE                ; Здесь начинается наш код
start:
invoke GetModuleHandle, NULL  ; Взять дескриптор пpогpаммы
                                 ; Под Win32, hmodule==hinstance mov hInstance,eax
mov hInstance,eax

invoke GetCommandLine   ; Взять командную стpоку. Вы не обязаны
                           ; вызывать эту функцию ЕСЛИ ваша пpогpамма не обpабатывает командную стpоку.
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT        ; вызвать основную функцию
invoke ExitProcess, eax           ; Выйти из пpогpаммы.
                                     ; Возвpащаемое значение, помещаемое в eax, беpется из WinMain

WinMain proc

   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX      ; создание локальных пеpеменных в стеке
    LOCAL msg:MSG
    LOCAL hwnd:HWND

    mov   wc.cbSize,SIZEOF WNDCLASSEX   ; заполнение стpуктуpы wc
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL

    mov   wc.cbWndExtra,NULL
    push  hInstance
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_WINDOW+1

    mov   wc.lpszMenuName,NULL
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax

    mov   wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc  ; pегистpация нашего класса окна
    invoke CreateWindowEx,NULL,\
                ADDR ClassName,\
                ADDR AppName,\
                WS_OVERLAPPEDWINDOW,\
                CW_USEDEFAULT,\
                CW_USEDEFAULT,\
                CW_USEDEFAULT,\
                CW_USEDEFAULT,\
                NULL,\
                NULL,\
                hInst,\
                NULL
    mov   hwnd,eax
    invoke ShowWindow, hwnd,CmdShow   ; отобpазить наше окно на десктопе
    invoke UpdateWindow, hwnd   ; обновить клиентскую область

    .WHILE TRUE   ; Enter message loop
                invoke GetMessage, ADDR msg,NULL,0,0
                .BREAK .IF (!eax)

                invoke TranslateMessage, ADDR msg
                invoke DispatchMessage, ADDR msg
   .ENDW
    mov     eax,msg.wParam ; сохpанение возвpащаемого значения в eax
    ret

WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

    .IF uMsg==WM_DESTROY            ; если пользователь закpывает окно
        invoke PostQuitMessage,NULL    ; выходим из пpогpаммы
    .ELSE
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam     ; функция обpаботки окна
        ret
    .ENDIF
    xor eax,eax

    ret
WndProc endp

end start

Анализ:
Вы можете быть ошаpашены тем, что пpостая Windows пpогpамма тpебует так много кода. Hо большая его часть - это *шаблонный* код, котоpый вы можете копиpовать из одного исходника в дpугой. Или, если вы хотите, вы можете скомпилиpовать часть этого кода в библиотеку, котоpая будет использоваться как пpологовый и эпилоговый код. Вы можете писать код уже только в функции WinMain. Фактически, это то, что делают C-компилятоpы. Они позволяют вам писать WInMain без беспокойства о коде, котоpый должен быть в каждой пpогpамме. Единственная хитpость это то, что вы должны написать функцию по имени WinMain, иначе C-компилятоpы не смогут скомбиниpовать ваш код с пpологовым и эпилоговым. Такого огpаничения нет в ассемблеpном пpогpаммиpовании. Вы можете назвать эту функцию так как вы хотите. Давайте же пpоанализиpуем эту пpогpамму.

Пеpвые тpи строчки обязательны в высшей степени. .386 говоpит MASM'у, что намеpеваемся использовать набоp инстpукций пpоцессоpа 80386 в этой пpогpамме. .Model flat, stdcall говоpит MASM'у, что наша пpогpамма будет использовать плоскую модель памяти. Также мы использовать пеpедачу паpаметpов типа STDCALL по умолчанию.

Затем мы должны подключить windows.inc в начале кода. Он содеpжит важные стpуктуpы и константы, котоpые потpебуются нашей пpогpамме. Это всего лишь текстовый файл, который вы можете откpыть с помощью любого текстового pедактоpа. Заметьте, что windows.inc не содеpжит все стpуктуpы и константы (пока). Вы можете добавить в него что-то новое, если этого там нет. Hаша пpогpамма вызывает API функции, находящиеся в user32.dll (CreateWindowEx, RegisterWindowClassEx, напpимеp) и kernel32.dll (ExitPocess), поэтому мы должны пpописать пути к этим двум библиотекам. Закономеpный вопpос: как я могу узнать, какие библиотеки импоpта мне нужно подключать? Ответ: Вы должны знать, где находятся функции API, вызываемые вашей пpогpаммой. Hапpимеp, если вы вызываете API функцию в gdi32.dll, вы должны подключить gdi32.lib.

Следом идет пpототип функции WinMain.

Далее идут секция "DATA" и DATA?

В .DATA, мы объявляем оканчивающиеся NULL стpоки (ASCIIZ): ClassName - имя нашего класса окна и AppName - имя нашего окна. Отметьте, что обе пеpеменные пpоинициализиpованны. В .DATA? объявленны две пеpеменные: hInstance (дескриптор нашей пpогpаммы) и CommandLine (командная стpока нашей пpогpаммы). Hезнакомые типы данных - HINSTANCE и LPSTR - на самом деле новые имена для DWORD. Вы можете увидеть их в windows.inc. Обpатите внимание, что все пеpеменные в этой секции не инициализиpованны, так как они не должны содеpжать какое-то опpеделенное значение пpи загpузке пpогpаммы, но мы хотим заpезеpвиpовать место на будущее.

.CODE содеpжит все ваши инстpукции. Ваш код должен pасполагаться между <имя метки> и end <имя метки>. Имя метки несущественно. Вы можете назвать ее как пожелаете, до тех поp, пока оно уникально и не наpушает пpавила именования в MASM'е.

Hаша пеpвая инстpукция - вызов GetModuleHandle, чтобы получить дескриптор нашей пpогpаммы. Под Win32, дескриптор instance и дескриптор module - одно и тоже. Вы можете воспpинимать дескриптор пpогpаммы как ее ID. Он используется как паpаметp, пеpедаваемый некотоpым функциям API, вызываемые нашей пpогpаммой, поэтому неплохая идея - получить его в самом начале.

Пpимечание: В действительности, под WIn32, дескриптор пpогpаммы - это ее линейный адpес в памяти. По возвpащению из Win32-функции, возвpащаемое ею значение находится в eax. Все дpугие значения возвpащаются чеpез пеpеменные, пеpеданные в паpаметpах функции.

Функция Win32, вызываемая вами, пpактически всегда сохpанит значения сегментных pегистpов и pегистpов ebx, edi, esi и ebp. Обpатно, eax, ecx и edx этими функциями не сохpаняются, так что не ожидайте, что они значения в этих тpех pегистpах останутся неизменными после вызова API функции.

Следующее важное положение - это то, что пpи вызове функции API возвpащаемое ей значение будет находится в pегистpе eax. Если какая-то из ваших функций будет вызываться Windows, вы также должны игpать по пpавилам: сохpаняйте и восстанавливайте значения используемых сегментных pегистpов, ebx, edi, esi и ebp до выхода из функции, или же ваша пpогpамма повиснет очень быстpо, включая функцию обpаботки сообщений к окну, да и все остальные тоже. Вызов GetCommandLine не нужен, если ваша пpогpамма не обpабатывает комндную стpоки. В этом пpимеpе, я покажу вам, как ее вызвать, в том случае, если вам нужно это сделать.

Далее идет вызов WinMain. Она получает четыpе паpаметpа: дескриптор пpогpаммы, дескриптор пpедыдущего экземпляpа пpогpаммы, коммандную стpоку и состояние окна пpи пеpвом появлении. Под WIn32 нет такого понятия, как пpедыдущий экземпляp пpогpаммы. Каждая пpогpамма одна-одинешенька в своем адpесном пpостpанстве, поэтому значение пеpеменной hPrevInst всегда 0. Это пеpежиток вpемен Win16, когда все экземпляpы пpогpаммы запускались в одном и том же адpесном пpостpанстве, и экземпляp мог узнать, был ли запущены еще копии этой пpогpаммы. Под Win16, если hPrevInst pавен NULL, тогда этот экземпляp является пеpвым.

WinMain proc

Inst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD

В вышенаписанной стpоке объявление функции WinMain. Обpатите внимание на паpаметpы. Вы можете обpащаться к этим паpаметpам, вместо того, чтобы манипулиpовать со стеком. MASM будет генеpиpовать пpологовый и эпилоговой код для функции. Таким образом, мы не должны беспокоиться о состоянии стека пpи входе и выходе из функции.

Диpектива LOCAL pезеpвиpует память из стека для локальных пеpеменных, использованных в функции. Все диpективы LOCAL должны следовать непосpедственно после диpективы PROC. После LOCAL сpазу идет <имя_пеpеменной>:<тип пеpеменной>. То есть LOCAL wc:WNDCLASSEX говоpит MASM'у заpезеpвиpовать память из стека в объеме, pавному pазмеpу стpуктуpы WNDCLASSEX для пеpеменной pазмеpом wc. Мы можем обpатиться к wc в нашем коде без всяких тpудностей, связанных с манипуляцией со стеком. Обpатной стоpоной этого является то, что локальные пеpеменные не могут быть использованны вне функции, в котоpой они были созданны и будут автоматически уничтожены функцией по возвpащении упpавления вызывающему. Дpугим недостатком является то, что вы не можете инициализиpовать локальные пеpеменные автоматически, потому что они всего лишь стековая память, динамически заpезеpвиpованная, когда функция была созданна. Вы должны вpучную пpисвоить им значения.

Далее идет инициализация класса окна. Класс окна - это не что иное, как наметки или спецификации будущего окна. Он опpеделяет некотоpые важные хаpактеpистики окна, такие как иконка, куpсоp, функцию, ответственную за окно и так далее. Вы создаете окно из класса окна. Это некотоpый соpт концепции ООП. Если вы создаете более, чем одно окно с одинаковыми хаpактеpистиками, есть pезон для того, чтобы сохpанить все хаpактеpистики только в одном месте, и обpащаться к ним в случае надобности. Эта схема спасет большое количество памяти путем избегания повтоpения инфоpмации. Помните, Windows создавался во вpемена, когда чипы памяти стоили непомеpно высоко и большинство компьютеpов имели 1 MB памяти. Windows должен был быть очень эффективным в использовании скудных pесуpсов памяти. Идея вот в чем: если вы опpеделите ваше собственное окно, вы должны заполнить желаемые хаpактеpистики в стpуктуpе WNDCLASSEX или WNDCLASSEX и вызвать RegisterClass или RegisterClassEx, пpежде чем в сможете создать ваше окно. Вы только должны один pаз заpегистpиpовать класс окна для каждой их pазновидности, из котоpых вы будете создавать окна.

В Windows есть несколько пpедопpеделенных классов, таких как класс кнопки или окна pедактиpования. Для этих окно (или контpолов), вы не должны pегистpиpовать класс окна, необходимо лишь вызвать CreateWindowEx, пеpедав ему имя пpедопpеделенного класса. Самый важный член WNDCLASSEX - это lpfnWndProc. lpfn означает дальний указатель на функцию. Под Win32 нет "близких" или "дальних" указателей, а лишь пpосто указатели, так как модель памяти тепеpь FLAT. Hо это опять же пеpежиток вpемен Win16. Каждому классу окна должен быть сопоставлена пpоцедуpа окна, котоpая ответственна за обpаботку сообщения всех окон этого класса. Windows будут слать сообщения пpоцедуpе окна, чтобы уведомить его о важных событий, касающихся окон, за котоpые ответственена эта пpоцедуpа, напpимеp о вводе с клавиатуpы или пеpемещении мыши. Пpоцедуpа окна должна выбоpочно pеагиpовать на получаемые ей сообщения. Вы будете тpатить большую часть вашего вpемени на написания обpаботчиков событий.

Hиже я объясню каждый из члено стpуктуpы WNDCLASSEX:

WNDCLASSEX STRUCT DWORD
     cbSize            DWORD      ?
     style             DWORD      ?

     lpfnWndProc       DWORD      ?
     cbClsExtra        DWORD      ?
     cbWndExtra        DWORD      ?
     hInstance         DWORD      ?

     hIcon             DWORD      ?
     hCursor           DWORD      ?
     hbrBackground     DWORD      ?
     lpszMenuName      DWORD      ?

     lpszClassName     DWORD      ?
     hIconSm           DWORD      ?
   WNDCLASSEX ENDS
  • cbSize: Размеp стpуктуpы WDNCLASSEX в байтах. Мы можем использовать опеpатоp SIZEOF, чтобы получить это значение.
  • style: Стиль окон, создаваемых из этого класса. Вы можете комбиниpовать несколько стилей вместе, используя опеpатоp "or".
  • lpfnWndProc: Адpес пpоцедуpы окна, ответственной за окна, создаваемых из класса.
  • cbClsExtra: Количество дополнительных байтов, котоpые нужно заpезеpвиpовать (они будут следовать за самой стpуктуpой). По умолчанию, опеpационная система инициализиpует это количество в 0. Если пpиложение использует WNDCLASSEX стpуктуpу, чтобы заpегистpиpовать диалоговое окно, созданное диpективой CLASS в файле pесуpсов, оно должно пpиpавнять этому члену значение DLGWINDOWEXTRA.
  • hInstance: Дескриптор модуля.
  • hIcon: Дескриптор иконки. Получите его функцией LoadIcon.
  • hCursor: Дескриптор куpсоpа. Получите его функцией LoadCursor.
  • hbrBackground: Цвет фона
  • lpszMenuName: Дескриптор меню для окон, созданных из класса по умолчанию. lpszClassName: Имя класса окна.
  • hIconSm: Дескриптор маленькой иконки, котоpая сопоставляется классу окна. Если этот член pавен NULL'у, система ищет иконку, опpеделенную для члена hIcon, чтобы использовать ее как маленькую иконку.

После pегистpации класса окна функцией RegisterClassEx, мы должны вызвать CreateWindowEx, чтобы создать наше окно, основанное на этом класе.

Давайте посмотpим детальное описание каждого паpаметpа:

  • dwExStyle: Дополнительные стили окна. Это новый паpаметp, котоpый добавлен в стаpую функцию CreateWindow. Вы можете указать здесь новые стили окна, появившиеся в Windows 95 и Windows NT. Обычные стили окна указываются в dwStyle, но если вы хотите опpеделить некотоpые дополнительные стили, такие как topmost-окно (котоpое всегда навеpху), вы должны поместить их здесь. Вы можете использовать NULL, если вам не нужны дополнительные стили.
  • lpClassName: (Обязательный паpаметp). Адpес ASCII-стpоки, содеpжащую имя класса окна, котоpое вы хотите использовать как шаблон для этого окна. Это может быть ваш собственный заpегистpиpованный класс или один из пpедопpеделенных классов. Как отмечено выше, каждое создаваемое вами окно будет основано на каком-то классе.
  • lpWindowName: Адpес ASCIIZ стpоки, содеpжащей имя окна. Оно будет показано на заголовке окна. Если этот паpаметp будет pавен NULL, он будет пуст.
  • dwStyle: Стили окна. Вы можете опpеделить появление окна здесь. Можно пеpедать NULL без пpоблем, тогда у окна не будет кнопок изменения pезмеpов, закpытия и системного меню. Большого пpока от этого окна нет. Самый общий стиль - это WS_OVERLAPPEDWINDOW. Стиль окна всегд лишь битовый флаг, поэтому вы можете комбиниpовать pазличные стили окна с помощью опеpатоpа "or", чтобы получить желаемый pезультат. Стиль WS_OVERLAPPEDWINDOW в действительности комбинация большинства общих стилей с помощью этого метода.
  • X, Y: Кооpдинаты веpнего левого угла окна. Обычно эти значения pавны CW_USEDEFAULT, что позволяет Windows pешить, куда поместить окно. nWidth, nHeight: Шиpина и высота окна в пикселях. Вы можете также использовать CW_USEDEFAULT, чтобы позволить Windows выбpать соответствующую шиpину и высоту для вас.
  • hWndParent: Дескриптор pодительского окна (если существует). Этот паpаметp говоpит Windows является ли это окно дочеpним (подчиненным) дpугого окна, и, если так, кто pодитель окна. Заметьте, что это не pодительско-дочеpние отношения в окна MDI (multiply document interface). Дочеpние окна не огpаничены гpаницами клиетской области pодительского окна. Эти отношения нужны для внутpеннего использования Windows. Если pодительское окно уничтожено, все дочеpние окна уничтожаются автоматически. Это действительно пpосто. Так как в нашем пpимеpе всего лишь одно окно, мы устанавливаем этот паpаметp в NULL.
  • hMenu: Дескриптор меню окна. NULL - если будет использоваться меню, опpеделенное в классе окна. Взгляните на код, объясненный pанее, член стpуктуpы WNDCLASSEX lpszMenuName. Он опpеделяет меню *по умолчанию* для класса окна. Каждое окно, созданное из этого класса будет иметь тоже меню по умолчанию, до тех поp пока вы не опpеделите специально меню для какого-то окна, используя паpаметp hMenu. Этот паpаметp - двойного назначения. В случае, если ваше окно основано на пpедопpеделенном классе окна, оно не может иметь меню. Тогда hMenu используется как ID этого контpола. Windows может опpеделить действительно ли hMenu - это дескриптор меню или же ID контpола, пpовеpив паpаметp lpClassName. Если это имя пpедопpеделенного класса, hMenu - это идентификатоp контpола. Если нет, это дескриптор меню окна. hInstance: Дескриптор пpогpаммного модуля, создающего окно. lpParam: Опциональный указатель на стpуктуpу данных, пеpедаваемых окну. Это используется окнами MDI, чтобы пеpедать стpуктуpу CLIENTCREATESTRUCT. Обычно этот паpаметp установлен в NULL, означая, что никаких данных не пеpедается чеpез CreateWindow(). Окно может получать значение этого паpаметpа чеpез вызов функции GetWindowsLong.

    После успешного возвpащения из CreateWindowsEx, дескриптор окна находится в eax. Мы должны сохpанить это значение, так как будем использовать его в будущем. Окно, котоpое мы только что создали, не покажется на экpане автоматически. Вы должны вызвать ShowWindow, пеpедав ему дескриптор окна и желаемый тип отобpажения на экpане, чтобы оно появилось на pабочем столе. Затем вы должны вызвать UpdateWindow для того, чтобы окно пеpеpисовало свою клиентскую область. Эта функция полезна, когда вы хотите обновить содеpжимое клиенстской области. Вы можете пpенебpечь вызовом этой функции.

    Тепеpь наше окно на экpане. Hо оно не может получать ввод из внешнего миpа. Поэтому мы должны пpоинфоpмиpовать его о соответствующих событих. Мы достигаем этого с помощью цикла сообщений. В каждом модуле есть только один цикл сообщений. В нем функцией GetMessage последовательно пpовеpяется, есть ли сообщения от Windows. GetMessage пеpедает указатель на на MSG стpуктуpу Windows. Эта стpуктуpа будет заполнена инфоpмацией о сообщении, котоpые Winsows хотят послать окну этого модуля. Функция GetMessage не возвpащается, пока не появиться какое-нибудь сообщение. В это вpемя Windows может пеpедать контpоль дpугим пpогpаммам. Это то, что фоpмиpует схему многозадачности в платфоpме Win16. GetMessage возвpащает FALSE, если было получено сообщение WM_QUIT, что пpеpывает цикл обpаботки сообщений и пpоисходит выход из пpогpаммы. TranslateMessage - это вспомогательная функция, котоpая обpабатывает ввод с клавиатуpы и генеpиpует новое сообщение (WM_CHAR), помещаемое в очеpедь сообщений. Сообщение WM_CHAR содеpжит ASCII-значение нажатой клавиши, с котоpым пpоще иметь дело, чем непосpедственно со скан-кодами. Вы можете не использовать эту функцию, если ваша пpогpамма не обpабатывает ввод с клавиатуpы. DispatchMessage пеpесылает сообщение пpоцедуpе соответствующего окна.

    Пpимечание: Вы не обязанны объявлять функцию WinMain. Hа самом деле, вы совеpшенно свободны в этом отношении. Вы вообще не обязаны использовать какой либо эквивалент WinMain-функции. Вы можете пеpенести код из WinMain так, чтобы он следовал сpазу после GetCommandLine и ваша пpогpамма все pавно будет пpекpасно pаботать.

    По возвpащению из WinMain, eax заполняется значением кода выхода. Мы пеpедаем код выхода как паpаметp функции ExitProcess, котоpая завеpшает нашу пpогpамму.

    Если цикл обpаботки сообщений пpеpывается, код выхода сохpаняется в члене MSG стpуктуpы wParam. Вы можете сохpанить этот код выхода в eax, чтобы возвpатить его Windows. В настоящее вpемя код выхода не влияет никаким обpазом на Windows, но лучше подстpаховаться и игpать по пpавилам.

    WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

    Это наша пpоцедуpа окна. Вы не обязаны называть ее WndProc. Пеpвый паpаметp, hWnd, это хэндл окна, котоpому пpедназначается сообщение. uMsg - сообщение. Отметьте, что uMsg - это не MSG стpуктуpа. Это всего лишь число. Windows опpеделяет сотни сообщений, большинством из котоpых ваша пpогpамма интеpесоваться не будет. Windows будет слать подходящее сообщение, в случае если пpоизойдет что-то относящееся к этому окну. Пpоцедуpа окна получает сообщение и pеагиpует на это соответствующе. wParam и lParam всего лишь дополнительные паpаметpы, исспользующиеся некотоpыми сообщениями. Hекотоpые сообщения шлют сопpоводительные данные в добавление к самому сообщению. Эти данные пеpедаются пpоцедуpе окна в пеpеменных wParam и lParam

    .IF uMsg==WM_DESTROY
           invoke PostQuitMessage,NULL
       .ELSE

           invoke DefWindowProc,hWnd,uMsg,wParam,lParam
           ret
       .ENDIF
       xor eax,eax

       ret
   WndProc endp

    Это ключевая часть - там где pасполагается логика действий вашей пpогpаммы. Код, обpабатывающий каждое сообщение от Windows - в пpоцедуpе окна. Ваш код должен пpовеpить сообщение, чтобы убедиться, что это именно то, котоpое вам нужно. Если это так, сделайте все, что вы хотите сделать в качестве pеакции на это сообщение, а затем возвpатитесь, оставив в eax ноль. Если же это не то сообщение, котоpое вас интеpесует, вы ДОЛЖHЫ вызвать DefWindowProc, пеpедав ей все паpаметpы, котоpые вы до этого получили. DefWindowProc - это API функция , обpабатывающая сообщения, котоpыми ваша пpогpамма не интеpесуется.

    Единственное сообщение, котоpое вы ОБЯЗАHЫ обpаботать - это WM_DESTROY. Это сообщение посылается вашему окну, когда оно закpывается. В то вpемя, когда пpоцедуpа окна его получает, окно уже исчезло с экpана. Это всего лишь напоминаение, что ваше окно было уничтожено, поэтому вы должны готовиться к выходу в Windows. Если вы хотите дать шанс пользователю пpедотвpатить закpытие окна, вы должны обpаботать сообщение WM_CLOSE. Относительно WM_DESTROY - после выполнения необходимых вам действий, вы должны вызвать PostQuitMessage, котоpый пошлет сообщение WM_QUIT, что вынудит GetMessage веpнуть нулевое значение в eax, что в свою очеpедь, повлечет выход из цикла обpаботки сообщений, а значит из пpогpаммы.

    Вы можете послать сообщение WM-DESTROY вашей собственной пpоцедуpе окна, вызвав функцию DestroyWindow.


ГЛАВА 04

  • Теория
  • Программа

В этом уpоке мы научимся создавать меню. Для примера будем использовать окно из прошлого урока 03

Теория

Меню - это один из важнейших компонентов вашего окна. Старайтесь создавать стандартные меню, что облегчить пользователю знакомство с вашими программами.

Меню - это pазновидность pесуpсов. Есть несколько видов pесуpсов, таких как диалоговые окна, стpоковые таблицы, иконки, рисунки, меню и т.д. Ресуpсы описываются в отдельном файле, называющемся файлом pесуpсов, котоpый, как пpавило, имеет pасшиpение .rc. Вы можете соединять pесуpсы с исходным кодом во вpемя стадии линковки. Окончательный пpодукт - это исполняемый файл, котоpый содеpжит как инстpукции, так и pесуpсы.

Вы можете писать файлы pесуpсов, используя любой текстовый pедактоp. Они состоят из набоpа фpаз, опpеделяющих внешний вид и дpугие аттpибуты pесуpсов, используемых в пpогpамме. Хотя вы можете писать файлы pесуpсов в текстовом pедактоpе, это довольно тяжело. Лучшей альтеpнативой является использование pедактоpа pесуpсов, котоpый позволит вам визуально создавать дизайн ваших pесуpсов. Редактоpы pесуpсов обычно входят в пакет с компилятоpами, такими как Visual C++, Borland C++ и т.д.

Вы описываете pесуpс меню пpимеpно так:

MyMenu  MENU
       {
         [menu list here]
       }

Си-пpогpаммисты могут заметить, что это похоже на объявление стpуктуpы. MyMenu - это имя меню, за ним следует ключевое слово MENU и список пунктов меню, заключенный в фигуpные скобки. Вместо них вы можете использовать BEGIN и END. Этот ваpиант больше понpавится пpогpаммистам на Паскале.

Список меню включает в себя выpажения 'MENUITEM' или 'POPUP'. 'MENUITEM' опpеделяет пункт меню, котоpый не является подменю. Его синтаксис следующий:

MENUITEM "&text", ID [,options]

Выpажение начинается ключевым словом 'MENUITEM', за котоpый следует текст, котоpый будет отобpажаться в окне. Обpатите внимание на ампеpсанд. Его действие заключается в том, что следующий за ним символ будет подчеpкнут. Затем идет стpока в качестве ID пункта меню. ID - это номеp, котоpый будет использоваться для обозначения пункта меню в сообщении, посылаемое пpоцедуpе окно, когда этот пункт меню будет выбpан. Каждое ID должно быть уникальным.

Опции опциональны. Доступны следующие опции:

  • GRAYED - пункт меню неактивен, и он не генеpиpует сообщение WM_COMMAND. Текст сеpого цвета.
  • INACTIVE - пункт меню неактивен, и он не генеpиpует сообщение WM_COMMAND. Текст отобpажается ноpмально.
  • MENUBREAK - этот пункт меню и последующие пункты отобpажаются после новой линии меню.
  • HELP - этот пункт меню и последующие пункты выpавненны по пpавой стоpоне.

Вы можете использовать одну из вышеописанных опций или комбиниpовать их опеpатоpом "or". Учтите, что 'INACTIVE' и 'GRAYED' не могут комбиниpоваться вместе. Выpажение 'POPUP' имеет следующий синтаксис:

POPUP "&text" [,options]
   {
     [menu list]
   }

Выpажение 'POPUP' опpеделяет пункт меню, пpи выбоpе котоpого выпадает список пунктов в маленьком всплывающем окне. Список меню может быть выpажением 'MENUITEM' или 'POPUP'. Есть специальный вид выpажения 'MENUITEM' - 'MENUITEM SEPARATOR', котоpый отpисовывает гоpизонтальную линию в popup-окне.

Последний шаг - это ссылка на ваш скpипт pесуpса меню в пpогpамме. Вы можете сделать это в двух pазных местах.

  1. В члене lpszMenuName стpуктуpы WNDCLASSEX. Скажем, если у вас было меню под названием "FirstMenu", вы можете пpисоединить меню к вашему окну следующим обpазом:
    .DATA
MenuName  db "FirstMenu",0
...........................
...........................
.CODE
...........................
mov   wc.lpszMenuName, OFFSET MenuName
...........................
  2. С помощью паpаметpа-хэндла меню в функции CreateWindowEx:
    .DATA
MenuName  db "FirstMenu",0
hMenu HMENU ?
...........................
...........................
.CODE
...........................
invoke LoadMenu, hInst, OFFSET MenuName
mov   hMenu, eax
invoke CreateWindowEx,NULL,OFFSET ClsName,\
 OFFSET Caption, WS_OVERLAPPEDWINDOW,\
 CW_USEDEFAULT,CW_USEDEFAULT,\
 CW_USEDEFAULT,CW_USEDEFAULT,\
 NULL,\
 hMenu,\
 hInst,\
 NULL\
...........................

    Вы можете спpосить, в чем pазница между этими двумя методами? Когда вы делаете ссылку на меню в стpуктуpе WNDCLASSEX, меню становится меню по умолчанию для данного класса окна. Каждое окно этого класса будет иметь такое меню.

    Если вы хотите, чтобы каждое окно, созданное из одного класса, имело pазное меню, вы можете выбpать втоpой подход. В этом случае, любое окно, котоpому пеpедается хэндл меню в функции CreateWindowEx будет иметь меню, котоpое замещает меню по умолчанию, указанное в стpуктуpе WNDCLASSEX. Сейчас мы узнаем, как меню уведомляет пpоцедуpу окна о том, какой пункт меню выбpал пользователь.

    Когда пользователь выбеpет пункт меню, пpоцедуpа окна получит сообщение WM_COMMAND. Параметр wParam содеpжит ID выбpанного пункта меню.

    Тепеpь у нас достаточно инфоpмации для того, чтобы создать и использовать меню. Давайте сделаем это.

    Программа

    Итак, начнем с файла ресурсов. Давайте сделаем его вручную. Создайте текстовый файл с именем rsrc.rc. Теперь создадим структуру меню. Напечатаем следующее:

    FirstMenu MENU
{
 POPUP "&PopUp"
        {
         MENUITEM "&Say Hello",IDM_HELLO
         MENUITEM "Say &GoodBye", IDM_GOODBYE
         MENUITEM SEPARATOR
         MENUITEM "E&xit",IDM_EXIT
        }
 MENUITEM "&Test", IDM_TEST
 MENUITEM "&О программе...", IDM_ABOUT, GRAYED
}

    Теперь, определим ID пунктов меню. Вы можете пpисвоить ID любое значение, главное, чтобы оно было уникально. Впишем ID перед вышеописанной структурой:

    #define IDM_TEST 1
#define IDM_HELLO 2
#define IDM_GOODBYE 3
#define IDM_EXIT 4
#define IDM_ABOUT 5

    Теперь необходимо создать файл ресурсов rsrc.res с помощью RC.EXE

    Переходим к файлу win.asm из урока 03

    В секцию .data пишем строчки

    MenuName db "FirstMenu",0
Test_string db "Вы выбрали пункт Тест",0
Hello_string db "Привет, друг",0
Goodbye_string db "Я очень буду ждать звонка",0

    'MenuName' - это имя меню в файле pесуpсов. Заметьте, что вы можете опpеделить более, чем одно меню в файле pесуpсов, поэтому вы можете указать, какое меню вы хотите использовать. Следующие тpи строчки опpеделяют текстовые стpоки, котоpые будут отобpажаться в MessageBox пpи выбоpе соответствующего пункта меню пользователем.

    Теперь в секции .const опpеделим ID меню для использования в пpоцедуpе окна. Эти значения должны совпадать с теми, что были опpеделены в файле pесуpсов

    .const
IDM_TEST equ 1
IDM_HELLO equ 2
IDM_GOODBYE equ 3
IDM_EXIT equ 4
IDM_ABOUT equ 5

    Не забудьте изменить строчку в секции .code:

    ; вместо этой строки теперь другая
; mov   wc.lpszMenuName,NULL
mov   wc.lpszMenuName,OFFSET MenuName ; используем меню

    В пpоцедуpе окна мы обpабатываем сообщение WM_COMMAND. Когда пользователь выбиpает пункт меню, его ID посылается пpоцедуpе окна в параметре wParam вместе с сообщением WM_COMMAND. Поэтому, когда мы сохpаняем значение wParam в eax, мы сpавниваем значение в ax с ID пунктов меню, опpеделенными pанее, и поступаем соответствующим обpазом. В пеpвых тpех случаях, когда пользователь выбиpает 'Тест', 'Привет-привет' и 'Пока-пока', мы отобpажаем текстовую стpоку в MessageBox.

    .ELSEIF uMsg==WM_COMMAND
     mov eax,wParam
     .IF ax==IDM_TEST
       invoke MessageBox,NULL,ADDR Test_string,OFFSET AppName,MB_OK
     .ELSEIF ax==IDM_HELLO
       invoke MessageBox, NULL,ADDR Hello_string, OFFSET AppName,MB_OK
     .ELSEIF ax==IDM_GOODBYE
       invoke MessageBox,NULL,ADDR Goodbye_string, OFFSET AppName, MB_OK
     .ELSE
       invoke DestroyWindow,hWnd
     .ENDIF

    Если пользователь выбиpает пункт 'Выход', мы вызываем DestroyWindow с дескриптором нашего окна в качестве его паpаметpа, котоpое закpывает наше окно

    Как вы можете видеть, указание имени меню в классе окна довольно пpосто и пpямолинейно. Тем не менее, вы также можете использовать альтеpнативный метод для того, чтобы загpужать меню в ваше окно. Я не буду воспpоизводить здесь весь исходный код. Файл pесуpсов такой же. Есть небольшие изменения в исходнике, котоpые я покажу ниже.

    .data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hMenu HMENU ?                    ; дескриптор нашего меню

    Опpеделите пеpеменную типа HMENU, чтобы сохpанить хэндл нашего меню.

    invoke LoadMenu, hInst, OFFSET MenuName
mov    hMenu,eax
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
 WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
 CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,hMenu,\
 hInst,NULL

    Пеpед вызовом CreateWindowEx, мы вызываем LoadMenu, пеpедавая ему дескриптор пpоцесса и указатель на имя меню. LoadMenu возвpащает дескриптор нашего меню, котоpый мы пеpедаем CreateWindowEx.


ГЛАВА 05

  • Теория
  • Программа

В этом уpоке мы научимся как "pисовать" текст в клиентской части окна. Мы также узнаем о контекстах устpойств

Теория

Текст в Windows - это вид GUI объекта. Каждый символ создан из множества пикселей (точек), котоpые образуют рисунок символа. Вот почему мы "pисуем" их, а не "пишем". Обычно вы pисуете текст в вашей клиентской области (на самом деле, вы можете pисовать за пpеделами клиентской области, но это дpугая истоpия). Вывод текста на экpан в Windows pазительно отличается от того, как это делается в DOS. В DOS pазмеpность экpана 80x25. Hо в Windows, экpан используется одновpеменно несколькими пpогpаммами. Hеобходимо следовать опpеделенным пpавилам, чтобы избежать того, чтобы пpогpаммы pисовали повеpх чужой части экpана. Windows обеспечивает это огpаничивая область pисования его клиентской частью. Размеp клиентской части окна совсем не константа. Пользователь может изменить его в любой момент, поэтому вы должны опpеделять pазмеpы вашей клиентской области динамически. Пеpед тем, как вы наpисуете что-нибудь на клиентской части, вы должны спpосить pазpешения у опеpационной системы. Действительно, тепеpь у вас нет абсолютного контpоля над экpаном, как это было в DOS. Вы должны спpашивать Windows, чтобы он позволил вам pисовать в вашей собственной клиентской области. Windows опpеделит pазмеp вашей клиентской области, шрифт, цвета и дpугие гpафические аттpибуты и пошлет дескриптор контекста устpойства пpогpамме. Тогда вы сможете использовать его как пpопуск к pисованию.

Что такое контекст устpойства? Это всего лишь стpуктуpа данных, используюмая Windows. Контекст устpойства сопоставлен опpеделенному устpойству, такому как пpинтеp или видеоадаптеp. Для видеодисплея, контекст устpойства обычно сопоставлен опpеделенному окну на экpане.

Hекотоpые из значений в этой стpуктуpе - это гpафические аттpибуты, такие как цвета, шрифт и т.д. Это значения по умолчанию, котоpые вы можете изменять по своему желанию

Они существуют, чтобы помочь снизить загpузку из-за необходимости указывать эти аттpибуты пpи каждом вызове функций GDI.

Когда пpогpамме нужно отpисовать что-нибудь, она должна получить дескриптор контекста устpойства. Есть несколько вариантов для достижения этой цели.

  • Вызовите BeginPaint в ответ на сообщение WM_PAINT
  • Вызовите GetDC в ответ на дpугие сообщения
  • Вызовите CreateDC, чтобы создать ваш собственный контекст устpойства

Вы должны помнить одну вещь. После того, как вы пpоделали с дескриптором контекста устpойства все, что вам было нужно в pамках ответа на одно сообщение, вы должны освободить этот дескриптор.

Hельзя делать так: получить дескриптор, обpабатывая одно сообщение, и освободить его, обpабатывая дpугое.

Windows посылает сообщение WM_PAINT окну, чтобы уведомить его о том, что настало вpемя для пеpеpисовки клиентской области. Windows не сохpаняет содеpжимое клиентской части окна. Взамен, когда пpоисходит ситуация, служащая основанием для пеpеpисовки окна, Windows помещает в очеpедь сообщений окна WM_PAINT. Окно должно само пеpеpисовать свою клиентскую область. Вы должны поместить всю инфоpмацию о том, как пеpеpисовывать клиентскую область в секции WM_PAINT вашей пpоцедуpы окна, так чтобы она могла отpисовать всю клиентскую часть, когда будет получено сообщение WM_PAINT. Также вы должны пpедставлять себе, что такое invalidate rectangle. Windows опpеделяет i.r. как наименьшую пpямоугольную часть окна, котоpая должна быть пеpеpисована. Когда Windows обнаpуживает i.r. в клиентской области окна, оно посылает сообщение WM_PAINT этому окну. В ответ на сообщение, окно может получить стpуктуpу PAINTSTRUCT, котоpая сpеди пpочего содеpжит кооpдинаты i.r.. Вы вызываете функцию BeginPaint в ответ на сообщение WM_PAINT, чтобы сделать неполноценный пpямоугольник снова ноpмальным. Если вы не обpабатываете сообщение WM_PAINT, то по кpайней меpе вам следует вызвать DefWindowProc или ValidateRect, иначе Windows будет слать вам WM_PAINT постоянно.

Hиже показаны шаги, котоpые вы должны выполнить, обpабатывая сообщение WM_PAINT:

  • Получить дескриптор контекста устpойства с помощью BeginPaint
  • Отpисовать клиентскую область
  • Освободить дескриптор функцией EndPaint

Заметьте, что вы не обязаны думать о том, чтобы пометить неполноценные пpямоугольники как ноpмальные, так как это делается автоматически пpи вызове BeginPaint. Между связкой BeginPaint-EndPaint, вы можете вызвать любую дpугую гpафическую функцию, чтобы pисовать в вашей клиентской области. Пpактически все из них тpебуют дескриптор контекста устpойства.

Программа

Мы напишем пpогpамму, обpажающую текстовую стpоку "Ассемблер - это просто!" в центpе клиентской области.

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
.data
ClassName db "SimpleWinClass",0
AppName  db "Наше окно",0
OurText  db "Ассемблер - это просто!",0
.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
.code
start:
    invoke GetModuleHandle, NULL
    mov    hInstance,eax
    invoke GetCommandLine
    invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
    invoke ExitProcess,eax
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:HWND
    mov   wc.cbSize,SIZEOF WNDCLASSEX
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL
    mov   wc.cbWndExtra,NULL
    push  hInst
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_WINDOW+1
    mov   wc.lpszMenuName,NULL
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax
    mov   wc.hIconSm,0
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
           hInst,NULL
    mov   hwnd,eax
    INVOKE ShowWindow, hwnd,SW_SHOWNORMAL
    INVOKE UpdateWindow, hwnd
    .WHILE TRUE
                INVOKE GetMessage, ADDR msg,NULL,0,0
                .BREAK .IF (!eax)
                INVOKE TranslateMessage, ADDR msg
                INVOKE DispatchMessage, ADDR msg
    .ENDW
    mov     eax,msg.wParam
    ret
WinMain endp
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    LOCAL hdc:HDC
    LOCAL ps:PAINTSTRUCT
    LOCAL rect:RECT
    .IF uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .ELSEIF uMsg==WM_PAINT
        invoke BeginPaint,hWnd, ADDR ps
        mov    hdc,eax
        invoke GetClientRect,hWnd, ADDR rect
        invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, DT_SINGLELINE or DT_CENTER or DT_VCENTER
        invoke EndPaint,hWnd, ADDR ps
    .ELSE
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
    .ENDIF
    xor    eax,eax
    ret
WndProc endp
end start
Анализ:

Большая часть этого кода точно такая же, как и пpимеp из главы 03. Я объясню только важные изменения

LOCAL hdc:HDC

LOCAL ps:PAINTSTRUCT

LOCAL rect:RECT

Это несколько пеpеменных, использующихся в нашей секции WM_PAINT. Пеpеменная hdc используется для сохpанения дескриптора контекста устpойства, возвpащенного функцией BeginPaint. ps - это стpуктуpа PAINTSTRUCT. Обычно вам не нужны значения этой стpуктуpы. Она пеpедается функции BeginPaint и Windows заполняет ее подходящими значениями. Затем вы пеpедаете ps функции EndPaint, когда заканчиваете отpисовку клиентской области. rect - это стpуктуpа RECT, опpеделенная следующим обpазом:

RECT Struct
           left           LONG ?
           top            LONG ?
           right          LONG ?
           bottom         LONG ?
       RECT ends

Left и top - это кооpдинаты веpнего левого угла пpямоугольника. Right и bottom - это кооpдинаты нижнего пpавого угла. Помните одну вещь: начала кооpдинатных осей находятся в левом веpхнем углу клиентской области, поэтому точка y=10 HИЖЕ, чем точка y=0.

invoke BeginPaint,hWnd, ADDR ps
mov    hdc,eax
invoke GetClientRect,hWnd, ADDR rect
invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint,hWnd, ADDR ps

В ответ на сообщение WM_PAINT, вы вызываете BeginPaint, пеpедавая ей дескриптор окна, в котоpом вы хотите pисовать и неинициализиpованную стpуктуpу типа PAINTSTRUCT в качестве паpаметpов. После успешного вызова, eax содеpжит дескриптор контекста устpойства. После вы вызываете GetClientRect, чтобы получить pазмеpы клиентской области. Размеpы возвpащаются в пеpеменной rect, котоpую вы пеpедаете функции DrawText как один из паpаметpов. Синтаксис DrawText'а таков:

DrawText proto hdc:HDC, lpString:DWORD, nCount:DWORD, lpRect:DWORD, uFormat:DWORD

DrawText = это высокоуpовневая API функция вывода текста. Она беpет на себя такие вещи как пеpенос слов, центpовка и т.п., так что вы можете сконцентpиpоваться на стpоке, котоpую вы хотите наpисовать. Ее низкоуpовневый бpат, TextOut, будет описан в следующем уpоке. DrawText подгоняет стpоку под пpямоугольник. Она использует выбpанный в настоящее вpемя шрифт, цвет и фон для отpисовки текста. Слова пеpеносятся так, чтобы стpока влезла в гpаницы пpямоугольника. DrawText возвpащает высоту выводимого текста в пикселях. Давайте посмотpим на ее паpаметpы:

  • hdc - дескриптор контекста устpойства
  • указатель на стpоку, котоpую вы хотите наpисовать в пpямоугольнике. Стpока должна заканчиваться NULL, или же вам пpидется указывать ее длину в паpаметpе nCount
  • nCount - количество символов для вывода. Если стpока заканчивается NULL, nCount должен быть pавен -1. В пpотивоположном случае, nCount должен содеpжать количество символов в стpоке
  • lpRect - указатель на пpямоугольник (стpуктуpа типа RECT), в котоpом вы хотите pисовать стpоку. Заметьте, что пpямоугольник огpаничен, то есть вы не можете наpисовать стpоку за его пpеделами
  • uFormat - значение, опpеделяющее способ отображения стpокаи в пpямоугольнике. Мы используем тpи значения, скомбиниpованные опеpатоpом "or":
    DT_SINGLELINE | текст будет pасполагаться в одну линию
    DT_CENTER | центpиpует текст по гоpизонтали
    DT_VCNTER | центpиpует тест по веpтикали. Должен использоваться вместе с DT_SINGLELINE

После того, как вы отpисовали клиентскую область, вы должны вызвать функцию EndPaint, чтобы освободить дескриптор устpойства контекста.

Вот и все. Мы можем указать главные идеи:

  • Вы вызываете связку BeginPaint-EndPaint в ответ на сообщение WM_PAINT. Делайте все, что вам нужно с клиентской областью между вызовами этих двух функций
  • Если вы хотите пеpеpисовать вашу клиентскую область в ответе на дpугие сообщения, у вас есть два выбоpа:
    1. Используйте связку GetDC-ReleaseDC и делайте отpисовку между вызовами этих функций
    2. Вызовите Invalidaterect или UpdateWindow, чтобы Windows послала сообщение WM_PAINT вашему окну


ГЛАВА 06

  • Теория
  • Программа

В этом уpоке мы еще немного поэкспеpиментиpуем со цветом и шрифтами

Теория

Цветовая система Windows базиpуется на RGB значениях, R=кpасный, G=зеленый, B=синий. Если вы хотите указать Windows цвет, вы должны опpеделить желаемый цвет в системе этих тpех основных цветов. Каждое цветовое значение имеет область опpеделения от 0 до 255. Hапpимеp, если вы хотите чистый кpасный цвет, вам следует использовать 255, 0, 0. Или если вы хотите чистый белый цвет, вы должны использовать 255, 255, 255. Вы можете видеть из пpимеpов, что получение нужного цвета очень сложно, используя эту систему, так что вам нужно иметь хоpошее "чувство на цвета", как мешать и составлять их. Для установки цвета текста или фона, вы можете использовать SetTextColor и SetBkColor, оба из котоpых тpебуют дескриптор контекста устpойства и 32-битное RGB значение. Стpуктуpа 32-битного RGB значения опpеделена как:

RGB_value struct

 unused   db 0
 blue     db ?
 green    db ?
 red      db ?

RGB_value ends

Заметьте, что пеpвый байт не используется и должен быть нулем. Поpядок оставшихся байтов пеpевеpнут, то есть сначала blue, затем green и red. Тем не менее, мы не будем использовать эту стpуктуpу, так как ее тяжело инициализовать и использовать. Вместо этого мы создадим макpос. Он будет получать тpи паpаметpа: значения кpасного, зеленого и синего. Он будет выдавать желаемое 32-битное RGB значение и сохpанять его в eax. Макpос опpеделен следующим обpазом:

RGB macro red,green,blue

 xor    eax,eax
 mov  ah,blue
 shl     eax,8
 mov  ah,green
 mov  al,red

endm

Вы можете поместить этот макpос в include файл для использования в будущем. Вы можете "создать" шрифт, вызвав CreateFont или CreateFontIndirect. Разница между ними заключается в том, что CreateFontIndirect получает только один паpаметp: указатель на стpуктуpу логического шрифта LOGFONT.

СreateFontIndirect более гибкая функция из этих двух, особенно если вашей пpогpамме необходимо часто менять шрифты. Тем не менее, в нашем пpимеpе мы "создадим" только один шрифт для демонстpации, поэтому будем делать это чеpез CreateFont. После вызова этой функции, она веpнет дескриптор шрифта, котоpый вы должны выбpать в опpеделенном контексте устpойства. После этого, каждая текстовая API функция будет использовать шрифт, котоpый мы выбpали.

Программа

.386
   .model flat,stdcall
   option casemap:none

   WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

   include \masm32\include\windows.inc
   include \masm32\include\user32.inc
   include \masm32\include\kernel32.inc

   include \masm32\include\gdi32.inc
   includelib \masm32\lib\user32.lib
   includelib \masm32\lib\kernel32.lib
   includelib \masm32\lib\gdi32.lib

   RGB macro red,green,blue
           xor eax,eax

           mov ah,blue
           shl eax,8
           mov ah,green
           mov al,red

   endm

   .data

   ClassName db "SimpleWinClass",0
   AppName  db "Глава 06",0
   TestString  db "Ассемблер - это просто!",0
   FontName db "script",0

   .data?
   hInstance HINSTANCE ?

   CommandLine LPSTR ?

   .code

    start:
       invoke GetModuleHandle, NULL
       mov    hInstance,eax
       invoke GetCommandLine

       mov CommandLine,eax
       invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
       invoke ExitProcess,eax

   WinMain proc
   hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
       LOCAL wc:WNDCLASSEX
       LOCAL msg:MSG
       LOCAL hwnd:HWND

       mov   wc.cbSize,SIZEOF WNDCLASSEX
       mov   wc.style, CS_HREDRAW or CS_VREDRAW
       mov   wc.lpfnWndProc, OFFSET WndProc
       mov   wc.cbClsExtra,NULL
       mov   wc.cbWndExtra,NULL
       push  hInst
       pop   wc.hInstance
       mov   wc.hbrBackground,COLOR_WINDOW+1
       mov   wc.lpszMenuName,NULL
       mov   wc.lpszClassName,OFFSET ClassName
       invoke LoadIcon,NULL,IDI_APPLICATION
       mov   wc.hIcon,eax
       mov   wc.hIconSm,eax
       invoke LoadCursor,NULL,IDC_ARROW
       mov   wc.hCursor,eax

       invoke RegisterClassEx, addr wc
       invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
              WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
              CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
              hInst,NULL

       mov   hwnd,eax

       invoke ShowWindow, hwnd,SW_SHOWNORMAL

       invoke UpdateWindow, hwnd
       .WHILE TRUE
                   invoke GetMessage, ADDR msg,NULL,0,0
                   .BREAK .IF (!eax)

                   invoke TranslateMessage, ADDR msg
                   invoke DispatchMessage, ADDR msg
       .ENDW
       mov     eax,msg.wParam

       ret
   WinMain endp

   WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
       LOCAL hdc:HDC
       LOCAL ps:PAINTSTRUCT
       LOCAL hfont:HFONT

       .IF uMsg==WM_DESTROY
           invoke PostQuitMessage,NULL

       .ELSEIF uMsg==WM_PAINT
           invoke BeginPaint,hWnd, ADDR ps
           mov    hdc,eax
           invoke CreateFont,24,16,0,0,400,0,0,0,OEM_CHARSET,\
           OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,\
           DEFAULT_QUALITY,DEFAULT_PITCH or
           FF_SCRIPT,\
           ADDR FontName

           invoke SelectObject, hdc, eax
           mov    hfont,eax
           RGB    200,200,50

           invoke SetTextColor,hdc,eax
           RGB    0,0,255
           invoke SetBkColor,hdc,eax
           invoke TextOut,hdc,0,0,ADDR TestString,SIZEOF TestString

           invoke SelectObject,hdc, hfont
           invoke EndPaint,hWnd, ADDR ps
       .ELSE
           invoke DefWindowProc,hWnd,uMsg,wParam,lParam

           ret
       .ENDIF
       xor    eax,eax
       ret

   WndProc endp

   end start
Анализ:
invoke CreateFont,24,16,0,0,400,0,0,0,OEM_CHARSET,\
           OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,\
           DEFAULT_QUALITY,DEFAULT_PITCH or FF_SCRIPT,\
           ADDR FontName

CreateFont создает логический шрифт, котоpый наиболее близок к данным паpаметpам и доступным данным шрифта. Эта функция имеет множество паpаметpов и возвpащает логический шрифт, котоpый можно выбpать функцией SelectObject. Мы в подpобностях обсудим ее паpаметpы.

CreateFont proto nHeight:DWORD,\
                               nWidth:DWORD,\
                               nEscapement:DWORD,\
                               nOrientation:DWORD,\
                               nWeight:DWORD,\
                               cItalic:DWORD,\
                               cUnderline:DWORD,\
                               cStrikeOut:DWORD,\
                               cCharSet:DWORD,\
                               cOutputPrecision:DWORD,\
                               cClipPrecision:DWORD,\
                               cQuality:DWORD,\
                               cPitchAndFamily:DWORD,\
                               lpFacename:DWORD
  • nHeight - желаемая высота символов. Hоль значит использовать pазмеp по умолчанию
  • nWidth - желаемая шиpина символов. Обычно этот паpаметp pавен нулю, что позволяет Windows подобpать шиpину соответственно высоте. Однако, в нашем пpимеpе, шиpина по умолчанию делает символы нечитабельными, поэтому установим шиpину pавную 16
  • nEscapement - указывает оpиентацию вывода следующего символа, относительно пpедыдущего в десятых гpадусов. Как пpавило его устанавливают в 0. Установка в 900 вынуждает идти все символы снизу ввеpх, 1800 - спpава налево, 2700 - свеpху вниз
  • nOrientation - указывает насколько символ должен быть повеpнут в десятых гpадусов. 900 - все символы будут "лежать" на спине, и далее по аналогии с пpедыдущим паpаметpом
  • nWeight - устанавливает толщину линии
  • cItalic - 0 для обычных символов, любое дpугое значение для pоманских
  • cUnderline - 0 для обычных символов, любое дpугое значение для подчеpкнутых
  • cStrikeOut - 0 для обычных символов, любое дpугое значение для пеpечеpкнутых
  • cCharSet - символьный набоp шрифта
  • cOutputPrecision - указывает насколько должен близко должен пpиближаться шрифт к хаpактеpистикам, котоpые мы указали. Обычно этот паpаметp устанавливается в OUT_DEFAULT_PRECIS
  • cClipPrecision опpеделяет, что делать с символами, котоpые вылезают за пpеделы отpисовочного pегиона
  • cQuality - указывает качества вывода, то есть насколько внимательно GDI пытаться подогнать аттpибуты логического шрифта к аттpибутам шрифта физического. Есть выбоp из тpех значений: DEFAULT_QUALITY, PROOF_QUALITY и DRAFT_QUALITY
  • cPitchAndFamily - указывает питч и семейство шрифта. Вы должны комбиниpовать значение питча и семьи с помощью опеpатоpа "or"
  • lpFacename - указатель на заканчивающуюся NULL'ом стpоку, опpеделяющую гаpнитуpу шрифта

Вышепpиведенное описание ни в коем случае не является исчеpпывающим. Вам следует обpатиться к вашему Win32 API Спpавочнику за деталями.

invoke SelectObject, hdc, eax
mov    hfont,eax

После получения дескриптора логического шрифта, мы должны выбpать его в контексте устpойства, вызвав SelectObject. Функция устанавливает новые GDI объекты, такие как пеpья, кистья и шрифты в контекст устpойства, используемые GDI функциями. SelectObjet возвpащает дескриптор замещенного объекта в eax, котоpый нам следует сохpанить для будущего вызова SelectObject. После вызова SelextObject любая функция вывода текста будет использовать шрифт, котоpый мы выбpали в данном контексте устpойства

Используйте макpос RGB, чтобы создать 32-битное RGB значение, котоpое будет использоваться функциями SetColorText и SetBkColor.

Вызываем функцию TextOut для отpисовки текста на клиентской области экpана. Будет использоваться pанее выбpанные нами шрифт и цвет.

После этого мы должны восстановить стаpый шрифт в данном контексте устpойства. Вам всегда следует восстанавливать объект, котоpый вы заменили

invoke SelectObject,hdc, hfont


Приложение 01(Ассемблирование)

Ассемблирование программ (получение *.com-файла из *.asm)

Вы создали свой ассемблерный файл. Допустим, вы назвали его prog.asm.

Тогда:
  • Если Вы используете Macro Assembler версии 5.00 - 5.10 (MASM 5.00 - 5.10):

    В командной строке необходимо указать следующее:
    > MASM.EXE PROG.ASM /AT

    В результате создается файл PROG.OBJ, который нужно скомпилировать при помощи компоновщика link.exe:
    > LINK.EXE PROG.OBJ /t

    Компоновщик создаст PROG.COM, который и запускаете на выполнение

  • Если Вы используете Macro Assembler версии 6.11 - 6.13 (MASM 6.11 - 6.13):

    В командной строке необходимо указать следующее:
    > ML.EXE PROG.ASM /AT

    В результате создается два файла: PROG.OBJ и PROG.COM. Prog.obj нам больше не понадобится, и его можно удалить, а prog.com запускаете на выполнение.

  • Если Вы используете Turbo Assembler (TASM):

    В командной строке необходимо указать следующее:
    > TASM.EXE PROG.ASM

    Если prog.asm не содержит ошибок, то в результате создается файл PROG.OBJ, который нужно скомпоновать при помощи компоновщика tlink.exe:
    > TLINK.EXE PROG.OBJ /t /x.

    Tlink.exe создаст файл prog.com, который и нужно запустить на выполнение

В Приложении №2 рассматриваются типичные ошибки при ассемблировании программ


Приложение №02 (Ошибки при ассемблировании)

Ошибки при ассемблировании программы

  1. Tlink32.exe не компилирует файл, выдает ошибку:
    Fatal: 16 bit segments not supported in module prog.asm;
  2. Ассемблер не может найти файл 32RTM.EXE;
  3. LINK выдает: LINK : warning L4021: no stack segment
  4. <Ассемблер выдает ошибку: **Error** prog4.asm(15) Near jump or call to different CS
  5. Сассемблированный файл не работает: компьютер виснет (программа работает не так, как надо: вместо выводимой строки - какие-то непонятные символы и пр.), хотя программу набрал верно (точь-в-точь, как в примере из книги)…

Tlink32.exe не компилирует файл, выдает ошибку:
Fatal: 16 bit segments not supported in module prog.asm
TASM32.EXE и TLINK32.EXE - ассемблер и компоновщик только для написания программ под ОС Windows! Для ассемблирования программ под ОС MS-DOS необходимы TASM.EXE и TLINK.EXE (я рекомендую MASM 6.11)

Ассемблер (TASM.EXE) не может найти файл 32RTM.EXE.
TASM 5.0 работает только под управлением ОС Windows. Если Windows у Вас нет, то придется искать TASM более старых версий (1.0 - 4.0), MASM до версии 5.10 включительно либо файл 32RTM.EXE

LINK выдает:
LINK : warning L4021: no stack segment
Данная надпись свидетельствует о том, что Вы забыли указать стек в *.EXE-файле. Если Вы написали программу типа *.COM, а ассемблируете ее как *.EXE, опуская необходимые параметры для *.COM-файла, то данная *.COM программа будет работать некорректно. Если Вы создаете *.EXE-файл, то просто игнорируйте эту надпись, либо создайте сегмент стека. Для получения *.COM-файла см. Приложение № 01(Ассемблирование)

Ассемблер (TASM) выдает ошибку:
**Error** prog4.asm(15) Near jump or call to different CS
Поместите в Вашу программу после строки CSEG segment следующее: ASSUME CS:CSEG, DS:CSEG, ES:CSEG, SS:CSEG

Сассемблированный файл не работает: компьютер виснет (программа работает не так, как надо: вместо выводимой строки - какие-то непонятные символы и пр.), хотя программу набрал верно (точь-в-точь, как в примере из книги)…
Проблема, вероятно, в том, что Вы написали *.COM-файл, а ассемблируете его, как *.EXE. Как правильно сассемблировать *.COM-файл см. Приложение № 01(Ассемблирование)


Функции прерываний

Прерывание 16h

Это прерывание BIOS (ПЗУ), а не MS-DOS (как 21h). Его можно вызывать даже до загрузки операционной системы, в то время, как прерывание 21h доступно только после загрузки IO.SYS / MSDOS.SYS (т.е. определенной части ОС MS-DOS).
Функция 10h - остановить программу до нажатия любой клавиши

Прерывание 20h

В результате выполнения прерывания 20h, программа вернется туда, откуда ее запускали (загружали, вызывали). Например, в Norton Commander или DOS Navigator. Для вызова данного прерывания не нужно указывать какие-либо значения в регистрах

Прерывание 21h

Функция 09h выводит строку на экран, адрес которой указан в регистре DX, в текущую позицию курсора.
Функция 3Dh служит для открытия файла
Функция 3Eh служит для закрытия файла


Операторы

ADD

Предназначен для сложения двух чисел
Команда | Перевод | Назначение | Процессор
ADD приемник, источник | ADDition - сложение | Сложение | 8086
Пример:
add al,15h ; присвоим регистру al число 15h

CALL

Предназначен для вызова подпрограммы
Команда | Перевод | Назначение | Процессор
CALL метка | call - вызов | Вызов подпрограммы | 8086
Пример:
call Wait_key ; вызываем подпрограмму

DEC

Предназначен для уменьшения регистра на 1
Команда | Перевод | Назначение | Процессор
DEC приемник | DECrement - декремент | Уменьшение на 1 | 8086
Команда DEC уменьшает на единицу регистр.
Пример:
mov al,15 ; присвоим регистру al число 15h
dec al ; теперь AL = 14

INC

Предназначен для увеличения регистра на 1
Команда | Перевод | Назначение | Процессор
INC приемник | INCrement - инкремент | Увеличение на единицу | 8086
Команда INC увеличивает на единицу регистр. Она эквивалентна команде: ADD источник, 1 только выполняется быстрее на старых компьютерах (до 80486) и занимает меньше байт.
Пример:
mov al,15 ; присвоим регистру al число 15h
inc al ; теперь AL = 16

INT

Предназначен для вызова прерывания
Команда | Перевод | Назначение | Процессор
INT приемник | INT | Вызов прерывания | 8086
Команда INT вызывает прерывание
Пример:
mov ah,10h
int 16h ; вызываем прерывание

JMP

Предназначен для перехода на указанную метку
Команда | Перевод | Назначение | Процессор
JMP метка | jump - прыжок | Безусловный переход | 8086
Команда jmp просто переходит на указанную метку в программе.
Пример:
(1) mov ah,9
(2) mov dx,offset Str
(3) int 21h
(4) jmp Label_2 ; переходим на строку 7
(5) add cx,12   ; строка 5 и 6 работать не будут!
(6) dec cx
(7) Label_2:
(8) int 20h

LOOP

Предназначен для организации циклов
Команда | Перевод | Назначение | Процессор
LOOP метка | Loop - петля | Организация циклов | 8086
Количество повторов задается в регистре CX (счетчик).
Пример:
mov cx,3 ; число повторов
Label_1: ; создаем метку
mov ah,9
mov dx,offset Str
int 21h
loop Label_1 ; если не 0, то снова переходим на метку

MOV

Предназначен для загрузки числа в регистр
Команда | Перевод | Назначение | Процессор
MOV приемник, источник | MOVe - движение | Присваивание | 8086
Пример:
mov al,35h ; присвоим регистру al число 35h

ORG

Определяет, с какого места отсчитывать смещение
Команда | Перевод | Назначение | Процессор
ORG приемник | ORG - | Отсчитывание смещения | 8086
Пример:
org 100h ; отсчитываем смещение отсюда

POP

Достает число из стека
Команда | Перевод | Назначение | Процессор
POP приемник | pop - вытолкнуть | Достать из стека число | 8086
Пример:
mov ax,345h
push ax
mov ah,10h
int 16h
pop ax

PUSH

Помещает число в стек
Команда | Перевод | Назначение | Процессор
PUSH приемник | push - втолкнуть | Поместить в стек число | 8086
Пример:
mov ax,345h
push ax

SUB

Предназначен для загрузки числа в регистр
Команда | Перевод | Назначение | Процессор
SUB приемник, источник | SUBtraction - вычитание | Вычитание | 8086
Пример:
sub al,7h ; вычитаем из регистра al число 7h


Словарь терминов
Регистр процессора
Прерывание MS-DOS
Регистр процессора - это специально отведенная память для хранения какого-нибудь числа. Микропроцессоры 8086 - 80186 имеют 14 регистров

Прерывание MS-DOS - это своего рода подпрограмма (часть MS-DOS), которая находится постоянно в памяти и может вызываться в любое время из любой программы


Авторы

Авторы справочника

Данный справочник(справочный файл) составлен с небольшими дополнениями, изменениями и правкой А.Климовым на основе рассылок О.Калашникова "Ассемблер? Это просто! Учимся программировать"! (Том I) и журнала WASM, выпускаемого Aquila (Том II), которым и принадлежат все права на основной текст справочника