В конец страницы

Знакомство с языком ассемблера.

  1. Работа с транслятором языка ассемблера.
  2. Регистры процессора.
  3. Память и процессор.
  4. Некоторые программные реализации.
  5. Стек.
  6. Вывод на экран символьной информации.

Работа с транслятором языка ассемблера.

Существует несколько версий программы ассемблер. Одним из наиболее часто используемых является пакет Turbo Assembler, водящий в состав комплекса программ Borland Pascal 7.0. Рассмотрим работу с этим пакетом более подробно.

Скачать транслятор Ассемблера можно здесь ASM.rar(560кб)

Входной информацией для ассемблера (TASM.EXE) является исходный файл — текст программы на языке ассемблера в кодах ASCII. В результате работы ассемблера может получиться до 3-х выходных файлов:
1) объектный файл – представляет собой вариант исходной программы, записанный в машинных командах;
2) листинговый файл – является текстовым файлом в кодах ASCII, включающим как исходную информацию, так и результат работы программы ассемблера;
3) файл перекрестных ссылок – содержит информацию об использовании символов и меток в ассемблерной программе (перед использованием этого файла необходима его обработка программой CREF).
Существует много способов указывать ассемблеру имена файлов. Первый и самый простой способ — это вызов команды без аргументов. В этом случае ассемблер сам поочередно запрашивает имена файлов: входной (достаточно ввести имя файла без расширения ASM), объектный, листинговый и файл перекрестных ссылок. Для всех запросов имеются режимы, применяемые по умолчанию, если в ответ на запрос нажать клавишу Enter:
- объектному файлу ассемблер присваивает то же имя, что и у исходного, но с расширением OBJ;
- для листингового файла и файла перекрестных ссылок принимается значение NUL — специальный тип файла, в котором все, что записывается, недоступно и не может быть восстановлено.
Если ассемблер во время ассемблирования обнаруживает ошибки, он записывает сообщения о них в листинговый файл. Кроме того, он выводит их на экран дисплея.
Другой способ указать ассемблеру имена файлов — это задать их прямо в командной строке через запятую при вызове соответствующей программы, например:
TASM Test, Otest, Ltest, Ctest
При этом первым задается имя исходного файла, затем объектного, листингового и, наконец, файла перекрестных ссылок. Если какое-либо имя пропущено, то это служит указанием ассемблеру сгенерировать соответствующий файл по стандартному соглашению об именах.
Программа, полученная в результате ассемблирования (объектный файл), еще не готова к выполнению. Ее необходимо обработать командой редактирования связей TLINK, которая может связать несколько различных объектных модулей в одну программу и на основе объектного модуля формирует исполняемый загрузочный модуль.
Входной информацией для программы TLINK являются имена объектных модулей (файлы указываются без расширение OBJ). Если файлов больше одного, то их имена вводятся через разделитель «+». Модули связываются в том же порядке, в каком их имена передаются программе TLINK. Кроме того, TLINK требует указания имени выходного исполняемого модуля. По умолчанию ему присваивается имя первого из объектных модулей, но с расширением ЕХЕ. Вводя другое имя, можно изменять имя файла, но не расширение. Далее можно указать имя файла, для хранения карты связей (по умолчанию формирование карты не производится). Последнее, что указывается программе TLINK – это библиотеки программ, которые могут быть включены в полученный при связывании модуль. По умолчанию такие библиотеки отсутствуют.
Информацию обо всех этих файлах программа TLINK запрашивает у пользователя после ее вызова.

Регистры процессора

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

Рис. 1.1 Регистры процессора

