Assembler - язык неограниченных возможностей

         

Блоки повторений


Простейший блок повторений REPT (не поддерживается WASM) выполняет ассемблирование участка программы заданное число раз. Например, если требуется создать массив байтов, проинициализированный значениями от 0 до 0FFh, это можно сделать путем повтора псевдокоманды DB следующим образом:

hexnumber = 0 hextable label byte ; Имя массива rept 256 ; Начало блока db hexnumber ; Эти две строки ассемблируются hexnumber = hexnumber+1 ; 256 раз. endm

Блоки повторений, так же как макроопределения, могут вызываться с параметрами. Для этого используются директивы IRP и IRPC:

irp параметр,<значение1,значение2...> ... endm

irpc параметр,строка ... endm

Блок, описанный директивой IRP, будет вызываться столько раз, сколько значений указано в списке (в угловых скобках), и при каждом повторении будет определена метка с именем параметр, равная очередному значению из списка. Например, следующий блок повторений сохранит в стек регистры AX, BX, CX и DX:

irp reg,<ax,bx,cx,dx> push reg endm

Директива IRPC (FORC в WASM) описывает блок, который выполняется столько раз, сколько символов содержит указанная строка, и при каждом повторении будет определена метка с именем параметр, равная очередному символу из строки. Если строка содержит пробелы или другие символы, отличные от разрешенных для меток, она должна быть заключена в угловые скобки. Например, следующий блок задает строку в памяти, располагая после каждого символа строки атрибут 0Fh (белый символ на черном фоне), так что эту строку впоследствии можно будет скопировать прямо в видеопамять.

irpc character,<строка символов> db ’&character&’,0Fh endm

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



Директивы управления программным счетчиком


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

org выражение

Устанавливает значение программного счетчика. Директива ORG с операндом 100h обязательно используется при написании файлов типа COM, которые загружаются в память после блока параметров размером 100h.

even

Директива EVEN делает текущее значение счетчика кратным двум, вставляя команду NOP, если оно было нечетным. Это увеличивает скорость работы программы, так как для доступа к слову, начинающемуся с нечетного адреса, процессор должен считать два слова из памяти. Если при описании сегмента не использовалось выравнивание типа BYTE, счетчик в начале сегмента всегда четный.

align значение

Округляет значение программного счетчика до кратного указанному значению. Оно может быть любым четным числом. Если счетчик некратен указанному числу, эта директива вставляет необходимое количество команд NOP.



Директивы задания набора допустимых команд


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

.8086 — используется по умолчанию. Разрешены только команды 8086;

.186 — разрешены команды 80186;



.286 и .286c — разрешены непривилегированные команды 80286;

.286p — разрешены все команды 80286;

.386 и .386c — разрешены непривилегированные команды 80386;

.386p — разрешены все команды 80386;

.486 и .486c — разрешены непривилегированные команды 80486;

.486p — разрешены все команды 80486;

.586 и .586c — разрешены непривилегированные команды P5 (Pentium);

.586p — разрешены все команды P5 (Pentium);

.686 — разрешены непривилегированные команды P6 (Pentium Pro, Pentium II);

.686p — разрешены все команды P6 (Pentium Pro, Pentium II);

.8087 — разрешены команды NPX 8087;

.287 — разрешены команды NPX 80287;

.387 — разрешены команды NPX 80387;

.487 — разрешены команды FPU 80486;

.587 — разрешены команды FPU 80586;

.MMX — разрешены команды IA MMX;

.K3D — разрешены команды AMD 3D.

Не все ассемблеры поддерживают каждую директиву, например MASM и WASM не поддерживают .487 и .587, так как их действие не отличается от .387. Естественно, ассемблеры, вышедшие до появления последних процессоров и расширений, не в состоянии выполнять соответствующие им команды.

Если присутствует директива .386 или выше, ассемблер WASM всегда определяет все сегменты как 32-битные при условии, что не указан явно операнд USE16. MASM и TASM действуют так же, только если директива задания набора команд указана перед директивой .model.



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


Директива EXITM (не поддерживается WASM) выполняет преждевременный выход из макроопределения или блока повторений. Например, следующее макроопределение не выполнит никаких действий, то есть не будет расширено в команды процессора, если параметр не указан:

pushreg macro reg ifb <reg> exitm endif push reg endm

LOCAL метка... — перечисляет метки, которые будут применяться внутри макроопределения, чтобы не возникало ошибки «метка уже определена» при использовании макроса более одного раза или если та же метка присутствует в основном тексте программы (в WASM директива LOCAL позволяет использовать макрос с метками несколько раз, но не разрешает применять метку с тем же именем в программе). Операнд для LOCAL — метка или список меток, которые будут использоваться в макросе.

PURGE имя_макроса — отменяет определенный ранее макрос (не поддерживается WASM). Эта директива часто применяется сразу после INCLUDE, включившей в текст программы файл с большим количеством готовых макроопределений.



Директивы и операторы ассемблера


Каждая программа на языке ассемблера помимо команд процессора содержит еще и специальные инструкции, указывающие самому ассемблеру, как организовывать различные секции программы, где располагаются данные, а где команды, позволяющие создавать макроопределения, выбирать тип используемого процессора, организовывать связи между процедурами и т.д. К сожалению, пока нет единого стандарта на эти команды (он существует для UNIX, о чем рассказано в главе 11). Разные ассемблеры используют различные наборы директив, но TASM и MASM (два самых популярных ассемблера для DOS и Windows) поддерживают общий набор, или, точнее, TASM поддерживает набор директив MASM наряду с несовместимым собственным, известным как Ideal Mode. Все примеры программ в книге написаны так, чтобы для их компиляции можно было воспользоваться TASM, MASM или WASM — еще одним популярным ассемблером, поэтому в данной главе рассмотрены те предопределенные идентификаторы, операторы и директивы, которые поддерживаются этими тремя ассемблерами одновременно.



Глобальные объявления


public язык метка... ; Для TASM и MASM

или

public метка язык... ; для WASM

Метка, объявленная директивой PUBLIC, становится доступной для других модулей программы. Так, можно объявлять имена процедур, переменные и константы, определенные директивой EQU. Необязательный операнд языка (C, PASCAL, BASIC, FORTRAN, SYSCALL или STDCALL) указывает, что метка будет вызываться из модуля, написанного на соответствующем языке, и при необходимости изменяет ее (например, добавляет _ перед первым символом метки).

comm расст язык метка:тип... ; для TASM comm язык расст метка:тип... ; для TASM comm расст метка:тип язык... ; для WASM

Директива COMM описывает общую переменную. Такие переменные доступны из всех модулей, и их размещение в программе определяется на этапе компоновки. Обязательные аргументы директивы COMM — метка (собственно имя общей переменной) и тип (BYTE, WORD, DWORD, FWORD, QWORD, TBYTE или имя структуры). Необязательный операнд «расстояние» (NEAR или FAR) указывает, находится ли переменная в группе сегментов DGROUP (ближняя переменная, для доступа достаточно смещения) или вне этих сегментов (дальняя переменная, для доступа потребуется сегментный адрес). Для моделей памяти TINY, SMALL и COMPACT по умолчанию значение этого операнда принимается за NEAR. И наконец, операнд «язык» действует аналогично такому же операнду для PUBLIC.

extrn язык метка:тип... ; для MASM и TASM extrn метка:тип язык... ; для WASM

Описывает метку, определенную в другом модуле (с помощью PUBLIC). Тип (BYTE, WORD, DWORD, FWORD, QWORD, TBYTE, имя структуры, FAR, NEAR, ABS) должен соответствовать типу метки в том модуле, где она была установлена (тип ABS используется для констант из других модулей, определенных директивой EQU). Необязательный операнд языка действует так же, как и для директивы PUBLIC.

global язык метка:тип... ; для MASM и TASM global метка:тип язык... ; для WASM

Директива GLOBAL действует, как PUBLIC и EXTRN одновременно. Когда указанная метка находится в этом же модуле, она становится доступной для других модулей, как если бы выполнилась директива PUBLIC. Если метка не описана — она считается внешней и выполняется действие, аналогичное действию директивы EXTRN.



в конце строки, возможны большие


Кроме обычных комментариев, начинающихся с символа ; (точка с запятой) и заканчивающихся в конце строки, возможны большие блоки комментариев, описываемых специальной директивой COMMENT.
comment @ любой текст @
Операнд для COMMENT — любой символ, который будет считаться концом комментария. Весь участок текста, вплоть до следующего появления этого символа, ассемблером полностью игнорируется.

Конец программы


end start_label

Этой директивой завершается любая программа на ассемблере. В роли необязательного операнда здесь выступает метка (или выражение), определяющая адрес, с которого начинается выполнение программы. Если программа состоит из нескольких модулей, только один файл может содержать начальный адрес, так же как в C только один файл может содержать функцию main().



Макрооператоры


Макрооператор & (амперсанд) нужен для того, чтобы параметр, переданный в качестве операнда макроопределению или блоку повторений, заменялся значением до обработки строки ассемблером. Так, например, следующий макрос выполнит команду PUSH EAX, если его вызвать как PUSHREG A:

pushreg macro letter push e&letter&x endm

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

irp number,<1,2,3,4> msg&number db ? endm

Макрооператор <> (угловые скобки) действует так, что весь текст, заключенный в эти скобки, рассматривается как текстовая строка, даже если он содержит пробелы или другие разделители. Как мы уже видели, этот макрооператор используется при передаче текстовых строк в качестве параметров для макросов. Другое частое применение угловых скобок — передача списка параметров вложенному макроопределению или блоку повторений.

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

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

Макрооператор ;; (две точки с запятой) — начало макрокомментария. В отличие от обычных комментариев текст макрокомментария не попадает в листинг и в текст программы при подстановке макроса. Это сэкономит память при ассемблировании программы с большим количеством макроопределений.



Макроопределения


Одно из самых мощных языковых средств ассемблера — макроопределения. Макроопределением (или макросом) называется участок программы, которому присвоено имя и который ассемблируется всякий раз, когда ассемблер встречает это имя в тексте программы. Макрос начинается директивой MACRO и заканчивается ENDM. Например: пусть описано макроопределение hex2ascii, переводящее шестнадцатеричное число, находящееся в регистре AL, в ASCII-код соответствующей шестнадцатеричной цифры:

hex2ascii macro cmp al,10 sbb al,69h das endm

Теперь в программе можно использовать слово hex2ascii, как если бы это было имя команды, и ассемблер заменит каждое такое слово на три команды, содержащиеся в макроопределении. Разумеется, можно оформить этот же участок кода в виде процедуры и вызывать его командой CALL — если процедура вызывается больше одного раза, этот вариант программы займет меньше места, но вариант с макроопределением станет выполняться быстрее, так как в нем не будет лишних команд CALL и RET. Однако скорость выполнения — не главное преимущество макросов. В отличие от процедур макроопределения могут вызываться с параметрами, следовательно, в зависимости от ситуации, включаемый код будет немного различаться, например:

s_mov macro register1,register2 push register1 pop register2 endm

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

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

fast_mul macro number if number eq 2 shl ax,1 ; Умножение на 2 elseif number eq 4 shl ax,2 ; Умножение на 4 elseif number eq 8 shl ax,3 ; Умножение на 8 ... ; Аналогично вплоть до: elseif number eq 32768 shl ax,15 ; Умножение на 32768 else mov dx,number ; Умножение на число, не являющееся mul dx ; степенью двойки. endif endm

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



Модели памяти и упрощенные директивы определения сегментов


Модели памяти задаются директивой .MODEL

.model модель,язык,модификатор

где модель — одно из следующих слов:

TINY — код, данные и стек размещаются в одном и том же сегменте размером до 64 Кб. Эта модель памяти чаще всего используется при написании на ассемблере небольших программ;

SMALL — код размещается в одном сегменте, а данные и стек — в другом (для их описания могут применяться разные сегменты, но объединенные в одну группу). Эту модель памяти также удобно использовать для создания программ на ассемблере;

COMPACT — код размещается в одном сегменте, а для хранения данных могут использоваться несколько сегментов, так что для обращения к данным требуется указывать сегмент и смещение (данные дальнего типа);

MEDIUM — код размещается в нескольких сегментах, а все данные — в одном, поэтому для доступа к данным используется только смещение, а вызовы подпрограмм применяют команды дальнего вызова процедуры;

LARGE и HUGE — и код, и данные могут занимать несколько сегментов;

FLAT — то же, что и TINY, но используются 32-битные сегменты, так что максимальный размер сегмента, содержащего и данные, и код, и стек, — 4 Мб.

Язык — необязательный операнд, принимающий значения C, PASCAL, BASIC, FORTRAN, SYSCALL и STDCALL. Если он указан, подразумевается, что процедуры рассчитаны на вызов из программ на соответствующем языке высокого уровня, следовательно, если указан язык C, все имена ассемблерных процедур, объявленных как PUBLIC, будут изменены так, чтобы начинаться с символа подчеркивания, как это принято в C.

Модификатор — необязательный операнд, принимающий значения NEARSTACK (по умолчанию) или FARSTACK. Во втором случае сегмент стека не будет объединяться в одну группу с сегментами данных.

После того как модель памяти установлена, вступают в силу упрощенные директивы определения сегментов, объединяющие действия директив SEGMENT и ASSUME. Кроме того, сегменты, объявленные упрощенными директивами, не требуется закрывать директивой ENDS — они закрываются автоматически, как только ассемблер обнаруживает новую директиву определения сегмента или конец программы.

Директива .CODE описывает основной сегмент кода

.code имя_сегмента


эквивалентно

_TEXT segment word public ’CODE’

для моделей TINY, SMALL и COMPACT и

name_TEXT segment word public ’CODE’

для моделей MEDIUM, HUGE и LARGE (name — имя модуля, в котором описан данный сегмент). В этих моделях директива .CODE также допускает необязательный операнд — имя определяемого сегмента, но все сегменты кода, описанные так в одном и том же модуле, объединяются в один сегмент с именем NAME_TEXT.

.stack размер

Директива .STACK описывает сегмент стека и эквивалентна директиве

STACK segment para public ’stack’

Необязательный параметр указывает размер стека. По умолчанию он равен 1 Кб.

.data

Описывает обычный сегмент данных и соответствует директиве

_DATA segment word public ’DATA’

.data?

Описывает сегмент неинициализированных данных:

_BSS segment word public ’BSS’

Этот сегмент обычно не включается в программу, а располагается за концом памяти, так что все описанные в нем переменные на момент загрузки программы имеют неопределенные значения.

.const

Описывает сегмент неизменяемых данных:

CONST segment word public ’CONST’

В некоторых операционных системах этот сегмент будет загружен так, что попытка записи в него может привести к ошибке.

.fardata имя_сегмента

Сегмент дальних данных:

имя_сегмента segment para private ’FAR_DATA’

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

.fardata? имя_сегмента

Сегмент дальних неинициализированных данных:

имя_сегмента segment para private ’FAR_BSS’

Как и в случае с FARDATA, доступ к данным из этого сегмента потребует загрузки сегментного регистра. Если имя сегмента не указано, используется FAR_BSS.

Во всех моделях памяти сегменты, представленные директивами .DATA, .DATA?, .CONST, .FARDATA и .FARDATA?, а также сегмент, описанный директивой .STACK, если не был указан модификатор FARSTACK, и сегмент .CODE в модели TINY автоматически объединяются в группу с именем FLAT — для модели памяти FLAT или DGROUP — для всех остальных моделей. При этом сегментный регистр DS (и SS, если не было FARSTACK, и CS в модели TINY) настраивается на всю эту группу, как если бы была выполнена команда ASSUME.


Порядок загрузки сегментов


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

.alpha

Эта директива устанавливает алфавитный порядок загрузки сегментов.

.dosseg ; для MASM и WASM

или

dosseg ; для MASM и TASM

Устанавливает порядок загрузки сегментов, существующий в MS DOS и часто требуемый для взаимодействия программ на ассемблере с программами на языках высокого уровня. DOSSEG устанавливает следующий порядок загрузки сегментов:

1. Все сегменты класса 'CODE'.

2. Все сегменты, не принадлежащие группе DGROUP и классу 'CODE'.

3. Группа сегментов DGROUP:

3.1. Все сегменты класса 'BEGDATA'.

3.2. Все сегменты, кроме классов 'BEGDATA', 'BSS' и 'STACK'.

3.3. Все сегменты класса 'BSS'.

3.4. Все сегменты класса 'STACK'.

.seq

Устанавливает загрузку сегментов в том порядке, в котором они описаны в тексте программы. Этот режим устанавливается по умолчанию, так что директива .SEQ просто отменяет действие .ALPHA или .DOSSEG.

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



Процедуры


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

метка proc язык тип USES регистры ; TASM

или

метка proc тип язык USES регистры ; MASM/WASM ... ret метка endp

Все операнды PROC необязательны.

Тип может принимать значения NEAR и FAR, и если он указан, все команды RET в теле процедуры будут заменены соответственно на RETN и RETF. По умолчанию подразумевается, что процедура имеет тип NEAR в моделях памяти TINY, SMALL и COMPACT.

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

USES — список регистров, значения которых изменяет процедура. Ассемблер помещает в начало процедуры набор команд PUSH, а перед командой RET — набор команд POP, так что значения перечисленных регистров будут восстановлены.



Псевдокоманды определения переменных


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

имя_переменной d* значение

где D* — одна из нижеприведенных псевдокоманд:

DB — определить байт;

DW — определить слово (2 байта);

DD — определить двойное слово (4 байта);

DF — определить 6 байт (адрес в формате 16-битный селектор: 32-битное смещение);

DQ — определить учетверенное слово (8 байт);

DT — определить 10 байт (80-битные типы данных, используемые FPU).

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

text_string db 'Hello world!' number dw 7 table db 1,2,3,4,5,6,7,8,9,0Ah,0Bh,0Ch,0Dh,0Eh,0Fh float_number dd 3.5e7

заполняет данными 33 байта. Первые 12 байт содержат ASCII-коды символов строки «Hello world!», и переменная text_string указывает на первую букву в этой строке, так что команда

mov al,text_string

считает в регистр AL число 48h (код латинской буквы H). Если вместо точного значения указан знак ?, переменная считается неинициализированной и ее значение на момент запуска программы может оказаться любым. Если нужно заполнить участок памяти повторяющимися данными, используется специальный оператор DUP, имеющий формат счетчик DUP (значение). Например, вот такое определение:

table_512w dw 512 dup(?)

создает массив из 512 неинициализированных слов, на первое из которых указывает переменная table_512w. В качестве аргумента в операторе DUP могут выступать несколько значений, разделенных запятыми, и даже дополнительные вложенные операторы DUP.



Сегменты


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

Сегмент программы описывается директивами SEGMENT и ENDS.

имя_сегмента segment readonly выравн. тип разряд 'класс' ... имя_сегмента ends

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

Все пять операндов директивы SEGMENT необязательны.

READONLY. Если этот операнд присутствует, MASM выдаст сообщение об ошибке на все команды, выполняющие запись в данный сегмент. Другие ассемблеры этот операнд игнорируют.

Выравнивание. Указывает ассемблеру и компоновщику, с какого адреса может начинаться сегмент. Значения этого операнда:

BYTE — с любого адреса;

WORD — с четного адреса;

DWORD — с адреса, кратного 4;

PARA — с адреса, кратного 16 (граница параграфа);

PAGE — с адреса, кратного 256.

По умолчанию используется выравнивание по границе параграфа.

Тип. Выбирает один из возможных типов комбинирования сегментов:

тип PUBLIC (иногда используется синоним MEMORY) означает, что все такие сегменты с одинаковым именем, но разными классами будут объединены в один;

тип STACK — то же самое, что и PUBLIC, но должен использоваться для сегментов стека, потому что при загрузке программы сегмент, полученный объединением всех сегментов типа STACK, будет использоваться как стек;

сегменты типа COMMON с одинаковым именем также объединяются в один, но не последовательно, а по одному и тому же адресу, следовательно, длина суммарного сегмента будет равна не сумме длин объединяемых сегментов, как в случае PUBLIC и STACK, а длине максимального. Таким способом иногда можно формировать оверлейные программы;


тип AT — выражение указывает, что сегмент должен располагаться по фиксированному абсолютному адресу в памяти. Результат выражения, использующегося в качестве операнда для AT, равен этому адресу, деленному на 16. Например: segment at 40h — сегмент, начинающийся по абсолютному адресу 0400h. Такие сегменты обычно содержат только метки, указывающие на области памяти, которые могут потребоваться программе;

PRIVATE (значение по умолчанию) — сегмент такого типа не объединяется с другими сегментами.

Разрядность. Этот операнд может принимать значения USE16 и USE32. Размер сегмента, описанного как USE16, не может превышать 64 Кб, и все команды и адреса в этом сегменте считаются 16-битными. В этих сегментах все равно можно применять команды, использующие 32-битные регистры или ссылающиеся на данные в 32-битных сегментах, но они будут использовать префикс изменения разрядности операнда или адреса и окажутся длиннее и медленнее. Сегменты USE32 могут занимать до 4 Гб, и все команды и адреса в них по умолчанию 32-битные. Если разрядность сегмента не указана, по умолчанию используется USE16 при условии, что перед директивой .MODEL не применялась директива задания допустимого набора команд .386 или старше.

Класс сегмента — это любая метка, взятая в одинарные кавычки. Все сегменты с одинаковым классом, даже сегменты типа PRIVATE, будут расположены в исполняемом файле непосредственно друг за другом.

Для обращения к любому сегменту следует сначала загрузить его сегментный адрес (или селектор в защищенном режиме) в какой-нибудь сегментный регистр. Если в программе определено много сегментов, удобно объединить несколько сегментов в группу, адресуемую с помощью одного сегментного регистра:

имя_группы group имя_сегмента...

Операнды этой директивы — список имен сегментов (или выражений, использующих оператор SEG), которые объединяются в группу. Имя группы теперь можно применять вместо имен сегментов для получения сегментного адреса и для директивы ASSUME.

assume регистр:связь...

Директива ASSUME указывает ассемблеру, с каким сегментом или группой сегментов связан тот или иной сегментный регистр. В качестве операнда «связь» могут использоваться имена сегментов, имена групп, выражения с оператором SEG или слово «NOTHING», означающее отмену действия предыдущей ASSUME для данного регистра. Эта директива не изменяет значений сегментных регистров, а только позволяет ассемблеру проверять допустимость ссылок и самостоятельно вставлять при необходимости префиксы переопределения сегментов, если они необходимы.

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


Структура программы


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

метка команда/директива операнды ; комментарий

Причем все эти поля необязательны. Метка может быть любой комбинацией букв английского алфавита, цифр и символов _, $, @, ?, но цифра не может быть первым символом метки, а символы $ и ? иногда имеют специальные значения и обычно не рекомендуются к использованию. Большие и маленькие буквы по умолчанию не различаются, но различие можно включить, задав ту или иную опцию в командной строке ассемблера. Во втором поле, поле команды, может располагаться команда процессора, которая транслируется в исполняемый код, или директива, которая не приводит к появлению нового кода, а управляет работой самого ассемблера. В поле операндов располагаются требуемые командой или директивой операнды (то есть нельзя указать операнды и не указать команду или директиву). И наконец, в поле комментариев, начало которого отмечается символом ; (точка с запятой), можно написать все что угодно — текст от символа «;» до конца строки не анализируется ассемблером.

Для облегчения читаемости ассемблерных текстов принято, что метка начинается на первой позиции в строке, команда — на 17-й (две табуляции), операнды — на 25-й (три табуляции) и комментарии — на 41-й или 49-й. Если строка состоит только из комментария, его начинают с первой позиции.

Если метка располагается перед командой процессора, сразу после нее всегда ставится символ «:» (двоеточие), который указывает ассемблеру, что надо создать переменную с этим именем, содержащую адрес текущей команды:

some_loop: lodsw ; cчитать слово из строки, cmp ax,7 ; если это 7 - выйти из цикла loopne some_loop

Когда метка стоит перед директивой ассемблера, она обычно оказывается одним из операндов этой директивы и двоеточие не ставится. Рассмотрим директивы, работающие напрямую с метками и их значениями, — LABEL, EQU и =.

метка label тип

Директива LABEL определяет метку и задает ее тип. Тип может быть одним из: BYTE (байт), WORD (слово), DWORD (двойное слово), FWORD (6 байт), QWORD (учетверенное слово), TBYTE (10 байт), NEAR (ближняя метка), FAR (дальняя метка). Метка получает значение, равное адресу следующей команды или следующих данных, и тип, указанный явно. В зависимости от типа команда

mov метка,0


запишет в память байт (слово, двойное слово и т.д.), заполненный нулями, а команда

call метка

выполнит ближний или дальний вызов подпрограммы.

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

метка equ выражение

Директива EQU присваивает метке значение, которое определяется как результат целочисленного выражения в правой части. Результатом этого выражения может быть целое число, адрес или любая строка символов:

truth equ 1 message1 equ 'Try again$' var2 equ 4[si] cmp ax,truth ; cmp ax,1 db message1 ; db 'Try again$' mov ax,var2 ; mov ax, 4[si]

