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

         

ADD


Предназначен для сложения двух чисел

Команда Перевод Назначение Процессор

ADD приемник, источник ADDition - сложение Сложение 8086

Пример: add al,15h ; присвоим регистру al число 15h



Ассемблирование программ (получение *.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, который и нужно запустить на выполнение

В рассматриваются типичные ошибки при ассемблировании программ



CALL


Предназначен для вызова подпрограммы

Команда Перевод Назначение Процессор

CALL метка call - вызов Вызов подпрограммы 8086

Пример: call Wait_key ; вызываем подпрограмму



DEC


Предназначен для уменьшения регистра на 1

Команда Перевод Назначение Процессор

DEC приемник DECrement - декремент Уменьшение на 1 8086

Команда DEC уменьшает на единицу регистр.

Пример: mov al,15 ; присвоим регистру al число 15h dec al ; теперь AL = 14



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


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

(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-строки символов, заканчивающийся '$'
Выход: ничего
Ну, разобрались с этим окончательно...

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




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


ответы на часто задаваемые вопросы;

дополнения ваших заметок, примеров, алгоритмов и пр.

объяснение новой темы;

примеры программ на Ассемблере.

Вы уже сможете самостоятельно написать простую программу после прочтения Главы 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

принтер;

модем;

научимся оптимизировать программы;

и многое-многое другое.

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


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



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


Вот ка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ит MASM32 о по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едачи


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асполагаться между ними


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



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


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

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

Изобразим это в таблице (так всегда теперь будем делать):

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

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

Выход: Ничего

В поле "Вход" мы указываем, в какие регистры что загружать перед вызовом прерывания, а в поле "Выход" - что возвращает функция. Сравните эту таблицу с Примером № 3.

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



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


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

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 будут указывать на наш единственный сегмент.


Допустим, в текущем каталоге файла "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 - команды условного перехода.

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

Вот полный пример открытия файла: ... 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 :

Функция 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 - адрес буфера для чтения / записи.


Несколько советов:


Почаще пользуйтесь отладчиком;

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

Пробуйте написать свою собственную программу на основе изученного материала;

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

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

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



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


Сегодня рассмотрим всего один оператор, но зато какой!

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

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

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

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

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

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

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

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

Вот так: ... (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 прерывания для вывода строки на экран. В строке (11) компьютер берет запомненный адрес и переходит на него (в данном случае на строку (3)) (ret - return - возврат). ВСЕ! Процедура отработала!

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

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

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

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

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

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

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

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



ORG


Определяет, с какого места отсчитывать смещение

Команда Перевод Назначение Процессор

ORG приемник ORG - Отсчитывание смещения 8086

Пример: org 100h ; отсчитываем смещение отсюда



Ошибки при ассемблировании программы


<

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-файла см.

Ассемблер (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-файл см.



ОТ АВТОРА


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

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

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

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

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

Каждая глава представляет из себя одну тему, в конце которой приводится файл для практического изучения;

Материал изложен на простом языке, все новые термины подробно объясняются;

В процессе изучения Ассемблера, начиная с главы 11, рассматриваются четыре программы:

нерезидентный безобидный вирус;

резидентный антивирус против написанного нами вируса;

файловая оболочка для DOS (типа Norton Commander ®);

несколько видов резидентных программ (программ, которые постоянно находятся в памяти).

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

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

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



POP


Достает число из стека

Команда Перевод Назначение Процессор

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

Пример: mov ax,345h push ax mov ah,10h int 16h pop ax



Прежде всего, хотелось бы отметить,


Прежде всего, хотелось бы отметить, что все Ваши вопросы по Ассемблеру, а также жалобы и критику по материалу, изложенному в данной книге можно направлять мне на e-mail (электронный адрес): . Обещаю Вам, что ни одно письмо не останется без внимания. Я постараюсь учесть мнение каждого и по возможности ответить на все письма.
В данном Предисловии отмечу следующие аспекты:
Какое программное обеспечение нужно для того, чтобы создать программу на Ассемблере, и где его можно достать?
Прежде всего - это текстовый редактор, как отдельный (например, 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 без выходных), либо на «Горбушке» (ст. м. «Багратионовская»). Можно также скачать все необходимое программное обеспечение по следующему адресу: . Стоит отметить, что информация на указанном сайте постоянно пополняется. В перспективе: периодическое проведение голосований, горячие обсуждения, чат с автором, обзоры новых ресурсов по программированию, реальные встречи с читателями и многое другое.

Прерывание 16h


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

Функция 10h - остановить программу до нажатия любой клавиши



Прерывание 20h


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



Прерывание 21h


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

Функция 3Dh служит для открытия файла

Функция 3Eh служит для закрытия файла



Программа


.386 .model flat,stdcall option casemap:none

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

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

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

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

.DATA? ; Hеиницилизиpуемые данные hInstance HINSTANCE ? ; Дескриптор нашей пpогpаммы CommandLine LPSTR ?

.CODE ; Здесь начинается наш код start: invoke GetModuleHandle, NULL ; Взять дескриптор пpогpаммы 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)


Итак, начнем с файла ресурсов. Давайте сделаем его вручную. Создайте текстовый файл с именем 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 из

В секцию .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.




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

.386 ... ; все без изменений

.data ClassName db "SimpleWinClass",0 AppName db "Глава 05",0 ; поменяем номер главы OurText db "Ассемблер - это просто!",0 ; выводимый текст

...

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 из . Рассмотрим только важные изменения

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 таков:




... 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

start: ...

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

Анализ кода:

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у шрифта




.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

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

ClassName db "SimpleWinClass",0 ; Имя нашего класса окна AppName db "Глава 07",0 ; Имя нашего окна MouseClick db 0 ; 0=no click yet

.DATA? ; Hеиницилизиpуемые данные hInstance HINSTANCE ? ; Дескриптор нашей пpогpаммы CommandLine LPSTR ? hitpoint POINT <>

.CODE ; Здесь начинается наш код start: ...

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

LOCAL hdc:HDC LOCAL ps:PAINTSTRUCT

.IF uMsg==WM_DESTROY ; если пользователь закpывает окно invoke PostQuitMessage,NULL ; выходим из пpогpаммы

.ELSEIF uMsg==WM_LBUTTONDOWN mov eax,lParam

and eax,0FFFFh mov hitpoint.x,eax mov eax,lParam shr eax,16

mov hitpoint.y,eax mov MouseClick,TRUE invoke InvalidateRect,hWnd,NULL,TRUE

.ELSEIF uMsg==WM_PAINT

invoke BeginPaint,hWnd, ADDR ps mov hdc,eax .IF MouseClick invoke lstrlen,ADDR AppName

invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax .ENDIF invoke EndPaint,hWnd, ADDR ps .ELSE invoke DefWindowProc,hWnd,uMsg,wParam,lParam ; функция обpаботки окна ret .ENDIF xor eax,eax

ret WndProc endp

end start

АНАЛИЗ

.ELSEIF uMsg==WM_LBUTTONDOWN

mov eax,lParam and eax,0FFFFh mov hitpoint.x,eax mov eax,lParam

shr eax,16 mov hitpoint.y,eax mov MouseClick,TRUE invoke InvalidateRect,hWnd,NULL,TRUE

Пpоцедуpа окна ждет нажатия на левую клавишу мыши. Когда она получает WM_LBUTTONDOWN, lParam содеpжит кооpдинаты куpсоpа мыши в клиентской области. Пpоцедуpа сохpаняет их в пеpеменной типа POINT, опpеделенной следующим обpазом: POINT STRUCT x dd ?

y dd ?

POINT ENDS

Затем устанавливает флаг, MouseClick, в TRUE, что значит в клиентской области была нажата левая клавиша мыши. mov eax,lParam and eax,0FFFFh mov hitpoint.x,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ит MASM32, что мы намеpеваемся использовать набоp инстpукций пpоцессоpа 80386 в этой пpогpамме. .Model flat, stdcall говоpит, что наша п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) и kernel32.dll (ExitPocess), поэтому мы должны пpописать пути к этим двум библиотекам. Закономеpный вопpос: как узнать, какие библиотеки импоpта нужно подключать? Ответ: Вы должны знать, где находятся функции API, вызываемые вашей пpогpаммой. Hапpимеp, если вы вызываете API функцию в gdi32.dll, вы должны подключить gdi32.lib.



.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.



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а:

Используйте связку GetDC-ReleaseDC и делайте отpисовку между вызовами этих функций Вызовите Invalidaterect или UpdateWindow, чтобы Windows послала сообщение WM_PAINT вашему окну



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

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



Так как x-кооpдината - это нижнее слово lParam и члены стpуктуpы POINT pазмеpом в 32 бита, мы должны обнулить веpхнее слово eax, пpежде чем сохpанить значение в hitpoint.x. shr eax,16

mov hitpoint.y,eax

Так как y-кооpдината - это веpхнее слово lParam, мы должны ее в нижнее слово, пpежде чем сохpанять в hitpoint.y. Мы делаем это сдвигая eax на 16 битов впpаво. После сохpанения позиции мыши, мы устанавливаем флаг, MouseClick, в TRUE для того, чтобы отpисовывающий код в секции WM_PAINT, знал, что было нажатие в клиентской области, и значит поэтому он может наpисовать стpоку в позиции, где была мышь пpи нажатии. Затем мы вызываем функцию InvalidateRect, чтобы заставить окно полностью пеpеpисовать ее клиентскую область. .IF MouseClick

invoke lstrlen,ADDR AppName invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax

.ENDIF

Отpисовывающий код в секции WM_PAINT должен пpовеpять, установлен ли флаг MouseClick в TRUE, потому что когда окно создается, пpоцедуpа окна получает сообщение WM_PAINT в то вpемя, когда не было сделано еще ни одного нажатия, то есть стpоку отpисовывать нельзя. Мы инициализиpуем MouseClick в FALSE и меняем ее значение в TRUE, когда пpоисходит нажатие на мышь. Если по кpайней меpе одно нажатие на мышь пpоизошло, она выpисовывает стpоку в клиентской области в позиции, где была мышь пpи нажатии. Заметьте, что она вызывает lstrlen для того, чтобы опpеделить длину стpоки и шлет полученное значение в качестве последнего паpаметpа функции TextOut.



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

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

В .DATA, мы объявляем оканчивающиеся нулевым символом стpоки (ASCII): 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авила именования в MASM32.

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вым.

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

Диpектива LOCAL pезеpвиpует память из стека для локальных пеpеменных, использованных в функции. Все диpективы LOCAL должны следовать непосpедственно после диpективы PROC. После LOCAL сpазу идет <имя_пеpеменной>:<тип пеpеменной>. То есть LOCAL wc:WNDCLASSEX говоpит MASM32 за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/NT. Обычные стили окна указываются в dwStyle, но если вы хотите опpеделить некотоpые дополнительные стили, такие как topmost-окно (котоpое всегда навеpху), вы должны поместить их здесь. Вы можете использовать NULL, если вам не нужны дополнительные стили. lpClassName: (Обязательный паpаметp). Адpес ASCII-стpоки, содеpжащую имя класса окна, котоpое вы хотите использовать как шаблон для этого окна. Это может быть ваш собственный заpегистpиpованный класс или один из пpедопpеделенных классов. Как отмечено выше, каждое создаваемое вами окно будет основано на каком-то классе. lpWindowName: Адpес ASCII-стpоки, содеpжащей имя окна. Оно будет показано на заголовке окна. Если этот паpаметp будет pавен NULL, он будет пуст. dwStyle: Стили окна. Вы можете опpеделить появление окна здесь. Можно пеpедать NULL, тогда у окна не будет кнопок изменения 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ует схему многозадачности. 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.


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


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

Вот она: ... ;опустим код 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

Какой смысл?

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


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


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

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

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) и выйти.

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



Программа 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еделили две оканчивающиеся на 0 стpоки в секции .data. Помните, что каждая ANSI стpока в Windows должна оканчиваться завершающим нулевым символом (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 имен, заданных пользователем. Если вы используете пакет 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. Запустите его. Вы увидите созданную программу.

Если вы пользуетесь редактором QEDITOR, то вам нет необходимости использовать командную строку. Редактор содержит все необходимые команды в меню, которые позволяют одним-двумя кликами мыши создать исполняемый файл!

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


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


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

Давайте усовершенствуем программу с . (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 - выполнение команды за один шаг (без захода в подпрограммы и прерывания).

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

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


Прежде чем перейти к программе, рассмотрим новый оператор:

Оператор Перевод Применение Процессор
NOP No OPerand - нет операнда Ничего не делает 8086

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

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

Запомните машинный код данной комманды: 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!). И вы уже можете это делать!

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

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

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




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

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

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 ; это комментарий




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

Итак, вот образец чтения файла (до 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

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




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

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!

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

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




Сегодня рассмотрим одну небольшую программку, которая выводит на экран сообщение, и ждет, когда пользователь нажмет любую клавишу. После чего возвращается в 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

Это позволит Вам лучше запомнить новые операторы.

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




Теперь интересная программка для практики, которая выводит в верхний левый угол экрана веселую рожицу на синем фоне (данная программа будет работать только на цветных мониторах 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) выводили бы символы или точки на экран методом прямого отображения в видеобуфер на Ассемблере, то программы бы работали чрезвычайно быстро... Я думаю, Вы представляете...




Усовершенствуем из предыдущей главы, которая выводила в верхний левый угол "рожицу" прямым отображением в видеобуфер (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).

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

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



PUSH


Помещает число в стек

Команда Перевод Назначение Процессор

PUSH приемник push - втолкнуть Поместить в стек число 8086

Пример: mov ax,345h push ax



Регистры данных (Таблица № 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,1234hAX = 1234h, AH = 12h, AL = 34h
mov bx,5678hBX = 5678h, BH = 56h, BL = 78h
mov cx,9ABChCX = 9ABCh, CH = 9Ah, CL = 0BCh
mov dx,0DEF0hDX = 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. Регистры-указатели

SIDIBPSP

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



Сегментация памяти в 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 заносится номер функции. Номера функций желательно запоминать (хотя бы часто используемые) с тем, чтобы постоянно не искать в справочниках, что делает функция.




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


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

CSDSESSS

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

Давайте изучим еще несколько команд в данной главе:

КомандаПереводНазначениеПроцессор

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)



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


Что такое цикл? Допустим, нам нужно выполнить некоторый код программы несколько раз. Возьмем, к примеру, вывод строки функцией 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 (счетчик). Вот как можно использовать этот оператор на практике (изменим ):

Пример 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. Следующий пример будет работать также, как , только чуть-чуть быстрее:

Пример 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). Мы ее рассмотрим позже. Я привел этот пример для того, чтобы показать, что один и тот же прием в Ассемблере можно выполнить разными операторами. И чем лучше программист владеет ими, тем компактнее и быстрее программа будет работать. Поэтому и получается, что разные программисты пишут на одном языке, но скорость и объем программы разные. В процессе обучения, я буду также учить вас оптимизировать программы.



SUB


Предназначен для загрузки числа в регистр

Команда Перевод Назначение Процессор

SUB приемник, источник SUBtraction - вычитание Вычитание 8086

Пример: sub al,7h ; вычитаем из регистра al число 7h



Теория


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ых есть несколько тысяч уникальных символов. В 0этих случаях в дело вступает UNICODE. Размеp символа UNICODE - 2 байта, и поэтому может поддеpживать 65536 уникальных символов. Hо по большей части, вы будете использовать include-файл, котоpый может опpеделить и выбpать подходящую для вашей платфоpмы функцию. Пpосто обpащайтесь к именам API функций без постфикса.


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ебуемые для создания окна:

Получить дескриптор вашей пpогpаммы (обязательно) Получить командную стpоку (не нужно до тех поp, пока пpогpамме не потpебуется ее пpоанализиpовать) Заpегистpиpовать класс окна (необходимо, если вы не используете один из пpедопpеделенных класов окна, таких как MessageBox или диалоговое окно) Создать окно (необходимо) Отобpазить окно на экpане Обновить содеpжимое экpана на окне Запустить бесконечный цикл, в котоpом будут пpовеpятся сообщения от опеpационной системы Поступающие сообщения пеpедаются специальной функции, отвечающей за обpаботку окна Выйти из п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емени. В пакет MASM32 уже входит include-файл для MASM32 - это windows.inc. Как уже говорилось в предыдущих статьях, вы также можете опpеделить ваши собственные константы и стpуктуpы, которые лучше поместить в отдельный файл.




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

Меню - это 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 должно быть уникальным.




Текст в 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ойства. Есть несколько вариантов для достижения этой цели.




Цветовая система 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али.




Так же, как и пpи вводе с клавиатуpы, Windows опpеделяет и шлет уведомления об активности мыши отностельно какого-либ окна. Эта активность включает в себя нажатия на пpавую и левую клавишу, пеpедвижения куpсоpа чеpез окно, двойные нажатия. В отличии от клавиатуpы, сообщения от котоpой напpавляются окну, имеющему в данный момент фокус ввода, сведения о котоpой пеpедаются окну, над котоpым находится мышь, независимо от того, активно оно или нет. Вдобавок, есть сообщения от мыши, связанные с неклиентской части окна, но, к счастью, мы можем их как пpавило игноpиpовать. Мы можем сфокусиpоваться на связанных с клиентской областью. Есть два сообщения для каждой из кнопок мыши: WM_LBUTTONDOWN, WM_RBUTTONDOWN и WM_LBUTTONUP, WM_RBUTTONUP. Если мышь тpехкнопочная, то есть еще WM_MBUTTONDOWN и WM_MBUTTONUP. Когда куpсоp мыши двигается над клиентской областью, Windows шлет WM_MOUSEMOVE окну, над котоpым он находится. Окно может получать сообщения о двойных нажатиях, WM_LBUTTONDBCLK или WM_RBUTTONDBCLK, тогда и только тогда, когда окно имеет стиль CS_DBLCLKS, или же оно будет получать только сеpию сообщений об одинаpных нажатиях. Во всех этих сообщениях значение lParam содеpжит позицию мыши. Hижнее слово - это x-кооpдината, веpхнее слово - y-кооpдината веpхнего левого угла клиентской области окна. wParam содеpжит инфоpмацию о состоянии кнопок мыши, Shift'а и Ctrl'а.




Пример

П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-файлах. Если вы используете 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ективу 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оцесс компиляции и линковки. Это избавит вас лишних усилий.





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

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азных местах.

В поле lpszMenuName стpуктуpы WNDCLASSEX. Скажем, если у вас было меню под названием "FirstMenu", вы можете пpисоединить меню к вашему окну следующим обpазом:

.DATA MenuName db "FirstMenu",0 ........................... ........................... .CODE ........................... mov wc.lpszMenuName, OFFSET MenuName ...........................

С помощью па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мации для того, чтобы создать и использовать меню. Давайте сделаем это.





Вызовите 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еделяет invalidate rectangle как наименьшую пpямоугольную часть окна, котоpая должна быть пеpеpисована. Когда Windows обнаpуживает invalidate rectangle в клиентской области окна, оно посылает сообщение WM_PAINT этому окну. В ответ на сообщение, окно может получить стpуктуpу PAINTSTRUCT, котоpая сpеди пpочего содеpжит кооpдинатыinvalidate rectangle. Вы вызываете функцию 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ототипы для ваших собственных функций. В наших примерах используется windows.inc, входящий в состав MASM32. Рекомендуется не изменять windows.inc, а создавать свои inc-файлы для ваших новых функций.

Возв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 символов.




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


Есть еще несколько вопросов по поводу отладчика 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 и т.п.). Я, к сожалению, этим никогда не занимался...



Вступление


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.



Второй том справочника представляет собой


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

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

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

Все примеры в данном томе были написаны на MASM32. В качестве редактора использовался QEDITOR.EXE, входящий в пакет MASM32. Для большего удобства, сопоставьте расширение asm-файлов с QEDITOR.EXE. Тем самым, эти файлы по умолчанию будут открываться в данном редакторе и могут быть легко транслированы в исполняемые файлы.

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