Регистры общего назначения. К ним относятся 16-разрядные регистры АХ, ВХ, СХ, DX, каждый из которых разделен на 2 части по 8 разрядов:
АХ состоит из АН (старшая часть) и AL (младшая часть);
ВХ состоит из ВH и BL;
СХ состоит из СН и CL;
DX состоит из DH и DL;
В общем случае функция, выполняемая тем или иным регистром, определяется командами, в которых он используется. При этом с каждым регистром связано некоторое стандартное его значение. Ниже перечисляются наиболее характерные функции каждого регистра:
- регистр АХ служит для временного хранения данных (регистр аккумулятор); часто используется при выполнении операций сложения, вычитания, сравнения и других арифметических и логических операции;
- регистр ВХ служит для хранения адреса некоторой области памяти (базовый регистр), а также используется как вычислительный регистр;
- регистр СХ иногда используется для временного хранения данных, но в основном служит счетчиком; в нем хранится число повторений одной команды или фрагмента программы;
- регистр DX используется главным образом для временного хранения данных; часто служит средством пересылки данных между разными программными системами, в качестве расширителя аккумулятора для вычислений повышенной точности, а также при умножении и делении.
Регистры для адресации. В микропроцессоре существуют четыре 16-битовых (2 байта или 1 слово) регистра, которые могут принимать участие в адресации операндов. Один из них одновременно является и регистром общего назначения — это регистр ВХ, или базовый регистр. Три другие регистра — это указатель базы ВР, индекс источника SI и индекс результата DI. Отдельные байты этих трех регистров недоступны.
Любой из названных выше 4 регистров может использоваться для хранения адреса памяти, а команды, работающие с данными из памяти, могут обращаться за ними к этим регистрам. При адресации памяти базовые и индексные регистры могут быть использованы в различных комбинациях. Разнообразные способы сочетания в командах этих регистров и других величин называются способами или режимами адресации.
Регистры сегментов. Имеются четыре регистра сегментов, с помощью которых память можно организовать в виде совокупности четырех различных сегментов. Этими регистрами являются:
- CS — регистр программного сегмента (сегмента кода) определяет местоположение части памяти, содержащей программу, т. е. выполняемые процессором команды;
- DS — регистр информационного сегмента (сегмента данных) идентифицирует часть памяти, предназначенной для хранения данных;
- SS — регистр стекового сегмента (сегмента стека) определяет часть памяти, используемой как системный стек;
- ES — регистр расширенного сегмента (дополнительного сегмента) указывает дополнительную область памяти, используемую для хранения данных.
Эти 4 различные области памяти могут располагаться практически в любом месте физической машинной памяти. Поскольку местоположение каждого сегмента определяется только содержимым соответствующего регистра сегмента, для реорганизации памяти достаточно всего лишь, изменить это содержимое.
Регистр указателя стека. Указатель стека SP – ЭТО 16-битовый регистр, который определяет смещение текущей вершины стека. Указатель стека SP вместе с сегментным регистром стека SS используются микропроцессором для формирования физического адреса стека. Стек всегда растет в направлении меньших адресов памяти, т.е. когда слово помещается в стек, содержимое SP уменьшается на 2, когда слово извлекается из стека, микропроцессор увеличивает содержимое регистра SP на 2.
Регистр указателя команд IP. Регистр указателя команд IP, иначе называемый регистром счетчика команд, имеет размер 16 бит и хранит адрес некоторой ячейки памяти – начало следующей команды. Микропроцессор использует регистр IP совместно с регистром CS для формирования 20-битового физического адреса очередной выполняемой команды, при этом регистр CS задает сегмент выполняемой программы, а IР – смещение от начала сегмента. По мере того, как микропроцессор загружает команду из памяти и выполняет ее, регистр IP увеличивается на число байт в команде. Для непосредственного изменения содержимого регистра IP служат команды перехода.
Регистр флагов. Флаги – это отдельные биты, принимающие значение 0 или 1. Регистр флагов (признаков) содержит девять активных битов (из 16). Каждый бит данного регистра имеет особое значение, некоторые из этих бит содержат код условия, установленный последней выполненной командой. Другие биты показывают текущее состояние микропроцессора.

Таблица 1.1. Флаги

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
X X X X OF DF IF TF SF ZF X AF X PF X CF

Биты регистра флагов имеют следующее назначение:
OF (признак переполнения) – равен 1, если возникает арифметическое переполнение, то есть когда объем результата превышает размер ячейки назначения;
DF (признак направления) – устанавливается в 1 для автоматического декремента в командах обработки строк, и в 0 – для инкремента;
IF (признак разрешения прерывания) – прерывания разрешены, если IF=1. Если IF=0, то распознаются лишь немаскированные прерывания;
TF (признаков трассировки) - если TF=1, то процессор переходит в состояние прерывания INT 3 после выполнения каждой команды;
SF (признак знака) – SF=1, когда старший бит результата равен 1. Иными словами, SF=0 для положительных чисел, и SF=1 для отрицательных чисел;
ZF (признак нулевого результата) – ZF=1, если результат равен нулю;
AF (признак дополнительного переноса) – этот флаг устанавливается в 1 во время выполнения команд десятичного сложения и вычитания при возникновении переноса или заема между полубайтами;
PF (признак четности) – этот признак устанавливается в 1, если результат имеет четное число единиц;
CF (признак переноса) – этот флаг устанавливается в 1, если имеет место перенос или заем из старшего бита результата; он полезен для произведения операций над числами длиной в несколько слов, которые сопряжены с переносами и заемами из слова в слово;
X – зарезервированные биты.
Легко заметить, что все флаги младшего байта регистра флагов устанавливаются арифметическими или логическими операциями процессора. За исключением флага переполнения, все флаги старшего байта отражают состояние микропроцессора и влияют на характер выполнения программы. Флаги старшего байта устанавливаются и сбрасываются специально предназначенными для этого командами. Флаги младшего байта являются кодами условного перехода для изменения порядка выполнения программы.

Память и процессор.

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

Рис. 1.2. Взаимодействие оперативной памяти и процессора

Выполнив первую команду, процессор переходит к следующей, и так далее до конца программы. Завершив программу, процессор не будет знать, что ему дальше делать, поэтому любая программа должна завершаться командами, передающими управление операционной системе компьютера. Оперативная память компьютера представляет собой электронное устройство, состоящее из большого числа двоичных запоминающих элементов, а также схем управления ими. Минимальный объем информации, к которому имеется доступ в памяти, составляет один байт (8 двоичных разрядов, или битов). Все байты оперативной памяти нумеруются, начиная с нуля. Нужные байты отыскиваются в памяти по их номерам, выполняющим функции адресов. Некоторые данные (например, коды символов) требуют для своего хранения одного байта; для других данных этого места на хватает, и под них в памяти выделяется 2, 4, 8 или еще большее число байтов. Обычно пары байтов называют словами, а четверки - двойными словами (рис.1.3), хотя иногда термином "слово" обозначают любую порцию машинной информации.

Рис. 1.3. Байт, слово и двойное слово