Директива EQU чаще всего используется с целью введения параметров, общих для всей программы, аналогично команде #define препроцессора языка С.

метка = выражение

Директива = эквивалентна EQU, но определяемая ею метка может принимать только целочисленные значения. Кроме того, метка, указанная этой директивой, может быть переопределена.

Каждый ассемблер предлагает целый набор специальных предопределенных меток — это может быть текущая дата (@date или ??date), тип процессора (@cpu) или имя того или иного сегмента программы, но единственная предопределенная метка, поддерживаемая всеми рассматриваемыми нами ассемблерами, — $. Она всегда соответствует текущему адресу. Например, команда

jmp $

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


Структуры


Директива STRUC позволяет определить структуру данных аналогично структурам в языках высокого уровня. Последовательность директив

имя struc поля

имя ends

где поля — любой набор псевдокоманд определения переменных или структур, устанавливает, но не инициализирует структуру данных. В дальнейшем для ее создания в памяти используют имя структуры как псевдокоманду:

метка имя <значения>

И наконец, для чтения или записи в элемент структуры используется оператор «.» (точка). Например:

point struc ; Определение структуры x dw 0 ; Три слова со значениями y dw 0 ; по умолчанию 0,0,0 z dw 0 color db 3 dup(?) ; и три байта point ends

cur_point point <1,1,1,255,255,255> ; Инициализация mov ax,cur_point.x ; Обращение к слову "x"

Если была определена вложенная структура, доступ к ее элементам осуществляется через еще один оператор «.» (точка).

color struc ; Определить структуру color. red db ? green db ? blue db ? color ends

point struc x dw 0 y dw 0 z dw 0 clr color <> point ends

cur_point point <> mov cur_point.clr.red,al ; Обращение к красной компоненте ; цвета точки cur_point.



Управление файлами


INCLUDE имя_файла — директива, вставляющая в текст программы текст файла аналогично команде препроцессора C #include. Обычно используется для включения файлов, содержащих определения констант, структур и макросов.

INCLUDELIB имя_файла — директива, указывающая компоновщику имя дополнительной библиотеки или объектного файла, который потребуется при составлении данной программы. Например, если используются вызовы процедур или обращение к данным, определенным в других модулях. Использование этой директивы позволяет не указывать имена дополнительных библиотек при вызове компоновщика.



Управление листингом


Обычно ассемблеры, помимо создания объектного файла, предоставляют возможность создания листинга программы (TASM /L — для TASM, ml /Fl — для MASM). Листинг — это файл, содержащий текст ассемблерной программы, код каждой ассемблированной команды, список определенных меток, перекрестных ссылок, сегментов и групп. Формат файла листинга отличается для разных ассемблеров, и директивы управления форматом этого файла также сильно различаются, но несколько наиболее общих директив все-таки поддерживаются всеми тремя ассемблерами, рассмотренными в этой книге.

TITLE текст — определяет заголовок листинга. Заголовок появляется в начале каждой страницы;

SUBTTL текст — определяет подзаголовок листинга. Подзаголовок появляется на следующей строке после заголовка;

PAGE высота,ширина — устанавливает размеры страниц листинга (высота 10—255, ширина 59—255). Директива PAGE без аргументов начинает новую страницу, директива PAGE + начинает новую секцию, и нумерация страниц ведется с самого начала;

NAME текст — определяет имя модуля программы. Если NAME не указан, в качестве имени используются первые 6 символов из TITLE; если нет ни NAME, ни TITLE, за имя берется название файла;

.XLIST — отменить выдачу листинга;

.LIST — разрешить выдачу листинга;

.SALL — запретить листинг макроопределений;

.SFCOND — запретить листинг неассемблированных условных блоков;

.LFCOND — разрешить листинг неассемблированных условных блоков;

.TFCOND — изменить режим листинга условных блоков на противоположный;

.CREF — разрешить листинг перекрестных ссылок;

.XCREF — запретить листинг перекрестных ссылок.



Условное ассемблирование


В большинстве языков программирования присутствуют средства, позволяющие игнорировать тот или иной участок программы в зависимости от выполнения условий, например: в языке C это осуществляется командами препроцессора #if, #ifdef, #ifndef и т.д. Ассемблер тоже предоставляет такую возможность.

if выражение ... endif

Если значение выражения — ноль (ложь), весь участок программы между IF и ENDIF игнорируется. Директива IF может также сочетаться с ELSE и ELSEIF:

if выражение ... else ... endif

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

if выражение1 ... elseif выражение2 ... elseif выражение3 ... else ... endif

Так, если, например, выражение 2 не равно нулю, будет ассемблироваться участок программы между первой и второй директивой ELSEIF. Если все три выражения равны нулю, ассемблируется фрагмент от ELSE до ENDIF. Данная структура директив может использоваться в частном случае аналогично операторам switch/case языков высокого уровня, если выражения — проверки некоторой константы на равенство.

Кроме общих директив IF и ELSEIF ассемблеры поддерживают набор специальных команд, каждая из которых проверяет специальное условие:

IF1/ELSEIF1 — если ассемблер выполняет первый проход ассемблирования;

IF2/ELSEIF2 — если ассемблер выполняет второй проход ассемблирования (часто не работает на современных ассемблерах);

IFE выражение/ELSEIFE выражение — если выражение равно нулю (ложно);

IFDEF метка/ELSEIFDEF метка — если метка определена;

IFNDEF метка/ELSEIFNDEF метка — если метка не определена;

IFB <аргумент>/ELSEIFB <аргумент> — если значение аргумента — пробел (эти и все следующие директивы используются в макроопределениях для проверки параметров);

IFNB <аргумент>/ELSEIFNB <аргумент> — если значение аргумента — не пробел (используется в макроопределениях для проверки переданных параметров);


IFDIF <арг1>,<арг2>/ELSEIFDIF <арг1>,<арг2> — если аргументы отличаются (с различием больших и маленьких букв);

IFDIFI <арг1>,<арг2>/ELSEIFDIFI <арг1>,<арг2> — если аргументы отличаются (без различия больших и маленьких букв);

IFIDN <арг1>,<арг2>/ELSEIFIDN <арг1>,<арг2> — если аргументы одинаковы (с различием больших и маленьких букв);

IFIDNI <арг1>,<арг2>/ELSEIFIDNI <арг1>,<арг2> — если аргументы одинаковы (без различия больших и маленьких букв).

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

if $ gt 65535 ; Если адрес вышел за пределы сегмента. .err endif

Встретив директиву .ERR, ассемблер прекратит работу с сообщением об ошибке. Аналогично командам условного ассемблирования существуют модификации команды .ERR:

.ERR1 — ошибка при первом проходе ассемблирования;

.ERR2 — ошибка при втором проходе ассемблирования;

.ERRE выражение — ошибка, если выражение равно нулю (ложно);

.ERRNZ выражение — ошибка, если выражение не равно нулю (истинно);

.ERRDEF метка — ошибка, если метка определена;

.ERRNDEF метка — ошибка, если метка не определена;

.ERRB <аргумент> — ошибка, если аргумент пуст (эта и все следующие директивы используются в макроопределениях для проверки параметров);

.ERRNB <аргумент> — ошибка, если аргумент не пуст;

.ERRDIF <арг1>,<арг2> — ошибка, если аргументы различны;

.ERRDIFI <арг1>,<арг2> — ошибка, если аргументы отличаются (сравнение не различает большие и маленькие буквы);

.ERRIDN <арг1>,<арг2> — ошибка, если аргументы совпадают;

.ERRIDNI <арг1>,<арг2> — ошибка, если аргументы совпадают (сравнение не различает большие и маленькие буквы).


Выражения


Мы уже упоминали выражения при описании многих директив ассемблера. Выражение — это набор чисел, меток или строк, связанных друг с другом операторами. Например: 2 + 2 — выражение, состоящее из двух чисел (2 и 2) и оператора +. Каждое выражение имеет значение, которое определяется как результат действия операторов. Так, значение выражения 2 + 2 — число 4. Все выражения вычисляются в ходе ассемблирования программы, следовательно, в полученном коде используются только значения.

Оператор <> (угловые скобки). Часть выражения, заключенная в угловые скобки, не вычисляется, а применяется как строка символов, например:

message1 equ <foobar>

Оператор () (круглые скобки). Часть выражения, заключенная в круглые скобки, вычисляется в первую очередь.

mov al, 2*(3+4) ; mov al,14

Арифметические операторы: + (плюс), – (минус), * (умножение), / (целочисленное деление), MOD (остаток от деления). Они выполняют соответствующие арифметические действия.

mov al,90 mod 7 ; mov al,6

Кроме того, к арифметическим операторам относится унарный минус — минус, который ставят перед отрицательным числом.

Логические операторы: AND (И), NOT (НЕ), OR (ИЛИ), XOR (исключающее ИЛИ), SHL (сдвиг влево), SHR (сдвиг вправо). Эти операторы выполняют соответствующие логические действия.

mov ax,1234h AND 4321h ; mov ax,0220h

Операторы сравнения: EQ (равно), GE (больше или равно), GT (больше), LE (меньше или равно), LT (меньше), NE (не равно). Результат действия каждого из этих операторов — единица, если условие выполняется, и ноль — если не выполняется.

.errnz $ gt 65535 ; Если адрес больше 64 Кб – ошибка

Операторы адресации:

SEG выражение — сегментный адрес;

OFFSET выражение — смещение;

THIS тип — текущий адрес (MASM и TASM);

Тип PTR выражение — переопределение типа;

LARGE выражение — 32-битное смещение (TASM и WASM);

SMALL выражение — 16-битное смещение (TASM и WASM);


SHORT выражение — 8-битное смещение.

SEG и OFFSET возвращают соответствующую часть адреса своего аргумента:

mov dx, offset msg ; Занести в DX смещение переменной msg

THIS создает операнд, адресом которого является текущее значение счетчика:

mov al, this byte-1 ; Занести в AX последний байт кода ; предыдущей команды.

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

mov dword ptr [si],0 ; Записать 4 байта нулей по адресу DS:SI

LARGE, SMALL и SHORT используются с командами передачи управления, если возникают двусмысленности при косвенных переходах:

jmp large dword ptr old_address ; Переменная old_address содержит 32-битное смещение jmp small dword ptr old_address ; Переменная old_address содержит 16-битный сегментный адрес ; и 16-битное смещение. jmp short short_label ; Метка short_label находится ; ближе, чем +128/-127 байт от этой команды, так что можно ; использовать короткую форму команды JMP.

Другие операторы:

. (точка) — ссылка на элемент структуры;

: (двоеточие) — переопределение сегмента;

[] (прямые скобки) — косвенная адресация;

? — неинициализированное значение;

число DUP (значение) — повторяющееся значение.

Эти пять операторов описаны ранее, когда говорилось о структурах данных, методах адресации и псевдокомандах определения данных.

LENGTH метка – число элементов данных table dw 0,1,2,3,4,5,6,7 ; Определить таблицу из 8 слов. table_count = length table ; table_count = 8 SIZE метка – размер данных table_size = size table ; table_size = 16


Чтение и запись в файл


Функция DOS 3Fh — Чтение из файла или устройства

Ввод: АН = 3Fh
ВХ = идентификатор
СХ = число байт
DS:DX = адрес буфера для приема данных
Вывод: CF = 0 и АХ = число считанных байт, если не произошла ошибка
CF = 1 и АХ = 05h, если доступ запрещен, 06h, если неправильный идентификатор

Если при чтении из файла число фактически считанных байт в АХ меньше, чем заказанное число в СХ, при чтении был достигнут конец файла. Каждая следующая операция чтения, так же как и записи, начинается не с начала файла, а с того байта, на котором остановилась предыдущая операция чтения/записи. Если требуется считать (или записать) произвольный участок файла, используют функцию 42h (функция lseek в С).

Функция DOS 42h — Переместить указатель чтения/записи

Ввод: АН = 42h
ВХ = идентификатор
CX:DX = расстояние, на которое надо переместить указатель (со знаком)
AL = перемещение от:0 — от начала файла
1 — от текущей позиции
2 — от конца файла
Вывод: CF = 0 и CX:DX = новое значение указателя (в байтах от начала файла), если не произошла ошибка
CF = 1 и АХ = 06h, если неправильный идентификатор

Указатель можно установить за реальными пределами файла: если указатель устанавливается в отрицательное число, следующая операция чтения/записи вызовет ошибку; если указатель устанавливается в положительное число, большее длины файла, следующая операция записи увеличит размер файла. Эта функция также часто используется для определения длины файла — достаточно вызвать ее с СХ = 0, DX = 0, AL = 2, и в CX:DX будет возвращена длина файла в байтах.

Функция DOS 40h — Запись в файл или устройство

Ввод: АН = 40h
ВХ = идентификатор
СХ = число байт
DS:DX = адрес буфера с данными
Вывод: CF = 0 и АХ = число записанных байт, если не произошла ошибка
CF = 1 и АХ = 05h, если доступ запрещен, 06h, если неправильный идентификатор

Если при записи в файл указать СХ = 0, файл будет обрезан по текущему значению указателя. При записи в файл на самом деле происходит запись в буфер DOS, данные из которого сбрасываются на диск при закрытии файла или если их количество превышает размер сектора диска. Для немедленного сброса буфера можно использовать функцию 68h (функция fflush в С).


Функция DOS 68h — Сброс файловых буферов DOS на диск

Ввод: АН = 68h
ВХ = идентификатор
Вывод: CF = 0, если операция выполнена
CF = 1, если произошла ошибка (АХ = код ошибки)
Для критических участков программ использовать более эффективную функцию 0Dh.

Функция DOS 0Dh — Сброс всех файловых буферов на диск

Ввод: АН = 0Dh
Вывод: никакого

Интерфейс EMS


Дополнительная память (EMS) — способ для программ, запускающихся в реальном режиме (или в режиме V86), обращаться к памяти, находящейся за пределами первого мегабайта. EMS позволяет отобразить сегмент памяти, начинающийся обычно с 0D000h, на любые участки памяти, аналогично тому, как осуществляется доступ к видеопамяти в SVGA-режимах. Вызывать функции EMS (прерывание 67h) можно, только если в системе присутствует драйвер с именем ЕММХХХХ0. Для проверки его существования можно, например, вызвать функцию 3Dh (открыть файл или устройство), причем на тот случай, если драйвер EMS отсутствует, а в текущей директории есть файл с именем ЕММХХХХ0, следует дополнительно вызвать функцию IOCTL — INT 21h с АХ = 4400h и ВХ = идентификатор файла или устройства, полученный от функции 3Dh. Если значение бита 7 в DX после вызова этой функции 1, то драйвер EMS наверняка присутствует в системе.

Основные функции EMS:

INT 67h, АН = 46h — Получить номер версии

Ввод: AH = 46h
Вывод: АН = 0 и AL = номер версии в упакованном BCD (40h для 4.0)

Во всех случаях, если АН не ноль, произошла ошибка.

INT 67h, АН = 41h — Получить сегментный адрес окна

Ввод: AH = 41h
Вывод: АН = 0 и ВХ = сегментный адрес окна

INT 67h, АН = 42h — Получить объем памяти

Ввод: AH = 42h
Вывод: АН = 0
DX = объем EMS-памяти в 16-килобайтных страницах
ВХ = объем свободной EMS-памяти в 16-килобайтных страницах

INT 67h, АН = 43h — Выделить идентификатор и EMS-память

Ввод: АН = 43h
ВХ = требуемое число 16-килобайтных страниц
Вывод: АН = 0, DX = идентификатор

Теперь указанный в этой функции набор страниц в EMS-памяти описывается как занятый и другие программы не смогут его выделить для себя.

INT 67h, АН = 44h — Отобразить память

Ввод: АН = 44h
AL = номер 16-килобайтной страницы в 64-килобайтном окне EMS (0 – 3)
ВХ = номер 16-килобайтной страницы в EMS-памяти
DX = идентификатор
Вывод: АН = 0
<
/p> Теперь запись/чтение в указанную страницу в реальном адресном пространстве приведет к записи/чтению в указанную страницу в EMS-памяти.

INT 67h, АН = 45h — Освободить идентификатор и EMS-память

Ввод: АН = 45h
DX = идентификатор
Вывод: АH = 00h
Спецификация EMS была разработана для компьютеров IBM XT, снабжавшихся специальной платой, на которой и находилась дополнительная память. С появлением процессора 80286 появилась возможность устанавливать больше одного мегабайта памяти на материнской плате и для работы с ней была введена новая спецификация — XMS. Тогда же появились менеджеры памяти, эмулировавшие EMS поверх XMS, для совместимости со старыми программами, причем работа через EMS оказывалась значительно медленнее. Позже, когда в процессорах Intel появился механизм страничной адресации, оказалось, что теперь уже EMS можно реализовать значительно быстрее XMS. Большинство программ для DOS, которым требуется дополнительная память, поддерживают обе спецификации.


Интерфейс XMS


Спецификация доступа к дополнительной памяти (XMS) — еще один метод, позволяющий программам, запускающимся под управлением DOS в реальном режиме (или в режиме V86), использовать память, расположенную выше границы первого мегабайта.

INT 2Fh, АН = 43 — XMS- и DPMS-сервисы

Ввод: АХ = 4300h: проверить наличие XMS
Вывод: АН = 80h, если HIMEM.SYS или совместимый драйвер загружен
Ввод: АХ = 4310h: получить точку входа XMS
Вывод: ES:BX = дальний адрес точки входа XMS

После определения точки входа все функции XMS вызываются с помощью команды CALL на указанный дальний адрес.

Функция XMS 00h — Определить номер версии

Ввод: AH = 00h
Вывод: АХ = номер версии, не упакованный BCD (0300h для 3.0)
ВХ = внутренний номер модификации
DX = 1, если HMA существует, 0, если нет

Функция XMS 08h — Определить объем памяти

Ввод: АН = 08h
BL = 00h
Вывод: АХ = размер максимального доступного блока в килобайтах
DX = размер всей XMS-памяти в килобайтах
BL = код ошибки (A0h, если вся XMS-память занята, 00, если нет ошибок)

Так как возвращаемый размер памяти оказывается ограниченным размером слова (65 535 Кб), начиная с версии XMS 3.0, введена более точная функция 88h.

Функция XMS 88h — Определить объем памяти

Ввод: AH = 88h
Вывод: ЕАХ = размер максимального доступного блока в килобайтах
BL = код ошибки (A0h, если вся XMS-память занята, 00 — если нет ошибок)
ЕСХ = физический адрес последнего байта памяти (верный для ошибки А0h)
EDX = размер XMS-памяти всего в килобайтах (0 для ошибки А0h)

Функция XMS 09h — Выделить память

Ввод: АН = 09h
DX = размер запрашиваемого блока (в килобайтах)
Вывод: АХ = 1, если функция выполнена
ВХ = идентификатор блока
АХ = 0:
BL = код ошибки (A0h, если не хватило памяти)

Функция XMS 0Ah — Освободить память

Ввод: АН = 0Ah
DX = идентификатор блока
Вывод: АХ = 1, если функция выполнена
Иначе — АХ = 0 и BL = код ошибки (A2h — неправильный идентификатор, ABh — участок заблокирован)
<
/p> Функция XMS 0Bh — Пересылка данных

Ввод: АН = 0Bh
DS:SI = адрес структуры для пересылки памяти
Вывод: АХ = 1, если функция выполнена
Иначе — АХ = 0 и BL = код ошибки
Структура данных, адрес которой передается в DS:SI:

+00h: 4 байта — число байт для пересылки

+04h: слово — идентификатор источника (0 для обычной памяти)

+06h: 4 байта — смещение в блоке-источнике или адрес в памяти

+0Ah: слово — идентификатор приемника (0 для обычной памяти)

+0Ch: 4 байта — смещение в блоке-приемнике или адрес в памяти

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

Функция XMS 0Fh — Изменить размер XMS-блока

Ввод: АН = 0Fh
ВХ = новый размер
DX = идентификатор блока
Вывод: АХ = 1, если функция выполнена
Иначе — АХ = 0 и BL = код ошибки
Кроме того, XMS позволяет программам использовать область НМА и блоки UMB, если они не заняты DOS при запуске (так как в CONFIG.SYS не было строк DOS = HIGH или DOS = UMB).

; mem.asm ; сообщает размер памяти, доступной через EMS и XMS ; .model tiny .code .186 ; для команд сдвига на 4 org 100h ; СОМ-программа start: cld ; команды строковой обработки ; будут выполняться вперед

; проверка наличия EMS

mov dx,offset ems_driver ; адрес ASCIZ-строки "EMMXXXX0" mov ax,3D00h int 21h ; открыть файл или устройство jc no_emmx ; если не удалось открыть - EMS нет mov bx,ax ; идентификатор в ВХ mov ax,4400h int 21h ; IOCTL: проверить состояние файла/устройства jc no_ems ; если не произошла ошибка, test dx,80h ; проверить старший бит DX, jz no_ems ; если он - 0, EMMXXXXO - файл в текущем ; каталоге

; определение версии EMS



mov ah,46h int 67h ; получить версию EMS test ah,ah jnz no_ems ; если EMS выдал ошибку - не стоит продолжать ; с ним работать mov ah,al and al,0Fh ; AL = старшая цифра shr ah,4 ; AH = младшая цифра call output_version ; выдать строку о номере версии EMS

; определение доступной EMS-памяти

mov ah,42h int 67h ; получить размер памяти ; в 16-килобайтных страницах shl dx,4 ; DX = размер памяти в килобайтах shl bx,4 ; ВХ = размер свободной памяти в килобайтах mov ax,bx mov si,offset ems_freemem ; адрес строки для output_info call output_info ; выдать строки о размерах памяти

no_ems: mov ah,3Eh int 21h ; закрыть файл/устройство EMMXXXX0 no_emmx:

; проверка наличия XMS

mov ax,4300h int 2Fh ; проверка XMS, cmp al,80h ; если AL не равен 80h, jne no_xms ; XMS отсутствует, mov ax,4310h ; иначе: int 2Fh ; получить точку входа XMS mov word ptr entry_pt,bx ; и сохранить ее в entry_pt mov word ptr entry_pt+2,es ; (старшее слово - по старшему адресу!) push ds pop es ; восстановить ES

; определение версии XMS

mov ah,00 call dword ptr entry_pt ; Функция XMS 00h - номер версии mov byte ptr mem_version,'X' ; изменить первую букву строки ; "EMS версии" на "X" call output_version ; выдать строку о номере версии XMS

; определение доступной XMS-памяти

mov ah,08h xor bx,bx call dword ptr entry_pt ; Функция XMS 08h mov byte ptr totalmem,'X' ; изменить первую букву строки ; "EMS-памяти" на "X" mov si,offset xms_freemem ; строка для output_info

; вывод сообщений на экран: ; DX - объем всей памяти ; АХ - объем свободной памяти ; SI - адрес строки с сообщением о свободной памяти (разный для EMS и XMS)

output_info: push ax mov ax,dx ; объем всей памяти в АХ mov bp,offset totalmem ; адрес строки - в ВР call output_info1 ; вывод pop ах ; объем свободной памяти - в АХ mov bp,si ; адрес строки - в ВР

output_info1: ; вывод mov di,offset hex2dec_word

; hex2dec ; преобразует целое двоичное число в АХ ; в строку десятичных ASCII-цифр в ES:DI, заканчивающуюся символом "$" mov bx,10 ; делитель в ВХ xor сх,сх ; счетчик цифр в 0 divlp: xor dx,dx div bx ; разделить преобразуемое число на 10 add dl,'0' ; добавить к остатку ASCII-код нуля push dx ; записать полученную цифру в стек inc cx ; увеличить счетчик цифр test ax,ax ; и, если еще есть, что делить, jnz divlp ; продолжить деление на 10 store: pop ax ; считать цифру из стека stosb ; дописать ее в конец строки в ES:DI loop store ; продолжить для всех СХ-цифр mov byte ptr es:[di],'$' ; дописать '$' в конец строки mov dx,bp ; DX - адрес первой части строки mov ah,9 int 21h ; Функция DOS 09h - вывод строки mov dx,offset hex2dec_word ; DX - адрес строки с десятичным числом int 21h ; вывод строки mov dx,offset eol ; DX - адрес последней части строки int 21h ; вывод строки



no_xms: ret ; конец программы и процедур ; output_info и output_info1

; вывод версии EMS/XMS ; АХ - номер в неупакованном BCD-формате

output_version: or ax,3030h ; преобразование неупакованного BCD в ASCII mov byte ptr major,ah ; старшая цифра в major mov byte ptr minor,al ; младшая цифра в minor mov dx,offset mem_version ; адрес начала строки - в DX mov ah,9 int 21h ; вывод строки ret

ems_driver db "EMMXXXX0",0 ; имя драйвера для проверки EMS mem_version db "EMS версии " ; сообщение о номере версии major db "0." ; первые байты этой minor db "0 обнаружен ",0Dh,0Ah,"$" ; и этой строк будут ; заменены реальными номерами версий totalmem db "EMS-памяти: $" ems_freemem db "EMS-памяти: $" eol db "K",0Dh,0Ah,'$' ; конец строки xms_freemem db "Наибольший свободный блок XMS: $"

