Сообщений 0 Оценка 0 Оценить |
Трудно по-своему выразить общеизвестные истины. Квинт Гораций Флакк (65-8 гг. до н.э.)
Язык Ассемблера можно изучать по-разному. Можно начинать изучать язык сам по себе, а за результатами следить через отладчик — это обычный, классический путь (см. п. 5.1.1.3).
Можно изучать Ассемблер, делая полноценные ассемблерные модули (но сначала БЕЗ ввода-вывода) и используя в качестве помощников уже известные алгоритмические языки, например, такие, как Pascal или C/C++. В данной книге в основном применен именно этот, ВТОРОЙ, подход. Многолетний и многоассемблерный опыт преподавания убедил автора этой книги в его целесообразности. Используя знакомый алгоритмический язык в качестве помощника и не вникая сначала в особенности ввода-вывода на Ассемблере и организации исполняемых COM- и EXE-файлов, можно начать с простых команд типа a+b (а в Ассемблере, да и в любом алгоритмическом языке ГРАМОТНОЕ решение этой на вид простой задачки дело совсем НЕПРОСТОЕ!). А освоившись с азами программирования на Ассемблере, мы закончим программами на чистом Ассемблере с использованием прерываний и прочих прибамбасов.
Достоинства этого подхода, по мнению автора, заключаются в следующем:
Недостатки:
Именно по этому пути мы и пойдем с самого начала (см. п. 5.1.1.2). Таким образом, различные тонкости языка Ассемблера будут раскрываться постепенно, по мере усвоения материала и появления опыта, а значит, и уверенности в своих силах.
Есть еще и ТРЕТИЙ путь — использование встроенного Ассемблера. Этот метод мы тоже будем использовать по мере необходимости и там, где это целесообразно. Но встроенный Ассемблер лишен прелести настоящего Ассемблера: он ОЧЕНЬ зависит от среды его реализации и МНОГИХ команд в нем может просто НЕ быть (особенно это касается Borland/Turbo Pascal). Кроме того, зачастую встроенный Ассемблер скрывает (маскирует) КРУПНЫЕ огрехи, которые немедленно возникают в настоящем Ассемблере.
Итак, из п. 2.4.1 мы уже знаем, что в языке Ассемблера существуют команды и директивы, формат которых практически одинаков — см. рис. 2.12. В командах Имя интерпретируется как метка, поэтому за ней всегда ставится символ двоеточия ':'. Для Ассемблера в качестве имени (идентификатора) допускаются следующие символы:
Имя в Ассемблере может начинаться с любого допустимого символа, кроме цифры. Если имя содержит символ точки '.', то он должен быть ПЕРВЫМ символом. Имя НЕ может быть зарезервированным в Ассемблере словом (имя машинной команды или директивы).
С некоторыми директивами мы с вами уже успели и поработать — см. примеры 2.9 и 2.10.
При ассемблировании между директивами и командами существует одно принципиальное различие. Директивы (псевдооператоры, псевдокоманды) управляют работой компилятора или компоновщика, а НЕ микропроцессора. Они используются, например, для сообщения компилятору, какие константы и переменные применяются в программе и какие имена мы им дали, какой сегмент является кодовым, а какой — сегментом данных или стека, в каком формате выводить листинг исходного кода программы и прочее. Большинство директив НЕ генерирует машинных команд (объектный код).
Команда Ассемблера всегда генерирует машинный код.
Директивы имеют разный синтаксис в режимах MASM (поддерживается компиляторами Microsoft Assembler — masm и Borland (Turbo) Assembler — tasm) и Ideal (поддерживается компилятором tasm) — см. приложение 5.
При описании синтаксиса директив и команд обычно используют специальный язык, известный всем профессиональным программистам, — язык Бэкуса-Наура (названный так в честь этих двух достойных ученых). Мы будем использовать его упрощенный вариант:
Директив достаточно много. Практически каждая версия компилятора что-то из директив добавляет или немного изменяет синтаксис. Для определенности мы в данной главе остановимся на синтаксисе основных директив в более универсальном формате MASM для компиляторов masm-6.12 (или выше) и tasm-3.1 (или выше). Кроме того, известные компиляторы с языка программирования С/С++ (Borland C++, Visual C++) выдают ассемблерный листинг именно в формате MASM. И очень скоро мы научимся его читать…
Из п. 2.4.3 мы знаем, что любые ассемблерные программы содержат, по крайней мере, один сегмент — сегмент кода. В некоторых программах используется сегмент для стековой памяти и сегмент данных (основной и дополнительный) для определения данных.
Универсальная директива для описания сегмента имеет следующий формат: имяС SEGMENT [параметры] ; начало СЕГМЕНТА имяС . . . имяС ENDS ; конец СЕГМЕНТА имяС |
Имя сегмента (имяС) должно обязательно присутствовать, быть уникальным и соответствовать соглашениям для имен в Ассемблере или в другом алгоритмическом языке, для стыковки с которым делается ассемблерный модуль. Например, при стыковке Ассемблера с Turbo/Borland Pascal имяС должно быть СТРОГО определенным:
Директива ENDS обозначает конец сегмента. Обе директивы SEGMENT и ENDS должны иметь одинаковые имена имяС.
В одном модуле можно открывать и закрывать сегмент с одним и тем же именем имяС несколько раз, но пересекаться (вкладываться друг в друга) разные сегменты НЕ должны. Компилятор просматривает ассемблерный модуль и объединяет вместе все части сегментов с одинаковым именем в том порядке, в каком он их обнаруживает (сверху-вниз).
Директива SEGMENT может содержать три основных типа необязательных параметров, определяющих выравнивание (align), объединение (combine) и класс ('class'), между которыми должен быть хотя бы один пробел в качестве разделителя. Параметры имеют смысл при разработке БОЛЬШИХ ассемблерных программ.
1. Выравнивание (align). Этот параметр сообщает компоновщику, чтобы он разместил данный сегмент, начиная с указанной границы. Это может быть ОЧЕНЬ важно, т.к. при правильном выравнивании данные загружаются процессором с большей скоростью. При попытке сосчитать невыравненные данные процессор сделает одно из двух: либо возбудит исключение, либо сосчитает их в несколько приемов (конечно, при этом быстродействие программы снизится) [Л7-8].
Параметр | Значение |
---|---|
BYTE | Выравнивание НЕ выполняется. Сегмент размещается, начиная со следующего байта. |
WORD | Начало сегмента выравнивается на границу слова (четный адрес, кратный 2) |
DWORD | Начало сегмента выравнивается на границу двойного слова (четный адрес, кратный 4) |
PARA | Начало сегмента выравнивается на границу параграфа (четный адрес, кратный 16, см.п.3.4.3). Это значение принято по умолчанию. |
PAGE | Начало сегмента выравнивается на границу страницы (четный адрес, кратный 256) |
MEMPAGE | Начало сегмента выравнивается на границу страницы памяти (четный адрес, кратный 4K) |
2. Объединение (combine). Настоящий элемент предназначен для указания компоновщику, каким образом объединять сегменты, находящиеся в разных модулях и имеющие одинаковые имена, или как соединить данный сегмент с другими сегментами в процессе компоновки после ассемблирования.
3. Класс ('class'). Данный элемент, заключенный в апострофы, используется для группирования сегментов при компоновке. Компоновщик группирует вместе все сегменты с ОДИНАКОВЫМ классом.
Параметр | Значение |
---|---|
PRIVATE | Сегмент НЕ будет объединяться с сегментами с тем же именем, находящимися в других модулях. Это значение принято по умолчанию. |
PUBLIC | Сегмент будет объединяться с сегментами с тем же именем, находящимися в других модулях или в том же модуле, в один сегмент. |
MEMORY | Параметр подобен PUBLIC, но для сегмента стека. |
COMMON | Размещает данный сегмент и все сегменты с тем же именем по ОДНОМУ и тому же адресу. Получаются перекрывающиеся сегменты (overlay), занимающие разделяемую область памяти (shared memory). |
VIRTUAL | Описывает сегмент специального вида, который должен объявляться ВНУТРИ другого сегмента (общая область памяти). |
AT xxx | Устанавливает сегмент по абсолютному адресу параграфа xxx для получения доступа по идентификаторам к фиксированным адресам памяти (например, видеобуфер, таблица векторов прерываний – см. прил.6) |
Например, сегмент стека, в котором зарезервировано 100*8 = 800 байтов (со словом My Stack — 8 символов) может быть описан следующим образом:
SStack SEGMENT PARA PUBLIC 'Stack' DB 100 dup ('My Stack') SStack ENDS |
Эта директива используется для объединения сегментов в группу. Она имеет следующий формат:
имяG GROUP имяС1[, имяС2…] |
Группа позволяет осуществлять доступ к данным из всех сегментов, которые находятся в ней, с помощью одной загрузки адреса группы в сегментный регистр. Этим широко пользуются компиляторы С/С++.
Эта директива сообщает компилятору, что указанный в ней сегментный регистр необходимо связать с именем сегмента или группы. Она имеет следующий формат:
ASSUME сегм_регистр1:имя1[, сегм_регистр2: имя2…] |
В качестве сегментных регистров для базового Ассемблера принимаются уже известные нам регистры: CS, DS, ES или SS.
Для ОТМЕНЫ назначения для данного сегментного регистра используется ДРУГОЙ формат этой директивы:
ASSUME сегм_регистр1:NOTHING[,сегм_регистр2: NOTHING …] |
Чаще всего эта директива используется в начале модуля на Ассемблере.
Рассмотрим фрагмент листинга ассемблерного файла, который был получен компилятором Borland C++ 5.02:
_TEXT segment byte public 'CODE' _TEXT ends DGROUP group _DATA,_BSS assume cs:_TEXT,ds:DGROUP _DATA segment word public 'DATA' ……………………………………… _DATA ends _BSS segment word public 'BSS' ……………………………………… _BSS ends _TEXT segment byte public 'CODE' assume cs:_TEXT,ds:DGROUP ……………………………………… _TEXT ends _DATA segment word public 'DATA' ……………………………………… _DATA ends _TEXT segment byte public 'CODE' _TEXT ends end |
Ну, как? Все понятно? Вот мы и начинаем немного понимать, что же делают компиляторы с алгоритмических языков…
Современные версии компиляторов с Ассемблера позволяют упростить описание сегментов с помощью использования стандартных моделей памяти — см. п. 3.4.2. В этом случае директивы SEGMENT, ENDS и ASSUME становятся НЕНУЖНЫМИ.
Директива MODEL позволяет задавать в ассемблерной программе одну из нескольких стандартных моделей сегментации памяти (модель_памяти) — см. табл. 3.3. Приведем ее упрощенный формат, которым мы будем пользоваться в дальнейшем:
.MODEL модель_памяти [, язык] |
Параметр язык позволяет немного упростить вопросы интерфейса (стыковки) модуля на Ассемблере с общепринятыми алгоритмическими языками. Если вы их используете, то параметр язык должен быть равен одному из терминальных элементов: C, CPP, BASIC, PASCAL, FORTRAN, PROLOG. Если этот параметр НЕ указан, он считается NOLANGUAGE. Например:
.MODEL Large , C .MODEL Small , Pascal |
Определившись с используемой моделью, можно использовать упрощенные директивы описания основных сегментов (регистр букв НЕСУЩЕСТВЕНЕН!):
.Code ; формат MASM CODESEG ; формат Ideal — см. Прил. 5. .DATA ; формат MASM DATASEG ; формат Ideal .STACK ; формат MASM STACK ; формат Ideal |
Модуль на Ассемблере, как и модули на алгоритмических языках, обычно состоит из процедур. Для описания процедур используются две директивы. Визуально они похожи на соответствующие директивы описания сегмента — см. п. 4.2:
имяP PROC [параметры] ; начало процедуры имяP .…………….…………….…………….…………….…………….…………… RET ; КОМАНДА возврата в точку вызова процедуры .…………….…………….…………….…………….…………….……………. имяP ENDP ; конец процедуры имяP |
У директивы PROC достаточно много параметров. С ними мы будем знакомиться постепенно, по мере необходимости.
Обратите внимание на ОБЯЗАТЕЛЬНУЮ команду RET. Она может быть в любом нужном месте процедуры и НЕ единственная. Если ее в процедуре НЕ будет, то ассемблерная программа НЕ сможет нормально работать — возникнет зависание.
Как было заявлено в п. 4.1, мы будем использовать алгоритмические языки Pascal и C/C++ в качестве помощников при изучении Ассемблера. Таким образом, сразу начинаем работать с РАЗНЫМИ модулями, да еще и на разных языках! Поэтому нам не миновать ВНЕШНИХ ссылок. Что это такое? Это — использование в одном модуле имен, описанных в других модулях.
PUBLIC [язык] имя1[,[язык] имя2…] |
Эта директива указывает компилятору и компоновщику, что данное имя (его адрес) должно быть доступно для других программ. Имена могут быть метками, переменными или именами подпрограмм.
Например, если мы хотим использовать в языке С/С++ функцию с именем Prim, реализованную в Ассемблере, то она в ассемблерном модуле должна быть описана следующим образом:
Public C Prim Prim Proc ……………………………………… Prim EndP |
Обратите внимание
Язык С/С++ различает регистр букв в именах. Это же должен делать и Ассемблер, для чего служит специальный ключ, определяющий чувствительность Ассемблера к РЕГИСТРУ выбора символов: ml=all (все символы), mx=globals (только глобальные), mu=none (символы к регистру НЕ чувствительны — принято по умолчанию):
TASM имя.asm[ /ml] TASM имя.asm[ /mx] TASM имя.asm[ /mu] |
В нашем случае ассемблерный модуль должен быть откомпилирован ОБЯЗАТЕЛЬНО с ключом /ml или /mx. Иначе компоновщик С/С++ НЕ сможет подключить ассемблерный модуль. Если эти рассуждения вам пока НЕПОНЯТНЫ, НЕ зацикливайтесь, — мы к этому еще вернемся на КОНКРЕТНЫХ примерах.
EXTRN имя1:тип1[, имя2:тип2…] |
Эта директива указывает компилятору и компоновщику, что данное имя имеет определенный тип, его предполагается использовать в данном ассемблерном модуле, но память для него выделена в другом модуле. Параметр тип может принимать следующие значения: ABS (для констант), BYTE, WORD, DWORD, QWORD, TBYTE (см. соответствующий столбец в табл. 2.2), FAR, NEAR.
Например, в модуле на алгоритмическом языке Pascal (Borland/Turbo Pascal-5.5/6.0/7.0х) мы описали следующие глобальные переменные:
Var a, b, c : Integer; X : LongInt; |
А использовать их собираемся в ассемблерном модуле. В этом случае директива будет иметь вид:
Extrn a:Word, b:Word, c:Word, x:Dword |
Обратите внимание
Язык Pascal НЕ различает регистр букв в именах. А директива EXTRN содержит только ОДНУ гласную букву — начинающие программисты часто на этом спотыкаются… Будьте ВНИМАТЕЛЬНЫ!
Если вы правильно ответили на ВСЕ вопросы, значит, вы ХОРОШО УСВОИЛИ главу 2 (п. 2.2) — поздравляю!!! Если НЕТ, не беда — все у вас еще впереди (просто пока маловато опыта).
Сообщений 0 Оценка 0 Оценить |