При обсуждении содержимого многобайтового данного приходится ссылаться на составляющие его байты; эти байты условно нумеруются от нуля и располагаются (при их изображении на бумаге) в порядке возрастания номера справа налево, так что слева оказываются байты с большими номерами, а справа - байты с меньшими номерами. Крайний слева байт принято называть старшим, а крайний справа - младшим. Такой порядок расположения байтов связан с привычной для нас формой записи чисел: в многоразрядном числе слева указываются старшие разряды, а справа - младшие. Следующее число, если его написать за предыдущим, опять начнется со старшего разряда и закончится младшим. Однако в памяти компьютера данные располагаются в более естественном порядке непрерывного возрастания номеров байтов и, таким образом, каждое слово или двойное слово в памяти начинается с его младшего байта и заканчивается старшим (рис.1.4).

Рис. 1.4. Нумерация байтов в многобайтовых данных

Строго говоря, в памяти компьютера можно хранить только целые двоичные числа, так как память состоит из двоичных запоминающих элементов. Для записи иных данных, например, символов или дробных чисел, для них предусматриваются правила кодирования, т.е. представления в виде последовательности битов той или длины. Так, действительное число одинарной точности занимает в памяти двойное слово (32 бит), в котором 23 бит отводятся под мантиссу, 8 бит под порядок и еще один бит под знак числа. Программы, работающие с такого рода данными, должны, естественно, знать правила их записи и руководствоваться ими при обработке и представлении этих данных. Двоичная система счисления, в которой работают все цифровые электронные устройства, неудобна для человека. Для удобства представления двоичного содержимого ячеек памяти или регистров процессора используют иногда восьмеричную, а чаще - шестнадцатеричную системы счисления. Для процессоров Intel используется шестнадцатеричная система. Каждый разряд шестнадцатеричного числа может принимать 16 значений, из которых первые 10 обозначаются обычными десятичными цифрами, а последние 6 - буквами латинского алфавита от А до F, где А обозначает 10, В - И, С - 12, D - 13, Е - 14, a F - 15. В языке ассемблера шестнадцатеричные числа, чтобы отличать их от десятичных, завершаются буквой h (или Н). Таким образом, 100 - это десятичное число, a l00h - шестнадцатеричное (равное 256). Поскольку одна шестнадцатеричная цифра требует для записи ее в память компьютера четырех двоичных разрядов, то содержимое байта описывается двумя шестнадцатеричными цифрами (от 00h до FFh, или от 0 до 255) , а содержимое слова - четырьмя (от 0000h до FFFFh, или от 0 до 65535). Помимо ячеек оперативной памяти, для хранения данных используются еще запоминающие ячейки, расположенные в процессоре и называемые регистрами. Достоинство регистров заключается в их высоком быстродействии, гораздо большем, чем у оперативной памяти, а недостаток в том, что их очень мало - всего около десятка. Поэтому регистры используются лишь для кратковременного хранения данных. В режиме МП 86, который мы здесь обсуждаем, все регистры процессора имеют длину 16 разрядов, или 1 слово (в действительности в современных процессорах их длина составляет 32 разряда, но в МП 86 от каждого регистра используется лишь его половина). За каждым регистром закреплено определенное имя (например, АХ или DS), по которому к нему можно обращаться в программе. Состав и правила использования регистров процессора будут подробно описаны ниже, здесь же мы коснемся только назначения сегментных регистров, с помощью которых осуществляется обращение процессора к ячейкам оперативной памяти. Казалось бы, для передачи процессору адреса какого-либо байта оперативной памяти, достаточно записать в один из регистров процессора его номер. В действительности поступить таким образом в 16-разрядном процессоре нельзя, так как максимальное число (данное или адрес), которое можно записать в 16-разрядный регистр, составляет всего 216 - 1 = 65535, или 64К-1, и мы получим возможность обращения лишь к первым 64 Кбайт памяти. Для того, чтобы с помощью 16-разрядных чисел адресовать любой байт памяти, в МП 86 предусмотрена сегментная адресация памяти, реализуемая с помощью сегментных регистров процессора. Суть сегментной адресации заключается в следующем. Обращение к памяти осуществляется исключительно с помощью сегментов - логических образований, накладываемых на те или иные участки физической памяти. Исполнительный адрес любой ячейки памяти вычисляется процессором путем сложения начального адреса сегмента, в котором располагается эта ячейка, со смещением к ней (в байтах) от начала сегмента (рис.1.6). Это смещение иногда называют относительным адресом.

Рис. 1.5. Образование физического адреса из сегментного адреса и смещения

Начальный адрес сегмента без четырех младших битов, т.е. деленный на 16, помещается в один из сегментных регистров и называется сегментным адресом. Сам же начальный адрес хранится в специальном внутреннем регистре процессора, называемом теневым регистром. Для каждого сегментного регистра имеется свой теневой регистр; начальный адрес сегмента загружается в него процессором в тот момент, когда программа заносит в соответствующий сегментный регистр новое значение сегментного адреса. Процедура умножения сегментного адреса на 16 (или, что то же самое, на 10h) является принципиальной особенностью реального режима, ограничивающей диапазон адресов, доступных в реальном режиме, величиной 1 Мбайт. Действительно, максимальное значение сегментного адреса составляет FFFFh, или 64К-1, из чего следует, что максимальное значение начального адреса сегмента в памяти равно FFFF0h, или 1 Мбайт - 16. Если, однако, учесть, что к начальному адресу сегмента можно добавить любое смещение в диапазоне от 0 до FFFFh, то адрес последнего адресуемого байта окажется равен 10FFEFh, что соответствует величине 1 Мбайт + 64 Кбайт - 17. Диапазон адресов, формируемых процессором, называют адресным пространством процессора; как мы видим, в реальном режиме он немного превышает 1 Мбайт. Заметим еще, что для описания адреса в пределах 1 Мбайт требуются 20 двоичных разрядов, или 5 шестнадцатеричных. Процессор 8086 имел как раз 20 адресных линий и не мог, следовательно, выйти за пределы 1 Мбайт; современным 32-разрядным процессорам, если они работают в реальном режиме, доступно несколько большее (почти на 64 Кбайт) адресное пространство. Если же процессор работает в защищенном режиме (с использованием 32-разрядных регистров), то его адресное пространство увеличивается до 232 = 4 Гбайт.