entry_pt: ; сюда записывается точка входа XMS hex2dec_word equ entry_pt+4 ; буфер для десятичной строки end start


Командные параметры и переменные среды


В случае если команда не передавалась бы интерпретатору DOS, а выполнялась нами самостоятельно, то оказалось бы: чтобы запустить любую программу из-под shell.com, потребовалось бы предварительно переходить в каталог с этой программой или вводить ее с полным путем. Дело в том, что COMMAND.COM при запуске файла ищет его по очереди в каждом из каталогов, указанных в переменной среды PATH. DOS создает копию всех переменных среды (так называемое окружение DOS) для каждого запускаемого процесса. Сегментный адрес копии окружения для текущего процесса располагается в PSP по смещению 2Ch. В этом сегменте записаны все переменные подряд в форме ASCIZ-строк вида "COMSPEC=C:/WINDOWS/COMMAND.COM",0. По окончании последней строки стоит дополнительный нулевой байт, затем слово (обычно 1) — количество дополнительных строк окружения, а потом — дополнительные строки. Первая дополнительная строка — всегда полный путь и имя текущей программы — также в форме ASCIZ-строки. При запуске новой программы с помощью функции 4Bh можно создать полностью новое окружение и передать его сегментный адрес запускаемой программе в блоке ЕРВ или просто указать 0, позволив DOS скопировать окружение текущей программы.

Кроме того, в предыдущем примере мы передавали запускаемой программе (command.com) параметры (/с команда), но пока не объяснили, как программа может определить, что за параметры были переданы ей при старте. При запуске программы DOS помещает всю командную строку (включая последний символ 0Dh) в блок PSP запущенной программы по смещению 81h и ее длину в байт 80h (таким образом, длина командной строки не может быть больше 7Eh (126) символов). Под Windows 95 и 4DOS, если командная строка превышает эти размеры, байт PSP:0080h (длина) устанавливается в 7Fh, в последний байт PSP (PSP:00FFh) записывается 0Dh, первые 126 байт командной строки размещаются в PSP, а вся строка целиком — в переменной среды CMDLINE.

; cat.asm ; копирует объединенное содержимое всех файлов, указанных в командной строке, ; в стандартный вывод. Можно как указывать список файлов, так и использовать ; маски (символы "*" и "?") в одном или нескольких параметрах, ; например: ; cat header *.txt footer > all-texts помещает содержимое файла ; header, всех файлов с расширением .txt в текущем каталоге и файла ; footer - в файл all-texts ; длинные имена файлов не используются, ошибки игнорируются ; .model tiny .code org 80h ; по смещению 80h от начала PSP находятся: cmd_length db ? ; длина командной строки cmd_line db ? ; и сама командная строка org 100h ; начало СОМ-программы - 100h от начала PSP start: cld ; для команд строковой обработки mov bp,sp ; сохранить текущую вершину стека в ВР mov cl,cmd_length cmp cl,1 ; если командная строка пуста - jle show_usage ; вывести информацию о программе и выйти


mov ah,1Ah ; функция DOS 1Ah mov dx,offset DTA int 21h ; переместить DTA (по умолчанию она совпадает ; с командной строкой PSP)

; преобразовать список параметров в PSP:81h следующим образом: ; каждый параметр заканчивается нулем (ASCIZ-строка) ; адреса всех параметров помещаются в стек в порядке обнаружения ; в переменную argc записывается число параметров

mov cx,-1 ; для команд работы со строками mov di,offset cmd_line ; начало командной строки в ES:DI

find_param: mov al,' ' ; искать первый символ, repz scasb ; не являющийся пробелом dec di ; DI - адрес начала очередного параметра push di ; поместить его в стек inc word ptr argc ; и увеличить argc на один mov si,di ; SI = DI для следующей команды lodsb

scan_params: lodsb ; прочитать следующий символ из параметра, cmp al,0Dh ; если это 0Dh - это был последний параметр je params_ended ; и он кончился, cmp al,20h ; если это 20h - этот параметр кончился, jne scan_params ; но могут быть еще

dec si ; SI - первый байт после конца параметра mov byte ptr [si],0 ; записать в него 0 mov di,si ; DI = SI для команды scasb inc di ; DI - следующий после нуля символ jmp short find_param ; продолжить разбор командной строки

params_ended: dec si ; SI - первый байт после конца последнего mov byte ptr [si],0 ; параметра - записать в него 0

; каждый параметр воспринимается как файл или маска для поиска файлов, ; все найденные файлы выводятся на stdout. Если параметр - не имя файла, ; то ошибка игнорируется

mov si,word ptr argc ; SI - число оставшихся параметров

next_file_from_param: dec bp dec bp ; BP - адрес следующего адреса параметра dec si ; уменьшить число оставшихся параметров, js no_more_params ; если оно стало отрицательным - все mov dx,word ptr [bp] ; DS:DX - адрес очередного параметра mov ah,4Eh ; Функция DOS 4Eh mov cx,0100111b ; искать все файлы, кроме каталогов ; и меток тома int 21h ; найти первый файл, jc next_file_from_param ; если произошла ошибка - файла нет call output_found ; вывести найденный файл на stdout



find_next: mov ah,4Fh ; Функция DOS 4Fh mov dx,offset DTA ; адрес нашей области DTA int 21h ; найти следующий файл, jc next_file_from_param ; если ошибка - файлы кончились call output_found ; вывести найденный файл на stdout jmp short find_next ; продолжить поиск файлов

no_more_params: mov ax,word ptr argc shl ax,1 add sp,ax ; удалить из стека 2 * argc байт ; (то есть весь список адресов ; параметров командной строки) ret ; конец программы

; процедура show_usage ; выводит информацию о программе

show_usage: mov ah,9 ; Функция DOS 09h mov dx,offset usage int 21h ; вывести строку на экран ret ; выход из процедуры

; процедура output_found ; выводит в stdout файл, имя которого находится в области DTA

output_found: mov dx,offset DTA+1Eh ; адрес ASCIZ-строки с именем файла mov ax,3D00h ; Функция DOS 3Dh int 21h ; открыть файл (al = 0 - только на чтение), jc skip_file ; если ошибка - не трогать этот файл mov bx,ax ; идентификатор файла - в ВХ mov di,1 ; DI будет хранить идентификатор STDOUT

do_output: mov cx,1024 ; размер блока для чтения файла mov dx,offset DTA+45 ; буфер для чтения/записи располагается ; за концом DTA mov ah,3Fh ; Функция DOS 3Fh int 21h ; прочитать 1024 из файла, jc file_done ; если ошибка - закрыть файл mov cx,ax ; число реально прочитанных байт в СХ, jcxz file_done ; если это не ноль - закрыть файл mov ah,40h ; Функция DOS 40h xchg bx,di ; BX = 1 - устройство STDOUT int 21h ; вывод прочитанного числа байт в STDOUT xchg di,bx ; вернуть идентификатор файла в ВХ, jc file_done ; если ошибка - закрыть файл jmp short do_output ; продолжить вывод файла

file_done: mov ah,3Eh ; Функция DOS 3Eh int 21h ; закрыть файл

skip_file: ret ; конец процедуры output_found

usage db "cat.com v1.0",0Dh,0Ah db "объединяет и выводит файлы на stdout",0Dh,0Ah db "использование: cat имя_файла, ...",0Dh,0Ah db "(имя файла может содержать маски * и ?)",0Dh,0Ah,'$' argc dw 0 ; число параметров (должен быть 0 при старте программы!)

DTA: ; область DTA начинается сразу за концом файла, ; а сразу за областью DTA начинается ; 1024-байтный буфер для чтения файла end start

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


Область памяти HMA


Область памяти от FFFFh:0010h (конец первого мегабайта) до FFFFh:FFFFh (конец адресного пространства в реальном режиме), 65 520 байт, может использоваться на компьютерах, начиная с 80286. Доступ к этой области осуществляется с помощью спецификации XMS, причем вся она выделяется целиком одной программе. Обычно, если загружен драйвер HIMEM.SYS и если в файле CONFIG.SYS присутствует строка DOS = HIGH, DOS занимает эту область, освобождая почти 64 Кб в основной памяти. При этом DOS может оставить небольшой участок HMA (16 Кб или меньше) для пользовательских программ, которые обращаются к нему с помощью недокументированной функции мультиплексора 4Ah.

INT 2Fh, AX= 4A01h — Определить размер доступной части HMA (DOS 5.0+)

Ввод: АХ = 4A01h
Вывод: ВХ = размер доступной части HMA в байтах, 0000h, если DOS не в HMA
ES:DI = адрес начала доступной части НМА (FFFFh:FFFFh, если DOS не в НМА)

INT 2Fh, АХ= 4A02h — Выделить часть НМА (DOS 5.0+)

Ввод: АХ = 4А02h
ВХ = размер в байтах
Вывод: ES:DI = адрес начала выделенного блока
ВХ = размер выделенного блока в байтах

В версиях DOS 5.0 и 6.0 нет функций освобождения выделенных таким образом блоков НМА. В DOS 7.0 (Windows 95) выделение памяти в НМА было организовано аналогично выделению памяти в обычной памяти и UMB, с функциями изменения размера и освобождения блока.

INT 2Fh, АХ = 4A03h — Управление распределением памяти в НМА (DOS 7.0+)

Ввод: АХ = 4A03h
DL = 0 — выделить блок (ВХ = размер в байтах)
DL = 1 — изменить размер блока (ES:DI = адрес, ВХ = размер)
DL = 2 — освободить блок (ES:DI = адрес)
СХ = сегментный адрес владельца блока
Вывод: DI = FFFFh, если не хватило памяти,
ES:DI = адрес блока (при выделении)

Следует помнить, что область НМА доступна для программ только в том случае, если адресная линия процессора А20 разблокирована. Если DOS не занимает НМА, она почти всегда заблокирована для совместимости с программами, написанными для процессора 8086/8088, которые считают, что адреса FFFFh:0010h — FFFFh:FFFFh всегда совпадают с 0000h:0000h — 0000h:FFEFh. Функции XMS 01 – 07 предоставляют возможность управления состоянием этой адресной линии.



Область памяти UMB


Функция DOS 58h — Считать/изменить стратегию выделения памяти

Ввод: АН = 58h
AL = 00h — считать стратегию
AL = 01h — изменить стратегию
ВХ = новая стратегия

биты 2 – 0:

00 — первый подходящий блок

01 — наиболее подходящий блок

11 — последний подходящий блок

биты 4 – 3:

00 — обычная память

01 — UMB (DOS 5.0+)

10 — UMB, затем обычная память (DOS 5.0+)

AL = 02h — считать состояние UMB
AL = 03h — установить состояние UMB
ВХ = новое состояние: 00 — не используются, 01 — используются

