- •Модульное программирование
- •Технологии программирования
- •Структурное программирование
- •Концепция модульного программирования
- •Процедуры в языке асемблера
- •Организация интерфейса с процедурой
- •Передача аргументов через регистры
- •Передача аргументов через общую область памяти
- •Передача аргументов через стек
- •Использование директив extrn и public
- •Возврат результата из процедуры
- •Связь ассемблера с языками высокого уровня
- •Связь Pascal — асемблер
- •Связь с—асемблер
- •Подведем некоторые итоги:
Связь Pascal — асемблер
Организацию такой связи рассмотрим на примере следующей задачи: разработаем программу на языке Pascal, которая выводит символ заданное количество раз, начиная с определенной позиции на экране (листинги 14.12 и 14.13). Все числовые аргументы определяются в программе на Pascal. Вывод символа осуществляет процедура ассемблера. Очевидно, что основная проблема в этой задаче — организация взаимодействия модулей на Pascal и ассемблере.
Листинг 14.12. Взаимодействие Pascal—ассемблер (модуль на Pascal)
Листинг 14.13. Взаимодействие Pascal—ассемблер (модуль на ассемблере)
Типовая схема организации такой связи следующая:
Написать процедуру на ассемблере дальнего (far) или ближнего типа (near). Назовем ее для примера asmproc. В программе на языке ассемблера (назовем ее prg14_13. asm), в которую входит процедура asmproc, необходимо объявить имя этой процедуры внешним с помощью директивы PUBLIC:
PUBLIC asmproc
Для того чтобы процедура на ассемблере при компоновке с программой на Pascal воспринималась компилятором Borland Pascal 7.0 как far или near, недостаточно будет просто объявить ее таковой в директиве ргос (строка 11 листинга 14.13). Кроме того, вам нужно включить или выключить опцию компилятора, доступную через меню интегрированной среды: Options |Compiler | Force far calls. Установка данной опции заставляет компилятор генерировать дальние вызовы подпрограмм. Альтернатива данной опции — ключ в программе {$F+} или {$F-} (соответственно, включено или выключено). Это — локальные ключи, то есть в исходном тексте программы на Pascal их может быть несколько, и они могут, чередуясь друг с другом, поочередно менять форму генерируемых адресов перехода — для одних подпрограмм дальние вызовы, для других — ближние.
Произвести компиляцию программы prg14_13. asm с целью устранения синтаксических ошибок и получения объектного модуля программы prg14_13.obj:
tasm /zi prg14_13,,,
В программе на Pascal prg14_12. pas, которая будет вызывать внешнюю процедуру на ассемблере, следует вставить директиву компилятора
{$L \путь\ргд_14_12. obj}. Эта директива заставит компилятор в процессе ком-пиляции программы prg14_12.pas загрузить с диска объектный модуль про¬граммы prg14_12.obj. В программе prg14_12.pas необходимо объявить процедуру asmproc как внешнюю. В итоге последние два объявления в программе на Pascal будут выглядеть так:
{$L my_asm}
procedure asmproc(ch:char;kol,x,y:integer); external;
если вы собираетесь исследовать в отладчике работу программы, то необходимо потребовать, чтобы компилятор включил отладочную информацию в генерируемый им исполняемый модуль. Для этого есть две возможности. Первая заключается в использовании глобального ключа {$D+}. Этот ключ должен быть установлен сразу после заголовка программы на Pascal. Вторая, альтернативная возможность заключается в установке опции компилятора: Options|Compiler|Debug Information;
выполнить компиляцию программы на Pascal. Для компиляции удобно использовать интегрированную среду. Для изучения особенностей связки Pascal — ассемблер удобно прямо в интегрированной среде перейти к работе в отладчике через меню Tools|Turbo Debugger (или клавишами Shift-F4). Будет загружен отладчик. Его среда вам хорошо знакома; в данном случае в окне Module вы увидите текст программы на Pascal. Нажимая клавишу F7, вы в пошаговом режиме будете исполнять программу на Pascal. Когда очередь дойдет до вызова процедуры на ассемблере, отладчик откроет окно с текстом программы на ассемблере. Но наш совет вам — не ждать этого момента, так как вы уже пропустили некоторые интересные вещи. Дело в том, что отладчик скрывает от вас момент перехода из программы на Pascal в процедуру на ассемблере. Поэтому лучше всего исполнять программу при открытом окне CPU отладчика. И тогда вы станете свидетелями тех процессов, которые мы будем обсуждать ниже.
Если бы взаимодействие программ ограничивалось только передачей и возвратом управления, то на этом обсуждение можно было бы и закончить. Но дело значительно усложняется, когда требуется передать аргументы (в случае процедуры) или передать аргументы и возвратить результат (в случае функции). Рассмотрим процессы, которые при этом происходят.
Передача аргументов при связи модулей на разных языках всегда производится через стек. Компилятор Pascal генерирует соответствующие команды при обработке вызова процедуры ассемблера. Это как раз те команды, которые отладчик пытался скрыть от нас. Они записывают в стек аргументы и генерируют команду call для вызова процедуры ассемблера. Чтобы убедиться в этом, просмотрите исполнительный код программы в окне CPU отладчика. После обработки вызова процедуры и в момент передачи управления процедуре asmproc содержимое стека будет таким, как показано на рис. 14.1, а. Для доступа к этим аргументам можно использовать различные методы; наиболее удобный из них — использование регистра bр.
Рис. 14.1. Изменение содержимого стека при передаче управления Pascal—ассемблер
Регистр bр, как уже отмечалось, специально предназначен для организации произвольного доступа к стеку. Когда мы рассматривали связь ассемблерных модулей, то говорили о необходимости добавления в текст вызываемого модуля фрагментов, настраивающих его на передаваемые ему аргументы. При организации связи разноязыковых модулей также нужно вставлять подобные дополнительные фрагменты кода. Они, кроме всего прочего, позволят учесть особенности конкретного языка. Фрагмент, вставляемый в самое начало вызываемого модуля, называется прологом модуля (процедуры). Фрагмент, вставляемый перед командами передачи управления вызывающему модулю, называется эпилогом модуля (процедуры). Его назначение — восстановление состояния вычислительной среды на момент вызова данного модуля.
Рассмотрим действия, выполняемые кодами пролога и эпилога при организации связи Pascal—ассемблер.
Действия, выполняемые кодом пролога:
1. Сохранить значение bр в стеке. Это делается с целью запоминания контекста вызывающего модуля. Стек при этом будет выглядеть как на рис. 14.1, б.
2. Записать содержимое sp в bр. Тем самым Ьр теперь тоже будет указывать на вершину стека (рис. 14.1, в).
После написания кода пролога все обращения к аргументам в стеке можно организовывать относительно содержимого регистра bр. Из рис. 14.1, в видно, что для обращения к верхнему и последующим аргументам в стеке содержимое bр необходимо откорректировать. Нетрудно посчитать, что величина корректировки будет отличаться для процедур дальнего (far) и ближнего (near) типов. Причина понятна: при вызове пеаг-процедуры, в зависимости от установленного режима адресации — use16 или use32 — в стек записывается 2/4 байта в качестве адреса возврата (содержимое ip/eip), а при вызове far-процедуры в стек записывается 4/8 байт (содержимое ip/eip и cs).
Таким образом, коды пролога для near- и far-процедур соответственно будут выглядеть следующим образом:
Далее доступ к переданным в стеке данным осуществляется, как показано в листинге 14.7.
Как видите, все достаточно просто. Но если мы вдруг решили изменить тип нашей процедуры ассемблера с far на near или наоборот, то нужно явно изменить и код пролога. Это не совсем удобно. TASM предоставляет выход в виде директивы ARG, которая служит для работы с аргументами процедуры. Формат директивы ARG следующий (рис. 14.2).
Рис. 14.2. Синтаксис директивы ARG
Несколько слов об обозначениях на рис. 14.2:
имя — идентификатор переменной, который будет использоваться в процедуре на ассемблере для доступа к соответствующей переменной в стеке;
тип — определяет тип данных аргумента (по умолчанию word для use16 и dword для use32);
значение_1 — определяет количество аргументов с данным именем. Место в стеке для них будет определено, исходя из расчета: значение_1 * значение_2 * (размер_типа). По умолчанию значение_1 = 1;
значение_2 — определяет, сколько элементов данного типа задает данный аргумент. По умолчанию его значение равно 1, но для типа byte значе- ние_2 = 2, так как стековые команды не работают с отдельными байтами. Хотя, если явно задать значение_2 = 1, то транслятор действительно будет считать, что в ячейку стека помещен один байт;
идентификатор — имя константы, значение которой присваивает транслятор. Об идентификаторе мы подробно поговорим чуть ниже.
Таким образом, директива ARG определяет аргументы, передаваемые в процедуру. Ее применение позволяет обращаться к аргументам по их именам, а не смещениям относительно содержимого bр. К примеру, если в начале рассматриваемой нами процедуры на ассемблере asmproc задать директиву ARG в виде arg kol:word,у:word,x:word,chr:byte, то к аргументам процедуры можно будет обращаться по их именам, без подсчета смещений. Ассемблер сам выполнит всю необходимую работу. В этом можно убедиться, запустив программу в отладчике. Обратите внимание: порядок следования аргументов в директиве arg является обратным порядку их следования в описании процедуры (строка procedure asmproc(ch:char;x,y,kol:integer); external; в программе на Pascal). Процедура asmproc с использованием директивы arg представлена в листинге 14.14.
После того как решена проблема передачи аргументов в процедуру и выполнены все необходимые действия, возникает очередной вопрос — как правильно возвратить управление? При возврате управления в программу на Pascal нужно помнить, что соглашения этого языка требуют, чтобы вызываемые процедуры самостоятельно очищали за собой стек. Программа на ассемблере также должна удовлетворять этому требованию и заботиться об очистке стека перед своим завершением. Для этого необходимо составить эпилог.
Действия, выполняемые кодом эпилога для связи Pascal—ассемблер:
1. Восстановить сохраненный в стеке регистр bр.
2. Удалить из стека переданные процедуре аргументы.
Для удаления из стека аргументов можно использовать различные способы:
явно скорректировать значение sp, переместив указатель стека на необходимое количество байт в положительную сторону. Это — не универсальный способ, к тому же он чреват ошибками, особенно при частых модификациях программы;
использовать в директиве arg после записи последнего аргумента операнд, состоящий из символа равенства «=» и идентификатора, указанного за ним в следующей синтаксической конструкции: =идентификатор. В этом случае TASM при обработке директивы arg подсчитает количество байт, занятых всеми аргументами, и присвоит их значение идентификатору. В нашем случае директиву arg можно определить так: arg ch: byte;х:word;у:word; kol:word=a_size. TASM после обработки данной директивы присвоит имени a_size значение 8 (байт). Это имя впоследствии нужно будет указать в качестве операнда команды ret:
ret a_size
Листинг 14.14. Использование директивы arg
Есть еще одна возможность организации данных Pascal—ассемблер — использование операндов директивы model. Вcпомним, что она позволяет задать модель памяти и учесть соглашения языков высокого уровня о вызове процедур. Для связи Pascal — ассемблер ее можно задавать в виде
MODEL large,pascal
Задание в таком виде директивы model позволяет:
описать аргументы процедуры непосредственно в директиве ргос:
asmproc ргос near ch:byte,х:word,y:word, kol:word
автоматически сгенерировать код пролога и эпилога в процедуре на ассемблере;
для доступа к аргументам, объявленным в ргос, использовать их имена. В этом отношении данный вариант является аналогом предыдущего варианта с директивой arg.
Листинг 14.15 демонстрирует, как отразились особенности данного варианта на тексте процедуры ассемблера. Обратите внимание, что пролога уже нет, так как он формируется транслятором автоматически; вместо эпилога обязательно нужно задавать только ret без операндов. Интересно изучить текст листинга, который получится в результате трансляции листинга 14.15. В нем видны сформированные транслятором коды пролога и эпилога. Кроме того, транслятор заместил команду ret без операндов командой ret 0008, которая, в соответствии с требованиями к взаимодействию с программами на Pascal, удалит из стека аргументы, переданные вызываемой процедуре.
Листинг 14.15. Использование директивы MODEL
Листинг 14.16. Результат трансляции листинга 14.15
Таким образом, можно считать, что мы разобрались со стандартными способами вызова ассемблерных процедур из программ на Pascal и передачи им аргументов. Эти способы будут работать всегда, но компилятор может предоставлять и болем удобные средства. Их мы рассматривать не будем, так как они будут сводиться, в конечном счете, к разобранной нами процедуре.
Остались открытыми два вопроса:
1.Как быть с передачей данных остальных типов Pascal — ведь мы рассмотрели только данные размером в байт и слово?
2.Как возвратить значение в программу на Pascal?
Что касается ответа на первый вопрос, то необходимо вспомнить о том, что в языке Pascal существует два способа передачи аргументов в процедуру: по ссылке и по значению.
Тип аргументов, передаваемых по ссылке, совпадает с типом ассемблера dword и с типом pointer в Pascal. По сути, это указатель из четырех байт на некоторый объект. Структура указателя обычная: два младших байта — смещение, два старших байта — значение сегментной составляющей адреса. С помощью такого указателя в программу на ассемблере передаются адреса следующих объектов:
всех аргументов, объявленные при описании в программе на Pascal как var, независимо от их типа;
аргументов pointer и longint;
строк string;
множеств;
массивов и записей, имеющих размер более четырех байт.
Аргументы по значению передаются следующим образом:
для типов char и byte — как байт;
для типа boolean — как байт со значением 0 или 1;
для перечисляемых типов со значением 0...255 — как байт; более 255 — как два байта;
для типов integer и word — как два байта (слово);
для типа real — как шесть байт (три слова);
массивы и записи, длина которых не превышает четырех байт, передаются «как есть».
Заметим, что аргументы таких типов, как single, double, extended и comp, передаются через стек сопроцессора.
Что касается ответа на второй вопрос, то рассмотрим его на конкретном примере листингов 14.17 и 14.18. Помните, что мы рассматриваем вызов из программы на Pascal внешней процедуры на ассемблере. Понятно, что вызов ради вызова вряд ли нужен — вызываемая процедура должна иметь возможность вернуть данные в вызывающую программу. Поэтому такую вызываемую процедуру правильнее рассматривать как функцию. В связке Pascal—ассемблер для того чтобы возвратить результат, процедура на ассемблере должна поместить его значение в строго определенное место (табл. 14.2).
Таблица 14.2. Возврат результата из процедуры на ассемблере
в программу на Pascal
В листинге 14.17 приведен текст вызывающего модуля на Pascal, а в листин¬ге 14.18 — код вызываемого модуля на ассемблере. Программа на Pascal инициализирует две переменные valuel и value2, после чего вызывает функцию на ассемблере AddAsm для их сложения. Результат возвращается в программу на Pascal и присваивается переменной rez.
Листинг 14.17. Вызывающая программа на Pascal
Листинг 14.18. Вызываемая процедура на ассемблере
Здесь была использована еще одна возможность доступа к разделяемым данным — использование сегментов типа public. Совместное использование сегментов данных стало возможным благодаря тому, что компилятор Pascal создает внутреннее представление программы в виде сегментов, как и положено для программ, выполняющихся на микропроцессоре Intel. Сегмент данных в этом представлении тоже имеет название data, и директива segment для него выглядит так: data segment word public и т. д.