Некоторые программные реализации.

Начнем изучение языка ассемблера с рассмотрения простой программы, которая выводит на экран строку с текстом. В MS-DOS существует два типа исполняемых файлов, каждый из которых отличается структурой, ограничение на максимальный размер и т.д. Соответственно программы, предназначенные для получения каждого из типов файлов, будут немого различаться, все дело в инициализации основных сегментных регистров. В примере 1.1. будет приведена программа, ориентированная для получения исполняемого файла .EXE.

Пример 1.1. Простейшая программа.

text	segment	‘code’	         ;(1)Начало сегмента команд
	assume	CS:text, DS:text ;(2)Сопоставление сегментного регистра и сегмента
begin:	mov 	AX,text	         ;(3)Адрес сегмента команд занесен в регистр AX
	mov	DS,AX	         ;(4)А затем в DS
	mov	AH,09h	         ;(5)AH=09h номер функции вывода на экран
	mov	DX,offset message;(6)В DX заносится адрес выводимого сообщения
	int	21h	         ;(7)Вызов прерывания MS-DOS
	mov	AH,4Ch	         ;(8)AH=4Ch номер функции завершения программы
	mov	AL,00h	         ;(9)AL=00h, параметр функции 4Ch–код успешного завершения
	int	21h	         ;(10)пользовательской программы	
message	db	‘Ассемблер$’	 ;(11)Выводимое сообщение
text	ends		         ;(12)Конец сегмента команд
end	begin		         ;(13)Конец программы, с указание точки входа

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

mov ds,ax
MOV DS,AX
mov DS,AX