Вывод: CF = 0, АХ = текущая стратегия для AL = 0, состояние UMB для AL = 2
CF = 1, AX = 01h, если функция не поддерживается (если не запущен менеджер памяти (например, EMM386) или нет строки DOS = UMB в CONFIG.SYS

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



Обычная память


До сих пор, если требовалось создать массив данных в памяти, мы просто обращались к памяти за концом программы, считая, что там есть еще хотя бы 64 килобайта свободной памяти. Разумеется, как и во всех операционных системах, в DOS есть средства управления распределением памяти — выделение блока (аналог стандартной функции языка С malloc), изменение его размеров (аналог realloc) и освобождение (free).

Функция DOS 48h — Выделить память

Ввод: АН = 48h
ВХ = размер блока в 16-байтных параграфах
Вывод: CF = 0, если блок выделен
АХ = сегментный адрес выделенного блока
CF = 1, если произошла ошибка:АХ = 7 — блоки управления памятью разрушены
АХ = 8 — недостаточно памяти:ВХ = размер максимального доступного блока

Эта функция с большим значением в ВХ (обычно FFFFh) используется для определения размера самого большого доступного блока памяти.

Функция DOS 49h — Освободить память

Ввод: АН = 49h
ES = сегментный адрес освобождаемого блока
Вывод: CF = 0, если блок освобожден
CF = 1:АХ = 7, если блоки управления памятью разрушены,
АХ = 9, если в ES содержится неверный адрес

Эта функция не позволит освободить блок памяти, которым текущая программа не владеет, но с помощью функции DOS 50h (AX = 50h, ВХ = сегментный адрес PSP процесса) программа может «притвориться» любым другим процессом.

Функция DOS 4Ah — Изменить размер блока памяти

Ввод: АН = 4Ah
ВХ = новый размер в 16-байтных параграфах
ES = сегментный адрес модифицируемого блока
Вывод: CF = 0, если размер блока изменен
CF = 1:

АХ = 7, если блоки управления памятью разрушены,
АХ = 8, если не хватает памяти (при увеличении),
АХ = 9, если ES содержит неверный адрес

ВХ = максимальный размер, доступный для этого блока

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

При запуске СОМ-программы загрузчик DOS выделяет самый большой доступный блок памяти для этой программы, так что при работе с основной памятью эти функции требуются редко (в основном для того, чтобы сократить выделенный программе блок памяти до минимума перед загрузкой другой программы), но уже в MS-DOS 5.0 и далее с помощью этих же функций можно выделять память в областях UMB — неиспользуемых участках памяти выше 640 Кб и ниже 1 Мб, для этого требуется сначала подключить UMB к менеджеру памяти и изменить стратегию выделения памяти с помощью функции DOS 58h.



Основы программирования для MS-DOS


Программа, написанная на ассемблере, так же как и программа, написанная на любом другом языке программирования, выполняется не сама по себе, а при помощи операционной системы. Операционная система выделяет области памяти для программы, загружает ее, передает ей управление и обеспечивает взаимодействие программы с устройствами ввода-вывода, файловыми системами и другими программами (разумеется, кроме тех случаев, когда эта программа сама является операционной системой или ее частью). Способы взаимодействия программы с внешним миром различны для разных операционных систем, так что программа, написанная для Windows, не будет работать в DOS, а программа для Linux — в Solaris/x86, хотя все эти системы могут работать на одном и том же компьютере.

Самая простая и распространенная операционная система для компьютеров, основанных на процессорах Intel, — DOS (Дисковая Операционная Система). Она распространяется как сама по себе несколькими производителями — Microsoft (MS-DOS), IBM (PC-DOS), Novell (Novell DOS), Caldera (Open DOS) и др., так и в виде части систем Microsoft Windows 95 и старше. DOS предоставляет программам полную свободу действий, никак не ограничивая доступ к памяти и внешним устройствам, позволяя им самим управлять процессором и распределением памяти. По этой причине DOS лучше всего подходит для того, чтобы близко познакомиться с устройством компьютера и возможностями, которые может использовать программа на ассемблере, но которые часто скрываются компиляторами с языков высокого уровня и более совершенными операционными системами.

Итак, чтобы программа выполнилась любой ОС, она должна быть скомпилирована в исполнимый файл. Основные два формата исполнимых файлов в DOS — СОМ и ЕХЕ. Файлы типа СОМ содержат только скомпилированный код без какой-либо дополнительной информации о программе. Весь код, данные и стек такой программы располагаются в одном сегменте и не могут превышать 64 килобайта. Файлы типа ЕХЕ содержат заголовок, в котором описывается размер файла, требуемый объем памяти, список команд в программе, использующих абсолютные адреса, которые зависят от расположения программы в памяти, и т.д. ЕХЕ-файл может иметь любой размер. Формат ЕХЕ также используется для исполнимых файлов в различных версиях DOS-расширителей и Windows, но со значительными изменениями.

Несмотря на то что файлам типа СОМ принято давать расширение .com, а файлам типа EXE — .exe, DOS не использует расширения для определения типа файла. Первые два байта заголовка ЕХЕ-файла — символы «MZ» или «ZM», и если файл начинается с этих символов и длиннее некоторого порогового значения, разного для разных версий DOS, он загружается как ЕХЕ, если нет — как СОМ.

Кроме обычных исполнимых программ DOS может загружать драйверы устройств — специальные программы, используемые для упрощения доступа к внешним устройствам. Например, драйвер устройства LPT, входящий в IO.SYS, позволяет посылать тексты на печать из DOS простым копированием файла в LPT, а драйвер RAMDISK.SYS позволяет выделить область памяти и обращаться к ней, как к диску. Написание драйверов значительно сложнее, чем написание обычных программ, и рассмотрено далее.



Параллельный порт


Параллельные порты используются в первую очередь для подключения принтеров, хотя встречаются и другие устройства, например переносные жесткие диски, которые могут подключаться к этим портам. Базовые средства DOS и BIOS для работы с параллельными портами аналогичны соответствующим средствам для работы с последовательными портами: DOS инициализирует стандартное устройство PRN, соответствующее первому порту LPT1, которое может быть переопределено командой MODE, и предоставляет прерывание для вывода в это устройство.

Функция DOS 05h — Вывод символа в стандартное устройство PRN

Ввод: АН = 05h
DL = символ

Кроме того, можно пользоваться функцией записи в файл или устройство, поместив в ВХ число 4, соответствующее устройству PRN. BIOS, в свою очередь, предоставляет базовый набор из трех функций для работы с принтером.

INT 17h, АН = 00 — Вывести символ в принтер

Ввод: АН = 00h
AL = символ
DX = номер параллельного порта (00 — LPT1, 01 — LPT2, 02 — LPT3)
Вывод: АН = состояние принтера:

бит 7: принтер не занят

бит 6: подтверждение

бит 5: нет бумаги

бит 4: принтер в состоянии on-line

бит 3: ошибка ввода-вывода

бит 0: тайм-аут

INT 17h, АН = 01 — Выполнить аппаратный сброс принтера

Ввод: АН = 01h
DX = номер порта (00h — 02h)
Вывод: АН = состояние принтера

INT 17h, AH = 02 — Получить состояние принтера

Ввод: АН = 02h
DX = номер порта (00h – 02h)
Вывод: АН = состояние принтера

Например, чтобы распечатать содержимое экрана на принтере, можно написать такую программу:

; prtscr.asm ; распечатать содержимое экрана на принтере ; .model tiny .code .186 ; для команды push 0B800h org 100h ; начало СОМ-файла start: mov ah,1 mov dx,0 ; порт LPT1 int 17h ; инициализировать принтер, cmp ah,90h ; если принтер не готов, jne printer_error ; выдать сообщение об ошибке, push 0B800h ; иначе: pop ds ; DS = сегмент видеопамяти в текстовом режиме xor si,si ; SI = 0 mov cx,80*40 ; CX = число символов на экране cld ; строковые операции вперед main_loop: lodsw ; AL - символ, АН - атрибут, SI = SI + 2 mov ah,0 ; АН - номер функции int 17h ; вывод символа из AL на принтер loop main_loop ret ; закончить программу

printer_error: mov dx,offset msg ; адрес сообщения об ошибке в DS:DX mov ah,9 int 21h ; вывод строки на экран ret

msg db "Принтер на LPT1 находится в состоянии offline или занят$" end start

Чтобы распечатать экран в текстовом режиме на LPT1, достаточно всего лишь одной команды INT 05h, что в точности эквивалентно нажатию клавиши PrtScr.



Поиск файлов


Найти нужный файл на диске намного сложнее, чем просто открыть его, — для этого требуются две функции при работе с короткими именами (найти первый файл и найти следующий файл) и три — при работе с длинными именами в DOS 7.0 (найти первый файл, найти следующий файл, прекратить поиск).

Функция DOS 4Eh — Найти первый файл

Ввод: АН = 4Eh
AL используется при обращении к функции APPEND
СХ = атрибуты, которые должен иметь файл (биты 0 (только для чтения) и 5 (архивный бит) игнорируются, если бит 3 (метка тома) установлен, все остальные биты игнорируются)
DS:DX = адрес ASCIZ-строки с именем файла, которое может включать путь и маски для поиска (символы * и ?)
Вывод: CF = 0 и область DTA заполняется данными, если файл найден
CF = 1 и АХ = 02h, если файл не найден, 03h — если путь не найден, 12h — если неправильный режим доступа

Вызов этой функции заполняет данными область памяти DTA (область передачи данных), которая начинается по умолчанию со смещения 0080h от начала блока данных PSP (при запуске СОМ- и ЕХЕ-программ сегменты DS и ES содержат сегментный адрес начала PSP), но ее можно переопределить с помощью функции 1Ah.

Функция DOS 1Ah — Установить область DTA

Ввод: АН = 1Ah
DS:DX = адрес начала DTA (128-байтный буфер)

Функции поиска файлов заполняют DTA следующим образом:

+00h: байт — биты 0 – 6: ASCII-код буквы диска; бит 7: диск сетевой

+01h: 11 байт — маска поиска (без пути)

+0СН: байт — атрибуты для поиска

+0Dh: слово — порядковый номер файла в каталоге

+0Fh: слово — номер кластера начала внешнего каталога

+11h: 4 байта — зарезервировано

+15h: байт — атрибут найденного файла

+16h: слово — время создания файла в формате DOS:

биты 15 – 11: час (0 — 23)

биты 10 – 5: минута

биты 4 – 0: номер секунды, деленный на 2 (0 – 30)


+18h: слово — дата создания файла в формате DOS:

биты 15 – 9: год, начиная с 1980

биты 8 – 5: месяц

биты 4 – 0: день

+1Ah: 4 байта — размер файла

+1Eh: 13 байт — ASCIZ-имя найденного файла с расширением

После того как DTA заполнена данными, для продолжения поиска следует вызывать функцию 4Fh, пока не будет возвращена ошибка.

Функция DOS 4Fh — Найти следующий файл

Ввод: АН = 4Fh
DTA — содержит данные от предыдущего вызова функции 4Е или 4F
Вывод: CF = 0 и DTA содержит данные о следующем найденном файле, если не произошла ошибка
CF = 1 и АХ = код ошибки, если произошла ошибка
Для случая длинных имен файлов (LFN) употребляется набор из трех подфункций функции DOS 71h, которые можно использовать, только если запущен IFSmgr (всегда запускается при обычной установке Windows 95, но не запускается, например, с загрузочной дискеты MS-DOS 7.0).

Функция LFN 4Eh — Найти первый файл с длинным именем

Ввод: АХ = 714Eh
CL = атрибуты, которые файл может иметь (биты 0 и 5 игнорируются)
СН = атрибуты, которые файл должен иметь
SI = 0: использовать Windows-формат даты/времени
SI = 1: использовать DOS-формат даты/времени
DS:DX = адрес ASCIZ-строки с маской для поиска (может включать * и ?. Для совместимости маска *.* ищет все файлы в каталоге, а не только файлы, содержащие точку в имени)
ES:DI = адрес 318-байтного буфера для информации о файле
Вывод: CF = 0
АХ = поисковый идентификатор
СХ = Unicode-флаг:

бит 0: длинное имя содержит подчеркивания вместо непреобразуемых Unicode-символов
бит 1: короткое имя содержит подчеркивания вместо непреобразуемых Unicode-символовCF = 1, АХ = код ошибки, если произошла ошибка (7100h — функция не поддерживается)
Если файл, подходящий под маску и атрибуты поиска, найден, область данных по адресу ES:DI заполняется следующим образом:

+00h: 4 байта — атрибуты файла



биты 0 – 6: атрибуты файла DOS

бит 8: временный файл

+04h: 8 байт — время создания файла

+0Ch: 8 байт — время последнего доступа к файлу

+14h: 8 байт — время последней модификации файла

+1Ch: 4 байта — старшее двойное слово длины файла

+20h: 4 байта — младшее двойное слово длины файла

+24h: 8 байт — зарезервировано

+2Ch: 260 байт — ASCIZ-имя файла длинное

+130h: 14 байт — ASCIZ-имя файла короткое

Причем даты создания/доступа/модификации записываются в одном из двух форматов, в соответствии со значением SI при вызове функции. Windows-формат — 64-битное число 100-наносекундных интервалов с 1 января 1601 года, а если используется DOS-формат — в старшее двойное слово записывается DOS-дата, а в младшее — DOS-время.

Функция LFN 4Fh — Найти следующий файл

Ввод: АХ = 714Fh
ВХ = поисковый идентификатор (от функции 4Eh)
SI = формат даты/времени
ES:DI = адрес буфера для информации о файле
Вывод: CF = 0 и СХ = Unicode-флаг, если следующий файл найден
CF = 1, АХ = код ошибки, если произошла ошибка (7100h — функция не поддерживается)
Функция LFN A1h — Закончить поиск файла

Ввод: АХ = 71A1h
ВХ = поисковый идентификатор
Вывод: CF = 0, если операция выполнена
CF = 1 и АХ = код ошибки, если произошла ошибка (7100h — функция не поддерживается)
В качестве примера программы, использующей многие из функций работы с файлами, рассмотрим программу, заменяющую русские буквы «Н» на латинские «Н» во всех файлах с расширением .ТХТ в текущем каталоге (такая замена требуется для всех текстов, которые будут пересылаться через сеть Fidonet, программное обеспечение которой воспринимает русскую букву «Н» как управляющий символ).

; fidoh.asm ; заменяет русские "Н" на латинские "Н" во всех файлах с расширением .ТХТ ; в текущем каталоге .model tiny .code org 100h ; СОМ-файл start: mov ah,4Eh ; поиск первого файла xor cx,cx ; не системный, не каталог и т.д. mov dx,offset filespec ; маска для поиска в DS:DX file_open: int 21h jc no_more_files ; если CF = 1 - файлы кончились



mov ax,3D02h ; открыть файл для чтения и записи mov dx,80h+1Eh ; смещение DTA + смещение имени файла int 21h ; от начала DTA jc find_next ; если файл не открылся - перейти ; к следующему mov bx,ax ; идентификатор файла в ВХ mov cx,1 ; считывать один байт mov dx,offset buffer ; начало буфера - в DX read_next: mov ah,3Fh ; чтение файла int 21h jc find_next ; если ошибка - перейти к следующему dec ах ; если АХ = 0 - файл кончился - js find_next ; перейти к следующему cmp byte ptr buffer,8Dh ; если не считана русская "Н", jne read_next ; считать следующий байт, mov byte ptr buffer,48h ; иначе - записать в буфер ; латинскую букву "Н" mov ax,4201h ; переместить указатель файла от текущей dec cx ; позиции назад на 1 dec cx ; CX = FFFFh mov dx,cx ; DX = FFFFh int 21h mov ah,40h ; записать в файл inc cx inc cx ; один байт (СХ = 1) mov dx,offset buffer ; из буфера в DS:DX int 21h jmp short read_next ; считать следующий байт

find_next: mov ah,3Eh ; закрыть предыдущий файл int 21h mov ah,4Fh ; найти следующий файл mov dx,80h ; смещение DTA от начала PSP jmp short file_open

no_more_files: ; если файлы кончились, ret ; выйти из программы

filespec db "*.txt",0 ; маска для поиска buffer label byte ; буфер для чтения/записи - end start ; за концом программы


Последовательный порт


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

DOS всегда инициализирует первый порт СОМ1 как 2400 бод, 8N1 (8 бит в слове, 1 стоп-бит, четность не проверяется) и связывает с ним устройство STDAUX. В это устройство можно записывать и считывать один байт функциями 3 и 4.

Функция DOS 03h — Считать байт из STDAUX

Ввод: АН = 03h
Вывод: AL = считанный байт

Функция DOS 04h — Записать байт в STDAUX

Ввод: АН = 04h
DL = байт

Или же можно воспользоваться функциями записи в файл (40h) и чтения из файла (3Fh), поместив в ВХ число 3, как это было показано ранее для вывода на экран.

Хотя установленную DOS скорость работы порта (2400 бод) и можно изменить командой MODE, все равно отсутствие обработки ошибок, буферизации и гибкого управления состоянием порта делает эти функции DOS практически неприменимыми. BIOS позволяет управлять любым из портов, писать и читать один байт и считывать состояние порта с помощью функций прерывания 14h, но BIOS (так же как и DOS) не позволяет инициализировать порт на скорость выше, чем 9600 бод. Таким образом выясняется, что большинство программ вынуждено программировать порты напрямую, но, если в системе присутствует драйвер, предоставляющий набор сервисов FOSSIL (такие как Х00 или BNU), оказывается возможным пользоваться для полноценного буферированного обмена данными с последовательными портами только функциями прерывания 14h.


INT 14h АН = 04 — Инициализация FOSSIL-драйвера

Ввод: АН = 04h
DX = номер порта (0 — для СОМ1, 1 — для COM2 и т.д.)
Вывод: АХ = 1954h
BL = максимальный поддерживаемый номер функции
ВН = версия спецификации FOSSIL
INT 14h АН = 05 — Деинициализация FOSSIL-драйвера

Ввод: АН = 05h
DX = номер порта (00h – 03h)
INT 14h АН = 00 — Инициализация последовательного порта

Ввод: АН = 00h

AL = параметры инициализации:

биты 7 – 5:

000 — 19 200 бод (110 бод без FOSSIL)

001 — 38 400 бод (150 бод без FOSSIL)

010 — 300 бод

011 — 600 бод

100 — 1200 бод

101 — 2400 бод

110 — 4800 бод

111 — 9600 бод

биты 4 – 3: четность (01 — четная, 11 — нечетная, 00 или 10 — нет)

бит 2: число стоп-бит (0 — один, 1 — два)

биты 1 – 0: длина слова (00 — 5, 01 — 6, 10 — 7, 11 — 8)

DX = номер порта (00h – 03h)
Вывод: АН = состояние порта

бит 7: тайм-аут

бит 6: буфер вывода пуст ( без FOSSIL: регистр сдвига передатчика пуст)

бит 5: в буфере вывода есть место (без FOSSIL: регистр хранения передатчика пуст)

бит 4: обнаружено состояние BREAK

бит 3: ошибка синхронизации

бит 2: ошибка четности

бит 1: ошибка переполнения — данные потеряны

бит 0: в буфере ввода есть данные

AL = состояние модема

бит 7: обнаружена несущая (состояние линии DCD)

бит 6: обнаружен звонок (состояние линии RI)

бит 5: запрос для передачи (состояние линии DSR)

бит 4: сброс для передачи (состояние линии CTS)

бит 3: линия DCD изменила состояние

бит 2: линия RI изменила состояние

бит 1: линия DSR изменила состояние

бит 0: линия CTS изменила состояние

<


/p> INT 14h АН =01 — Запись символа в последовательный порт

Ввод: АН = 01h
AL = символ
DX = номер порта (00h – 03h)
Вывод: АН = состояние порта
INT 14h АН = 02 — Чтение символа из последовательного порта с ожиданием

Ввод: АН = 02h
DX = номер порта
Вывод: АН = состояние порта
AL = считанный символ, если бит 7 АН равен нулю (не было тайм-аута)
INT 14h AH = 03 — Получить текущее состояние порта

Ввод: АН = 03h
DX = номер порта (00h – 03h)
Вывод: АН = состояние линии
AL = состояние модема
Воспользуемся этими функциями, чтобы написать короткую терминальную программу:

; term.asm ; Простая терминальная программа для модема на COM2. Выход по Alt-X ; .model tiny .code org 100h ; Начало СОМ-файла start: mov ah,0 ; инициализировать порт mov al,11100011b ; 9600/8n1 mov dx,1 ; порт COM2 int 14h

main_loop: mov ah,2 int 14h ; получить байт от модема, test ah,ah ; если что-нибудь получено, jnz no_input int 29h ; вывести его на экран no_input: ; иначе: mov ah,1 int 16h ; проверить, была ли нажата клавиша, jz main_loop ; если да: mov ah,8 int 21h ; считать ее код (без отображения на экране), test al,al ; если это нерасширенный ASCII-код, jnz send_char ; перейти к посылке его в модем, int 21h ; иначе - получить расширенный ASCII-код, cmp al,2Dh ; если это Alt-X, jne send_char ret ; завершить программу send_char: mov ah, 1 int 14h ; послать введенный символ в модем jmp short main_loop ; продолжить основной цикл

end start

Этот терминал тратит чрезмерно много процессорного времени на постоянные вызовы прерываний 14h и 16h. Более эффективным оказывается подход, состоящий в перехвате прерываний от внешних устройств, о котором рассказано далее.


Прямая работа с видеопамятью


Все, что изображено на мониторе — и графика, и текст, одновременно присутствует в памяти, встроенной в видеоадаптер. Для того чтобы изображение появилось на мониторе, оно должно быть записано в память видеоадаптера. Для этого отводится специальная область памяти, начинающаяся с абсолютного адреса B800h:0000h (для текстовых режимов) и заканчивающаяся на B800h:FFFFh. Все, что программы пишут в эту область памяти, немедленно пересылается в память видеоадаптера. В текстовых режимах для хранения каждого изображенного символа используются два байта: байт с ASCII-кодом символа и байт с его атрибутом, так что по адресу B800h:0000h лежит байт с кодом символа, находящимся в верхнем левом углу экрана; по адресу B800h:0001h лежит атрибут этого символа; по адресу B800h:0002h лежит код второго символа в верхней строке экрана и т.д.

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

; dirout.asm ; Выводит на экран все ASCII-символы без исключения, ; используя прямой вывод на экран .model tiny .code .386 ; будет использоваться регистр ЕАХ ; и команда STOSD org 100h ; начало СОМ-файла start: mov ax,0003h int 10h ; видеорежим 3 (очистка экрана) cld ; обработка строк в прямом направлении ; подготовка данных для вывода на экран mov еах,1F201F00h ; первый символ 00 с атрибутом 1Fh, ; затем пробел (20h) с атрибутом 1Fh mov bx,0F20h ; пробел с атрибутом 0Fh mov cx,255 ; число символов минус 1 mov di,offset ctable ; ES:DI - начало таблицы cloop: stosd ; записать символ и пробел в таблицу ctable inc al ; AL содержит следующий символ test cx,0Fh ; если СХ не кратен 16, jnz continue_loop ; продолжить цикл, push cx ; иначе: сохранить значение счетчика mov cx,80-32 ; число оставшихся до конца строки символов xchg ax,bx rep stosw ; заполнить остаток строки пробелами ; с атрибутом 0F xchg bx.ax ; восстановить значение ЕАХ pop cx ; восстановить значение счетчика continue_loop: loop cloop


stosd ; записать последний (256-й) символ и пробел

; собственно вывод на экран mov ax,0B800h ; сегментный адрес видеопамяти mov es,ax xor di,di ; DI = 0, адрес начала видеопамяти в ES:DI mov si,offset ctable ; адрес таблицы в DS:SI mov cx,15*80+32 ; 15 строк по 80 символов, последняя строка - 32 rep movsw ; скопировать таблицу ctable в видеопамять ret ; завершение СОМ-файла ctable: ; Данные для вывода на экран начинаются сразу ; за концом файла. В ЕХЕ-файле такие данные ; определяют в сегменте .data? end start

При подготовке данных для копирования в видеопамять в этой программе использовался тот факт, что в архитектуре Intel при записи слова (или двойного слова) в память старший байт располагается по старшему адресу. Так что при записи в память двойного слова 1F201F00h сначала записывается самый младший байт 00h (ASCII-код текущего символа), потом 1Fh, используемый в этом примере атрибут, потом 20h (код пробела) и потом, по самому старшему адресу, — самый старший байт, 1Fh, атрибут для этого пробела. Кроме того, в этом примере использовались некоторые 32-битные команды (MOV и STOSD). Этими командами можно пользоваться из 16-битной программы (разумеется, если процессор 80386 и выше), но не стоит этим злоупотреблять, так как каждая такая команда оказывается длиннее на 1 байт и выполняется дольше на 1 такт.


Программа типа ЕХЕ


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

; hello-2.asm ; Выводит на экран сообщение "Hello World!" и завершается .model small ; модель памяти, используемая для ЕХЕ .stack 100h ; сегмент стека размером в 256 байт .code start: mov ax,DGROUP ; сегментный адрес строки message mov ds,ax ; помещается в DS mov dx,offset message mov ah,9 int 21h ; функция DOS "вывод строки" mov ax,4C00h int 21h ; функция DOS "завершить программу" .data message db "Hello World!",0Dh,0Ah,'$' end start

В этом примере определяются три сегмента — сегмент стека директивой .STACK размером в 256 байт, сегмент кода, начинающийся с директивы .CODE, и сегмент данных, начинающийся с .DATA и содержащий строку. При запуске ЕХE-программы регистр DS уже не содержит адреса сегмента со строкой message (он указывает на сегмент, содержащий блок данных PSP), а для вызова используемой функции DOS этот регистр должен иметь сегментный адрес строки. Команда MOV AX,DGROUP загружает в АХ сегментный адрес группы сегментов данных DGROUP, a MOV DS,AX копирует его в DS. Для ассемблеров MASM и TASM можно использовать вместо DGROUP предопределенную метку «@data», но единственная модель памяти, в которой группа сегментов данных называется иначе, — FLAT (ей мы пока пользоваться не будем). И наконец, программы типа ЕХЕ должны завершаться системным вызовом DOS 4Ch: в регистр АН помещается значение 4Ch, в регистр AL помещается код возврата (в данном примере код возврата 0 и регистры АН и AL загружаются одной командой MOV AX,4C00h), после чего вызывается прерывание 21h.

Компиляция hello-2.asm:

Для TASM:

tasm hello-2.asm tlink /x hello-2.obj

Размер получаемого файла hello-2.exe — 559 байт.

Для MASM:

ml /с hello-2.asm link hello-2.obj

Размер получаемого файла hello-2.exe — 545 байт.

Для WASM:

wasm hello-2.asm wlink file hello-2.obj form DOS

Размер получаемого файла hello-2.exe — 81 байт.

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



Программа типа СОМ


Традиционно первая программа для освоения нового языка программирования — программа, выводящая на экран текст «Hello world!». He будет исключением и эта книга, так как такая программа всегда была удобной отправной точкой для дальнейшего освоения языка.

Итак, наберите в любом текстовом редакторе, который может записывать файлы как обычный текст (например: EDIT.COM в DOS, встроенный редактор в Norton Commander или аналогичной программе, NOTEPAD в Windows), следующий текст:

; hello-l.asm ; Выводит на экран сообщение "Hello World!" и завершается .model tiny ; модель памяти, используемая для СОМ .code ; начало сегмента кода org 100h ; начальное значение счетчика - 100h start: mov ah,9 ; номер функции DOS - в АН mov dx,offset message ; адрес строки - в DX int 21h ; вызов системной функции DOS ret ; завершение СОМ-программы message db "Hello World!",0Dh,0Ah,'$' ; строка для вывода end start ; конец программы

и сохраните его как файл hello-l.asm. Можно также использовать готовый файл с этим именем. (Все программы, использующиеся в этой книге в качестве примеров, вы можете найти в Internet: .) Чтобы превратить программу в исполнимый файл, сначала надо вызвать ассемблер, для того чтобы скомпилировать ее в объектный файл с именем hello-1.obj, набрав в командной строке следующую команду:

Для TASM:

tasm hello-1.asm

Для MASM:

ml /c hello-1.asm

Для WASM:

wasm hello-1.asm

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

Формат объектных файлов, используемых всеми тремя рассматриваемыми ассемблерами по умолчанию (OMF-формат), совпадает, так что можно пользоваться ассемблером из одного пакета и компоновщиком из другого.

Для TLINK:

tasm /t /x hello-1.obj


Для MASM (команда link должна вызывать 16-битную версию LINK.EXE):

link hello-1.obj,,NUL,,, exe2bin hello-1.exe hello-1.com

Для WASM:

wlink file hello-1.obj form DOS COM

Теперь получился файл HELLO-1. COM размером 23 байта. Если его выполнить, на экране появится строка «Hello World!» и программа завершится.

Рассмотрим исходный текст программы, чтобы понять, как она работает.

Первая строка определяет модель памяти TINY, в которой сегменты кода, данных и стека объединены. Эта модель предназначена для создания файлов типа СОМ.

Директива .CODE начинает сегмент кода, который в нащем случае также должен содержать и данные.

ORG 100h устанавливает значение программного счетчика в 100h, так как при загрузке СОМ-файла в память DOS занимает первые 256 байт (100h) блоком данных PSP и располагает код программы только после этого блока. Все программы, которые компилируются в файлы типа СОМ, должны начинаться с этой директивы.

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

Команда MOV АН,9 помещает число 9 в регистр АН. Это — номер функции DOS «вывод строки».

Команда MOV DX,OFFSET MESSAGE помещает в регистр DX смешение метки MESSAGE относительно начала сегмента данных, который в нашем случае совпадает с сегментом кода.

Команда INT 21h вызывает системную функцию DOS. Эта команда — основное средство взаимодействия программ с операционной системой. В нашем примере вызывается функция DOS номер 9 — вывести строку на экран. Эта функция выводит строку от начала, адрес которого задается в регистрах DS:DX, до первого встреченного символа $. При загрузке СОМ-файла регистр DS автоматически загружается сегментным адресом программы, а регистр DX был загружен предыдущей командой.

Команда RET используется обычно для возвращения из процедуры. DOS вызывает СОМ-программы так, что команда RET корректно завершает программу.

DOS при вызове СОМ-файла помещает в стек сегментный адрес программы и ноль, так что RET передает управление на нулевой адрес текущего сегмента, то есть на первый байт PSP. Там находится код команды INT 20h, которая и используется для возвращения управления в DOS. Можно сразу заканчивать программу командой INT 20h, хотя это длиннее на 1 байт.
Следующая строка программы HELLO-1.ASM определяет строку данных, содержащую текст «Hello World!», управляющий символ ASCII «возврат каретки» с кодом 0Dh, управляющий символ ASCII «перевод строки» с кодом 0Ah и символ «$», завершающий строку. Эти два управляющих символа переводят курсор на первую позицию следующей строки точно так же, как в строках на языке С действует последовательность «\n».

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


Работа с файлами


Возможно, основная функция DOS как операционной системы — организация доступа к дискам как к набору файлов и каталогов. DOS поддерживает только один тип файловой системы — FAT и, начиная с версии 7.0 (Windows 95), его модификацию VFAT с длинными именами файлов. Первоначальный набор функций для работы с файлами, предложенный в MS-DOS 1.0, оказался очень неудобным: каждый открытый файл описывался 37-байтной структурой FCB (блок управления файлом), адрес которой требовался для всех файловых операций, а передача данных осуществлялась через структуру данных DTA (область передачи данных). Уже в MS-DOS 2.0, вместе с усовершенствованием FAT (например, появлением вложенных каталогов), появился набор UNIX-подобных функций работы с файлами, использующих для описания файла всего одно 16-битное число, идентификатор файла или устройства. Все остальные функции работы с файлами используют затем только это число. Первые пять идентификаторов инициализируются системой следующим образом:

0: STDIN — стандартное устройство ввода (обычно клавиатура),

1: STDOUT — стандартное устройство вывода (обычно экран),

2: STDERR — устройство вывода сообщений об ошибках (всегда экран),

3: AUX — последовательный порт (обычно СОМ1),

4: PRN — параллельный порт (обычно LPT1),

так что функции чтения/записи (а также сброс буферов на диск) файлов можно точно так же применять и к устройствам.



Работа с мышью


Все общение с мышью в DOS выполняется через прерывание 33h, обработчик которого устанавливает драйвер мыши, загружаемый обычно при запуске системы. Современные драйверы поддерживают около 60 функций, позволяющих настраивать разрешение мыши, профили ускорений, виртуальные координаты, настраивать дополнительные обработчики событий и т.п. Большинство этих функций требуются редко, сейчас рассмотрим основные:

INT 33h, AX = 0 — Инициализация мыши

Ввод: AX = 0000h
Вывод: АХ = 0000h, если мышь или драйвер мыши не установлены
АХ = FFFFh, если драйвер и мышь установлены
ВХ = число кнопок:0002 или FFFF — две
0003 — три
0000 — другое количество

Выполняется аппаратный и программный сброс мыши и драйвера.

INT 33h, AX = 1 — Показать курсор

Ввод: AX = 0001h

INT 33h, AX = 2 — Спрятать курсор

Ввод: AX = 0002h

Драйвер мыши поддерживает внутренний счетчик, управляющий видимостью курсора мыши. Функция 2 уменьшает значение счетчика на единицу, а функция 1 увеличивает его, но только до значения 0. Если значение счетчика — отрицательное число, он спрятан, если ноль — показан. Это позволяет процедурам, использующим прямой вывод в видеопамять, вызывать функцию 2 в самом начале и 1 — в самом конце, не заботясь о том, в каком состоянии был курсор мыши у вызвавшей эту процедуру программы.

INT 33h, AX = 3 — Определить состояние мыши

Ввод: AX = 0003h
Вывод: ВХ = состояние кнопок:

бит 0 — нажата левая кнопка

бит 1 — нажата правая кнопка

бит 2 — нажата средняя кнопкаСХ = Х-координата
DX = Y-координата

Возвращаемые координаты совпадают с пиксельными координатами соответствующей точки на экране в большинстве графических режимов, кроме 04, 05, 0Dh, 13h, в которых Х-координату мыши нужно разделить на 2, чтобы получить номер столбца соответствующей точки на экране. В текстовых режимах обе координаты надо разделить на 8, чтобы получить номер строки и столбца соответственно.


В большинстве случаев эта функция не используется в программах, так как для того, чтобы реагировать на нажатие кнопки или перемещение мыши в заданную область, требуется вызывать это прерывание постоянно, что приводит к трате процессорного времени. Функции 5 (определить положение курсора при последнем нажатии кнопки), 6 (определить положение курсора при последнем отпускании кнопки) и 0Bh (определить расстояние, пройденное мышью) могут помочь оптимизировать работу программы, самостоятельно следящей за всеми передвижениями мыши, но гораздо эффективнее указать драйверу самому следить за ее передвижениями (чем он, собственно, и занимается постоянно) и передавать управление в программу, как только выполнится заранее определенное условие, например пользователь нажмет на левую кнопку мыши. Такой сервис обеспечивает функция 0Ch — установить обработчик событий.

INT 33h, AX = 0Ch — Установить обработчик событий

Ввод: AX = 000Ch
ES:DX = адрес обработчика
СХ = условие вызова

бит 0 — любое перемещение мыши

бит 1 — нажатие левой кнопки

бит 2 — отпускание левой кнопки

бит 3 — нажатие правой кнопки

бит 4 — отпускание правой кнопки

бит 5 — нажатие средней кнопки

бит 6 — отпускание средней кнопки

СХ = 0000h — отменить обработчик
Обработчик событий должен быть оформлен, как дальняя процедура (то есть завершаться командой RETF). На входе в процедуру обработчика АХ содержит условие вызова, ВХ — состояние кнопок, СХ, DX — X- и Y-координаты курсора, SI, DI — счетчики последнего перемещения по горизонтали и вертикали (единицы измерения для этих счетчиков — мики, 1/200 дюйма), DS — сегмент данных драйвера мыши. Перед завершением программы установленный обработчик событий должен быть обязательно удален (вызов функции 0Ch с СХ = 0), так как иначе при первом же выполнении условия управление будет передано по адресу в памяти, с которого начинался обработчик.



Функция 0Ch используется так часто, что у нее появилось несколько модификаций — функция 14h, позволяющая установить одновременно три обработчика с разными условиями, и функция 18h, также позволяющая установить три обработчика и включающая в условие вызова состояние клавиш Shift, Ctrl и Alt. Воспользуемся обычной функцией 0Ch, чтобы написать простую программу для рисования.

; mousedr.asm ; Рисует на экране прямые линии с концами в позициях, указываемых мышью ; .model tiny .code org 100h ; СОМ-файл .186 ; для команды shr cx,3 start: mov ax,12h int 10h ; видеорежим 640x480 mov ax,0 ; инициализировать мышь int 33h mov ax,1 ; показать курсор мыши int 33h mov ax,000Ch ; установить обработчик событий мыши mov cx,0002h ; событие - нажатие левой кнопки mov dx,offset handler ; ES:DX - адрес обработчика int 33h mov ah,0 ; ожидание нажатия любой клавиши int 16h mov ax,000Ch mov cx,0000h ; удалить обработчик событий мыши int 33h mov ax,3 ; текстовый режим int 10h ret ; конец программы

; Обработчик событий мыши: при первом нажатии выводит точку на экран, ; при каждом дальнейшем вызове проводит прямую линию от предыдущей ; точки к текущей

handler: push 0A000h pop es ; ES - начало видеопамяти push cs pop ds ; DS - сегмент кода и данных этой программы push сх ; СХ (Х-координата) и push dx ; DX (Y-координата) потребуются в конце

mov ax, 2 ; спрятать курсор мыши перед выводом на экран int 33h cmp word ptr previous_X,-1 ; если это первый вызов, je first_point ; только вывести точку,

call line_bresenham ; иначе - провести прямую exit_handler: pop dx ; восстановить СХ и DX pop сх mov previous_X,cx ; и запомнить их как предыдущие mov previous_Y,dx ; координаты

mov ax,1 ; показать курсор мыши int 33h retf ; выход из обработчика - команда RETF

first_point: call putpixel1b ; вывод одной точки (при первом вызове) jmp short exit_handler

; Процедура рисования прямой линии с использованием алгоритма Брезенхама ; Ввод: СХ, DX - X, Y конечной точки ; previous_X,previous_Y - X, Y начальной точки



line_bresenham: mov ax, сх sub ax,previous_X ; AX = длина проекции прямой на ось X jns dx_pos ; если АХ отрицательный - neg ax ; сменить его знак, причем mov word ptr X_increment,1 ; координата X при выводе jmp short dx_neg ; прямой будет расти, dx_pos: mov word ptr X_increment,-1 ; а иначе - уменьшаться

dx_neg: mov bx,dx sub bx,previous_Y ; BX = длина проекции прямой на ось Y jns dy_pos ; если ВХ отрицательный - neg bx ; сменить его знак, причем mov word ptr Y_increment,1 ; координата Y при выводе jmp short dy_neg ; прямой будет расти, dy_pos: mov word ptr Y_increment,-1 ; а иначе - уменьшаться

dy_neg: shl ax,1 ; удвоить значения проекций, shl bx,1 ; чтобы избежать работы с полуцелыми числами

call putpixel1b ; вывести первую точку (прямая рисуется от ; CX,DX к previous_X,previous_Y) cmp ax,bx ; если проекция на ось X больше, чем на Y: jna dx_le_dy mov di,ax ; DI будет указывать, в какую сторону мы shr di,1 ; отклонились от идеальной прямой neg di ; оптимальное начальное значение DI: add di,bx ; DI = 2 * dy - dx cycle: cmp ex,word ptr previous_X ; основной цикл выполняется, je exit_bres ; пока Х не станет равно previous_X cmp di,0 ; если DI > 0, jl fractlt0 add dx,word ptr Y_increment ; перейти к следующему Y sub di,ax ; и уменьшить DI на 2 * dx fractlt0: add cx,word ptr X_increment ; следующий Х (на каждом шаге) add di,bx ; увеличить DI на 2 * dy call putpixel1b ; вывести точку jmp short cycle ; продолжить цикл dx_le_dy: ; если проекция на ось Y больше, чем на X mov di,bx shr di,1 neg di ; оптимальное начальное значение DI: add di,ax ; DI = 2 * dx - dy cycle2: cmp dx,word ptr previous_Y ; основной цикл выполняется, je exit_bres ; пока Y не станет равным previous_Y, cmp di,0 ; если DI > 0, jl fractlt02 add cx,word ptr X_increment ; перейти к следующему X sub di,bx ; и уменьшить DI на 2 * dy fractlt02: add dx,word ptr Y_increment ; следующий Y (на каждом шаге) add di,ax ; увеличить DI на 2 * dy call putpixel1b ; вывести точку jmp short cycle2 ; продолжить цикл exit_bres: ret ; конец процедуры



; Процедура вывода точки на экран в режиме, использующем один бит для ; хранения одного пикселя. ; DХ = строка, СХ = столбец ; Все регистры сохраняются

putpixel1b: pusha ; сохранить регистры xor bx,bx mov ax,dx ; AX = номер строки imul ах,ах,80 ; АХ = номер строки * число байт в строке push cx shr сх,3 ; СХ = номер байта в строке add ах,сх ; АХ = номер байта в видеопамяти mov di,ax ; поместить его в SI и DI для команд mov si,di ; строковой обработки

pop cx ; СХ снова содержит номер столбца mov bx,0080h and cx,07h ; последние три бита СХ = ; остаток от деления на 8 = номер бита в байте, считая справа налево shr bx,cl ; теперь в BL установлен в 1 нужный бит lods es:byte ptr some_label ; AL = байт из видеопамяти or ax,bx ; установить выводимый бит в 1, ; чтобы стереть пиксель с экрана, эту команду OR можно заменить на ; not bx ; and ax,bx ; или лучше инициализировать ВХ не числом 0080h, а числом FF7Fh и использовать ; только and stosb ; и вернуть байт на место рора ; восстановить регистры ret ; конец

previous_X dw -1 ; предыдущая Х-координата previous_Y dw -1 ; предыдущая Y-координата Y_increment dw -1 ; направление изменения Y X_increment dw -1 ; направление изменения X some_label: ; метка, используемая для переопределения ; сегмента-источника для lods с DS на ES end start

Алгоритм Брезенхама, использованный в этой программе, является самым распространенным алгоритмом рисования прямой. Существуют, конечно, и более эффективные алгоритмы, например алгоритм Цаолинь By, работающий на принципе конечного автомата, но алгоритм Брезенхама стал стандартом де-факто.

Приведенную реализацию этого алгоритма можно значительно ускорить, использовав самомодифицирующийся код, то есть после проверки на направление прямой в начале алгоритма вписать прямо в дальнейший текст программы команды INС СХ, DEC CX, INС DX и DEC DX вместо команд сложения этих регистров с переменными X_increment и Y_increment. Самомодифицирующийся код часто применяется при программировании для DOS, но во многих многозадачных системах текст программы загружается в область памяти, защищенную от записи, так что в последнее время область применения этого приема становится ограниченной.

Работа с SVGA-режимами


В режиме VGA 320x200 с 256 цветами для отображения видеопамяти на основное адресное пространство используется 64 000 байт, располагающихся с адреса A000h:0000h. Дальнейшее увеличение разрешения или числа цветов приводит к тому, что объем видеопамяти превышает максимальные границы сегмента в реальном режиме (65 535 байт), а затем и размер участка адресного пространства, отводимого для видеопамяти (160 Кб, от A000h:0000h до B800h:FFFFh. С адреса C800h:0000h начинается область ROM BIOS). Чтобы вывести изображение, используются два механизма — переключение банков видеопамяти для реального режима и LFB (линейный кадровый буфер) для защищенного.

Во втором случае вся видеопамять отображается на непрерывный кусок адресного пространства, но начинающегося не с адреса 0A0000h, а с какого-нибудь другого адреса, так чтобы весь образ видеопамяти, который может занимать несколько мегабайтов, отобразился в один непрерывный массив. В защищенном режиме максимальный размер сегмента составляет 4 гигабайта, поэтому никаких сложностей с адресацией этого буфера не возникает. Буфер LFB можно использовать, только если видеоадаптер поддерживает спецификацию VBE 2.0 (см. пример в главе 6.4).

В реальном режиме вывод на экран осуществляется по-прежнему копированием данных в 64-килобайтный сегмент, обычно начинающийся с адреса A000h:0000h, но эта область памяти соответствует только части экрана. Чтобы вывести изображение в другую часть экрана, требуется вызвать функцию перемещения окна (или, что то же самое, переключения банка видеопамяти), изменяющую область видеопамяти, которой соответствует сегмент A000h. Например, в режиме 640x480 с 256 цветами требуется 307 200 байт для хранения всего видеоизображения. Заполнение сегмента A000h:0000h – A000h:FFFFh приводит к закраске приблизительно 1/5 экрана, перемещение окна А на позицию 1 (или переключение на банк 1) и повторное заполнение этой же области приводит к закраске следующей 1/5 экрана, и т.д. Перемещение окна осуществляется подфункцией 05 видеофункции 4Fh или передачей управления прямо на процедуру, адрес которой можно получить, вызвав подфункцию 01, как будет показано ниже. Некоторые видеорежимы позволяют использовать сразу два таких 64-килобайтных окна, окно А и окно В, так что можно записать 128 Кб данных, не вызывая прерывания.


Стандартные графические режимы SVGA могут быть 4-, 8-, 15-, 16-, 24- и 32-битными.

4-битные режимы (16 цветов):

VGA

012h: 640x480 (64 Кб)

VESA VBE 1.0

102h: 800x600 (256 Кб)

104h: 1024x768 (192 Кб)

106h: 1280x1024 (768 Кб)

Каждый пиксель описывается одним битом, для вывода цветного изображения требуется программирование видеоадаптера на уровне портов ввода-вывода (глава 5.10.4).

8-битные режимы (256 цветов):

VGA

013h: 320x200 (64 Кб)

VBE 1.0

100h: 640x400 (256 Кб)

101h: 640x480 (320 Кб)

103h: 800x600 (512 Кб)

105h: 1024x768 (768 Кб)

107h: 1280x1024 (1,3 Мб)

VBE 2.0

120h: 1600x1200 (1,9 Мб)

Каждый пиксель описывается ровно одним байтом. Значение байта — нoмер цвета из палитры, значения цветов которой можно изменять, например вызывая подфункцию 09 видеофункции 4Fh.

15-битные режимы (32 К цветов):

VBE 1.2

10Dh: 320x200 (128 Кб)

110h: 640x480 (768 Кб)

113h: 800x600 (1 Мб)

116h: 1024x768 (1,5 Мб)

119h: 1280x1024 (2,5 Мб)

VBE 2.0

121h: 1600x1200 (3,8 Мб)

Каждый пиксель описывается ровно одним словом (16 бит), в котором биты 0 – 4 содержат значение синей компоненты цвета, биты 5 – 9 — зеленой, а биты 10 – 14 — красной. Бит 15 не используется.

16-битные режимы (64 К цветов):

VBE 1.2

10Eh: 320x200 (128 Кб)

111h: 640x480 (768 Кб)

114h: 800x600 (1 Мб)

117h: 1024x768 (1,5 Мб)

11Ah: 1280x1024 (2,5 Мб)

VBE 2.0

121h: 1600x1200 (3,8 Мб)

Так же как и в 15-битных режимах, каждый пиксель описывается ровно одним словом. Обычно биты 0 – 4 (5 бит) содержат значение синей компоненты, биты 5 – 10 (6 бит) — зеленой, а биты 11 – 15 (5 бит) — красной. В нестандартных режимах число бит, отводимое для каждого цвета, может отличаться, так что при их использовании следует вызвать подфункцию 01 видеофункции 4Fh и получить информацию о видеорежиме, включающую битовые маски и битовые смещения для цветов.



24-битные и 32-битные режимы (16 М цветов):

VBE 1.2

10Fh: 320x200 (192 Кб)

112h: 640x480 (1 Мб)

115h: 800x600 (1,4 Мб)

118h: 1024x768 (2,3 Мб)

11Bh: 1280x1024 (3,7 Мб)

VBE 2.0

122h: 1600x1200 (7,7 Мб)

В режимах с 24-битным и 32- битным цветом каждому пикселю на экране соответствуют три байта и одно двойное слово (4 байта). Если видеорежим использует модель памяти 6 (Direct Color), то младший байт (байт 0) содержит значение синего цвета, байт 1 содержит значение зеленого, байт 2 — значение красного, а байт 3 — в 32-битных режимах резервный и используется либо для выравнивания, либо содержит значение для альфа-канала. Некоторые видеорежимы могут использовать не Direct Color, a YUV (модель памяти 7) — здесь младший байт соответствует насыщенности красного, байт 1 — насыщенности синего, а байт 2 — яркости.

Видеоадаптер может поддерживать и собственные нестандартные видеорежимы. Список их номеров можно получить, вызвав подфункцию 00h, а получить информацию о режиме по его номеру — вызвав подфункцию 01h видеофуикции 4Fh. Более того, для стандартных режимов также следует вызывать подфункцию 01h, чтобы проверить реальную доступность режима (например, режим может быть в списке, но не поддерживаться из-за нехватки памяти). VBE 2.0 разрешает видеоадаптерам не поддерживать никаких стандартных режимов вообще.

INT 10h АН = 4Fh, AL = 00 — Получить общую SVGA-информацию

Ввод: AX = 4F00h
ES:DI = адрес буфера (512 байт)
Вывод: AL = 4Fh, если функция поддерживается
АН = 01, если произошла ошибка
АН = 00, если данные получены и записаны в буфер
Буфер для общей SVGA-информации:

+00h: 4 байта — будет содержать «VESA» после вызова прерывания, чтобы получить поля, начиная с 14h, здесь надо предварительно записать строку «VBE2»
+04h: слово — номер версии VBE в двоично-десятичном формате (0102h — для 1.2, 0200h — для 2.0)
+06h: 4 байта — адрес строки-идентификатора производителя
+0Ah: 4 байта — флаги:

бит 0 — АЦП поддерживает 8-битные цветовые компоненты (см. подфункцию 08h)

бит 1 — видеоадаптер несовместим с VGA

бит 2 — АЦП можно программировать только при обратном ходе луча

бит 3 — поддерживается спецификация аппаратного ускорения графики VBE/AF 1.0

бит 4 — требуется вызов EnableDirectAccess перед использованием LFB

бит 5 — поддерживается аппаратный указатель мыши

бит 6 — поддерживается аппаратное отсечение

бит 7 — поддерживается аппаратное копирование блоков

биты 8 – 31 зарезервированы
+0Eh: 4 байта — адрес списка номеров поддерживаемых видеорежимов (массив слов, последнее слово = FFFFh, после которого обычно следует список нестандартных режимов, также заканчивающийся словом FFFFh)
+12h: слово — объем видеопамяти в 64-килобайтных блоках
+14h: слово — внутренняя версия данной реализации VBE
+16h: 4 байта — адрес строки с названием производителя
+1Ah: 4 байта — адрес строки с названием видеоадаптера
+1Eh: 4 байта — адрес строки с версией видеоадаптера
+22h: слово — версия VBE/AF (BCD, то есть 0100h для 1.0)
+24h: 4 байта — адрес списка номеров режимов, поддерживающих аппаратное ускорение (если бит поддержки VBE/AF установлен в 1)
+28h: 216 байт — зарезервировано VESA
+100h: 256 байт — зарезервировано для внутренних данных VBE. Так, например, в эту область копируются строки с названиями производителя, видеоадаптера, версии и т.д.
<


/p> INT 10h АН = 4Fh, AL = 01 — Получить информацию о режиме

Ввод: AX = 4F01h
СХ = номер SVGA-режима ( бит 14 соответствует использованию LFB, бит 13 — аппаратному ускорению)
ES:DI = адрес буфера для информации о режиме (256 байт)
Вывод: AL = 4Fh, если функция поддерживается
АН = 01, если произошла ошибка
АН = 00, если данные получены и записаны в буфер
Буфер для информации о SVGA-режиме:

+00h:

слово — атрибуты режима:

бит 0 — режим присутствует

бит 1 — дополнительная информация (смещения 12h – 1Eh) присутствует (для VBE 2.0 эта информация обязательна и бит всегда установлен)

бит 2 — поддерживается вывод текста на экран средствами BIOS

бит 3 — режим цветной

бит 4 — режим графический

бит 5 — режим несовместим с VGA

бит 6 — переключение банков не поддерживается

бит 7 — LFB не поддерживается

бит 8 — не определен

бит 9 — (для VBE/AF) приложения должны вызвать EnableDirectAccess, прежде чем переключать банки
+02h: байт — атрибуты окна А:

бит 1 — окно существует

бит 2 — чтение из окна разрешено

бит 3 — запись в окно разрешена
+03h: байт — атрибуты окна В
+04h: слово — гранулярность окна — число килобайтов, которому всегда кратен адрес начала окна в видеопамяти (обычно 64)
+06h: слово — размер окна в килобайтах (обычно 64)
+08h: слово — сегментный адрес окна А (обычно A000h)
+0Ah: слово — сегментный адрес окна В
+0Ch: 4 байта — адрес процедуры перемещения окна (аналог подфункции 05h, но выполняется быстрее)
+10h: слово — число целых байт в логической строке
+12h: слово — ширина в пикселях (для графики) или символах (для текста)
+14h: слово — высота в пикселях (для графики) или символах (для текста)
+16h: байт — высота символов в пикселях
+17h: байт — ширина символов в пикселях
+18h: байт — число плоскостей памяти (4 — для 16-цветных режимов, 1 — для обычных, число переключений банков, требуемое для доступа ко всем битам (4 или 8), — для модели памяти 5)
+19h: байт — число бит на пиксель
+1Ah: байт — число банков для режимов, в которых строки группируются в банки (2 — для CGA, 4 — для HGC)
+1Bh: байт — модель памяти:

00h — текст

01h — CGA-графика

02h — HGC-графика

03h — EGA-графика (16 цветов)

04h — VGA-графика (256 цветов в одной плоскости)

05h — Режим X (256 цветов в разных плоскостях)

06h — RGB (15-битные и выше)

07h — YUV

08h – 0Fh — зарезервированы VESA

10h – FFh — нестандартные модели
+1Ch: байт — размер банка в килобайтах (8 — для CGA и HGC, 0 — для остальных)
+1Dh: байт — число видеостраниц
+1Eh: байт — зарезервирован
+1Fh: байт — битовая маска красной компоненты
+20h: байт — первый бит красной компоненты
+21h: байт — битовая маска зеленой компоненты
+22h: байт — первый бит зеленой компоненты
+23h: байт — битовая маска синей компоненты
+24h: байт — первый бит синей компоненты
+25h: байт — битовая маска зарезервированной компоненты
+26h: байт — первый бит зарезервированной компоненты
+27h: байт — дополнительные флаги:

бит 0: —поддерживается перепрограммирование цветов (подфункция 09h)

бит 1 — приложение может использовать биты в зарезервированной компоненте
+28h: 4 байта — физический адрес начала LFB
+2Ch: 4 байта — смещение от начала LFB, указывающее на первый байт после конца участка памяти, отображающейся на экране
+30h: слово — размер памяти в LFB, не отображающейся на экране, в килобайтах
+32h: 206 байт — зарезервировано
<


/p> INT 10h АН = 4FH, AL = 02 — Установить режим

Ввод: AX=4F02h
ВХ = номер режима:

биты 0 – 6 — собственно номер режима

бит 7 — видеопамять не очищается при установке режима, если все следующие биты — нули

бит 8 — стандартный VBE SVGA-режим

бит 9 — нестандартный SVGA-режим

биты 10 – 12 — зарезервированы

бит 13 — режим использует аппаратное ускорение

бит 14 — режим использует LFB

бит 15 — видеопамять не очищается при установке режима

Кроме того, специальный номер режима 81FFh соответствует доступу ко всей видеопамяти и может использоваться для сохранения ее содержимого.
Вывод: AL = 4Fh, если функция поддерживается
АН = 00, если режим установлен
АН = 01 или 02, если произошла ошибка
INT 10h АН = 4Fh, AL = 03 — Узнать номер текущего видеорежима

Ввод: АХ = 4F03h
Вывод: AL = 4Fh, если функция поддерживается
ВХ = номер режима
INT 10h АН = 4Fh AL = 05 — Перемещение окна (переключение банка видеопамяти)

Ввод: АХ = 4F03h
ВН = 00 — установить окно
ВН = 01 — считать окно
BL = 00 — окно А
BL = 01 — окно В
DX = адрес окна в видеопамяти в единицах гранулярности (номер банка), если ВН = 0
Вывод: AL = 4Fh, если функция поддерживается
DX = адрес окна в единицах гранулярности (номер банка), если ВН = 1
АН = 03, если функция была вызвана в режиме, использующем LFB
Всегда предпочтительнее переключать банки прямым дальним вызовом процедуры, адрес которой возвращается подфункцией 01h в блоке информации о видеорежиме. Все параметры передаются в процедуру точно так же, как и в подфункцию 05h, но содержимое регистров АХ и DX по возвращении не определено.

INT 10h АН = 4Fh AL = 07 — Установка начала изображения

Ввод: АХ = 4F07h
ВН = 00
BL = 00 — считать начало изображения
BL = 80h — установить начало изображения (в VBE 2.0 автоматически выполняется при следующем обратном ходе луча)
СХ = первый изображаемый пиксель в строке (для BL = 80h)
DX = первая изображаемая строка (для BL = 80h)
Вывод: AL = 4Fh, если функция поддерживается
АН = 01, если произошла ошибка
АН = 00, если функция выполнилась успешно
ВН = 00 (для BL = 00)
СХ = первый изображаемый пиксель в строке (для BL = 00)
DX = первая изображаемая строка (для BL = 00)
<


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

: scrolls.asm ; Изображает в разрешении 1024x768x64К окрашенный конус, который можно ; плавно перемещать по экрану стрелками вверх и вниз. ; .model tiny .code .386 ; используется команда shrd org 100h ; СОМ-файл start: mov ax,4F01h ; получить информацию о видеорежиме mov cx,116h ; 1024x768x64К mov di,offset vbe_mode_buffer int 10h ; здесь для простоты опущена проверка наличия режима mov ax,4F02h ; установить режим mov bx,116h int 10h push word ptr [vbe_mode_buffer+8] pop es ; поместить в ES адрес начала видеопамяти ; (обычно A000h) cld

; вывод конуса на экран

mov cx,-1 ; начальное значение цвета (белый) mov si,100 ; начальный радиус mov bx,300 ; номер столбца mov ax,200 ; номер строки main_loop: inc si ; увеличить радиус круга на 1 inc ax ; увеличить номер строки inc bx ; увеличить номер столбца call fast_circle ; нарисовать круг sub cx,0000100000100001b ; изменить цвет cmp si,350 ; если еще не нарисовано 250 кругов, jb main_loop ; продолжить, xor сх,сх ; иначе: выбрать черный цвет, call fast_circle ; нарисовать последний круг

; плавное перемещение изображения по экрану с помощью функции 4F07

xor bx,bx ; ВХ = 0 - установить начало экрана xor dx,dx ; номер строки = 0 ; номер столбца в СХ уже ноль main_loop_2: mov ax,4F07h int 10h ; переместить начало экрана mov ah,7 ; считать нажатую клавишу с ожиданием, без эха int 21h ; и без проверки на Ctrl-Break, test al,al ; если это обычная клавиша - jnz exit_loop_2 ; завершить программу, int 21h ; иначе: получить расширенный ASCII-код, cmp al,50h ; если это стрелка вниз je key_down cmp al,48h ; или вверх - вызвать обработчик, je key_up exit_loop_2: ; иначе - завершить программу mov ах,З ; текстовый режим int 10h ret ; завершить СОМ-программу

key_down: ; обработчик нажатия стрелки вниз dec dx ; уменьшить номер строки начала экрана, jns main_loop_2 ; если знак не изменился - продолжить цикл, ; иначе (если номер был 0, а стал -1) - ; увеличить номер строки key_up: ; обработчик нажатия стрелки вверх inc dx ; увеличить номер строки начала экрана jmp short main_loop_2



; Процедура вывода точки на экран в 16-битном видеорежиме ; Ввод: DX = номер строки, ВХ = номер столбца, ES = А000, СХ = цвет ; модифицирует АХ

putpixel16b: push dx push di хоr di,di shrd di,dx,6 ; DI = номер строки * 1024 mod 65 536 shr dx,5 ; DX = номер строки / 1024 * 2 inc dx cmp dx,current_bank ; если номер банка для выводимой точки jne bank_switch ; отличается от текущего - переключить банки switched: add di,bx ; добавить к DI номер столбца mov ax,cx ; цвет в АХ shl di,1 ; DI = DI * 2, так как адресация идет в словах stosw ; вывести точку на экран pop di ; восстановить регистры pop dx ret bank_switch: ; переключение банка push bx xor bx,bx ; BX = 0 -> Установить начало экрана mov current_bank,dx ; сохранить новый номер текущего банка call dword ptr [vbe_mode_buffer+0Ch] ; переключить ; банк pop bx jmp short switched

; Алгоритм рисования круга, используя только сложение, вычитание и сдвиги ; (упрощенный алгоритм промежуточной точки). ; Ввод: SI = радиус, СХ = цвет, АХ = номер столбца центра круга, ; ВХ = номер строки центра круга модифицирует DI, DX

fast_circle: push si push ax push bx xor di,di ; DI - относительная Х-координата текущей точки dec di ; (SI - относительная Y-координата, начальное mov ax,1 ; значение - радиус) sub ax,si ; AX - наклон (начальное значение 1-Радиус) circle_loop: inc di ; следующий X (начальное значение - 0) cmp di,si ; цикл продолжается, пока X < Y ja exit_main_loop

pop bx ; BX = номер строки центра круга pop dx ; DX = номер столбца центра круга push dx push bx

push ax ; сохранить АХ (putpixel16b его изменяет) add bx,di ; вывод восьми точек на окружности: add dx,si call putpixel16b ; центр_Х + X, центр_Y + Y sub dx,si sub dx,si call putpixel16b ; центр_X + X, центр_Y - Y sub bx,di sub bx,di call putpixel16b ; центр_Х - X, центр_Y - Y add dx,si add dx,si call putpixel16b ; центр_Х - X, центр_Y + Y sub dx,si add dx,di add bx,di add bx,si call putpixel16b ; центр_Х + Y, центр_Y + X sub dx,di sub dx,di call putpixel16b ; центр_Х + Y, центр_Y - X sub bx,si sub bx,si call putpixel16b ; центр_Х - Y, центр_Y - X add dx,di add dx,di call putpixel16b ; центр_Х - Y, центр_Y + X pop ax



test ax,ax ; если наклон положительный js slop_negative mov dx,di sub dx,si shl dx,1 inc dx add ax,dx ; наклон = наклон + 2(Х - Y) + 1 dec si ; Y = Y - 1 jmp circle_loop slop_negative: ; если наклон отрицательный mov dx,di shl dx,1 inc dx add ax,dx ; наклон = наклон + 2X + 1 jmp circle_loop ; и Y не изменяется exit_main_loop: pop bx pop ax pop si ret

current_bank dw 0 ; номер текущего банка vbe_mode_buffer: ; начало буфера данных о видеорежиме end start

В этом примере для наглядности отсутствуют необходимые проверки на поддержку VBE (все прерывания должны возвращать 4Fh в AL), на поддержку видеорежима (атрибут видеорежима в первом байте буфера, заполняемого подфункцией 02) или на объем видеопамяти (должно быть как минимум 2 Мб) и на другие ошибки (все прерывания должны возвращать 0 в АН).

Для вывода точки на экран используется выражение типа

номер_банка = номер_строки * байт_в_строке / байт_в_банке смещение = номер_строки * байт_в_строке MOD байт_в_банке

Но так как и число байт в строке, и число байт в банке являются степенями двойки, умножение, деление и вычисление остатка от деления можно заменить более быстрыми операциями сдвига, как это сделано в процедуре putpixel16b.

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


Работа с VGA-режимами


Функция 00 прерывания BIOS 10h позволяет переключаться не только в текстовые режимы, использовавшиеся в предыдущих главах, но и в некоторые графические. Эти видеорежимы стандартны и поддерживаются всеми видеоадаптерами (начиная с VGA), см. табл. 19.

Таблица 19. Основные графические режимы VGA

Номер режима Разрешение Число цветов
11h 640x480 2
12h 640x480 16
13h 320x200 256

Существуют еще несколько видеорежимов, использовавшихся более старыми видеоадаптерами CGA и EGA (с номерами от 4 до 10h); их список приведен в приложении 2.

BIOS также предоставляет видеофункции чтения и записи точки на экране в графических режимах, но эти функции настолько медленно исполняются, что никогда не используются в реальных программах.

INТ 10h АН = 0Ch — Вывести точку на экран

Ввод: АН = 0Ch
ВН = номер видеостраницы (игнорируется для режима 13h, поддерживающего только одну страницу)
DX = номер строки
СХ = номер столбца
AL = номер цвета (для режимов 10h и llh, если старший бит 1, номер цвета точки на экране будет результатом операции «исключающее ИЛИ»)
Вывод: Никакого

INТ 10h AH = 0Dh — Считать точку с экрана

Ввод: АН = 0Dh
ВН = номер видеостраницы (игнорируется для режима 13h, поддерживающего только одну страницу)
DX = номер строки
СХ = номер столбца
Вывод: AL = номер цвета

Попробуем тем не менее воспользоваться средствами BIOS для вывода на экран. Следующая программа переводит экран в графический режим 13h (320x200), заселяет его точками случайным образом, после чего эти точки эволюционируют согласно законам алгоритма «Жизнь»: если у точки меньше двух или больше трех соседей, она погибает, а если у пустой позиции есть три соседа, в ней появляется новая точка. Мы будем использовать очень простой, но неоптимальный способ реализации этого алгоритма: сначала для каждой точки вычисляется число соседей, затем каждая точка преобразуется в соответствии с полученным числом соседей, и затем каждая точка выводится на экран.

; lifebios.asm ; Игра "Жизнь" на поле 320x200, использующая вывод на экран средствами BIOS .model small .stack 100h ; явное задание стека - для ЕХЕ-программ .code .186 ; для команд shl al,4 и shr al,4 start: push FAR_BSS ; сегментный адрес буфера в DS pop ds


; заполнение массива ячеек псевдослучайными значениями xor ах,ах int 1Ah ; Функция АН = 0 INT 1Ah: получить текущее ; время DX теперь содержит число секунд, ; прошедших с момента включения компьютера, ; которое используется как начальное значение ; генератора случайных чисел mov di,320*200+1 ; максимальный номер ячейки fill_buffer: imul dx,4E35h ; простой генератор случайных чисел inc dx ; из двух команд mov ax,dx ; текущее случайное число копируется в АХ, shr ax,15 ; от него оставляется только один бит, mov byte ptr [di],al ; и в массив копируется 00, если ячейка ; пуста, и 01, если заселена dec di ; следующая ячейка jnz fill_buffer ; продолжить цикл, если DI не стал равен нулю

mov ах,0013h ; графический режим 320x200, 256 цветов int 10h

; основной цикл

new_cycle:

; Шаг 1: для каждой ячейки вычисляется число соседей ; и записывается в старшие 4 бита этой ячейки

mov di,320*200+1 ; максимальный номер ячейки step_1: mov al,byte ptr [di+1] ; в AL вычисляется сумма add al,byte ptr [di-1] ; значений восьми соседних ячеек, add al,byte ptr [di+319] ; при этом в младших четырех add al,byte ptr [di-319] ; битах накапливается число add al,byte ptr [di+320] ; соседей add al,byte ptr [di-320] add al,byte ptr [di+321] add al,byte ptr [di-321] shl al,4 ; теперь старшие четыре бита AL - число ; соседей текущей ячейки or byte ptr [di],al ; поместить их в старшие четыре бита ; текущей ячейки dec di ; следующая ячейка jnz step_1 ; продолжить цикл, если DI не стал равен нулю

; Шаг 2: изменение состояния ячеек в соответствии с полученными в шаге 1 ; значениями числа соседей

mov di,320*200+1 ; максимальный номер ячейки flip_cycle: mov al,byte ptr [di] ; считать ячейку из массива shr al,4 ; AL = число соседей cmp al,3 ; если число соседей = 3, je birth ; ячейка заселяется, cmp al,2 ; если число соседей = 2, je f_c_continue ; ячейка не изменяется, mov byte ptr [di],0 ; иначе - ячейка погибает jmp short f_c_continue birth: mov byte ptr [di],1 f_c_continue: and byte ptr [di],0Fh ; обнулить число соседей в старших ; битах ячейки dec di ; следующая ячейка jnz flip_cycle ; ; Вывод массива на экран средствами BIOS ; mov si,320*200+1 ; максимальный номер ячейки mov сх,319 ; максимальный номер столбца mov dx,199 ; максимальный номер строки zdisplay: mov al,byte ptr [si] ; цвет точки (00 - черный, 01 - синий) mov ah,0Ch ; номер видеофункции в АН int 10h ; вывести точку на экран dec si ; следующая ячейка dec cx ; следующий номер столбца jns zdisplay ; если столбцы не закончились - продолжить, mov сх,319 ; иначе: снова максимальный номер столбца в СХ dec dx ; и следующий номер строки в DX, jns zdisplay ; если и строки закончились - выход из цикла mov ah,1 ; если не нажата клавиша int 16h jz new_cycle ; следующий шаг жизни



mov ах,0003h ; восстановить текстовый режим int 10h mov ax,4C00h ; и завершить программу int 21h

.fardata? ; сегмент дальних неинициализированных данных db 320*200+1 dup(?) ; содержит массив ячеек end start

Этот пример оформлен как ЕХЕ-программа, так как используется массив, близкий по размерам к размеру сегмента, и если разместить его в одном сегменте с СОМ-программой, стек, растущий от самых старших адресов, может затереть область данных. Наш пример не использует стек, но это делает обработчик прерывания BIOS 10h.

Скорость работы этой программы — в среднем 200 тактов процессора Pentium на точку (измерения выполнены с помощью команды RDTSC, см. главу 10.2), то есть всего 16 поколений в секунду для Pentium-200 (200 миллионов тактов в секунду разделить на 200 тактов на точку и на 320x200 точек). Разумеется, используемый алгоритм крайне нерационален и кажется очевидным, что его оптимизация приведет к значительному выигрышу во времени. Но если измерить скорость выполнения каждого из трех циклов, то окажется, что первый цикл выполняется в среднем за 20,5 такта на точку, второй — за 13, а третий — за 170,5!

Исправить эту ситуацию весьма просто — достаточно отказаться от видеофункций BIOS для работы с графикой и перейти к прямому копированию в видеопамять.

В видеорежиме 13h каждый байт в области памяти, начинающейся с адреса A000h:0000h, соответствует одной точке на экране, а значение, которое может принимать этот байт (0 – 255), соответствует номеру цвета этой точки. (Цвета, которые соответствуют этим номерам, могут быть перепрограммированы с помощью видеофункции 10h BIOS.) В видеорежимах 11h и 12h каждый бит соответствует одной точке на экране, так что простым копированием в видеопамять можно получить только черно-белое изображение (для вывода цветного изображения в режиме 12h необходимо перепрограммировать видеоадаптер, об этом см. в главе 5.10.4).

В нашем примере для хранения информации о каждой ячейке также используется один байт, так что для вывода данных на экран в режиме 13h достаточно выполнить простое копирование. Переименуем программу LIFEBIOS.ASM в LIFEDIR.ASM, заменив цикл вывода на экран от команды

mov si,320*200+1



до команды

jns zdisplay

следующим фрагментом кода:

push 0A000h ; сегментный адрес видеопамяти pop es ; в ES mov cx,320*200 ; максимальный номер точки mov di,cx ; в видеопамяти - 320 * 200 mov si,cx ; а в массиве - inc si ; 320 * 200 + 1 rep movsb ; выполнить копирование в видеопамять

Теперь программа обрабатывает одну точку приблизительно за 61,5 такта процессора Pentium, что дает 51 поколение в секунду на Pentium-200. Кроме того, теперь эту программу можно переписать в виде СОМ-файла, так как и код, и массив, и стек точно умещаются в одном сегменте размером 64 Кб. Такая СОМ-программа (LIFECOM.ASM) займет 143 байта.

Оптимизация программы «Жизнь» — хорошее упражнение для программирования на ассемблере. В 1997 году проводился конкурс на самую короткую и на самую быструю программу, выполняющую в точности то же, что и наш пример, — заполнение экрана случайными точками, их эволюция и выход по нажатию любой клавиши. Самой короткой тогда оказалась программа размером в 72 байта, которая с тех пор была усовершенствована до 64 байт (ее скорость 52 такта на точку), а самая быстрая из 16-битных программ тратит на каждую точку в среднем всего 6 тактов процессора Pentium и имеет размер 689 байт. В ней состояния ячеек описываются отдельными битами массива, а для их обработки используются команды логических операций над целыми словами, так что одна команда обрабатывает сразу 16 точек. Использование 32-битных команд с тем же алгоритмом позволяет ускорить программу до 1,5 такта на точку.


Системный таймер


Начиная с IBM AT, персональные компьютеры содержат два устройства для управления процессами — часы реального времени (RTC) и собственно системный таймер. Часы реального времени получают питание от аккумулятора на материнской плате и работают даже тогда, когда компьютер выключен. Это устройство можно использовать для определения/установки текущих даты и времени, установки будильника с целью выполнения каких-либо действий и для вызова прерывания IRQ8 (INT 4Ah) каждую миллисекунду. Системный таймер используется одновременно для управления контроллером прямого доступа к памяти, для управления динамиком и как генератор импульсов, вызывающий прерывание IRQ0 (INT 8h) 18,2 раза в секунду. Таймер предоставляет богатые возможности для препрограммирования на уровне портов ввода-вывода, но на уровне DOS и BIOS и часы реального времени, и системный таймер используются только как средство определения/установки текущего времени и организации задержек.

Функция DOS 2Ah — Определить дату

Ввод: AX = 2Ah
Вывод: СХ = год (1980 – 2099)
DH = месяц
DL = день
AL = день недели (0 — воскресенье, 1 — понедельник...)

Функция DOS 2Ch — Определить время

Ввод: AX = 2Ch
Вывод: СН = час
CL = минута
DH = секунда
DL = сотая доля секунды

Эта функция использует системный таймер, так что время изменяется только 18,2 раза в секунду и число в DL увеличивается сразу на 5 или 6.

Функция DOS 2Bh — Установить дату

Ввод: АН = 2Bh
СХ = год (1980 – 2099)
DH = месяц
DL = день
Вывод: АН = FFh, если введена несуществующая дата,
АН = 00h, если дата установлена

Функция DOS 2Dh — Установить время

Ввод: АН = 2Dh
СН = час
CL = минута
DH = секунда
DL = сотая доля секунды
Вывод: AL = FFh, если введено несуществующее время,
AL = 00h, если время установлено

Функции 2Bh и 2Dh устанавливают одновременно как внутренние часы DOS, которые управляются системным таймером и обновляются 18,2 раза в секунду, так и часы реального времени. BIOS позволяет управлять часами напрямую:


INT 1Ah АН = 04h — Определить дату RTC

Ввод: АН = 04h
Вывод: CF = 0, если дата прочитана
СХ = год ( в формате BCD, то есть 1998h для 1998-го года)
DH = месяц (в формате BCD)
DL = день (в формате BCD)
CF = 1, если часы не работают или попытка чтения пришлась на момент обновления
INT 1Ah АН = 02h — Определить время RTC

Ввод: АН = 02h
Вывод: CF = 0, если время прочитано
СН = час (в формате BCD)
CL = минута (в формате BCD)
DH = секунда (в формате BCD)
DL = 01h, если действует летнее время, 00h, если нет
CF = 1, если часы не работают или попытка чтения пришлась на момент обновления
INT 1Ah АН = 05h — Установить дату RTC

Ввод: АН = 05h
СХ = год (в формате BCD)
DH = месяц
DL = день
INT 1Ah АН = 03h — Установить время RTC

Ввод: АН = 03h
СН = час (в формате BCD)
CL = минута (в формате BCD)
DH = секунда (в формате BCD)
DL = 01h, если используется летнее время, 0 — если нет
Кроме того, BIOS позволяет использовать RTC для организации будильников и задержек:

INT 1Ah АН = 06h — Установить будильник

Ввод: АН = 06h
СН = час (BCD)
CL = минута (BCD)
DH = секунда (BCD)
Вывод: CF = 1, если произошла ошибка (будильник уже установлен или прерывание вызвано в момент обновления часов)
CF = 0, если будильник установлен
Теперь каждые 24 часа, когда время совпадет с заданным, часы реального времени вызовут прерывание IRQ8 (INT 4Ah), которое должна обрабатывать установившая будильник программа. Если при вызове СН = FFh, CL*nbsp;= FFh, a DH = 00h, то будильник начнет срабатывать раз в минуту.

INT 1Ah АН = 07 — Отменить будильник

Ввод: АН = 07h
Эта функция позволяет отменить будильник, например для того, чтобы установить его на другое время.

BIOS отслеживает каждый отсчет системного таймера с помощью своего обработчика прерывания IRQ0 (INT 8h) и увеличивает на 1 значение 32-битного счетчика, который располагается в памяти по адресу 0000h:046Ch, причем при переполнении этого счетчика байт по адресу 0000h:0470h увеличивается на 1.



INT 1Ah АН = 00h — Считать значение счетчика времени

Ввод: АН = 00h
Вывод: CX:DX = значение счетчика
AL = байт переполнения счетчика
INT 1Ah АН = 01h — Изменить значение счетчика времени

Ввод: АН = 01h
CX:DX = значение счетчика
Программа может считывать значение этого счетчика в цикле (через прерывание или просто командой MOV) и организовывать задержки, например пока счетчик не увеличится на 1. Но так как этот счетчик использует системный таймер, минимальная задержка будет равна приблизительно 55 микросекундам. Частоту таймера можно изменить, программируя его на уровне портов, но BIOS предоставляет для этого специальные функции.

INT 15h АН = 86h — Формирование задержки

Ввод: АН = 86h
CX:DX = длительность задержки в микросекундах (миллионных долях секунды!)
Вывод: AL = маска, записанная обработчиком в регистр управления прерываниями
CF = 0, если задержка выполнена
CF = 1, если таймер был занят
Если нужно запустить счетчик времени и продолжить выполнение программы, можно воспользоваться еще одной функцией.

INT 15h АН = 83h — Запуск счетчика времени

Ввод: АН = 83h
AL = 0 — запустить счетчик
AL = 1 — прервать счетчик
CX:DX = длительность задержки в микросекундах
ES:BX = адрес байта, старший бит которого по окончании работы счетчика будет установлен в 1
Вывод: AL = маска, записанная обработчиком в регистр управления прерываниями
CF = 0, если задержка выполнена
CF = 1, если таймер был занят
Минимальный интервал для этих функций на большинстве систем обычно составляет около 1000 микросекунд. Воспользуемся функцией организации задержки для небольшой интерактивной игры:

; worm.asm ; Игра "Питон" (или "Змея", или "Червяк"). Управление осуществляется клавишами ; управления курсором, питон погибает, если он выходит за верхнюю или нижнюю ; границу экрана или самопересекается. .model tiny .code .186 ; для команды push 0A000h org 100h ; СОМ-файл start: mov ax,cs ; текущий сегментный адрес плюс add ax,1000h ; 1000h = следующий сегмент, mov ds,ax ; который будет использоваться ; для адресов головы и хвоста push 0A000h ; 0A000h - сегментный адрес pop es ; видеопамяти (в ES) mov ax,13h ; графический режим 13h int 10h mov di,320*200 mov cx,600h ; заполнить часть видеопамяти, ; остающуюся за пределами rep stosb ; экрана, ненулевыми значениями ; (чтобы питон не смог выйти ; за пределы экрана) xor si,si ; начальный адрес хвоста в DS:SI mov bp,10 ; начальная длина питона - 10 jmp init_food ; создать первую еду main_cycle: ; использование регистров в этой программе: ; АХ - различное ; ВХ - адрес головы, хвоста или еды на экране ; СХ - 0 (старшее слово числа микросекунд для функции задержки) ; DX - не используется (модифицируется процедурой random) ; DS - сегмент данных программы (следующий после сегмента кода) ; ES - видеопамять ; DS:DI - адрес головы ; DS:SI - адрес хвоста ; ВР - добавочная длина (питон растет, пока ВР > 0, ВР уменьшается ; на каждом шаге, пока не станет нулем)



mov dx,20000 ; пауза - 20 000 микросекунд mov ah,86h ; (СХ = 0 после REP STOSB ; и больше не меняется) int 15h ; задержка mov ah,1 ; проверка состояния клавиатуры int 16h jz short no_keypress ; если клавиша не нажата - xor ah,ah ; АН = 0 - считать скан-код int 16h ; нажатой клавиши в АН, cmp ah,48h ; если это стрелка вверх, jne short not_up mov word ptr cs:move_direction,-320 ; изменить ; направление движения на "вверх", not_up: cmp ah,50h ; если это стрелка вниз, jne short not_down mov word ptr cs:move_direction,320 ; изменить ; направление движения на "вниз", not_down: cmp ah,4Bh ; если это стрелка влево, jne short not_left mov word ptr cs:move_direction,-1 ; изменить ; направление движения на "влево", not_left: cmp ah,4Dh ; если это стрелка вправо, jne short no_keypress mov word ptr cs:move_direction,1 ; изменить ; направление движения на "вправо", no_keypress: and bp,bp ; если питон растет (ВР > 0), jnz short advance_head ; пропустить стирание хвоста, lodsw ; иначе: считать адрес хвоста из ; DS:SI в АХ и увеличить SI на 2 xchg bx,ax mov byte ptr es:[bx],0 ; стереть хвост на экране, mov bx,ax inc bp ; увеличить ВР, чтобы следующая ; команда вернула его в 0, advance_head: dec bp ; уменьшить ВР, так как питон ; вырос на 1, если стирание хвоста было пропущено, ; или чтобы вернуть его в 0 - в другом случае add bx,word ptr cs:move_direction ; bx = следующая координата головы mov al,es:[bx] ; проверить содержимое экрана в точке ; с этой координатой, and al,al ; если там ничего нет, jz short move_worm ; передвинуть голову, cmp al,0Dh ; если там еда, je short grow_worm ; увеличить длину питона, mov ax,3 ; иначе - питон умер, int 10h ; перейти в текстовый режим retn ; и завершить программу

move_worm: mov [di],bx ; поместить адрес головы в DS:DI inc di inc di ; и увеличить DI на 2, mov byte ptr es:[bx],09 ; вывести точку на экран, cmp byte ptr cs:eaten_food,1 ; если предыдущим ; ходом была съедена еда, je if_eaten_food ; создать новую еду, jmp short main_cycle ; иначе - продолжить основной цикл



grow_worm: push bx ; сохранить адрес головы mov bx,word ptr cs:food_at ; bx - адрес еды xor ах,ах ; АХ = 0 call draw_food ; стереть еду call random ; AX - случайное число and ax,3Fh ; AX - случайное число от 0 до 63 mov bp,ax ; это число будет добавкой ; к длине питона mov byte ptr cs:eaten_food,1 ; установить флаг ; для генерации еды на следующем ходе pop bx ; восстановить адрес головы ВХ jmp short move_worm ; перейти к движению питона

if_eaten_food: ; переход сюда, если еда была съедена mov byte ptr cs:eaten_food,0 ; восстановить флаг init_food: ; переход сюда в самом начале push bx ; сохранить адрес головы make_food: call random ; AX - случайное число and ax,0FFFEh ; AX - случайное четное число mov bx,ax ; BX - новый адрес для еды xor ах,ах cmp word ptr es:[bx],ax ; если по этому адресу ; находится тело питона, jne make_food ; еще раз сгенерировать случайный адрес, cmp word ptr es:[bx+320],ax ; если на строку ниже ; находится тело питона - jne make_food ; то же самое, mov word ptr cs:food_at,bx ; поместить новый адрес ; еды в food_at, mov ax,0D0Dh ; цвет еды в АХ call draw_food ; нарисовать еду на экране pop bx jmp main_cycle

; процедура draw_food ; изображает четыре точки на экране - две по адресу ВХ и две на следующей ; строке. Цвет первой точки из пары - AL, второй - АН

draw_food: mov es:[bx],ax mov word ptr es:[bx+320],ax retn

; генерация случайного числа ; возвращает число в АХ, модифицирует DX

random: mov ах,word ptr cs:seed, mov dx,8E45h mul dx inc ax mov cs:word ptr seed,ax retn

; переменные

eaten_food db 0 move_direction dw 1 ; направление движения: 1 - вправо, ; -1 - влево, 320 - вниз, -320 - вверх seed: ; это число хранится за концом программы, food_at equ seed+2 ; а это - за предыдущим end start


Создание и открытие файлов


Функция DOS 3Ch — Создать файл

Ввод: AX = 3Ch
СХ = атрибут файла

бит 7: файл можно открывать разным процессам в Novell Netware

бит 6: не используется

бит 5: архивный бит (1, если файл не сохранялся)

бит 4: каталог (должен быть 0 для функции 3Ch)

бит 3: метка тома (игнорируется функцией 3Ch)

бит 2: системный файл

бит 1: скрытый файл

бит 0: файл только для чтения

DS:DX = адрес ASCIZ-строки с полным именем файла (ASCIZ-строка ASCII-символов, оканчивающаяся нулем)

Вывод: CF = 0 и АХ = идентификатор файла, если не произошла ошибка
CF = 1 и АХ = 03h, если путь не найден
CF = 1 и АХ = 04h, если слишком много открытых файлов
CF = 1 и АХ = 05h, если доступ запрещен

Если файл уже существует, функция 3Ch все равно открывает его, присваивая ему нулевую длину. Чтобы этого не произошло, следует пользоваться функцией 5Bh.

Функция DOS 3Dh — Открыть существующий файл

Ввод: AX = 3Dh
AL = режим доступа

биты 0 – : права доступа

00: чтение

01: запись

10: чтение и запись

бит 1: открыть для записи

биты 2 – 3: зарезервированы (0)

биты 6 – 4: режим доступа для других процессов

000: режим совместимости (остальные процессы также должны открывать этот файл в режиме совместимости)

001: все операции запрещены

010: запись запрещена

011: чтение запрещено

100: запрещений нет

бит 7: файл не наследуется порождаемыми процессами

DS:DX = адрес ASCIZ-строки с полным именем файла

CL = маска атрибутов файлов

Вывод: CF = 0 и АХ = идентификатор файла, если не произошла ошибка
CF = 1 и АХ = код ошибки (02h — файл не найден, 03h — путь не найден, 04h — слишком много открытых файлов, 05h — доступ запрещен, 0Ch — неправильный режим доступа)

Функция DOS 5Bh — Создать и открыть новый файл


Ввод: AX = 5Bh
СХ = атрибут файла
DS:DX = адрес ASCIZ-строки с полным именем файла
Вывод: CF = 0 и АХ = идентификатор файла, открытого для чтения/записи в режиме совместимости, если не произошла ошибка
CF = 1 и АХ = код ошибки (03h — путь не найден, 04h — слишком много открытых файлов, 05h — доступ запрещен, 50h — файл уже существует)
Функция DOS 5Ah — Создать и открыть временный файл

Ввод: AX = 5Ah
СХ = атрибут файла
DS:DX = адрес ASCIZ-строки с путем, оканчивающимся символом «\», и тринадцатью нулевыми байтами в конце
Вывод: CF = 0 и АХ = идентификатор файла, открытого для чтения/записи в режиме совместимости, если не произошла ошибка (в строку по адресу DS:DX дописывается имя файла)
CF = 1 и АХ = код ошибки (03h — путь не найден, 04h — слишком много открытых файлов, 05h — доступ запрещен)
Функция 5Ah создает файл с уникальным именем, который не является на самом деле временным, его следует специально удалять, для чего его имя и записывается в строку в DS:DX.

Во всех случаях строка с полным именем файла имеет вид типа

filespec db 'с:\data\filename.ext',0

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

Для работы с длинными именами файлов в DOS 7.0 (Windows 95) и старше используется еще один дополнительный набор функций, которые вызываются как функция DOS 71h.

Функция LFN 6Ch — Создать или открыть файл с длинным именем

Ввод: AX = 716Сh
ВХ = режим доступа Windows 95

биты 2 – 0: доступ

000 — только для чтения

001 — только для записи

010 — для чтения и записи

100 — только для чтения, не изменять время последнего обращения к файлу

биты 6 – 4: доступ для других процессов (см. функцию 3Dh)

бит 7: файл не наследуется порождаемыми процессами

бит 8: данные не буферизуются

бит 9: не архивировать файл, если используется архивирование файловой системы (DoubleSpace)

бит 10: использовать число в DI для записи в конец короткого имени файла

бит 13: не вызывать прерывание 24h при критических ошибках

бит 14: сбрасывать буфера на диск после каждой записи в файл

СХ = атрибут файла

DX = действие

бит 0: открыть файл (ошибка, если файл не существует)

бит 1: заменить файл (ошибка, если файл не существует)

бит 4: создать файл (ошибка, если файл существует)

DS:SI = адрес ASCIZ-строки с именем файла

DI = число, которое будет записано в конце короткого варианта имени файла
Вывод: CF = 0
АХ = идентификатор файла
СХ = 1, если файл открыт
СХ = 2, если файл создан
СХ = 3, если файл заменен
CF = 1, если произошла ошибка
АХ = код ошибки (7100h, если функция не поддерживается)
<


/p> Если функции открытия файлов возвращают ошибку «слишком много открытых файлов» (АХ = 4), следует увеличить число допустимых идентификаторов с помощью функции 67h.

Функция DOS 67h — Изменить максимальное число идентификаторов файлов

Ввод: AX = 67h
ВХ = новое максимальное число идентификаторов (20 – 65 535)
Вывод: CF = 0, если не произошла ошибка
CF = 1 и АХ = код ошибки, если произошла ошибка (например: 04h, если заданное число меньше, чем количество уже открытых файлов, или 08h, если DOS не хватает памяти для новой таблицы идентификаторов)
Следует помнить, что все дочерние процессы будут наследовать только первые 20 идентификаторов и должны вызывать функцию 67h сами, если им требуется больше.


Средства BIOS


Функции DOS вывода на экран позволяют перенаправлять вывод в файл, но не позволяют вывести текст в любую позицию экрана и не позволяют изменить цвет текста. DOS предполагает, что для более тонкой работы с экраном программы должны использоваться видеофункции BIOS. BIOS (базовая система ввода-вывода) — это набор программ, расположенных в постоянной памяти компьютера, которые выполняют его загрузку сразу после включения и обеспечивают доступ к некоторым устройствам, в частности к видеоадаптеру. Все функции видеосервиса BIOS вызываются через прерывание 10h. Рассмотрим функции, которые могут быть полезны для вывода текстов на экран (полностью видеофункции BIOS описаны в приложении 2).

Выбор видеорежима

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

INT 10h, АН = 00 — Установить видеорежим

Ввод: AL = номер режима в младших 7 битах
Вывод: Обычно никакого, но некоторые BIOS (Phoenix и AMI) помещают в AL 30Н для текстовых режимов и 20h для графических

Вызов этой функции приводит к тому, что экран переводится в выбранный режим. Если старший бит AL не установлен в 1, экран очищается. Номера текстовых режимов — 0, 1, 2, 3 и 7. 0 и 1 — 16-цветные режимы 40x25 (с 25 строками по 40 символов в строке), 2 и 3 — 16-цветные режимы 80x25, 7 — монохромный режим 80x25. Мы не будем пока рассматривать графические режимы, хотя функции вывода текста на экран DOS и BIOS могут работать и в них. Существует еще много текстовых режимов с более высоким разрешением (80x43, 80x60, 132x50 и т.д.), но их номера для вызова через эту функцию различны для разных видеоадаптеров (например, режим 61h — 132x50 для Cirrus 5320 и 132x29 для Genoa 6400). Однако, если видеоадаптер поддерживает стандарт VESA BIOS Extention, в режимы с высоким разрешением можно переключаться, используя функцию 4Fh.


INT 10h, АН = 4Fh, AL = 02 — Установить SuperVGA-видеорежим

Ввод: ВХ = номер режима в младших 13 битах
Вывод: AL = 4Fh, если эта функция поддерживается
АН = 0, если переключение произошло успешно
АН = 1, если произошла ошибка
Если бит 15 регистра ВХ установлен в 1, видеопамять не очищается. Текстовые режимы, которые можно вызвать с использованием этой функции: 80x60 (режим 108h), 132x25 (109h), 132x43 (10Ah), 132x50 (10Bh), 132x60 (10Ch).

Видеорежим, используемый в DOS по умолчанию, — текстовый режим 3.

Управление положением курсора

INT 10h, АН = 02 — Установить положение курсора

Ввод: АН = 02
ВН = номер страницы
DH = строка
DL = столбец
С помощью этой функции можно установить курсор в любую позицию экрана, и дальнейший вывод текста будет происходить из этой позиции. Отсчет номера строки и столбца ведется от верхнего левого угла экрана (символ в левой верхней позиции имеет координаты 0, 0). Номера страниц 0 – 3 (для режимов 2 и 3)и 0 – 7 (для режимов 1 и 2) соответствуют области памяти, содержимое которой в данный момент отображается на экране. Можно вывести текст в неактивную в настоящий момент страницу, а затем переключиться на нее, чтобы изображение изменилось мгновенно.

INТ 10h, АН = 03 — Считать положение и размер курсора

Ввод: АН = 03
ВН = номер страницы
Вывод: DH, DL = строка и столбец текущей позиции курсора
СН, CL = первая и последняя строки курсора
Возвращает текущее состояние курсора на выбранной странице (каждая страница использует собственный независимый курсор).

Вывод символов на экран

Каждый символ на экране описывается двумя байтами — ASCII-кодом символа и байтом атрибута, указывающим цвет символа и фона, а также является ли символ мигающим.

Атрибут символа:

Бит 7: символ мигает (по умолчанию) или фон яркого цвета (если его действие было переопределено видеофункцией 10h).

Биты 6 – 4: цвет фона.



Бит 3: символ яркого цвета ( по умолчанию) или фон мигает (если его действие было переопределено видеофункцией 11h).

Биты 2 – 0: цвет символа.

Цвета кодируются в битах, как показано в таблице 18.

Таблица 18. Атрибуты символов

  Обычный цвет Яркий цвет
000b черный темно-серый
001b синий светло-синий
010b зеленый светло-зеленый
011b голубой светло-голубой
100b красный светло-красный
101b пурпурный светло-пурпурный
110b коричневый желтый
111b светло-серый белый
INT 10h, АН = 08 — Считать символ и атрибут символа в текущей позиции курсора

Ввод: АН = 08
ВН = номер страницы
Вывод: АН = атрибут символа
AL = ASCII-код символа
INT 10h, АН = 09 — Вывести символ с заданным атрибутом на экран

Ввод: АН = 09
ВН = номер страницы
AL = ASCII-код символа
BL = атрибут символа
СХ = число повторений символа
С помощью этой функции можно вывести на экран любой символ, включая даже символы CR и LF, которые обычно интерпретируются как конец строки. В графических режимах СХ не должен превышать число позиций, оставшееся до правого края экрана.

INT 10h, АН = 0Ah — Вывести символ с текущим атрибутом на экран

Ввод: АН = 0Ah
ВН = номер страницы
AL = ASCII-код символа
СХ = число повторений символа
Эта функция также выводит любой символ на экран, но в качестве атрибута символа используется атрибут, который имел символ, находившийся ранее в этой позиции.

INT 10h, АН = 0Eh — Вывести символ в режиме телетайпа

Ввод: АН = 0Eh
ВН = номер страницы
AL = ASCII-код символа
Символы CR (0Dh), LF (0Ah), BEL (7) интерпретируются как управляющие символы. Если текст при записи выходит за пределы нижней строки, экран прокручивается вверх. В качестве атрибута используется атрибут символа, находившегося в этой позиции.

INT 10h, AH = 13h — Вывести строку символов с заданными атрибутами

Ввод: АН = 13h
AL = режим вывода:бит 0 — переместить курсор в конец строки после вывода
бит 1 — строка содержит не только символы, но также и атрибуты, так что каждый символ описывается двумя байтами: ASCII-код и атрибут
биты 2 – 7 зарезервированыСХ = длина строки (только число символов)
BL = атрибут, если строка содержит только символы
DH,DL = строка и столбец, начиная с которых будет выводиться строки
ES:BP = адрес начала строки в памяти
<


/p> Функция 13h выводит на экран строку символов, интерпретируя управляющие символы CR (0Dh), LF (0Ah), BS (08) и BEL (07). Если строка подготовлена в формате символ,атрибут — гораздо быстрее просто скопировать ее в видеопамять, о чем рассказано в следующем разделе.

Воспользуемся теперь функциями BIOS, чтобы усовершенствовать программу DOSOUT1 и вывести на экран все 256 символов, включая даже символы перевода строки. Кроме того, для лучшей читаемости таблицы после каждого символа будет выводиться пробел.

; biosout.asm ; Выводит на экран все ASCII-символы без исключения .model tiny .code org 100h ; Начало СОМ-файла start: mov ax,0003h int 10h ; Видеорежим 3 (очистка экрана ; и установка курсора в 0, 0) mov dx,0 ; DH и DL будут использоваться ; для хранения положения курсора. ; Начальное положение - 0,0 mov si,256 ; SI будет счетчиком цикла mov al,0 ; Первый символ - с кодом 00h mov ah,9 ; Номер видеофункции "вывод символа с атрибутом" mov cx,1 ; Выводится один символ за раз mov bl,00011111b ;атрибут символа - белый на синем cloop: int 10h ; Вывести символ на экран push ax ; Сохранить текущий символ и номер функции mov ah,2 ; Номер видеофункции 2 - ; изменить положение курсора inc dl ; Увеличить текущий столбец на 1 int 10h ; Переместить курсор mov ax,0920h ; АН = 09, AL = 20h (ASCII-код пробела) int 10h ; Вывести пробел mov ah,2 ; Номер видеофункции 2 inc dl ; Увеличить столбец на 1 int 10h ; Переместить курсор pop ax ; Восстановить номер функции в ah ; и текущий символ в al inc al ; Увеличить AL на 1 - следующий символ test al,0Fh ; Если AL не кратен 16, jnz continue_loop ; продолжить цикл, push ax ; иначе - сохранить номер функции ; и текущий символ mov ah,2 ; Номер видеофункции 2 inc dh ; Увеличить номер строки на 1 mov dl,0 ; Столбец = 0 int 10h ; Установить курсор на начало следующей строки pop ax ; Восстановить номер видеофункции ; и текущий символ continue_loop: dec si ; Уменьшить SI на 1, ; если он не стал нулем - продолжить jnz cloop ; CX используется внутри цикла, ; так что нельзя использовать команду LOOP ; для его организации ret ; Завершение СОМ-файла end start

Так как функция 09 выводит символ в позиции курсора, но не перемещает сам курсор, это приходится делать каждый раз специально.

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


Так же как и для вывода на экран, BIOS предоставляет больше возможностей по сравнению с DOS для считывания данных и управления клавиатурой. Например, функциями DOS нельзя определить нажатие комбинаций клавиш типа Ctrl-Alt-Enter или нажатие двух клавиш Shift одновременно, DOS не может определить момент отпускания нажатой клавиши, и наконец, в DOS нет аналога функции С ungetch(), помещающей символ в буфер клавиатуры, как если бы его ввел пользователь. Все это можно осуществить, используя различные функции прерывания 16h и операции с байтами состояния клавиатуры.

INT 16h, АН = 0, 10h, 20h — Чтение символа с ожиданием

Ввод: АН = 00h (83/84-key), 10h (101/102-key), 20h (122-key)
Вывод: AL = ASCII-код символа, 0 или префикс скан-кода
АН = скан-код нажатой клавиши или расширенный ASCII-код

Каждой клавише на клавиатуре соответствует так называемый скан-код (см. приложение 1), соответствующий только этой клавише. Этот код посылается клавиатурой при каждом нажатии и отпускании клавиши и обрабатывается BIOS (обработчиком прерывания INT 9). Прерывание 16h дает возможность получить код нажатия, не перехватывая этот обработчик. Если нажатой клавише соответствует ASCII-символ, то в АН возвращается код этого символа, а в AL — скан-код клавиши. Если нажатой клавише соответствует расширенный ASCII-код, в AL возвращается префикс скан-кода (например, Е0 для серых клавиш) или 0, если префикса нет, а в АН — расширенный ASCII-код. Функция 00Н обрабатывает только комбинации, использующие клавиши 84-клавишной клавиатуры, l0h обрабатывает все 101 – 105-клавишные комбинации, 20h — 122-клавишные. Тип клавиатуры можно определить с помощью функции 09h прерывания 16h, если она поддерживается BIOS (поддерживается ли эта функция, можно узнать с помощью функции C0h прерывания 15h).

INT 16h, АН = 1, 11h, 21h — Проверка символа

Ввод: АН = 01h (83/84-key), 11h (101/102-key), 21h (122-key)
Вывод: ZF = 1, если буфер пуст
ZF = 0, если в буфере присутствует символ, в этом случае
AL = ASCII-код символа, 0 или префикс скан-кода
АН = скан-код нажатой клавиши или расширенный ASCII-код
<
/p> Символ остается в буфере клавиатуры, хотя некоторые BIOS удаляют символ из буфера при обработке функции 01h, если он соответствует расширенному ASCII-коду, отсутствующему на 84-клавишных клавиатурах.

INT 16h, АН = 05h — Поместить символ в буфер клавиатуры

Ввод: АН = 05h
СН = скан-код
CL = ASCII-код
Вывод: AL = 00, если операция выполнена успешно
AL = 01h, если буфер клавиатуры переполнен
АН модифицируется многими BIOS
Обычно можно поместить 0 вместо скан-кода в СН, если функция, которая будет выполнять чтение из буфера, будет использовать именно ASCII-код. Например, следующая программа при запуске из DOS вызывает команду DIR (но при запуске из некоторых оболочек, например FAR, этого не произойдет).

; ungetch.asm ; заносит в буфер клавиатуры команду DIR так, чтобы она ; выполнилась сразу после завершения программы ; .model tiny .code org 100h ; СОМ-файл start: mov cl,'d' ; CL = ASCII-код буквы "d" call ungetch mov cl,'i' ; ASCII-код буквы "i" call ungetch mov cl,'r' ; ASCII-код буквы "r" call ungetch mov cl,0Dh ; перевод строки ungetch: mov ah,5 ; AH = номер функции mov ch,0 ; CH = 0 (скан-код неважен) int 16h ; поместить символ в буфер ret ; завершить программу

end start

INT 16h, AH = 02h, 12h, 22h — Считать состояние клавиатуры

Ввод: АН = 02h (83/84-key), 12h (101/102-key), 22h (122-key)
Вывод: AL = байт состояния клавиатуры 1
АН = байт состояния клавиатуры 2 (только для функций 12h и 22h)
Байт состояния клавиатуры 1 (этот байт всегда расположен в памяти по адресу 0000h:0417h или 0040h:0017h):

Бит 7: Ins включена

Бит 6: CapsLock включена

Бит 5: NumLock включена

Бит 4: ScrollLock включена

Бит 3: Alt нажата (любая Alt для функции 02h, часто только левая Alt для 12h/22h)

Бит 2: Ctrl нажата (любая Ctrl)

Бит 1: Левая Shift нажата

Бит 0: Правая Shift нажата

Байт состояния клавиатуры 2 (этот байт всегда расположен в памяти по адресу 0000h:0418h или 0040h:0018h):


Средства DOS


На примере первой программы на ассемблере мы уже познакомились с одним из способов вывода текста на экран — вызовом функции DOS 09h. Это далеко не единственный способ вывода текста — DOS предоставляет для этого несколько функций.

Функция DOS 02h — Записать символ в STDOUT с проверкой на Ctrl-Break

Ввод: АН = 02h
DL = ASCII-код символа
Вывод: Никакого, согласно документации, но на самом деле: AL = код последнего записанного символа (равен DL, кроме случая, когда DL = 09h (табуляция), тогда в AL возвращается 20h).

Эта функция при выводе на экран обрабатывает некоторые управляющие символы — вывод символа BEL (07h) приводит к звуковому сигналу, символ BS (08h) приводит к движению курсора влево на одну позицию, символ НТ (09h) заменяется на несколько пробелов, символ LF (0Ah) опускает курсор на одну позицию вниз, и CR (0Dh) приводит к переходу на начало текущей строки.

Если в ходе работы этой функции была нажата комбинация клавиш Ctrl-Break, вызывается прерывание 23h, которое по умолчанию осуществляет выход из программы.

Например, напишем программу, выводящую на экран все ASCII-символы, 16 строк по 16 символов в строке.

; dosoutl.asm ; Выводит на экран все ASCII-символы ; .model tiny .code org 100h ; начало СОМ-файла start: mov ex,256 ; вывести 256 символов mov dl,0 ; первый символ - с кодом 00 mov ah,2 ; номер функции DOS "вывод символа" cloop: int 21h ; вызов DOS inc dl ; увеличение DL на 1 - следующий символ test dl,0Fh ; если DL не кратен 16, jnz continue_loop ; продолжить цикл, push dx ; иначе: сохранить текущий символ mov dl,0Dh ; вывести CR int 21h mov dl,0Ah ; вывести LF int 21h pop dx ; восстановить текущий символ continue_loop: loop cloop ; продолжить цикл ret ; завершение СОМ-файла end start

Это программа типа СОМ, и компилироваться она должна точно так же, как hello-1.asm в разделе 4.1. Здесь с помощью команды LOOP оформляется цикл, выполняющийся 256 раз (значение регистра СХ в начале цикла). Регистр DL содержит код символа, который равен нулю в начале цикла и увеличивается каждый раз на 1 командой INC DL. Если значение DL сразу после увеличения на 1 кратно 16, оно временно сохраняется в стеке и на экран выводятся символы CR и LF, выполняющие переход на начало новой строки. Проверка выполняется командой TEST DL,0Fh — результат операции AND над DL и 0Fh будет нулем, только если младшие четыре бита DL равны нулю, что и соответствует кратности шестнадцати.


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

hello-1.com > hello-1.out

то на экран ничего выдано не будет, а в текущем каталоге появится файл hello-1.out, содержащий строку «Hello World!». Точно так же, если написать

dosout1.com > dosout1.out

то в файле dosout1.out окажутся все символы ASCII, причем символы BEL и BS не будут интерпретироваться и запишутся в файл как есть. Символы CR и LF тоже запишутся как есть, но так как они отмечают конец строки, редакторы и просмотрщики текстовых файлов будут разрывать первую строку символов.

Функция DOS 06h — Записать символ в STDOUT без проверки на Ctrl-Break

Ввод: АН = 06h
DL = ASCII-код символа (кроме FFh)
Вывод: Никакого, согласно документации, но на самом деле: AL = код записанного символа (копия DL)
Эта функция не обрабатывает управляющие символы (CR, LF, HT и BS выполняют свои функции при выводе на экран, но сохраняются при перенаправлении вывода в файл) и не проверяет нажатие Ctrl-Break. Можно заменить в программе dosoutl.asm команду MOV АН,2 на MOV АН,6 и перекомпилировать этот пример, чтобы получить более полную таблицу символов.

Функция DOS 09h — Записать строку в STDOUT с проверкой на Ctrl-Break

Ввод: АН = 09h
DS:DX = адрес строки, заканчивающейся символом $ (24h)
Вывод: Никакого, согласно документации, но на самом деле: AL = 24h (код последнего символа)
Действие этой функции полностью аналогично действию функции 02h, но выводится не один символ, а целая строка, как в программах hello-1.asm и hello-2.asm.

Функция DOS 40h — Записать в файл или устройство

Ввод: АН = 40h
ВХ = 1 для STDOUT или 2 для STDERR
DS:DX = адрес начала строки
СХ = длина строки
Вывод: CF = 0,
АХ = число записанных байт
Эта функция предназначена для записи в файл, но, если в регистр ВХ поместить число 1, функция 40h будет выводить данные на STDOUT, а если ВХ = 2 — на устройство STDERR. STDERR всегда выводит данные на экран и не перенаправляется в файлы. На этой функции основаны используемые в С функции стандартного вывода — фактически функция С fputs() просто вызывает это прерывание, помещая свой первый аргумент в ВХ, адрес строки (второй аргумент) — в DS:DX и длину — в СХ.

; dosout2.asm ; Выводит на экран строку "This function can print $", ; используя вывод в STDERR, так что ее нельзя перенаправить в файл. .model tiny .code org 100h ; начало СОМ-файла start: mov ah,40h ; номер функции DOS mov bx,2 ; устройство STDERR mov dx,offset message ; DS:DX - адрес строки mov cx, message_length ; CX - длина строки int 21h ret ; завершение СОМ-файла message db "Эта функция может выводить знак $" message_length = $-message ; длина строки = текущий адрес ; минус адрес начала строки end start

Если скомпилировать эту программу и запустить ее командой

dosout2.com > dosout2.out

то сообщение появится на экране, а файл dosout2.out окажется пустым.

И наконец, последняя функция DOS вывода на экран — недокументированное прерывание 29h.

INT 29h: Быстрый вывод символа на экран

Ввод: AL = ASCII-код символа
В большинстве случаев INT 29h просто немедленно вызывает функцию BIOS «вывод символа на экран в режиме телетайпа», так что никаких преимуществ, кроме экономии байт при написании как можно более коротких программ, она не имеет.



/p> При чтении с помощью этой функции введенный символ автоматически немедленно отображается на экране (посылается в устройство STDOUT — так что его можно перенаправить в файл). При нажатии Ctrl-C или Ctrl-Break выполняется команда INT 23h. Если нажата клавиша, не соответствующая какому-нибудь символу (стрелки, функциональные клавиши Ins, Del и т.д.), то в AL возвращается 0 и функцию надо вызвать еще один раз, чтобы получить расширенный ASCII-код (см. приложение 1).

В трех следующих вариантах этой функции код символа возвращается в AL по такому же принципу.

Функция DOS 08h — Считать символ из STDIN без эха, с ожиданием и проверкой на Ctrl-Break

Ввод: АН = 08h
Вывод: AL = код символа
Функция DOS 07h — Считать символ из STDIN без эха, с ожиданием и без проверки на Ctrl-Break

Ввод: АН = 07h
Вывод: AL = код символа
Функция DOS 06h — Считать символ из STDIN без эха, без ожидания и без проверки на Ctrl-Break

Ввод: АН = 07h
DL = 0FFh
Вывод: ZF = 1, если не была нажата клавиша, и AL = 00
ZF = 0, если клавиша была нажата. В этом случае AL = код символа
Кроме перечисленных функций могут потребоваться и некоторые служебные функции DOS для работы с клавиатурой.

Функция DOS 0Bh — Проверить состояние клавиатуры

Ввод: АН = 0Bh
Вывод: AL = 0, если не была нажата клавиша
AL = 0FFh, если была нажата клавиша
Эту функцию удобно использовать перед функциями 01, 07 и 08, чтобы не ждать нажатия клавиши. Кроме того, вызов этой функции позволяет проверить, не считывая символ с клавиатуры, была ли нажата комбинация клавиш Ctrl-Break; если это произошло, выполнится прерывание 23h.

Функция DOS 0Ch — Очистить буфер и считать символ

Ввод: АН = 0Ch
AL = Номер функции DOS (01, 06, 07, 08, 0Ah)
Вывод: Зависит от вызванной функции
Функция 0Ch очищает буфер клавиатуры, так что следующая функция чтения символа будет ждать ввода с клавиатуры, а не использовать нажатый ранее и еще не обработанный символ. Например, именно эта функция используется для считывания ответа на вопрос «Уверен ли пользователь в том, что он хочет отформатировать диск?».



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

; dosin2.asm ; Изображает пентамино F, которое можно перемещать по экрану клавишами ; управления курсором и вращать клавишами X и Z. Выход из программы - Esc. ; line_length = 3 ; число символов в строке изображения number_of_lines = 3 ; число строк

.model tiny .code org 100h ; начало СОМ-файла start: cld ; будут использоваться команды ; строковой обработки mov ax,0B800h ; адрес начала текстовой видеопамяти mov es,ax ; в ES mov ax,0003h int 10h ; текстовый режим 03 (80x25) mov ah,02h ; установить курсор mov bh,0 mov dh,26 ; на строку 26, то есть за пределы экрана mov dl,1 int 10h ; теперь курсора на экране нет call update_screen ; вывести изображение

; основной цикл опроса клавиатуры main_loop: mov ah,08h ; считать символ с клавиатуры int 21h ; без эха, с ожиданием, с проверкой на Ctrl-Break test al,al ; если AL = 0 jz eASCII_entered ; введен символ расширенного ASCII cmp al,1Bh ; иначе: если введен символ 1Bh (Esc), je key_ESC ; выйти из программы, cmp al,'Z' ; если введен символ Z, je key_Z ; перейти на его обработчик cmp al,'z' ; то же для z je key_Z cmp al,'X' ; если введен символ X, je key_X ; перейти на его обработчик cmp al,'х' ; то же для х je key_X jmp short main_loop ; считать следующую клавишу

eASCII_entered: ; был введен расширенный ASCII-символ int 21h ; получить его код (повторный вызов функции) cmp al,48h ; стрелка вверх je key_UP cmp al,50h ; стрелка вниз je key_DOWN cmp al,4Bh ; стрелка влево je key_LEFT cmp al,4Dh ; стрелка вправо je key_RIGHT jmp short main_loop ; считать следующую клавишу ; ; обработчики нажатий клавиш ; key_ESC: ; Esc ret ; завершить СОМ-программу

key_UP: ; стрелка вверх cmp byte ptr start_row,0 ; если изображение на верхнем ; краю экрана, jna main_loop ; считать следующую клавишу, dec byte ptr start_row ; иначе - уменьшить номер строки, call update_screen ; вывести новое изображение jmp short main_loop ; и считать следующую клавишу



key_DOWN: ; стрелка вниз cmp byte ptr start_row,25-number_of_lines ; если ; изображение на нижнем краю экрана, jnb main_loop ; считать следующую клавишу, inc byte ptr start_row ; иначе - увеличить номер строки, call update_screen ; вывести новое изображение jmp short main_loop ; и считать следующую клавишу

key_LEFT: ; стрелка влево cmp byte ptr start_col,0 ; если изображение на левом краю ; экрана, jna main_loop ; считать следующую клавишу, dec byte ptr start_col ; иначе - уменьшить номер столбца, call update_screen ; вывести новое изображение jmp short main_loop ; и считать следующую клавишу

key_RIGHT: ; стрелка вправо cmp byte ptr start_col,80-line_length ; если ; изображение на правом краю экрана, jnb main_loop ; считать следующую клавишу, inc byte ptr start_col ; иначе - увеличить номер столбца, call update_screen ; вывести новое изображение jmp short main_loop ; и считать следующую клавишу

key_Z: ; клавиша Z (вращение влево) mov ax,current_screen ; считать номер текущего изображения ; (значения 0, 1, 2, 3), dec ax ; уменьшить его на 1, jns key_Z_ok ; если получился -1 (поменялся знак), mov ах,3 ; АХ = 3 key_Z_ok: mov current_screen,ax ; записать номер обратно, call update_screen ; вывести новое изображение jmp main_loop ; и считать следующую клавишу

key_X: ; клавиша X (вращение вправо) mov ax,current_screen ; считать номер текущего изображения ; (значения 0, 1, 2, 3), inc ax ; увеличить его на 1, cmp ax,4 ; если номер стал равен 4, jne key_X_ok xor ах,ах ; АХ = 0 key_X_ok: mov current_screen,ax ; записать номер обратно, call update_screen ; вывести новое изображение jmp main_loop ; и считать следующую клавишу

; процедура update_screen ; очищает экран и выводит текущее изображение ; модифицирует значения регистров АХ, ВХ, СХ, DX, SI, DI update_screen: mov cx,25*80 ; число символов на экране mov ax,0F20h; ; символ 20h (пробел) с атрибутом 0Fh ; (белый на черном) xor di,di ; ES:DI = начало видеопамяти rep stosw ; очистить экран mov bx,current_screen ; номер текущего изображения в ВХ shl bx,1 ; умножить на 2, так как screens - массив слов mov si,screens[bx] ; поместить в ВХ смещение начала ; текущего изображения из массива screens, mov ax,start_row ; вычислить адрес начала mul row_length ; изображения в видеопамяти add ax,start_col ; (строка * 80 + столбец) * 2 shl ax,1 mov di,ax ; ES:DI - начало изображения в видеопамяти mov ah,0Fh ; используемый атрибут - белый на черном mov dx,number_of_lines ; число строк в изображении сору_lines: mov cx,line_length ; число символов в строке copy_1: lodsb ; считать ASCII-код в AL, stosw ; записать его в видеопамять ; (AL - ASCII, АН - атрибут), loop copy_1 ; вывести так все символы в строке, add di,(80-line_length)*2 ; перевести DI на начало ; следующей строки экрана, dec dx ; если строки не закончились - jnz copy_lines ; вывести следующую ret ; конец процедуры update_screen



; изображение пентамино F screen1 db " XX" ; выводимое изображение db "XX " db " X "

screen2 db " X " ; поворот на 90 градусов вправо db "XXX" db " X"

screen3 db " X " ; поворот на 180 градусов db " XX" db "XX "

screen4 db "X " ; поворот на 90 градусов влево db "XXX" db " X " ; массив, содержащий адреса всех вариантов изображения screens dw screen1,screen2,screen3,screen4 current_screen dw 0 ; текущий вариант изображения start_row dw 10 ; текущая верхняя строка изображения start_col dw 37 ; текущий левый столбец row_length db 80 ; длина строки экрана для команды MUL

end start

В этом примере для вывода на экран используется прямое копирование в видеопамять, так как вызов функции BIOS вывода строки (INT 10h, АН = 13h) прокручивает экран вверх на одну строку при выводе символа в нижнем правом углу экрана.


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

Функция DOS 0Ah — Считать строку символов из STDIN в буфер

Ввод: АН = 0Ah
DS:DX = адрес буфера
Вывод: Буфер содержит введенную строку

Для вызова этой функции надо подготовить буфер, первый байт которого содержит максимальное число символов для ввода (1 – 254), а содержимое, если оно задано, может использоваться как подсказка для ввода. При наборе строки обрабатываются клавиши Esc, F3, F5, BS, Ctrl-C/Ctrl-Break и т.д., как при наборе команд DOS (то есть Esc начинает ввод сначала, F3 восстанавливает подсказку для ввода, F5 запоминает текущую строку как подсказку, Backspace стирает предыдущий символ). После нажатия клавиши Enter строка (включая последний символ CR (0Dh)) записывается в буфер, начиная с третьего байта. Во второй байт записывается длина реально введенной строки без учета последнего CR.

Рассмотрим пример программы, выполняющей преобразование десятичного числа в шестнадцатеричное.

; dosinl.asm ; Переводит десятичное число в шестнадцатеричное ; .model tiny .code .286 ; для команды shr al,4 org 100h ; начало СОМ-файла start: mov dx,offset message1 mov ah,9 int 21h ; вывести приглашение ко вводу message1 mov dx,offset buffer mov ah,0Ah int 21h ; считать строку символов в буфер mov dx,offset crlf mov ah,9 int 21h ; перевод строки

; перевод числа в ASCII-формате из буфера в бинарное число в АХ xor di,di ; DI = 0 - номер байта в буфере xor ах,ах ; АХ = 0 - текущее значение результата mov cl,blength xor ch,ch xor bx,bx mov si,cx ; SI - длина буфера mov cl,10 ; CL = 10, множитель для MUL asc2hex: mov bl,byte ptr bcontents[di] sub bl,'0' ; цифра = код цифры - код символа "0", jb asc_error ; если код символа был меньше, чем код "0", cmp bl,9 ; или больше, чем "9", ja asc_error ; выйти из программы с сообщением об ошибке, mul cx ; иначе: умножить текущий результат на 10, add ax,bx ; добавить к нему новую цифру, inc di ; увеличить счетчик cmp di,si ; если счетчик+1 меньше числа символов - jb asc2hex ; продолжить (счетчик считается от 0)

Управление файловой системой


Начиная с MS-DOS 2.0, файловая система организована в виде каталогов, которые могут содержать файлы и другие каталоги. Функции поиска файлов действуют только в пределах текущего каталога, а функции создания и удаления файлов не действуют на каталоги, несмотря на то, что на самом низком уровне каталог — тот же файл, в атрибуте которого бит 4 установлен в 1 и который содержит список имен вложенных файлов, их атрибутов и физических адресов на диске.

Функция DOS 39h — Создать каталог

Ввод: АН = 39h
DS:DX = адрес ASCIZ-строки с путем, в котором все каталоги, кроме последнего, существуют. Для версии DOS 3.3 и более ранних длина всей строки не должна превышать 64 байта
Вывод: CF = 0, если каталог создан
CF = 1 и АХ = 3, если путь не найден, 5 — если доступ запрещен

Функция LFN 39h — Создать каталог с длинным именем

Ввод: АХ = 7139h
DS:DX = адрес ASCIZ-строки с путем
Вывод: CF = 0, если каталог создан
CF = 1 и АХ = код ошибки (7100h, если функция не поддерживается)

Функция DOS 3Ah — Удалить каталог

Ввод: АН = 3Ah
DS:DX = адрес ASCIZ-строки с путем, последний каталог в котором будет удален (только если он пустой, не является текущим, не занят командой SUBST)
Вывод: CF = 0, если каталог удален
CF = 1 и АХ = 3, если путь не найден, 5 — если доступ запрещен, 10h — если удаляемый каталог — текущий

Функция LFN 3Ah — Удалить каталог с длинным именем

Ввод: АХ = 713Ah
DS:DX = адрес строки с путем
Вывод: CF = 0, если каталог удален, иначе CF = 1 и АХ = код ошибки

Функция DOS 47h — Определить текущий каталог

Ввод: АН = 47h
DL = номер диска (00h — текущий, 01h — А: и т.д.)
DS:SI = 64-байтный буфер для текущего пути (ASCIZ-строка без имени диска, первого и последнего символа «\»)
Вывод: CF = 0 и АХ = 0100h, если операция выполнена
CF = 1 и АХ = 0Fh, если указан несуществующий диск
<
/p> Функция LFN 47h — Определить текущий каталог с длинным именем

Ввод: АХ = 7147h
DL = номер диска
DS:SI = буфер для пути (ASCIZ- строка без имени диска, первого и последнего символа «\». Необязательно содержит только длинные имена — возвращается тот путь, который использовался при последней смене текущего каталога)
Вывод: CF = 0, если операция выполнена, иначе CF = 1 и АХ = код ошибки
Функция DOS 3Bh — Сменить каталог

Ввод: АН = 3Bh
DS:DX = адрес 64-байтного ASCIZ-буфера с путем, который станет текущим каталогом
Вывод: CF = 0, если произошла смена каталога, иначе CF = 1 и АХ = 3 (путь не найден)
Функция LFN 3Вh — Сменить каталог с длинным именем

Ввод: АХ = 713ВН
DS:DX = адрес ASCIZ-буфера с путем
Вывод: CF = 0, если произошла смена каталога, иначе CF = 1 и АХ = код ошибки
Перед работой с любыми функциями LFN следует один раз вызвать подфункцию A0h, чтобы определить размеры буферов для имен файлов и путей.

Функция LFN A0h — Получить информацию о разделе файловой системы VFAT

Ввод: АХ = 71A0Н
DS:DX = адрес ASCIZ-строки с именем раздела (например: db "С:\",0)
ES:DI = адрес буфера для имени файловой системы (FAT, NTFS, CDFS)
СХ = размер буфера в ES:DI (обычно достаточно 32 байта)
Вывод: СХ = 0, АХ = 0000h или 0200h

ВХ = флаги файловой системы:

бит 0: функции поиска учитывают регистр символов

бит 1: регистр символов сохраняется для имен каталогов

бит 2: используются символы Unicode

бит 14: поддерживаются функции LFN

бит 15: включено сжатие раздела (DoubleSpace)

СХ = максимальная длина имени файла (обычно 255)
DX = максимальная длина пути (обычно 260) в Windows 95 SP1 возвращает 0000h для CD-ROM
CF = 1 и АХ = код ошибки, если произошла ошибка (7100h, если функция не поддерживается)
Кроме того, при вызове любой функции LFN следует устанавливать CF в 1 для совместимости с ранними версиями DOS. Старые версии DOS не изменяли CF, так что в результате, если функция не поддерживается, CF останется равным 1.


Загрузка и выполнение программ


Как и любая операционная система, DOS загружает и выполняет программы. При загрузке программы в начале отводимого для нее блока памяти (для СОМ-программ это вся свободная на данный момент память) создается структура данных PSP (префикс программного сегмента) размером 256 байт (100h). Затем DOS создает копию текущего окружения для загружаемой программы, помещает полный путь и имя программы в конец окружения, заполняет поля PSP следующим образом:

+00h: слово — CDh 20h — команда INT 20h. Если СОМ-программа завершается командой RETN, управление передается на эту команду. Введено для совместимости с командой СР/М CALL 0.

+02h: слово — сегментный адрес первого байта после области памяти, выделенной для программы

+04h: байт — не используется DOS

+05h: 5 байт — 9Ah F0h FEh 1Dh F0h — команда CALL FAR на абсолютный адрес 000C0h, записанная так, чтобы второй и третий байты составляли слово, равное размеру первого сегмента для СОМ-файлов (в этом примере FEF0h). Введено для совместимости с командой СР/М CALL 5.

+0Ah: 4 байта — адрес обработчика INT 22h (выход из программы)

+0Eh: 4 байта — адрес обработчика INT 23h (обработчик нажатия Ctrl-Break).

+12h: 4 байта — адрес обработчика INT 24h (обработчик критических ошибок)

+16h: слово — сегментный адрес PSP процесса, из которого был запущен текущий.

+18h: 20 байт — JFT — список открытых идентификаторов, один байт на идентификатор, FFh — конец списка.

+2Ch: слово — сегментный адрес копии окружения для процесса.

+2Eh: 2 слова — SS:SP процесса при последнем вызове INT 21h.

+32h: слово — число элементов JFT (по умолчанию 20).

+34h: 4 байта — дальний адрес JFT (по умолчанию PSP:0018).

+38h: 4 байта — дальний адрес предыдущего PSP.


+3Ch: байт — флаг, указывающий, что консоль находится в состоянии ввода 2-байтного символа.

+3Dh: байт — флаг, устанавливаемый функцией В711h прерывания 2Fh (при следующем вызове INT 21h для работы с файлом имя файла будет замечено на полное).

+3Eh: слово — не используется в DOS.

+40h: слово — версия DOS, которую вернет функция DOS 30h (DOS 5.0+).

+42h: 12 байт — не используется в DOS.

+50h: 2 байта — CDh 21h — команда INT 21h.

+54h: 7 байт — область для расширения первого FCB.

+5Ch: 16 байт — первый FCB, заполняемый из первого аргумента командной строки.

+6Ch: 16 байт — второй FCB, заполняемый из второго аргумента командной строки.

+7Ch: 4 байта — не используется в DOS.

+80h: 128 байт — командная строка и область DTA по умолчанию.

и записывает программу в память, начиная с адреса PSP:0100h. Если загружается ЕХЕ-программа, использующая дальние процедуры или сегменты данных, DOS модифицирует эти команды так, чтобы используемые в них сегментные адреса соответствовали сегментным адресам, которые получили эти процедуры и сегменты данных при загрузке программы в память. При запуске СОМ-программы регистры устанавливаются следующим образом:

AL = FFh, если первый параметр командной строки содержит неправильное имя диска (например, z:/something), иначе — 00h.

АН = FFh, если второй параметр содержит неправильное имя диска, иначе 00h.

CS = DS = ES = SS = сегментный адрес PSP.

SP = адрес последнего слова в сегменте (обычно FFFEh; меньше, если не хватает памяти).

При запуске ЕХЕ-программы регистры SS:SP устанавливаются в соответствии с сегментом стека, определенным в программе, затем в любом случае в стек помещается слово 0000h и выполняется переход на начало программы (PSP:0100h для СОМ, собственная точка входа для ЕХЕ).

Все эти действия выполняет одна функция DOS — загрузить и выполнить программу.



Функция DOS 4Bh — Загрузить и выполнить программу

Ввод: АН = 4Bh

AL = 00h — загрузить и выполнить

AL = 01h — загрузить и не выполнять

DS:DX — адрес ASCIZ-строки с полным именем программы

ES:BX — адрес блока параметров ЕРВ:

+00h: слово — сегментный адрес окружения, которое будет скопировано для нового процесса (или 0, если используется текущее окружение)

+02h: 4 байта — адрес командной строки для нового процесса

+06h: 4 байта — адрес первого FCB для нового процесса

+0Ah: 4 байта — адрес второго FCB для нового процесса

+0Eh: 4 байта — здесь будет записан SS:SP нового процесса после его завершения (только для AL = 01)

+12h: 4 байта — здесь будет записан CS:IP (точка входа) нового процесса после его завершения (только для AL = 01)

AL = 03h — загрузить как оверлей

DS:DX — адрес ASCIZ-строки с полным именем программы

ES:BX — адрес блока параметров:

+00h: слово — сегментный адрес для загрузки оверлея

+02h: слово — число, которое будет использовано в командах, использующих непосредственные сегментные адреса, — обычно то же самое число, что и в предыдущем поле. 0 для СОМ-файлов

AL = 05h — подготовиться к выполнению (DOS 5.0+)

DS:DX — адрес следующей структуры:

+00h: слово — 00h

+02h: слово:бит 0 — программа — ЕХЕ
бит 1 — программа — оверлей

+04h: 4 байта — адрес ASCIZ-строки с именем новой программы

+08h: слово — сегментный адрес PSP новой программы

+0Ah: 4 байта — точка входа новой программы

+0Eh: 4 байта — размер программы, включая PSP
Вывод: CF = 0, если операция выполнена, ВХ и DX модифицируются,
CF = 1, если произошла ошибка, АХ = код ошибки (2 — файл не найден, 5 — доступ к файлу запрещен, 8 — не хватает памяти, 0Ah — неправильное окружение, 0Bh — неправильный формат)
<


/p> Для подфункций 00 и 01 требуется, чтобы было достаточно свободной памяти для загрузки программы, так что СОМ-программы должны воспользоваться функцией DOS 4Ah для уменьшения отведенного им блока памяти до минимально необходимого. При вызове подфункции 03 DOS загружает оверлей в память, выделенную текущим процессом, так что ЕХЕ-програмаш должны убедиться, что ее достаточно.

Эта функция игнорирует расширение файла и различает ЕХЕ- и СОМ-файлы по первым двум байтам заголовка («MZ» для ЕХЕ-файлов).

Подфункция 05 должна вызываться после загрузки и перед передачей управления на программу, причем никакие прерывания DOS и BIOS нельзя вызывать после возвращения из этой подфункции и до перехода на точку входа новой программы.

Загруженной и вызванной таким образом программе предоставляется несколько способов завершения работы. Способ, который чаще всего применяется для СОМ-файлов, — команда RETN. При этом управление передается на адрес PSP:0000, где располагается код команды INT 20h. Соответственно можно завершить программу сразу, вызвав INT 20h, но оба эти способа требуют, чтобы CS содержал сегментный адрес PSP текущего процесса. Кроме того, они не позволяют вернуть код возврата, который может передать предыдущему процессу информацию о том, как завершилась запущенная программа. Рекомендованный способ завершения программы — функция DOS 4Ch.

Функция DOS 4Ch — Завершить программу

Ввод: АН = 4Ch
AL = код возврата
Значение кода возврата можно использовать в пакетных файлах DOS как переменную ERRORLEVEL и определять из программы с помощью функции DOS 4Dh.

Функция DOS 4Dh — Определить код возврата последнего завершившегося процесса

Ввод: АН = 4Dh
Вывод: АН = способ завершения:00h — нормальный
01h — Ctrl-Break
02h — критическая ошибка
03h — программа осталась в памяти как резидентнаяAL = код возврата
CF = 0
Воспользуемся функциями 4Ah и 4Bh в следующем примере программы, которая ведет себя как командный интерпретатор, хотя на самом деле единственная команда, которую она обрабатывает, — команда exit. Все остальные команды передаются настоящему COMMAND.COM с ключом /С (выполнить команду и вернуться).

; shell.asm ; программа, выполняющая функции командного интерпретатора ; (вызывающая command.com для всех команд, кроме exit). .model tiny .code .186 org 100h ; СОМ-программа prompt_end equ "$" ; последний символ в приглашении ко вводу



start: mov sp,length_of_program+100h+200h ; перемещение стека на 200h ; после конца программы ; (дополнительные 100h - для PSP) mov ah,4Ah

stack_shift = length_of_program + 100h + 200h

mov bx,stack_shift shr 4+1 int 21h ; освободить всю память после конца ; программы и стека

; Заполнить поля ЕРВ, содержащие сегментные адреса mov ax,cs mov word ptr EPB+4,ax ; сегментный адрес командной строки mov word ptr EPB+8,ax ; сегментный адрес первого FCB mov word ptr EPB+0Ch,ax ; сегментный адрес второго FCB

main_loop:

; построение и вывод приглашения для ввода

mov ah,19h ; Функция DOS 19h int 21h ; определить текущий диск add al,'A' ; теперь AL = ASCII-код диска (А, В, С,) mov byte ptr drive_letter,al ; поместить его в строку mov ah,47h ; Функция DOS 47h mov dl,00 mov si,offset pwd_buffer int 21h ; определить текущий каталог mov al,0 ; найти ноль (конец текущего каталога) mov di,offset prompt_start ; в строке с приглашением mov cx,prompt_l repne scasb dec di ; DI - адрес байта с нулем mov dx,offset prompt_start ; DS:DX - строка приглашения sub di,dx ; DI - длина строки приглашения mov cx,di mov bx,1 ; stdout mov ah,40h int 21h ; вывод строки в файл или устройство mov al,prompt_end int 29h ; вывод последнего символа в приглашении

; получить команду от пользователя

mov ah,0Ah ; Функция DOS 0Ah mov dx,offset command_buffer int 21h ; буферированный ввод mov al,0Dh ; вывод символа CR int 29h mov al,0Ah ; вывод символа LF int 29h ; (CR и LF вместе - перевод строки) cmp byte ptr command_buffer+1,0 ; если введена пустая строка, je main_loop ; продолжить основной цикл

; проверить, является ли введенная команда командой "exit"

mov di,offset command_buffer+2 ; адрес введенной строки mov si,offset cmd_exit ; адрес эталонной строки "exit",0Dh mov ex,cmd_exit_l ; длина эталонной строки repe cmpsb ; сравнить строки jcxz got_exit ; если строки идентичны - выполнить exit

; передать остальные команды интерпретатору DOS (COMMAND.COM)

xor сх,сх mov si,offset command_buffer+2 ; адрес введенной строки mov di,offset command_text ; параметры для command.com mov cl,byte ptr command_buffer+1 ; размер введенной строки inc cl ; учесть 0Dh в конце rep movsb ; скопировать строку mov ax,4B00h ; функция DOS 4Bh mov dx,offset command_com ; адрес ASCIZ-строки с адресом mov bx,offset EPB int 21h ; исполнить программу jmp short main_loop ; продолжить основной цикл got_exit: int 20h ; выход из программы (ret нельзя, ; потому что мы перемещали стек)



cmd_exit db "exit",0Dh ; команда "exit" cmd_exit_l equ $-cmd_exit ; ее длина prompt_start db "tinyshell:" ; подсказка для ввода drive_letter db "С:" pwd_buffer db 64 dup (?) ; буфер для текущего каталога prompt_l equ $-prompt_start ; максимальная длина подсказки command_com db "С:\COMMAND.COM",0 ; имя файла EPB dw 0000 ; использовать текущее окружение dw offset commandline,0 ; адрес командной строки dw 005Ch,0,006Ch,0 ; адреса FCB, переданных DOS ; нашей программе при запуске ; (на самом деле они не используются) commandline db 125 ; максимальная длина командной строки db " /С" ; ключ /С для COMMAND.COM command_text db 122 dup (?) ; буфер для командной строки command_buffer db 122 ; здесь начинается буфер для ввода length_of_program equ 124+$-start ; длина программы + длина ; буфера для ввода end start

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

mov ah,47h

на

mov ax,7147h

и увеличить размер буфера для текущего каталога (pwd_buffer) с 64 до 260 байт, чтобы каталоги с длинными именами отображались корректно в подсказке для ввода. Но для совместимости следует также добавить проверку на поддержку функции 71h (LFN) и определить размер буфера для каталога с помощью подфункции LFN A0h.


Закрытие и удаление файла


Функция DOS 3Eh — Закрыть файл

Ввод: АН = 3Eh
ВХ = идентификатор
Вывод: CF = 0, если не произошла ошибка
CF = 1 и АХ = 6, если неправильный идентификатор

Если файл был открыт для записи, все файловые буфера сбрасываются на диск, устанавливается время модификации файла и записывается его новая длина.

Функция DOS 41h — Удаление файла

Ввод: АН = 41h
DS:DX = адрес ASCIZ-строки с полным именем файла
Вывод: CF = 0, если файл удален
CF = 1 и АН = 02h, если файл не найден, 03h — если путь не найден, 05h — если доступ запрещен

Удалить файл можно только после того, как он будет закрыт, так как DOS будет продолжать выполнять запись в несуществующий файл, что может привести к разрушению файловой системы. Функция 41h не позволяет использовать маски (символы * и ? в имени файла) для удаления сразу нескольких файлов, хотя этого можно добиться, вызывая ее через недокументированную функцию 5D00h. Но, начиная с DOS 7.0 (Windows 95), официальная функция удаления файла может работать сразу с несколькими файлами.

Функция LFN 41h — Удаление файлов с длинным именем

Ввод: АХ = 7141h
DS:DX = адрес ASCIZ-строки с длинным именем файла
SI = 0000h: маски не разрешены и атрибуты в СХ игнорируются
SI = 0001h: маски в имени файла и атрибуты в СХ разрешены:CL = атрибуты, которые файлы могут иметь
СН = атрибуты, которые файлы должны иметь
Вывод: CF = 0, если файл или файлы удалены
CF = 1 и АХ = код ошибки, если произошла ошибка. Код 7100h означает, что функция не поддерживается