каждая строка воспринимается одинаково. Для того чтобы транслятор различал регистр символов необходимо трансляцию выполнять с ключом /ML, тогда строки text segment ‘code’ и TEXT SEGMENT ‘CODE’ фактически будут описывать начала совершенно разных сегментов, кроме того, в этом случае существенное внимание стоит уделять всем символическим обозначениям и меткам.
Программа содержит 13 строк - предложений языка ассемблера. Первое предложение с помощью оператора segment открывает сегмент команд нашей программы. Сегменту дается произвольное имя text. Описатель 'code' (так называемый класс сегмента) говорит о том, что это сегмент команд. В конце предложения после точки с запятой располагается комментарий. Таким образом, предложение языка ассемблера может состоять из четырех полей: имени, оператора, операндов и комментария, располагаемых в перечисленном порядке. Любая программа должна обязательно состоять из сегментов - без сегментов программ не бывает. Обычно в программе задаются три сегмента: команд, данных и стека, но мы в нашей простой программе пока ограничились одним сегментом команд. Во втором предложении мы с помощью оператора assume сообщаем ассемблеру (программе-транслятору), что сегментные регистры CS и DS будут указывать на один и тот же сегмент text. Сегментные регистры (а всего их в процессоре четыре) играют очень важную роль. Когда программа загружается в память и становится известно, по каким адресам памяти она располагается, в сегментные регистры заносятся начальные адреса закрепленных за ними сегментов. В дальнейшем любые обращения к ячейкам программы осуществляются путем указания сегмента, в котором находится интересующая нас ячейка, а также номера того байта внутри сегмента, к которому мы хотим обратиться. Этот номер носит название относительного адреса, или смещения. Поскольку в единственном сегменте нашей программы будут размещаться и команды, и данные, мы указываем ассемблеру оператором assume (assume - предположим), что и сегментный регистр команд CS, и сегментный регистр данных DS будут указывать на сегмент text. При этом в регистр CS адрес начала сегмента будет загружен автоматически, а регистр DS нам придется загружать (или, как говорят, инициализировать) "вручную". Строго говоря, в приведенной программе, где нет прямых обращений к ячейкам сегмента данных, не было необходимости сопоставлять в операторе assume сегмент text с сегментным регистром DS (сопоставление сегмента команд с сегментным регистром команд CS обязательно во всех случаях). Учитывая, однако, что практически в любой разумной программе обращения к полям данных имеются, мы с самого начала написали оператор assume в том виде, в каком он используется в реальных программах. Первые два предложения программы служат для передачи служебной информации программе ассемблера. Ассемблер воспринимает и запоминает эту информацию и пользуется ею в своей дальнейшей работе. Однако в состав выполнимой программы, состоящей из машинных кодов, эти строки не попадут, так как процессору, выполняющему программу, они не нужны. Другими словами, операторы segment и assume не транслируются в машинные коды, а используются лишь самим ассемблером на этапе трансляции программы. Такие нетранслируемые операторы иногда называют псеводооператорами, или директивами ассемблера в отличие от истинных операторов - команд языка. Предложение 3, начинающееся с метки begin, является первой выполнимой строкой программы. Для того, чтобы процессор знал, с какой строки начать выполнять программу после се загрузки в память, начальная метка программы указывается в качестве операнда самого последнего оператора программы end (см. предложение 13). Можно подумать, что указание точки входа в программу излишне: ведь как будто и так ясно, что программу надо начать выполнять с начала, а закончить, дойдя до конца. Однако в действительности для программ, написанных на языке ассемблера, это совсем не так! Текст программы может начинаться с описания вспомогательных подпрограмм или полей данных. В этом случае предложение программы, с которого нужно начать ее выполнение, может располагаться где-то в середине текста программы. И завершается выполнение программы совсем не обязательно в ее последних строках, а там, где стоят предложения вызова специальной программы операционной системы, предназначенной именно для завершения текущей программы и передаче управления системе (см. предложения 8.-.10). Однако, начиная от точки входа, программа выполняется строка за строкой точно в том порядке, в каком эти строки написаны программистом. В предложениях 3 и 4 выполняется инициализация сегментного регистра DS. Сначала значение имени text (т.е. адрес сегмента text) загружается командой mov (от move, переместить) в регистр общего назначения процессора АХ, а затем из регистра АХ переносится в регистр DS. Такая двухступенчатая операция нужна потому, что процессор в силу некоторых особенностей своей архитектуры не может выполнить команду непосредственной загрузки адреса в сегментный регистр. Приходится пользоваться регистром АХ в качестве "перевалочного пункта". Кстати, обратите внимание на то, что операнды в командах языка ассемблера записываются в несколько неестественном для европейца порядке - действие команды осуществляется справа налево. Предложения 5, 6 и 7 реализуют существо программы - вывод на экран строки текста. Делается это не непосредственно, а путем обращения к служебным программам операционной системы MS-DOS, которую мы для краткости будет в дальнейшем называть просто DOS. Дело в том, что в составе команд процессора и, соответственно, операторов языка ассемблера нет команд вывода данных на экран (как и команд ввода с клавиатуры, записи в файл на диске и т.д.). Вывод даже одного символа на экран в действительности представляет собой довольно сложную операцию, для выполнения которой требуется длинная последовательность команд процессора. Конечно, эту последовательность команд можно было бы включить в нашу программу, однако гораздо проще обратиться за помощью к операционной системе. В состав DOS входит большое количество программ, осуществляющих стандартные и часто требуемые функции - вывод на экран и ввод с клавиатуры, запись в файл и чтение из файла, чтение или установка текущего времени, выделение или освобождение памяти и многие другие. Для того, чтобы обратиться к DOS, надо загрузить в регистр общего назначения АН номер требуемой функции, в другие регистры - исходные данные для выполнения этой функции, после чего выполнить команду int 21h, (int: - от interrupt, прерывание), которая передаст управление DOS. Вывод на экран строки текста можно осуществить функцией 09h, которая требует, чтобы в регистре АХ содержался адрес выводимой строки. В предложении 6 адрес строки message загружается в регистр DX, а в предложении 7 осуществляется вызов DOS. После того, как DOS выполнит затребованные действия, в данном случае выведет на экран текст "Ассемблер", выполнение программы продолжится. Вообще-то нам вроде бы ничего больше делать не нужно. Однако на самом деле это не так. После окончания работы пашей программы DOS должна выполнить некоторые служебные действия. Надо освободить занимаемую нашей программой память, чтобы туда можно было загрузить следующую программу. Надо вызвать системную программу, которая выведет на экран запрос DOS и будет ждать следующей команды оператора. Все эти действия выполняет функция DOS с номером 4Сh. Эта функция предполагает, что в регистре AL находится код завершения пашей программы, который она передаст DOS. При желании код завершения только что закончившейся программы можно "выловить" в DOS и проанализировать, но сейчас мы этим заниматься не будем. Если программа завершилась успешно, код завершения должен быть равен 0, поэтому в предложении 9 мы загружаем 0 в регистр AL и вызываем DOS уже знакомой командой int 21h. После последнего выполнимого предложения программы можно описывать используемые в ней данные. У нас в качестве данных выступает строка текста. Текстовые строки вводятся в программу с помощью директивы ассемблера db (от define byte, определить байт), и заключаются в апострофы. Для того, чтобы в программе можно было обращаться к данным, поля данных, как правило, предваряются именами. В нашем случае таким именем является вполне произвольное обозначение message, с которого начинается предложение 11.Выше, в предложении 6, мы через регистр DX передали DOS адрес начала выводимой на экран строки текста. Но как DOS определит, где эта строка закончилась? Хотя нам конец строки в программе отчетливо виден, однако в машинных кодах, из которых состоит выполнимая программа, он никак не отмечен, и DOS, выведя на экран слово "просто", продолжит вывод байтов памяти, расположенных за фразой. Поэтому DOS следует передать информацию о том, где кончается строка текста. Некоторые функции DOS требуют указания в одном из регистров длины выводимой строки, однако функция 09h работает иначе. Она выводит текст до символа $, которым мы и завершили нашу фразу. Директива ends (end segment, конец сегмента) в предложении 12 указывает ассемблеру, что сегмент text закончился. Последняя строка программы содержит директиву еnd. которая говорит программе ассемблера, что закончился вообще весь текст программы, и больше ничего транслировать не нужно. В качестве операнда этой директивы, как уже отмечалось, обычно указывается точка входа в программу, т.е. адрес первой выполнимой программной строки. В нашем случае это метка begin.

Замечание

Для того, чтобы инициализировать сегментный регистр DS сегментным адресом text нашего (единственного) сегмента, значение text загружается сначала в регистр общего назначения АХ, а из него - в сегментный регистр DS. В принципе в качестве "перевалочного пункта" вместо регистра АХ можно взять любой другой (например, ВХ или SI), однако некоторым трансляторам это может не понравиться, так что лучше все-таки использовать АХ. В предложении 5 в регистр АН заносится номер функции DOS, реализующей вывод на экран строки текста. DOS, получив управление с помощью команды int -21h, определяет номер требуемой функции именно по содержимому регистра АН, поэтому никаким другим регистром здесь воспользоваться нельзя. Функция DOS вывода строки извлекает адрес выводимой строки из регистра DX, поэтому в строке б использование регистра DX также предопределено. В действительности дело обстоит сложнее. Функция 09h предполагает, что строка с выводимым текстом находится в сегменте, на который указывает вполне определенный сегментный регистр, именно регистр DS. Поэтому перед вызовом функции 09h необходимо настроить этот регистр, что мы и сделали в предыдущих предложениях программы. Сведения о том, какие регистры требуется настроить для выполнения той или иной функции DOS, можно почерпнуть из справочника по функциям DOS, без которого, таким образом, практически невозможно писать программы с обращениями к системным средствам. В предложениях 8...10 осуществляется вызов системной функции 4Ch, служащей для завершения текущей программы. По-прежнему номер функции заносится в регистр АН; кроме этого, в AL можно поместить код завершения программы, который в нашем примере равен 0. В данном примере мы использовали лишь один сегмент, в котором располагались и команды, и данные. Такая конструкция программы вполне законна, но не очень наглядна. Кроме того, предусмотрев в программе лишь один сегмент, мы ограничили суммарный объем команд и данных величиной 64К. Разумнее разнести команды и данные по отдельным сегментам, что продемонстрировано в примере 1.2.

Рис. 1.6. Организация стека: а - исходное состояние, б - после загрузки одного элемента (в данном примере – содержимого регистра AX), в - после загрузки второго элемента (содержимого регистра DS), г - после выгрузки одного элемента, д - после выгрузки двух элементов и возврата в исходное состояние.

Загрузка в стек осуществляется специальной командой работы со стеком push (протолкнуть). Эта команда сначала уменьшает на 2 содержимое указателя стека, а затем помещает операнд по адресу в SP. Если, например, мы хотим временно сохранить в стеке содержимое регистра AX, следует выполнить команду
push AX
Стек переходит в состояние, показанное на рис.1.7,б. Видно, что указатель стека смещается на два байта вверх и по этому адресу записывается указанный в команде проталкивания операнд. Следующая команда загрузки в стек, например,
push DS
переведет стек в состояние, показанное на рис.1.7,в. В стеке будут теперь храниться два элемента, причем доступным будет только верхний, на который указывает указатель стека SP. Если спустя какое-то время нам понадобилось восстановить исходное содержимое сохраненных в стеке регистров, мы должны выполнить команды выгрузки из стека pop (вытолкнуть):
pop DS
pop AX
Состояние стека после выполнения первой команды показано на рис.1.7,г, а после второй – на рис.1.7,д. Для правильного восстановления содержимого регистров выгрузка из стека должна выполняться в порядке, строго противоположном загрузке – сначала выгружается элемент, загруженный последним, затем предыдущий элемент и т.д.
Обратите внимание на то, что после выгрузки сохраненных в стеке данных они физически не стерлись, а остались в области стека на своих местах. Правда, при “стандартной” работе со стеком они оказываются недоступными. Действительно, поскольку указатель стека SP указывает под дно стека, стек считается пустым; очередная команда push поместит новое данное на место сохраненного ранее содержимого AX, затерев его. Однако, пока стек физически не затерт, сохраненными и уже выбранными из него данными можно пользоваться, если помнить, в каком порядке они расположены в стеке. Этот прием часто используется при работе с подпрограммами и в дальнейшем будет описан подробнее. В примерах 1.1 и 1.2 мы не заботились о стеке, поскольку, на первый взгляд, нашей программе стек был не нужен. Однако на самом деле это не так. Стек автоматически используется системой в ряде случаев, в частности, при переходе на подпрограммы и при выполнении команд прерывания int. И в том, и в другом случае процессор заносит в стек адрес возврата, чтобы после завершения выполнения подпрограммы или программы обработки прерывания можно было вернутся в ту точку вызывающей программы, откуда произошел переход. Поскольку в нашей программе есть две команды int 21h, операционная система при выполнении программы дважды обращалась к стеку. Рассмотрим следующий пример.

Пример 1.3. Программа, работающая со стеком.

text	Segment 'code'	              ;(1) Начало сегмента команд
	Assume CS: text, DS: data     ;(2)  CS - на сегмент команд,
			              ;DS - на сегмент данных
begin:	Mov	AX, data	      ;(3) Адрес сегмента данных загрузим
	Mov	DS, AX	              ;(4) сначала в АХ, затем в DS
	Push	DS	              ;(5) Загрузим в стек содержимое DS
	Pop	ES	              ;(6) Выгрузим его из стека в ES
	mov	AH, 9	              ;(7) Функция DOS вывода на экран
	mov	DX,offset message     ;(8) Адрес выводимого сообщения
	int	21h 	              ;(9) Вызов DOS
	mov	AX,4C00h	      ;(10) Функция DOS завершения программы
			              ;с указанием кода завершения 00h
	int	21h	              ;(11) Вызов DOS
text	ends		              ;(12) Конец сегмента команд
data	segment	                      ;(13) Начало сегмента данных
message	Db ‘Знание - сила$ ‘	      ;(14) Выводимый текст
data	ends		              ;(15) Конец сегмента данных
	end	begin 	              ;(16) Конец текста программы с указанием  точки
			              ;входа точки входа

В предложении 5 содержимое DS сохраняется в стеке, а в следующем предложении выгружается из стека в ES. После этой операции оба сегментных регистра, и DS, и ES, будут указывать на один и тот же сегмент данных. В нашей программе эти строки не имеют практического смысла, но вообще здесь продемонстрирован удобный прием переноса содержимого одного сегментного регистра в другой. Выше уже отмечалось, что в силу особенностей архитектуры микропроцессора для сегментных регистров действуют некоторые ограничения. Так, в сегментный регистр нельзя непосредственно загрузить адрес сегмента; нельзя также перенести число из одного сегментного регистра в другой. При необходимости выполнить последнюю операцию в качестве “перевалочного пункта” часто используют стек.
Запустите под управлением отладчика программу 1.3. Посмотрите, чему равно содержимое регистров SS и SP. Вы увидите, что в SS находится тот же адрес памяти, что и в CS; отсюда можно сделать вывод, что сегменты команд и стека совпадают. Однако содержимое SP равно 0. Первая же команда PUSH уменьшит содержимое SP на 2, т.е. поместит в SP –2. Значит ли это, что стек будет расти, как ему и положено, вверх, но не внутри сегмента команд, а над ним по адресам –2, -4, -6 и т.д. относительно верхней границы сегмента команд? Оказывается, это не так. Если взять 16 – разрядный счетчик, в котором записан 0, и послать в него два вычитающих импульса, то после первого импульса в нем окажется число FFFFh, а после второго – FFFEh. При желании мы можем рассматривать число FFFEh, как 2 – (что и имеет место при работе со знаковыми числами, о которых будет идти речь позже), однако процессор при вычислении адресов рассматривает содержимое регистров, как целые числа без знака, и число FFFEh оказывается эквивалентным не –2, а 65534. В результате первая же команда занесения данного в стек поместит это данное не над сегментом команд, а в самый его конец, в последнее слово по адресу CS:FFFEh. При дальнейшем использовании стека его указатель будет смещаться в сторону меньших адресов, проходя значения FFFCh. FFFAh и т.д. Таким образом, если в программе отсутствует явное объявление стека, система сама создает стек по умолчанию в конце сегмента команд. Рассмотренное явление, когда при уменьшении адреса после адреса 0 у нас получится адрес FFFFh, т.е. от начала сегмента мы прыгнули сразу в его конец, носит название циклического возврата или оборачивания адреса. С этим явлением приходится сталкиваться довольно часто. Расположение стека в конце сегмента команд не приводит к каким-либо неприятностям, пока размер программы далек от граничной величины 64К. в этом случае начало сегмента команд занимают коды команд, а конец-стек. Если, однако, размер программы приближается к 64К, то для стека остается все меньше места. При интенсивном использовании стека в программе может получиться, что по мере занесения в стек новых данных, стек дорастет до последних команд сегмента команд и начнет затирать эти команды. В то же время система не проверяет, что происходит со стеком и никак не реагирует на затирание команд или данных. Таким образом, оценка размеров собственно программы, данных и стека является важным этапом разработки программы. Современные программы часто имеют значительный размер (даже не помещаясь в один сегмент команд), а стек иногда используется для хранения больших по объему массивов данных. Поэтому целесообразно ввести в программу отдельный сегмент стека, определив его размер, исходя из требований конкретной программы. Это и сделано в следующем примере. При загрузке программы в память она будет выглядеть так, как показано на рис. 1.7.

Рис. 1.7. Образ памяти программы .EXE со стеком по умолчанию.

Образ программы в памяти начинается с очень важной структуры данных, которую называют префиксом программы. PSP образуется и заполняется системой в процессе загрузки программы в память. Он всегда имеет размер 256 байт и содержит поля данных, используемые системой (а часто и самой программой) в процессе выполнения программы.
Вслед за PSP располагаются сегменты программы. Поскольку объявления сегментов сделаны нами наипростейшим образом (операторы segment не сопровождаются операндами-описателями), порядок размещения сегментов в памяти совпадает с порядком их объявления в программе, что упрощает исследование и отладку программы. Для большинства программ не имеет значение, в каком порядке вы будете объявлять сегменты, хотя встречаются программы, для которых порядок сегмента существен. Для таких программ предложение с операторами segment будут выглядеть сложнее.
В процессе загрузки программы в память сегментные регистры автоматически инициализируются следующим образом: ES и DS указывает на начало PSP (что дает возможность, сохранив их содержимое, обращаться затем к программе PSP), CS-на начало сегмента команд. SS, как мы экспериментально убедились, также в нашем случае указывает на начало сегмента команд. Как мы увидим позже, верхняя половина PSP занята важной для системы и самой программы информацией, а нижняя половина (128 байт) практически свободна. Поскольку после загрузки программы в память оба сегментных регистра данных указывают на PSP, сегмент данных программы оказывается не адресуемым. Если не инициализировать регистр DS так, как это сделано в предложениях 3-4, то нельзя будет обращаться к данным. При этом транслятор не выдаст никаких ошибок, но программа будет выполняться неправильно (проведите эксперимент – уберите строки инициализации регистра DS).
Рассмотрим теперь программу с тремя сегментами: команд, данных и стека. Такая структура широко используется для относительно несложных программ.

Пример 1.4.Программа с тремя сегментами.

text	segment 'code'	             ;(1) Начало сегмента команд
	assume CS: text, DS: data    ;(2) CS – на сегмент команд,
			             ;DS – на сегмент данных
begin:	mov	AX, data	     ;(3) Адрес сегмента данных загрузим
	mov	DS, AX	             ;(4) сначала в АХ, затем в DS
	push	DS	             ;(5) Загрузим в стек содержимое DS
	pop	ES	             ;(6) Выгрузим его из стека в ES
	mov	AH, 9	             ;(7) Функция DOS вывода на экран
	mov	DX,offset message    ;(8) Адрес выводимого сообщения
	int	21h 	             ;(9) Вызов DOS
	mov	AX,4C00h	     ;(10) Функция DOS завершения программы
	int	21h	             ;(11) Вызов DOS
text	ends		             ;(12) Конец сегмента команд
data	segment	                     ;(13) Начало сегмента данных
message	Db ‘Знание - сила$ ‘	     ;(14) Выводимый текст
data	ends		             ;(15) Конец сегмента данных
stk	Segment stack ‘stack’	     ;(16) Начало сегмента стека
	dw	128 dup	             ;(17) Под стек отведено 128 слов
stk	ends		             ;(18) конец сегмента стека
	end	begin 	             ;(19) Конец текста программы 

В примере 1.4 вслед за сегментом данных объявлен еще один сегмент, которому мы дали имя stk. Так же, как и другие сегменты, сегмент стека можно назвать как угодно. Строка описания сегмента стека (предложение 16) должна содержать так называемый тип объединения, в данном случае описатель stack. Тип объединения указывает компоновщику, каким образом должны объединяться одноименные сегменты разных программных модулей, и используется главным образом в тех случаях, когда отдельные части программы располагаются в разных исходных файлах (например, пишутся несколькими программистами) и объединяются на этапе компоновки. Хотя для одномодульных программ тип объединения обычно не имеет значения, для сегмента стека обязательно указание типа stack, поскольку в этом случае при загрузки программы выполняется автоматическая инициализация регистров SS (сегментным адресом стека) и SP (смещением конца сегмента стека). В предложении 16 объявляющим сегмент стека, имеется еще один описатель. Слово ‘stack’, стоящее в апострофах после оператора segment, определяет класс сегмента (в принципе имена классов можно выбирать произвольно). Классы сегментов анализируются компоновщиком и используются им при компоновки загрузочного модуля: сегменты, принадлежащие одному классу загружаются в память рядом. Для простых программ, входящих в единственный файл с исходным текстом и включающих по одному сегменту команд, данных и стека, указание класса не обязательно, однако для правильной работы компановщиков и отладчиков желательно, а в некоторых случаях необходимо указание классов сегментов: ‘code’ для сегмента команд и ‘stack’ для сегмента стека. Так, отладчик Code View не сможет реализовать некоторые из своих режимов вывода на экран текста программы без указания класса ‘code’ , а компоновщик TLINK не будет инициализировать сегмент стека, если не объявлен его класс ‘stack’. В приведенном примере для стека зарезервировано 128 слов памяти, что более чем достаточно для несложной программы.

Вывод на экран символьной информации.

Пример 1.5. Вывод на экран информационного сообщения.
Text    segment			  	;Начало сегмента команд
assume CS:text,DS:data
Begin:  mov AX,data			;Инициализация сегментного
mov DS,AX				;регистра DS
mov AH,09				;Функция DOS вывода на экран
mov DX,offset message		        ;Адрес выводимого сообщения
int 21h				        ;Вызов DOS
mov AX,4C00h			        ;Завершение программмы
int 21h
text    ends				;Конец сегмента команд
data    segment				;Начало сегмента данных
message db 80*18 dup (‘ ‘)
db 9,’Другую группу составляют различные аспекты реализации’
db’в программах на’,10,13,’ языке ассемблера аппаратных и’
db’системных возможностей персонального компьютера:’,10,13
db 9,’- программирование ввода-вывода;’,10,13
db 9,’- использование прерываний BIOS и функций DOS;’,10,13
db 9,’- программирование арифметического сопроцессора;’,10,13
db 9,’- работа в защищенном режиме;’,10,13
db 9,’- управление обычной и расширенной памятью.$’
data    ends				;Конец сегмента данных
stk     segment stack ’stack’		;Начало сегмента стека
	  dw 128 dup (0)		;Стек
stk     ends				;Конец сегмента стека
	  end begin			;Конец текста программы

Заметим, что эта программа написана стандартным образом: в ней имеются сегменты команд, данных и стека. Текст, предназначенный для вывода на экран, включен в программу с помощью директивы db (определить байт). В конце текста включен знак ‘$’, характеризующий конец строки для функции DOS 09h. Для получения более удобочитаемого вывода в символьную строку включены коды управления курсором:
09 - табуляция;
10 - перевод строки;
13 - «возврат каретки», т.е. возврат курсора в начало строки экрана.
Пробелы (18 строк по 80 пробелов в строке) описаны с помощью оператора dup (duplicate, дублировать). Перед словом dup указывается коэффициент повторения (в котором можно использовать арифметические выражения), а после оператора dup в скобках - повторяемая строка (состоящая не обязательно из одного символа). Коды табуляции создадут отступы красных строк. Для того, чтобы текст на экране выглядел аккуратно, мы а середину первого абзаца включили коды 10 и 13 перехода на следующую строку.

В начало страницы