Глава 18. Интерфейс Турбо Ассемблера и Borland C++
Хотя некоторые программисты могут разрабатывать программы
целиком на языке Ассемблера (и делают это), другие предпочитают
писать основную часть программы на языке высокого уровня, обраща-
ясь к языку Ассемблера только для осуществления управления нижне-
го уровня, или когда требуется высокая производительность. Неко-
торые предпочитают писать преимущественно на Ассемблере, только
иногда используя конструкции и библиотечные средства языков высо-
кого уровня.
Данная глава объясняет использование Турбо Ассемблера с ком-
пиляторами С++. В этой главе они называются компиляторами семейс-
тва Borland С++. Однако Турбо Ассемблер можно использовать также
с Турбо C++ и Турбо Си. Приводимая ниже таблица содержит перечень
компиляторов этого семейства.
Компиляторы Borland С++ и Си Таблица 18.1
---------------------T------------------------------------------¬
¦ Название продукта ¦ Имя файла компилятора ¦
+--------------------+------------------------------------------+
¦ Borland С++ ¦ bcc.exe, bccx.exe, bc.exe или bcx.exe ¦
¦ Турбо C++ ¦ tcc.exe или tc.exe ¦
¦ Турбо Cи ¦ tcc.exe ¦
L--------------------+-------------------------------------------
Например, если мы говорим, чтобы вы выполнили компиляцию
строкой:
bcc -S plusone.cpp
а вы работаете с Турбо С++, то вместо нее вы можете ввести следу-
ющую командную строку:
tcc -S plusone.cpp
Если при интерфейсе с Турбо Ассемблером вы используете Турбо
Си, то вы ограничены использованием только компилятора командной
строки. В случае же Borland С++ и Турбо С++ вы можете работать
как с компиляторами командной строки, так и с компилятором интег-
рированной среды.
Borland C++ дает хорошие возможности поддержки смешанного
программирования на С++ и на языке Ассемблера на произвольной ос-
нове и предоставляет не один, а целых три механизма объединения
модулей на Ассемблере и на С++. Имеющееся в С++ средство встроен-
ного ассемблирования позволяет быстро и просто вставить текст на
языке Ассемблера прямо в функцию С++. Вы можете выполнить ассемб-
лирование встроенного кода при помощи Турбо Ассемблера или встро-
енного Ассемблера Borland С++. Те, кто предпочитает держать ас-
семблерные части программы в отдельных модулях, написанных цели-
ком на языке Ассемблера, может ассемблировать их при помощи Турбо
Ассемблера и затем скомпоновать с модулями Borland С++.
Сначала мы рассмотрим использование встроенного в Borland
C++ Ассемблера. Далее мы подробно обсудим компоновку отдельно ас-
семблированных в Турбо Ассемблере модулей с Borland C++ и иссле-
дуем процесс вызова функций, написанных с помощью Турбо Ассембле-
ра, из программы, созданной в Borland C++. И наконец, мы рассмот-
рим вызов функций Borland C++ из программы на Турбо Ассемблере.
Вызов из Borland C++ функций Турбо Ассемблера
Обычно C++ и Ассемблер совместно используют путем написания
отдельных модулей целиком на C++ или Ассемблере, компиляции моду-
лей С++ и ассемблирования модулей Ассемблера с последующей сов-
местно компоновкой этих раздельно написанных модулей. Это пока-
зано на Рис. 1.18.
-------------------------------¬ -------------------------------¬
¦ Исходный файл на языке С++ ¦ ¦ Исходный файл на Ассемблере ¦
¦ имя_файла.СPP ¦ ¦ имя_файла.ASM ¦
L--------------T---------------- L------------T------------------
¦ ¦
Компиляция Ассемблирование
-=============¬ -==================¬
¦ Borland C++ ¦ ¦ Турбо Ассемблер ¦
L=============- L==================-
¦ ¦
-------------------------------¬ -------------------------------¬
¦ Объектный файл языка С++ ¦ ¦ Объектный файл языка С++ ¦
¦ имя_файла.OBJ ¦ ¦ имя_файла.OBJ ¦
L--------------T---------------- L-------------T-----------------
¦ ¦
¦ ¦
L-------------¬ ---------------
¦ ¦
-=============¬
¦ TLINK ¦ Компоновка
L=============-
¦
---------------------------------¬
¦ Выполняемый файл ¦
¦ имя_файла.EXE ¦
L---------------------------------
Рис. 1.18 Цикл компиляции, ассемблирования и компоновки
Borland C++, Турбо Ассемблера и компоновщика TLINK
Выполняемый файл получается из "смеси" модулей С++ и Ассемб-
лера. Этот цикл можно запустить командой:
bcc имя_файла_1.cpp имя_файла_2.asm
которая указывает Borland C++, что нужно сначала компилировать
файл имя_файла_1.СPP в файл имя_файла_1.OBJ, а затем вызвать Тур-
бо Ассемблер для ассемблирования файла имя_файла_2.asm в имя_фай-
ла_2.obj, и, наконец, вызвать компоновщик TLINK для компоновки
файла имя_файл_1.OBJ и имя_файл_2.OBJ в файл имя_файла.EXE.
Раздельную компиляцию полезно использовать для программ с
большим объемом кода на Ассемблере, так как это позволяет исполь-
зовать все возможности Турбо Ассемблера и программировать на язы-
ке Ассемблера в чисто ассемблерном окружении без ключевых слов
asm, дополнительного времени на компиляцию и связанными с С++
непроизводительными затратами при работе со встроенным Ассембле-
ром.
За раздельную компиляцию приходится платить следующую цену:
программист, работающий с Ассемблером, должен вникать во все де-
тали организации интерфейса между С++ и кодом Ассемблера. В то
время как при использовании встроенного Ассемблера Borland C++
сам выполняет спецификацию сегментов, передачу параметров, ссылку
на переменные С++ и т.д., отдельно компилируемые функции Ассемб-
лера должны все это (и даже более) делать самостоятельно.
В интерфейсе Турбо Ассемблера и Borland C++ есть два основ-
ных аспекта. Во-первых, различные части кода С++ и Ассемблера
должны правильно компоноваться, а функции и переменные в каждой
части кода должны быть доступны (если это необходимо) в остальной
части кода. Во-вторых, код Ассемблера должен правильно работать с
вызовами функций, соответствующих соглашениям языка С++, что
включает в себя доступ к передаваемым параметрам, возврат значе-
ний и соблюдение правил сохранения регистров, которых требуется
придерживаться в функциях С++.
Давайте теперь приступим к изучению правил компоновки прог-
рамм Турбо Ассемблера и Borland C++.
Основные моменты в интерфейсе Турбо Ассемблера и Borland C++
Чтобы скомпоновать вместе модули Borland C++ и Турбо Ассемб-
лера, должны быть соблюдены следующие три пункта:
1. В модулях Турбо Ассемблера должны использоваться соглаше-
ния об именах, принятые в Borland C++.
2. Borland C++ и Турбо Ассемблер должны совместно использо-
вать соответствующие функции и имена переменных в форме,
приемлемой для Borland C++.
3. Для комбинирования модулей в выполняемую программу нужно
использовать утилиту-компоновщик TLINK.
Здесь ничего не говориться о том, что в действительности де-
лают модули Турбо Ассемблера. Пока мы коснемся только основных
моментов, обеспечивающих разработку функций Турбо Ассемблера,
совместимых с С++.
Компоновка ассемблерных модулей с С++
Важной концепцией С++ является безопасная с точки зрения
стыковки типов компоновка. Компилятор и компоновщик должны рабо-
тать согласованно, чтобы гарантировать правильность типов переда-
ваемых между функциями аргументов. Процесс, называемый "корректи-
ровкой имен" (name-mangling), обеспечивает необходимую информацию
о типах аргументов. "Корректировка имени" модифицирует имя функ-
ции таким образом, чтобы оно несло информацию о принимаемых функ-
цией аргументах.
Когда программа пишется целиком на С++, корректировка имен
происходит автоматически и прозрачно для программы. Однако, когда
вы пишете ассемблерный модуль для последующей его компоновки с
программой на С++, вы сами обязаны обеспечить корректировку имен
в модуле. Это легко сделать, написав пустую функцию на С+ и ском-
пилировав ее с ассемблерным модулем. Генерируемый при этом
Borland С++ файл .ASM будет содержать исправленные имена. Затем
вы можете их использовать при написании реального ассемблерного
модуля.
Например, следующий фрагмент кода определяет четыре различ-
ные версии функции с именем test:
void test()
{
}
void test( int )
{
}
void test( int, int )
{
}
void test( float, double )
{
}
Если этот код компилируется с параметром -S, то компилятор
создает на выходе файл на языке Ассемблера (.ASM). Вот как он
выглядит (несущественные детали убраны):
; void test()
@testSqv proc near
push bp
mov bp,sp
popo bp
ret
@testSqv endp
; void test( int )
@testSqi proc near
push bp
mov bp,sp
popo bp
ret
@testSqi endp
; void test( int, int )
@testSqii proc near
push bp
mov bp,sp
popo bp
ret
@testSqii endp
; void test( float, double )
@testSqfd proc near
push bp
mov bp,sp
popo bp
ret
@testSqfd endp
Использование Extern "C" для упрощения компоновки
При желании вы можете использовать в ассемблерных функциях
неисправленные имена, не пытаясь выяснить, как должны выглядеть
правленные. Использование нескорректированных имен защитит ваши
ассемблерные функции от возможных изменений алгоритма в будущем.
Borland С++ позволяет определять в программах С++ стандартные
имена функций С++, как в следующем примере:
extern "C" {
int add(int *a, int b);
}
Любые функции, объявленные внутри фигурных скобок, получат
имена в стиле языка Си. Ниже показаны соответствующие определения
в ассемблерном модуле:
public _add
_add proc
Объявление ассемблерной функции в блоке extern "C" позволит
вам избежать проблем со "откорректированными именами". При этом
улучшится и читаемость кода.
Модели памяти и сегменты
Чтобы данная функция Ассемблера могла могла вызываться из
С++, она должна использовать ту же модель памяти, что и программа
на языке С++, а также совместимый с С++ сегмент кода. Аналогично,
чтобы данные, определенные в модуле Ассемблера, были доступны в
программе на языке С++ (или данные С++ были доступны в программе
Ассемблера), в программе на Ассемблере должны соблюдаться согла-
шения языка С++ по наименованию сегмента данных.
Модели памяти и обработку сегментов на Ассемблере может ока-
заться реализовать довольно сложно. К счастью, Турбо Ассемблер
сам выполняет почти всю работу по реализации моделей памяти и
сегментов, совместимых с Borland C++, при использовании упрощен-
ных директив определения сегментов.
Упрощенные директивы определения сегментов и Borland C++
Директива .MODEL указывает Турбо Ассемблеру, что сегменты,
создаваемые с помощью упрощенных директив определения сегментов,
должны быть совместимы с выбранной моделью памяти (TINY - крохот-
ной, SMALL - малой, COMPACT - компактной, MEDIUM - средней,
LARGEбольшой или HUGE - громадной) и управляет назначаемым по
умолчанию типом (FAR или NEAR) процедур, создаваемых по директиве
PROC. Модели памяти, определенные с помощью директивы .MODEL,
совместимы с моделями Borland C++ с соответствующими именами.
Наконец, упрощенные директивы определения сегментов .DATA,
.CODE, .DATA?, .FARDATA, .FARDATA? и .CONST генерируют сегменты,
совместимые с Borland C++.
Например, рассмотрим следующий модуль Турбо Ассемблера с
именем DOTOTAL.ASM:
.MODEL SMALL ; выбрать малую модель памяти
; (ближний код и данные)
.DATA ; инициализация сегмента данных,
; совместимого с Borland C++
EXTRN _Repetitions:WORD ; внешний идентификатор
PUBLIC _StartingValue ; доступен для других модулей
_StartValue DW 0
.DATA? ; инициализированный сегмент
; данных, совместимый с Borland C++
RunningTotal DW ?
.CODE ; сегмент кода, совместимый с
; Borland C++
PUBLIC _DoTotal
_DoTotal PROC ; функция (в малой модели памяти
; вызывается с помощью вызова
; ближнего типа)
mov cx,[_Repetitions] ; счетчик выполнения
mov ax,[_StartValue]
mov [RunningTotal],ax ; задать начальное
; значение
TotalLoop:
inc [RunningTotal] ; RunningTotal++
loop TotalLoop
mov ax,[RunningTotal] ; возвратить конечное
; значение (результат)
ret
_DoTotal ENDP
END
Написанная на Ассемблере процедура _DoTotal при использова-
нии малой модели памяти может вызываться из Borland C++ с помощью
оператора:
DoTotal();
Заметим, что в процедуре DoTotal предполагается, что где-то
в другой части программы определена внешняя переменная
Repetitions. Аналогично, переменная StartingValue объявлена, как
общедоступная, поэтому она доступна в других частях программы.
Следующий модуль Borland C++ (который называется SHOWTOT.CPP) об-
ращается к данным в DOTOTAL.ASM и обеспечивает для модуля
DOTOTAL.ASM внешние данные:
extern int StartingValue;
extern int DoTotal(word);
int Repetitions;
main()
{
int i;
Repetitions = 10;
StartingValue = 2;
print("%d\n", DoTotal());
}
Чтобы создать из модулей DOTOTAL.ASM и SHOWTOT.CPP выполняе-
мую программу SHOWTOT.EXE, введите команду:
bcc showtot.cpp dototal.asm
Если бы вы захотели скомпоновать процедуру _DoTotal с прог-
раммой на языке C++, использующей компактную модель памяти, то
пришлось бы просто заменить директиву .MODEL на .MODEL COMPACT, а
если бы вам потребовалось использовать в DOTATOL.ASM сегмент
дальнего типа, то можно было бы использовать директиву .FARDATA.
Короче говоря, при использовании упрощенных директив опреде-
ления сегментов генерация корректного упорядочивания сегментов,
моделей памяти и имен сегментов труда не составляет.
Старые директивы определения сегментов и Borland C++
Коснемся теперь проблемы организации интерфейса Турбо Ассем-
блера с кодом языка С++, где используются директивы определения
сегментов старого типа (стандартные директивы определения сегмен-
тов). Например, если вы замените в модуле DOTOTAL.ASM упрощенные
директивы определения сегментов директивами старого типа, то по-
лучите следующее:
DGROUP group _DATA,_BSS
_DATA segment word public 'DATA'
EXTRN _Repetitions:WORD ; внешний идентификатор
PUBLIC _StartingValue ; доступен для других модулей
_StartValue DW 0
_DATA ends
_BSS segment word public 'BSS'
RunningTotal DW ?
_BSS ends
_TEXT segment byte public 'CODE'
assume cs:_TEXT.ds:DGROUP,ss:DGROUP
PUBLIC _DoTotal
_DoTotal PROC ; функция (в малой модели памяти
; вызывается с помощью вызова
; ближнего типа)
mov cx,[_Repetitions] ; счетчик выполнения
mov ax,[_StartValue]
mov [RunningTotal],ax ; задать начальное
; значение
TotalLoop:
inc [RunningTotal] ; RunningTotal++
loop TotalLoop
mov ax,[RunningTotal] ; возвратить конечное
; значение (результат)
ret
_DoTotal ENDP
_TEXT ENDS
END
Данная версия директив определения сегментов не только длин-
нее, то также и хуже читается. К тому же при использовании в
программе на языке С++ различных моделей памяти ее труднее изме-
нять. При организации интерфейса с Borland C++ в общем случае в
использовании старых директив определения сегментов нет никаких
преимуществ. Если же вы тем не менее захотите использовать при
организации интерфейса с Borland C++ старые директивы определения
сегментов, вам придется идентифицировать корректные сегменты, со-
ответствующие используемым в коде на языке С++ моделям памяти.
Простейший способ определения, какие сегментные директивы
старых версий должны выбираться для компоновки с той или иной
программой Borland С++, заключается в компиляции главного модуля
программы на Borland С++ для желаемой модели памяти с параметром
-S, что тем самым заставит Borland С++ сгенерировать ассемблерную
версию соответствующей программы на Borland С++. В этой версии
кодов Си вы сможете найти все старые сегментные директивы, ис-
пользуемые Турбо Cи; просто скопируйте их в вашу ассемблерную
часть программы.
Вы также можете посмотреть, как будут выглядеть соответству-
ющие старые директивы, скомпилировав их обычным образом (без па-
раметра -S) и использовав TDUMP - утилиту, поставляемую Турбо Ас-
семблером, чтобы получить все записи определения сегмента. Ис-
пользуйте следующую командную строку:
tdump -OI segdef module.obj
Значения по умолчанию: когда необходимо загружать сегменты?
В некоторых случаях вызываемые из языка С++ функции Ассемб-
лера могут использовать (загружать) для обращения к данным ре-
гистры DS и/или ES. Полезно знать соотношение между значениями
сегментных регистров при вызове из Borland C++, так как иногда
Ассемблер использует преимущества эквивалентности двух сегментных
регистров. Давайте рассмотрим значения сегментных регистров в тот
момент, когда функция Ассемблера вызывается из Borland C++, а
также соотношения между сегментными регистрами, и случаи, когда в
функции Ассемблера требуется загружать один или более сегментных
регистров.
При входе в функцию Ассемблера из Borland C++ регистры CS и
DS имеют следующие значения, которые зависят от используемой мо-
дели памяти (регистр SS всегда используется для сегмента стека, а
ES всегда используется, как начальный сегментный регистр):
Значения регистров при входе в Ассемблер из Borland C++
Таблица 18.2
------------------------------------------------------------¬
¦ Модель CS DS ¦
+-----------------------------------------------------------+
¦ Крохотная _TEXT DGROUP ¦
¦ Малая _TEXT DGROUP ¦
¦ Компактная _TEXT DGROUP ¦
¦ Средняя имя_файла_TEXT DGROUP ¦
¦ Большая имя_файла_TEXT DGROUP ¦
¦ Громадная имя_файла_TEXT имя_вызывающего_файла_DATA¦
L------------------------------------------------------------
Здесь "имя_файла" - это имя модуля на Ассемблере, а "имя_вы-
зывающего_файла" - это имя модуля Borland C++, вызывающего модуль
на Ассемблере.
В крохотной модели памяти _TEXT и DGROUP совпадают, поэтому
при входе в функцию содержимое регистра CS равно содержимому DS.
При использовании крохотной, малой и компактной модели памяти
при входе в функцию содержимое SS равно содержимому регистра DS.
Когда же в функции на Ассемблере, вызываемой из программы на
языке С++, необходимо загружать сегментный регистр? Отметим для
начала, что вам никогда не придется (более того, этого не следует
делать) загружать регистры SS или CS: при дальних вызовах, пере-
ходах или возвратах регистр CS автоматически устанавливается в
нужное значение, а регистр SS всегда указывает на сегмент стека и
в ходе выполнения программы изменять его не следует (если только
вы не пишете программу, которая "переключает" стеки. В этом слу-
чае вам нужно четко понимать, что вы делаете).
Регистр ES вы можете всегда использовать так, как это требу-
ется. Вы можете установить его таким образом, чтобы он указывал
на данные с дальним типом обращения, или загрузить в ES сег-
мент-приемник для строковой функции.
С регистром DS дело обстоит иначе. Во всех моделях памяти
Borland C++, кроме сверхбольшой, регистр DS при входе в функцию
указывает на статический сегмент данных (DGROUP), и изменять его
не следует. Для доступа к данным с дальним типом обращения всегда
можно использовать регистр ES, хотя вы можете посчитать, что для
этого временно нужно использовать регистр DS (если вы собираетесь
осуществлять интенсивный доступ к данным), что исключит необходи-
мость использования в вашей программе множества инструкций с пре-
фиксом переопределения сегмента. Например, вы можете обратиться к
дальнему сегменту одним из следующих способов:
.
.
.
.FARDATA
Counter DW 0
.
.
.
.CODE
PUBLIC _AsmFunction
_AsmFunction PROC
.
.
.
mov ax,@FarData
mov es,ax ; ES указывает на
; сегмент данных с
; дальним типом
; обращения
inc es:[Counter] ; увеличить значение
; счетчика
.
.
.
_AsmFunction ENDP
.
.
.
или иначе:
.
.
.
.FARDATA
Counter DW 0
.
.
.
.CODE
PUBLIC _AsmFunction
_AsmFunction PROC
.
.
.
assume ds:@FarData
mov ax,@FarDAta
mov ds,ax ; DS указывает на
; сегмент данных с
; дальним типом
; обращения
inc [Counter] ; увеличить значение
; счетчика
assume ds:@Data
mov ax,@Data
mov dx,ax ; DS снова указывает
; на DGROUP
.
.
.
_AsmFunction ENDP
.
.
.
Второй вариант имеет то преимущество, что при каждом обраще-
нии к дальнему сегменту данных в нем не требуется переопределение
ES:. Если для обращения к дальнему сегменту вы загружаете регистр
DS, убедитесь в том, что перед обращением к другим переменным
DGROUP вы его восстанавливаете (как это делается в приведенном
примере). Даже если в данной функции на Ассемблере вы не обращае-
тесь к DGROUP, перед выходом из нее все равно обязательно нужно
восстановить содержимое DS, так как в Borland C++ подразумевает-
ся, что регистр DS не изменялся.
При использовании в функциях, вызываемых из С++, сверхболь-
шой модели памяти работать с регистром DS нужно несколько по-дру-
гому. В сверхбольшой модели памяти Borland C++ совсем не исполь-
зует DGROUP. Вместо этого каждый модуль имеет свой собственный
сегмент данных, который является дальним сегментом относительно
всех других модулей в программе (нет совместно используемого
ближнего сегмента данных). При использовании сверхбольшой модели
памяти на входе в функцию регистр DS должен быть установлен таким
образом, чтобы он указывал на этот дальний сегмент данных модуля
и не изменялся до конца функции, например:
.
.
.
.FARDATA
.
.
.
.CODE
PUBLIC _AsmFunction
_AsmFunction PROC
push ds
mov ax,@FarData
mov ds,ax
.
.
.
pop ds
ret
_AsmFunction ENDP
.
.
.
Заметим, что исходное состояние регистра DS сохраняется при
входе в функцию _AsmFunction с помощью инструкции PUSH и перед
выходом восстанавливается с помощью инструкции POP. Даже в сверх-
большой модели памяти Borland C++ требует, чтобы все функции сох-
раняли регистр DS.
Общедоступные и внешние идентификаторы
Программы Турбо Ассемблера могут вызывать функции С++ и ссы-
латься на внешние переменные Си. Программы Borland C++ аналогич-
ным образом могут вызывать общедоступные (PUBLIC) функции Турбо
Ассемблера и обращаться к переменным Турбо Ассемблера. После то-
го, как в Турбо Ассемблере устанавливаются совместимые с Borland
C++ сегменты (как описано в предыдущих разделах), чтобы совместно
использовать функции и переменные Borland C++ и Турбо Ассемблера,
нужно соблюдать несколько простых правил.
Подчеркивания и язык Си
Если вы пишете на языке Си или С++, то все внешние метки
должны начинаться с символа подчеркивания (_). Компилятор Си и
С++ вставляет символы подчеркивания перед всеми именами внешних
функций и переменных при их использовании в программе на Си/С++
автоматически, поэтому вам требуется вставить их самим только в
ассемблерных кодах. Вы должны убедиться, что все ассемблерные об-
ращения к функциям и переменным Си начинаются с символа подчерки-
вания, и кроме того, вы должны вставить его перед именами всех
ассемблерных функций и переменных, которые делаются общими и вы-
зываются из программы на языке Си/С++.
Например, следующая программа на языке Си (link2asm.cpp):
extrn int ToggleFlag();
int Flag;
main()
{
ToggleFlag();
}
правильно компонуется со следующей программой на Ассемблере
(CASMLINK.ASM):
.MODEL SMALL
.DATA
EXTRN _Flag:word
.CODE
PUBLIC _ToggleFlag
_ToggleFlag PROC
cmp [_Flag],0 ; флаг сброшен?
jz SetFlag ; да, установить его
mov [_Flag],0 ; нет, сбросить его
jmp short EndToggleFlag ; выполнено
SetFlag:
mov [_Flag],1 ; установить флаг
EndToggleFlag:
ret
_ToggleFlag ENDP
END
При использовании в директивах EXTERN и PUBLIC спецификатора
языка Си правильно компонуется со следующей программой на Ассемб-
лере (CSPEC.ASM):
.MODEL SMALL
.DATA
EXTRN C Flag:word
.CODE
PUBLIC C ToggleFlag
ToggleFlag PROC
cmp [Flag],0 ; флаг сброшен?
jz SetFlag ; да, установить его
mov [Flag],0 ; нет, сбросить его
jmp short EndToggleFlag ; выполнено
SetFlag:
mov [Flag],1 ; установить флаг
EndToggleFlag:
ret
ToggleFlag ENDP
END
Примечание: Метки, на которые отсутствуют ссылки в
программе не Си (такие, как SetFlag) не требуют предшеству-
ющих символов подчеркивания.
Турбо Ассемблер автоматически при записи имен Flag и
ToggleFlag в объектный файл поместит перед ними символ подчерки-
вания.
Различимость строчных и прописные символов в идентификаторах
В именах идентификаторов Турбо Ассемблер обычно не различает
строчные и прописные буквы (верхний и нижний регистр). Поскольку
в С++ они различаются, желательно задать такое различие и в Турбо
Ассемблере (по крайней мере для тех идентификаторов, которые сов-
местно используются Ассемблером и С++). Это можно сделать с по-
мощью параметров /ML и /MX.
Переключатель (параметр) командной строки /ML приводит к
тому, что в Турбо Ассемблере во всех идентификаторах строчные и
прописные символы будут различаться (считаться различными). Пара-
метр командной строки /MX указывает Турбо Ассемблеру, что строч-
ные и прописные символы (символы верхнего и нижнего регистра)
нужно различать в общедоступных (PUBLIC) идентификаторах, внешних
(EXTRN) идентификаторах глобальных (GLOBAL) идентификаторах и об-
щих (COMM) идентификаторах. В большинстве случаев следует также
использовать параметр /ML.
Типы меток
Хотя в программах Турбо Ассемблера можно свободно обращаться
к любой переменной или данным любого размера (8, 16, 32 бита и т.
д.), в общем случае хорошо обращаться к переменным в соответствии
с их размером. Например, если вы записываете слово в байтовую пе-
ременную, то обычно это приводит к проблемам:
.
.
.
SmallCount DB 0
.
.
.
mov WORD PTR [SmallCount],0ffffh
.
.
.
Поэтому важно, чтобы в операторе Ассемблера EXTRN, в котором
описываются переменные С++, задавался правильный размер этих пе-
ременных, так как при генерации размера доступа к переменной С++
Турбо Ассемблер основывается именно на этих описаниях.
Если в программе на языке С++ содержится оператор:
char c
то код Ассемблера:
.
.
.
EXTRN c:WORD
.
.
.
inc [c]
.
.
.
может привести к весьма неприятным ошибкам, поскольку после того,
как в коде на языке С++ переменная c увеличится очередные 256
раз, ее значение будет сброшено, а так как она описана, как пере-
менная размером в слово, то байт по адресу OFFSET c + 1 будет
увеличиваться некорректно, что приведет к непредсказуемым резуль-
татам.
Между типами данных С++ а Ассемблера существует следующее
соотношение:
--------------------------------T-------------------------------¬
¦ Тип данных С++ ¦ Тип данных Ассемблера¦
+-------------------------------+-------------------------------+
¦ unsigned char ¦ byte ¦
¦ char ¦ byte ¦
¦ enum ¦ word ¦
¦ unsigned short ¦ word ¦
¦ short ¦ word ¦
¦ unsigned int ¦ word ¦
¦ int ¦ word ¦
¦ unsigned long ¦ dword ¦
¦ long ¦ dword ¦
¦ float ¦ dword ¦
¦ double ¦ qword ¦
¦ long double ¦ tbyte ¦
¦ near* ¦ word ¦
¦ far* ¦ dword ¦
L-------------------------------+--------------------------------
Внешние дальние идентификаторы
Если вы используете упрощенные директивы определения сегмен-
тов, то описания идентификаторов EXTRN в сегментах дальнего типа
не должны размещаться ни в каком сегменте, так как Турбо Ассем-
блер рассматривает идентификаторы, описанные в данном сегменте,
как связанные с данным сегментом. Это имеет свои недостатки: Тур-
бо Ассемблер не может проверить возможность адресации к идентифи-
катору, описанному, как внешний (EXTRN), вне любого сегмента и
поэтому не может в случае необходимости сгенерировать определе-
ние сегмента или сообщить вам, что была попытка обратиться к дан-
ной переменной, когда сегмент не был загружен корректным значени-
ем. Тем не менее Турбо Ассемблер генерирует для ссылок на такие
внешние идентификаторы правильный код, но не может обеспечить
обычную степень проверки возможности адресации к сегменту.
Если вы все-таки захотите, то можно использовать для явного
описания каждого внешнего идентификатора сегмента старые директи-
вы определения сегментов, а затем поместить директиву EXTRN для
этого идентификатора внутрь описания сегмента. Это довольно уто-
мительно, поэтому если вы не хотите обеспечивать загрузку коррек-
тного значения сегмента при обращении к данным, то проще всего
просто разместить описания EXTRN для идентификаторов дальнего
типа вне всех сегментов. Предположим, например, что файл
FILE1.ASM содержит следующее:
.
.
.
.FARDATA
FileVariable DB 0
.
.
.
и он компонуется с файлом FILE2.ASM, который содержит:
.
.
.
.DATA
EXTRN FileVariable:BYTE
.CODE
Start PROC
mov ax,SEG FileVariable
mov ds,ax
.
.
.
SEG FileVariable не будет возвращать корректного значения
сегмента. Директива EXTRN размещена в области действия директивы
файла FILE2.ASM DATA, поэтому Турбо Ассемблер считает, что пере-
менная FileVariable должна находиться в ближнем сегменте DATA
файла FILE2.ASM, а не в дальнем сегмента DATA.
В следующем коде FILE2.ASM SEG FileVariable будет возвращать
корректное значение сегмента:
.
.
.
.DATA
@CurSeg ENDS
EXTRN FileVariable:BYTE
.CODE
Start PROC
mov ax,SEG FileVariable
mov ds,ax
.
.
.
"Фокус" здесь состоит в том, что директива @CurSeg ENDS за-
вершает сегмент .DATA, поэтому, когда переменная FileVariable
описывается, как внешняя, никакая сегментная директива не дейс-
твует.
Командная строка компоновщика
Простейший способ скомпоновать модули Borland C++ с модулями
Турбо Ассемблера состоит в том, чтобы ввести одну командную стро-
ку Borland C++, после чего он выполнит всю остальную работу. При
задании нужной командной строки Borland C++ выполнит компиляцию
исходного кода Си, вызовет Турбо Ассемблер для ассемблирования, а
затем вызовет утилиту TLINK для компоновки объектных файлов в вы-
полняемый файл. Предположим, например, что у вас есть программа,
состоящая из файлов на языке Си MAIN.CPP и STAT.CPP и файлов Ас-
семблера SUMM.ASM и DISPLAY.ASM. Командная строка:
bcc main.cpp stat.cpp summ.asm display.asm
выполняет компиляцию файлов MAIN.CPP и STAT.CPP, ассемблирование
файлов SUMM.ASM и DISPLAY.ASM и компоновку всех четырех объектных
файлов, а также кода инициализации С++ и необходимых библиотечных
функций в выполняемый файл MAIN.EXE. При вводе имен файлов Ас-
семблера нужно только помнить о расширениях .ASM.
Если вы используете утилиту TLINK в автономном режиме, то
генерируемые Турбо Ассемблером объектные файлы представляют собой
стандартные объектные модули и обрабатываются также, как объек-
тные модули С++. Описание TLINK в автономном режиме см. в Прило-
жении С.
Взаимодействие между Турбо Ассемблером и Borland C++
Теперь, когда вы понимаете, как нужно строить и компоновать
совместимые с С++ модули Ассемблера, нужно знать, какой код можно
помещать в функции Ассемблера, вызываемые из С++. Здесь нужно
проанализировать три момента: получение передаваемых параметров,
использование регистров и возврат значений в вызывающую програм-
му.
Передача параметров
Borland C++ передает функциям параметры через стек. Перед
вызовом функции С++ сначала заносит передаваемые этой функции па-
раметры, начиная с самого правого параметра и кончая левым, в
стек. В С++ вызов функции:
.
.
.
Test(i, j, 1);
.
.
.
компилируется в инструкции:
mov ax,1
push ax
push word ptr DGROUP:_j
push word ptr DGROUP:_i
call near ptr _Test
add sp,6
где видно, что правый параметр (значение 1), заносится в стек
первым, затем туда заносится параметр j и, наконец, i.
При возврате из функции занесенные в стек параметры все еще
находятся там, но они больше не используются. Поэтому непосредс-
твенно после каждого вызова функции Borland C++ настраивает ука-
затель стека обратно в соответствии со значением, которое он имел
перед занесением в стек параметров (параметры, таким образом, от-
брасываются). В предыдущем примере три параметра (по два байта
каждый) занимают в стеке вместе 6 байт, поэтому Borland C++ до-
бавляет значение 6 к указателю стека, чтобы отбросить параметры
после обращения к функции Test. Важный момент здесь заключается в
том, что в соответствии с используемыми по умолчанию соглашениями
Си/C++ за удаление параметров из стека отвечает вызывающая прог-
рамма.
Функции Ассемблера могут обращаться к параметрам, передавае-
мым в стеке, относительно регистра BP. Например, предположим, что
функция Test в предыдущем примере представляет собой следующую
функцию на Ассемблере (PRMSTACK.ASM):
.MODEL SMALL
.CODE
PUBLIC _Test
_Test PROC
push bp
mov bp,sp
mov ax,[bp+4] ; получить параметр 1
add ax,[bp+6] ; прибавить параметр 2
; к параметру 1
sub ax,[bp+8] ; вычесть из суммы 3
pop bp
ret
_Test ENDP
Как можно видеть, функция Test получает передаваемые из
программы на языке Си параметры через стек, относительно регистра
BP. (Если вы помните, BP адресуется к сегменту стека.) Но откуда
она знает, где найти параметры относительно BP?
На Рис. 18.2 показано, как выглядит стек перед выполнением
первой инструкции в функции Test:
i = 25;
j = 4;
Test(1, j, 1);
. .
. .
. .
¦ ¦
+-----------------------+
¦ ¦
+-----------------------+
SP -- ¦ Адрес возврата ¦
+-----------------------+
SP + 2 ¦ 25 (i) ¦
+-----------------------+
SP + 4 ¦ 4 (j) ¦
+-----------------------+
SP + 6 ¦ 1 ¦
+-----------------------+
¦ ¦
+-----------------------+
¦ ¦
. .
. .
. .
Рис. 18.2 Состояние стека перед выполнением первой инструк-
ции функции Test
Параметры функции Test представляют собой фиксированные ад-
реса относительно SP, начиная с ячейки, на два байта старше адре-
са, по которому хранится адрес возврата, занесенный туда при вы-
зове. После загрузки регистра BP значением SP вы можете обращать-
ся к параметрам относительно BP. Однако, вы должны сначала сохра-
нить BP, так как в вызывающей программе предполагается, что при
возврате BP изменен не будет. Занесение в стек BP изменяет все
смещения в стеке. На Рис. 18.3 показано состояние стека после вы-
полнения следующих строк кода:
.
.
.
push bp
mov bp,sp
.
.
.
. .
. .
. .
¦ ¦
+-----------------------+
SP -- ¦ BP вызывающей прогр. ¦ -- BP
+-----------------------+
SP + 2 ¦ Адрес возврата ¦ BP + 2
+-----------------------+
SP + 4 ¦ 25 (i) ¦ BP + 4
+-----------------------+
SP + 6 ¦ 4 (j) ¦ BP + 6
+-----------------------+
SP + 8 ¦ 1 ¦ BP + 8
+-----------------------+
¦ ¦
+-----------------------+
¦ ¦
. .
. .
. .
Рис. 18.3 Состояние стека после инструкций PUSH и MOVE
Организация передачи параметров функции через стек и исполь-
зование его для динамических локальных переменных - это стандарт-
ный прием в языке С++. Как можно заметить, неважно, сколько пара-
метров имеет программа на языке С++: самый левый параметр всегда
хранится в стеке по адресу, непосредственно следующим за сохра-
ненным в стеке адресом возврата, следующий возвращаемый параметр
хранится непосредственно после самого левого параметра и т.д.
Поскольку порядок и тип передаваемых параметров известны, их
всегда можно найти в стеке.
Пространство для динамических локальных переменных можно за-
резервировать, вычитая из SP требуемое число байт. Например,
пространство для динамического локального массива размером в 100
байт можно зарезервировать, если начать функцию Test с инструк-
ций:
.
.
.
push bp
mov bp,sp
sub sp,100
.
.
.
как показано на Рис. 18.4
. .
. .
. .
¦ ¦
+-----------------------+
SP -- ¦ ¦ - BP - 100
+-----------------------+
¦ ¦
+-----------------------+
. .
. .
. .
. .
¦ ¦
+-----------------------+
SP + 100 -- ¦ BP вызывающей прогр. ¦ -- BP
+-----------------------+
SP + 102 ¦ Адрес возврата ¦ BP + 2
+-----------------------+
SP + 104 ¦ 25 (i) ¦ BP + 4
+-----------------------+
SP + 106 ¦ 4 (j) ¦ BP + 6
+-----------------------+
SP + 108 ¦ 1 ¦ BP + 8
+-----------------------+
¦ ¦
+-----------------------+
¦ ¦
. .
. .
. .
Рис. 18.4 Состояние стека после инструкций PUSH, MOVE и SUB
Поскольку та часть стека, где хранятся динамические локаль-
ные переменные, представляет собой более младшие адреса, чем BP,
для обращения к динамическим локальным переменным используется
отрицательное смещение. Например, инструкция:
mov byte ptr [bp-100]
даст значение первого байта ранее зарезервированного 100-байтово-
го массива. При передаче параметров всегда используется положи-
тельная адресация относительно регистра BP.
Хотя можно выделять пространство для динамических локальных
переменных описанным выше способом, в Турбо Ассемблере предусмот-
рена специальная версия директивы LOCAL, которая существенно уп-
рощает выделение памяти и присваивание имен для динамических ло-
кальных переменных. Когда в процедуре встречается директива
LOCAL, то подразумевается, что она определяет для данной процеду-
ры динамические локальные переменные. Например, директива:
LOCAL LocalArray:BYTE:100,LocalCount:WORD=AUTO_SIZE
определяет динамические переменные LocalArray и LocalCount.
LocalArray на самом деле представляет собой метку, приравненную к
[BP-100], а LocalCount - это метка, приравненная к [BP-102]. Од-
нако вы можете использовать их, как имена переменных. При этом
вам даже не нужно будет знать их значения. AUTO_SIZE - это общее
число байт (объем памяти), необходимых для хранения динамических
локальных переменных. Чтобы выделить пространство для динамичес-
ких локальных переменных, это значение нужно вычесть из SP.
Приведем пример того, как нужно использовать директиву
LOCAL:
.
.
.
_TestSub PROC
LOCAL
LocalArray:BYTE:100,LocalCount:WORD=AUTO_SIZE
push bp ; сохранить указатель стека
; вызывающей программы
mov bp,sp ; установить собственный
; указатель стека
sub sp,AUTO_SIZE ; выделить пространство для
; динамических локальных
; переменных
mov [LocalCount],10 ; установить переменную
; LocalCount в значение 10
; (LocalCount это [BP-102])
.
.
.
mov cx,[LocalCount] ; получить значение
; (счетчик) из локальной
; переменной
mov al,'A' ; заполним символом 'A'
lea bx,[LocalArray] ; ссылка на локальный
; массив LocalArray
; (LocalArray это [BP-100])
FillLoop:
mov [bx],al ; заполнить следующий байт
inc bx ; ссылка на следующий байт
loop FillLoop ; обработать следующий байт,
; если он имеется
mov sp,bp ; освободить память,
; выделенную для динамичес-
; ких локальных переменных
; (можно также использовать
; add sp,AUTO_SIZE)
pop bp ; восстановить указатель
; стека вызывающей программы
ret
_TestSub ENDP
.
.
.
В данном примере следует обратить внимание не то, что первое
поле после определения данной динамической локальной переменной
представляет собой тип данных для этой переменной: BYTE, WORD,
DWORD, NEAR и т.д. Второе поле после определения данной динами-
ческой локальной переменной - это число элементов указанного ти-
па, резервируемых для данной переменной. Это поле является необя-
зательным и определяет используемый динамический локальный массив
(если он используется). Если данное поле пропущено, то резервиру-
ется один элемент указанного типа. В итоге LocalArray состоит из
100 элементов размером в 1 байт, а LocalCount - из одного элемен-
та размером в слово (см. пример).
Отметим также, что строка с директивой LOCAL в данном приме-
ре завершается полем =AUTO_SIZE. Это поле, начинающееся со знака
равенства, необязательно. Если оно присутствует, то метка, следу-
ющая за знаком равенства, устанавливается в значение числа байт
требуемой динамической локальной памяти. Вы должны затем исполь-
зовать данную метку для выделения и освобождения памяти для дина-
мических локальных переменных, так как директива LABEL только ге-
нерирует метки и не генерирует никакого кода или памяти для
данных. Иначе говоря, директива LOCAL не выделяет память для ди-
намических локальных переменных, а просто генерирует метки, кото-
рые вы можете использовать как для выделения памяти, так и для
доступа к динамическим локальным переменным.
Очень удобное свойство директивы LOCAL заключается в том,
что область действия меток динамических локальных переменных и
общего размера динамических локальных переменных ограничена той
процедурой, в которой они используются, поэтому вы можете свобод-
но использовать имя динамической локальной переменной в другой
процедуре.
Как можно заметить, с помощью директивы LOCAL определять и
использовать автоматические переменные намного легче. Отметим,
что при использовании в макрокомандах директива LOCAL имеет со-
вершенно другое значение.
Кстати, Borland C++ работает с границами стека так же, как
мы здесь описали. Вы можете скомпилировать несколько модулей
Borland C++ с параметром -S и посмотреть, какой код Ассемблера
генерирует Borland C++ и как там создаются и используются границы
стека.
Все это прекрасно, но здесь есть некоторые трудности.
Во-первых, такой способ доступа к параметрам, при котором исполь-
зуется постоянное смещение относительно BP достаточно неприятен:
при этом не только легко ошибиться, но если вы добавите другой
параметр, все другие смещения указателя стека в функции должны
измениться. Предположим, например, что функция Test воспринимает
три параметра:
Test(Flag, i, j, 1);
Тогда i находится по смещению 6, а не по смещению 4, j - по
смещению 8, а не 6 и т.д. Для смещений параметров можно использо-
вать директиву EQU:
.
.
.
Flag EQU 4
AddParm1 EQU 6
AddParm2 EQU 8
SubParm1 EQU 10
mov ax[bp+AddParm1]
add ax,[bp+AddParm1]
sub ax,[bp+SubParm1]
.
.
.
но вычислять смещения и работать с ними довольно сложно. Однако
здесь могут возникнуть и более серьезные проблемы: в моделях па-
мяти с дальним кодом размер занесенного в стек адреса возврата
увеличивается на два байта, как и размеры передаваемых указателей
на код и данные в моделях памяти с дальним кодом и дальними дан-
ными, соответственно. Разработка функции, которая с равным успе-
хом будет ассемблироваться и правильно работать с указателем сте-
ка при использовании любой модели памяти было бы весьма непростой
задачей.
Однако в Турбо Ассемблере предусмотрена директива ARG, с по-
мощью которой можно легко выполнять передачу параметров в прог-
раммах на Ассемблере.
Директива ARG автоматически генерирует правильные смещения в
стеке для заданных вами переменных. Например:
ARG FillArray:WORD, Count:WORD, FillValue:BYTE
Здесь задается три параметра: FillArray, параметр размером в
слово, Count, также параметр размером в слово и FillValue - пара-
метр размером в байт. Директива ARG устанавливает метку
FillArray в значение [BP+4] (подразумевается, что код находится
в процедуре ближнего типа), метку Count - в значение [BP+6], а
метку FillValue - в значение [BP+8]. Однако особенно ценна дирек-
тива ARG тем, что вы можете использовать определенные с ее по-
мощью метки не заботясь о тех значениях, в которые они установле-
ны.
Например, предположим, что у вас есть функция FillSub кото-
рая вызывается из С++ следующим образом:
extern "C" {
void FillSub(
char *FillArray,
int Count,
char FillValue);
}
main()
{
#define ARRAY_LENGTH 100
char TestArray[ARRAY_LENGTH];
FillSub(TestArray,ARRAY_LENGTH,'*');
}
В FillSub директиву ARG для работы с параметрами можно ис-
пользовать следующим образом:
_FillSub PROC NEAR
ARG FillArray:WORD, Count:WORD, FillValue:BYTE
push bp ; сохранить указатель стека
; вызывающей программы
mov bp,sp ; установить свой собственный
; указатель стека
mov bx,[FillArray] ; получить указатель на
; заполняемый массив
mov cx,[Count] ; получить заполняемую длину
mov al,[FillValue] ; получить значение-заполнитель
FillLoop:
mov [bx],al ; заполнить символ
inc bx ; ссылка на следующий символ
loop FillLoop ; обработать следующий символ
pop bp ; восстановить указатель стека
; вызывающей программы
ret
_FillSub ENDP
Не правда ли, удобно работать с параметрами с помощью дирек-
тивы ARG? Кроме того, директива ARG автоматически учитывает раз-
личные размеры возвратов ближнего и дальнего типа.
Сохранение регистров
При взаимодействии Турбо Ассемблера и Borland C++ вызываемые
из программы на языке С++ функции Ассемблера могут делать все что
угодно, но при этом они должны сохранять регистры BP, SP, CS, DS
и SS. Хотя при выполнении функции Ассемблера эти регистры можно
изменять, при возврате из вызываемой подпрограммы они должны
иметь в точности такие значения, какие они имели при ее вызове.
Регистры AX, BX, CX, DX и ES, а также флаги могут произвольно из-
меняться.
Регистры DI и SI представляют собой особый случай, так как в
Borland C++ они используются для регистровых переменных. Если в
модуле С++, из которого вызывается ваша функция на Ассемблере,
использование регистровых переменных разрешено, то вы должны сох-
ранить регистры SI и DI, если же нет, то сохранять их не нужно.
Однако неплохо всегда сохранять эти регистры, независимо от
того, разрешено или запрещено использование регистровых перемен-
ных. Трудно заранее гарантировать, что вам не придется компоно-
вать данный модуль Ассемблера с другим модулем на языке С++, или
перекомпилировать модуль С++ с разрешением использования регист-
ровых переменных. При этом вы можете забыть, что изменения нужно
также внести и в код Ассемблера.
Возврат значений
Вызываемые из программы на языке С++ функции на Ассемблере,
так же как и функции С++, могут возвращать значения. Значения
функций возвращаются следующим образом:
---------------------------T------------------------------------¬
¦Тип возвращаемого значения¦ Где находится возвращаемое значение¦
+--------------------------+------------------------------------+
¦ unsigned char ¦ AX ¦
¦ char ¦ AX ¦
¦ enum ¦ AX ¦
¦ unsigned short ¦ AX ¦
¦ short ¦ AX ¦
¦ unsigned int ¦ AX ¦
¦ int ¦ AX ¦
¦ unsigned long ¦ DX:AX ¦
¦ long ¦ DX:AX ¦
¦ float ¦ регистр вершины стека сопроцессора¦
¦ ¦ 8087 (ST(0)) ¦
¦ double ¦ регистр вершины стека сопроцессора¦
¦ ¦ 8087 (ST(0)) ¦
¦ long double ¦ регистр вершины стека сопроцессора¦
¦ ¦ 8087 (ST(0)) ¦
¦ near* ¦ AX ¦
¦ far* ¦ DX:AX ¦
L--------------------------+-------------------------------------
В общем случае 8- и 16-битовые значения возвращаются в ре-
гистре AX, а 32-битовые значения - в AX:DX (при этом старшие 16
бит значения находятся в регистре DX). Значения с плавающей точ-
кой возвращаются в регистре ST(0), который представляет собой ре-
гистр вершины стека сопроцессора 8087 или эмулятора сопроцессора
8087, если используется эмулятор операций с плавающей точкой.
Со структурами дело обстоит несколько сложнее. Структуры,
имеющие длину 1 или 2 байта, возвращаются в регистре AX, а струк-
туры длиной 4 байта - в регистрах AX:DX. Трехбайтовые структуры и
структуры, превышающие 4 байта должны храниться в области стати-
ческих данных, при этом должен возвращаться указатель на эти ста-
тические данные. Как и все указатели, указатели на структуры, ко-
торые имеют ближний тип (NEAR), возвращаются в регистре AX, а
указатели дальнего типа - в паре регистров AX:DX.
Давайте рассмотрим вызываемую из программы на языке С++
функцию на Ассемблере с малой моделью памяти FindLastChar, кото-
рая возвращает указатель на последний символ передаваемой строки.
На языке С++ прототип этой функции выглядел бы следующим образом:
extern char * FindLastChar(char * StringToScan);
где StringToScan - это непустая строка, для которой должен возв-
ращаться указатель на последний символ.
Функция FindLastChar имеет следующий вид:
.MODEL SMALL
.CODE
PUBLIC _FindLastChar
_FindLastChar PROC
push bp
mov bp,sp
cld ; в строковой инструкции нужно
; выполнять отсчет в прямом
; направлении
mov ax,ds
mov es,ax ; теперь ES указывает на
; ближний сегмент данных
mov di, ; теперь ES:DI указывает на
; начало передаваемой строки
mov al,0 ; найти нулевой символ,
; завершающий строку
mov cx,0ffffh ; работать в пределах
; 64К-1 байт
repne scasb ; найти нулевой символ
dec di ; установить указатель
; обратно на 0
dec di ; ссылка обратно на
; последний символ
mov ax,dx ; возвратить в AX указатель
; ближнего типа
pop bp
ret
_FindLastChar ENDP
END
Конечный результат, указатель на передаваемую строку, возв-
ращается в регистре AX.
Вызов функции Турбо Ассемблера из Borland C++
Теперь мы рассмотрим пример программы на Borland C++, вызы-
вающей функцию Турбо Ассемблера. Модуль Турбо Ассемблера
COUNT.ASM содержит функцию LineCount, которая возвращает значение
счетчика числа строк и символов в передаваемой строке:
; Вызываемая из С++ функция на Ассемблере с малой моделью памяти
; для подсчета числа строк и символов в завершающейся нулем
; "строке".
;
; Прототип функции:
; extern unsigned int LineCount(char * near StringToCount,
; unsigned int near * CharacterCountPtr);
;
; Ввод:
; char near * StringToCount: указатель на "строку", в
; которой нужно выполнить подсчет строк.
;
; unsigned int near * CharacterCountPtr: указатель на
; целую переменную, в которую нужно записать значение
; счетчика
NEWLINE EQU 0ah ; символ перевода строки в Си
.MODEL SMALL
.CODE
PUBLIC _LineCount
__LineCount PROC
push bp
mov bp,sp
push si ; сохранить регистровую
; переменную вызывающей
; программы
mov si,[bp+4] ; SI указывает на строку
sub cx,cx ; установить значение
; счетчика символов в 0
mov dx,cx ; установить в 0 счетчик
; строк
LineCountLoop:
lodsb ; получить следующий символ
and al,al ; это 0? конец строки?
jz EndLineCount ; да, выполнено
inc cx ; нет, подсчитать следующий
; символ
cmp al,NEWLINE ; это новая строка?
jnz LineCountLoop ; нет, проверить
; следующий символ
inc dx ; да, подсчитать еще одну
; строку
jmp LineCountLoop
EndLineCount:
inc dx ; подсчитать строку, которая
; завершается нулевым символом
mov [bx],cx ; задать значение переменной-
; счетчика
mov ax,dx ; возвратить счетчик строк в
; качестве значения счетчика
pop si ; восстановить регистровую
; переменную вызывающей
; программы
pop bp
ret
_LineCount ENDP
END
Следующий модуль на языке С++ с именем CALLCT.CPP представ-
ляет собой пример вызова функции LineCount:
char * TestString="Line 1\nline 2\nline 3";
extern "C" {
unsigned int LineCount(char * StringToCount,
unsigned int near * CharacterCountPtr); }
main()
{
unsigned int LCount;
unsigned int CCount;
Lcount = LineCount(TestString, &CCount);
printf("Lines: %d\nCharacters: %d\n", LCount, CCount);
}
Два модуля компилируются и компонуются вместе с помощью ко-
мандной строки:
bcc -ms callct.cpp count.asm
Как здесь показано, функция LineCount будет работать только
при компоновке с программами на языке С++, в которых используется
малая модель памяти, так как в других моделях размеры указателей
и адресов в стеке изменятся. Приведем пример версии функции
LineCount (COUNTLG.ASM), которая будет работать с программами на
С++, использующим большую модель памяти (но не малую модель: пос-
кольку передаются дальние указатель, функция LineCount также опи-
сана, как функция дальнего типа):
; Вызываемая из С++ функция на Ассемблере для подсчета числа
; строк и символов в завершающейся нулем "строке".
;
; Прототип функции:
; extern unsigned int LineCount(char * far StringToCount,
; unsigned int far * CharacterCountPtr);
;
; Ввод:
; char far * StringToCount: указатель на "строку", в
; которой нужно выполнить подсчет строк.
;
; unsigned int far * CharacterCountPtr: указатель на
; целочисленную переменную, в которую нужно записать
; значение счетчика
NEWLINE EQU 0ah ; символ перевода строки в Си
.MODEL LARGE
.CODE
PUBLIC _LinaCount
_LineCount PROC
push bp
mov bp,sp
push si ; сохранить регистровую
; переменную вызывающей
; программы
push ds ; сохранить стандартный
; сегмент данных
lds si,[bp+6] ; DS:SI указывает на строку
sub cx,cx ; установить значение
; счетчика символов в 0
mov dx,cx ; установить в 0 счетчик
; строк
LineCountLoop:
lodsb ; получить следующий символ
and al,al ; это 0? конец строки?
jz EndLineCount ; да, выполнено
inc cx ; нет, подсчитать следующий
; символ
cmp al,NEWLINE ; это новая строка?
jnz LineCountLoop ; нет, проверить
; следующий символ
inc dx ; да, подсчитать еще одну
; строку
jmp LineCountLoop
EndLineCount:
inc dx ; подсчитать строку, которая
; завершается нулевым символом
les bx,[bp+10] ; ES:BX указывает на ячейку,
; в которой возвращается
; значение счетчика
mov es:[bx],cx ; задать значение переменной-
; счетчика
mov ax,dx ; возвратить счетчик строк в
; качестве значения счетчика
pop ds ; восстановить стандартный
; сегмент данных Си
pop si ; восстановить регистровую
; переменную вызывающей
; программы
pop bp
ret
_LineCount ENDP
END
Программу COUNTLG.ASM можно скомпоновать с CALLCT.CPP с по-
мощью следующей командной строки:
bcc -ml callct.cpp countlg.asm
Написание на языке Ассемблера функций-элементов С++
Хотя можно написать функцию-элемент класса С++ целиком на
языке Ассемблера, это далеко не просто. Например, все функ-
ции-элементы классов С++ имеют "откорректированные" имена, что
обеспечивает безопасную по согласованности типов компоновку функ-
ций и делает возможным переопределение функций, а ваша ассемблер-
ная функция должна знать в точности, какое имя С++ ожидает для
данной функции-элемента. Для доступа к переменным-элементам вы
должны подготовить в ассемблерном коде определение STRUC, опреде-
ляющее все переменные-элементы с точно совпадающими размерами и
расположением. Если ваш класс является производным, то могут су-
ществовать и другие переменные-элементы, производные от базового
класса. Даже если класс не является производным (порожденным), то
расположение переменных-элементов в памяти изменяется в случае,
если класс этот включает в себя какие-либо виртуальные функции.
Если вы пишете функцию на встроенном Ассемблере, Borland С++
может взять на себя эти вопросы. Однако если вы работаете на язы-
ке Ассемблера отдельно (например, переделываете уже имеющийся
код), то существуют некоторые методы, позволяющие упростить эту
работу.
Создайте определение фиктивной функции С++ для ассемблерной
функции. Это определение удовлетворит компоновщик, так как будет
содержать откорректированное имя функции-элемента. Эта фиктивная
функция будет вызывать ассемблерную функцию и передавать ей пере-
менные-элементы и прочие параметры. Так как ассемблерный код бу-
дет иметь все нужные ему параметры посредством аргументов, вы мо-
жете не заботиться об изменениях в определении класса. Ваша
ассемблерная функция может быть описана в коде С++ как extern
"C", что показано в примерах. Например (countadd.cpp):
class count_add {
// Частные переменные-элементы (private)
int access_count; // число обращений
int count; // текущий счетчик
public:
count_add(void) { access_count=0;
count=0;
}
int get_count (void) {return Count;}
// Две функции, которые будут фактически написаны на
// Ассемблере:
void increment(void);
void add(int what_to_add=-1);
// Отметим, что умолчание влияет только
// на вызовы add; оно не влияет на код add
}
extern "C" {
// Для создания уникальных и осмысленных имен
// ассемблерных подпрограмм прибавим имя класса к
// имени ассемблерной подпрограммы. В отличие от прочих
// ассемблеров, Турбо Ассемблер не имеет проблем с
// длиной имен.
void count_add_increment(int *count); // Мы передадим
// указатель на
// переменную count.
// Ассемблер выполнит
// увеличение.
void count_add_add(int *count,int what_to_add);
}
void count_add::increment(void)
{
count_add_increment(&count);
}
void count_add(int what_to_add)
{
count_add(&count, int what_to_add);
}
Ваш ассемблерный модуль, определяющий подпрограммы count_add
_increment и count_add_add, должен иметь вид (COUNTADD.ASM):
.MODEL small ; выбор модели small (ближние код и данные)
.CODE
PUBLIC _count_add_increment
_count_add_increment PROC
ARG count_offset:word ; Адрес переменной-элемента
push bp ; Сохранение записи активации
; вызывающей программы
mov bp,sp ; Установка собственной записи
; активации
mov bx,[count_offset] ; Загрузка указателя
inc word ptr [bx] ; Увеличение переменной-элемента
pop bp ; Восстановление записи активации
; вызывающей программы
_count_add_increment ENDP
PUBLIC _count_add_add
_count_add_add PROC
ARG count_offset:word,what_to_add:word
push bp
mov bp,sp
mov bx,[count_offset] ; Загрузка указателя
mov ax,[what_to_add]
add [bx],ax
pop bp
ret
_count_add_add ENDP
end
Используя данный метод, вы можете не беспокоиться об измене-
ниях в определении класса. Даже если вы добавляете или удаляете
переменные-элементы, делаете этот класс производным или добавляе-
те виртуальные функции, вам не требуется изменять ассемблерный
модуль. Переассемблировать модуль нужно только в случае изменения
структуры переменной-элемента count, либо если вы ходите сделать
версию данного класса для модели памяти large. Переассемблирова-
ние в этих случаях необходимо, поскольку при обращении к перемен-
ной-элементу count вы имеете дело с сегментом и смещением.
Соглашения по вызовам, использующиеся в Паскале
Итак, теперь вы уже знаете, как обычно в С++ передаются па-
раметры функциям: вызывающая программа заносит параметры (справа
налево) в стек, вызывает функцию, и извлекает параметры из стека
(отбрасывает их) после вызова. Borland C++ может также работать
по соглашениям, принятым в Паскале. Согласно этим соглашениям па-
раметры передаются слева направо, а отбрасывает параметры (из
стека) вызываемая программа. Разрешить использование соглашений
Паскаля в Borland C++ можно с помощью параметра командной строки
-p или ключевого слова pascal.
Примечание: Более подробно соглашения о связях Паскаля
рассматриваются в Главе 19.
Приведем пример функции на Ассемблере, в которой используют-
ся соглашения Паскаля:
;
; Вызывается, как: TEST(i, j ,k)
;
i equ 8 ; левый параметр
j equ 6
k equ 4 ; правый параметр
;
.MODEL SMALL
.CODE
PUBLIC TEST
TEST PROC
push bp
mov bp,sp
mov ax,[bp+i] ; получить i
add ax,[bp+j] ; прибавить к i j
sub ax,[bp+k] ; вычесть из суммы k
pop bp
ret 6 ; возврат, отбросить
; 6 байт параметров
; (очистка стека)
TEST ENDP
END
Заметим, что для очистки стека от передаваемых параметров
используется инструкция RET 6.
На Рис. 18.5 показано состояние стека после выполнения инс-
трукции MOV BP,SP:
. .
. .
. .
¦ ¦
+-----------------------+
SP -- ¦ BP вызывающей прогр. ¦ -- BP
+-----------------------+
SP + 2 ¦ Адрес возврата ¦ BP + 2
+-----------------------+
SP + 4 ¦ k ¦ BP + 4
+-----------------------+
SP + 6 ¦ j ¦ BP + 6
+-----------------------+
SP + 8 ¦ i ¦ BP + 8
+-----------------------+
¦ ¦
+-----------------------+
¦ ¦
. .
. .
. .
Рис. 18.5 Состояние стека после инструкции MOV BP,SP
Соглашения по вызовам Паскаля требуют также, чтобы все внеш-
ние и общедоступные идентификаторы указывались в верхнем регистре
и без предшествующих подчеркиваний. Зачем может потребоваться ис-
пользовать в программе на С++ соглашения по вызовам Паскаля?
Программа, использующая соглашения Паскаля, занимает обычно нес-
колько меньше места в памяти и работает быстрее, чем обычная
программа на языке С++, так как для очистки стека от параметров
не требуется выполнять n инструкций ADD SP.
Вызов Borland C++ из Турбо Ассемблера
Хотя больше принято для выполнения специальных задач вызы-
вать из С++ функции, написанные на Ассемблере, иногда вам может
потребоваться вызывать из Ассемблера функции, написанные на языке
С++. Оказывается, на самом деле легче вызвать функцию Borland C++
из функции Турбо Ассемблера, чем наоборот, поскольку со стороны
Ассемблера не требуется отслеживать границы стека. Давайте расс-
мотрим кратко требования для вызова функций Borland C++ из Турбо
Ассемблера.
Компоновка с кодом инициализации С++
Хорошим правилом является вызов библиотечных функций Borland
C++ только из Ассемблера в программах, которые компонуются с мо-
дулем инициализации С++ (используя его в качестве первого компо-
нуемого модуля). Этот "надежный" класс включает в себя все прог-
раммы, которые компонуются с помощью командной строки TC.EXE или
TCC.EXE, и программы, в качестве первого компонуемого файла кото-
рых используется файл C0T, C0S, C0C, C0M, C0L или C0H.
В общем случае вам не следует вызывать библиотечные функции
Borland C++ из программ, которые не компонуются с модулем инициа-
лизации Borland C++, так как некоторые библиотечные функции
Borland C++ не будут правильно работать, если не выполнялась ком-
поновка с кодом инициализации. Если вы действительно хотите вызы-
вать библиотечные функции Borland C++ из таких программ, мы пред-
лагаем вам взглянуть на код инициализации (файл C0.ASM на дистри-
бутивных дисках Borland C++) и приобрести у фирмы Borland исход-
ный код библиотеки языка С++, после чего вы сможете обеспечить
правильную инициализацию для нужных библиотечных функций.
Вызов определяемых пользователем функций С++, которые в
свою очередь вызывают библиотечные функции языка С++, попадают в
ту же категорию, что и непосредственный вызов библиотечных функ-
ций С++. Отсутствие кода инициализации С++ может вызывать ошибки
в любой программе Ассемблера, которая прямо или косвенно обраща-
ется к библиотечным функциям С++.
Задание сегмента
Как мы уже говорили ранее, необходимо обеспечивать, чтобы
Borland C++ и Турбо Ассемблер использовали одну и ту же модель
памяти, и чтобы сегменты, которые вы используете в Турбо Ассемб-
лере, совпадали с теми сегментами, которые использует Borland
C++. В Турбо Ассемблере имеется модель памяти tchuge,которая под-
держивает модель huge Borland C++. Перечень моделей памяти и сег-
ментов можно найти в предыдущем разделе. Нужно не забывать также
помещать директиву EXTRN для внешних идентификаторов вне всех
сегментов или внутри правильного сегмента.
Выполнение вызова
Все, что требуется от вас для передачи параметров в функцию
C++, это занесение в стек самого правого параметра первым, затем
следующего по порядку параметра и так далее, пока в стеке не ока-
жется самый левый параметр. После этого нужно просто вызвать
функцию. Например, при программировании на Borland C++ для вызова
библиотечной функции Borland C++ strcpy для копирования строки
SourceString в строку DestString можно ввести:
strcpy(DestString, SourceString);
Для выполнения того же вызова на Ассемблере нужно использо-
вать инструкции:
lea ax,SourceString ; правый параметр
push ax
lea ax,DestString ; левый параметр
push ax
call _strcpy ; скопировать строку
add sp,4 ; отбросить параметры
При настройке SP после вызова не забывайте очищать стек от
параметров.
Можно упростить ваш код и сделать его независимым от языка,
воспользовавшись расширением команды Турбо Ассемблера CALL:
call назначение [язык [,аргумент_1] .]
где "язык" - это C, PASCAL, BASIC, FORTRAN, PROLOG или
NOLANGUAGE, а "аргумент_n" это любой допустимый аргумент програм-
мы, который может быть прямо помещен в стек процессора.
Используя данное средство, можно записать:
lea ax,SourceString
lea bx,DestString
call strcpy c,bx,ax
Турбо Ассемблер автоматически вставит команды помещения ар-
гументов в стек в последовательности, принятой в С++ (сначала AX,
затем BX), выполнит вызов _strcopy (перед именами С++ Турбо Ас-
семблер автоматически вставляет символ подчеркивания), и очищает
стек после вызова.
Если вы вызываете функцию С++, которая использует соглашения
Паскаля, заносите в стек параметры слева направо. После вызова
настраивать указатель стека SP не требуется.
lea ax,DestString ; левый параметр
push ax
lea ax,SourceString ; правый параметр
push ax
call CTRCPY ; скопировать строку
Можно опять упростить ваш код, воспользовавшись расширением
команды Турбо Ассемблера CALL:
lea bx,DestString ; самый левый параметр
lea ax,SourceString ; самый правый параметр
call strcpy pascal,bx,ax
Турбо Ассемблер автоматически вставит команды помещения ар-
гументов в стек в последовательности, принятой в Паскале (сначала
BX, затем AX), и выполнит вызов STRCPY (преобразуя имя к верхнему
регистру, как принято в соглашениях Паскаля).
В последнем случае конечно подразумевается, что вы переком-
пилировали функцию strcpy с параметром -p, так как в стандартной
библиотечной версии данной функции используются соглашения по вы-
зову, принятые в С++, а не в Паскале.
Функции С++ сохраняют следующие регистры (и только их): SI,
DI, BP, DS, SS, SP и CS. Регистры AX, BX, CX, DX, ES и флаги мо-
гут произвольно изменяться.
Вызов из Турбо Ассемблера функции Borland C++
Одним из случаев, когда вам может потребоваться вызвать из
Турбо Ассемблера функцию Borland C++, является необходимость вы-
полнения сложных вычислений, поскольку вычисления гораздо проще
выполнять на С++, чем на Ассемблера. Особенно это относится к
случаю смешанных вычислений, где используются и значения с плава-
ющей точкой и целые числа. Лучше возложить функции по выполнению
преобразования типов и реализации арифметики с плавающей точкой
на С++.
Давайте рассмотрим пример программы на Ассемблере, которая
вызывает функцию Borland C++, чтобы выполнить вычисления с плава-
ющей точкой. Фактически в данном примере функция Borland C++ пе-
редает последовательность целых чисел другой функции Турбо Ас-
семблера, которая суммирует числа и в свою очередь вызывает
другую функцию Borland C++ для выполнения вычислений с плавающей
точкой (вычисление среднего значения).
Часть программы CALCAVG.CPP, реализованная на С++
(CALCAVG.CPP), выглядит следующим образом:
#include <stdio.h>
extern float Average(int far * ValuePtr, int
NumberOfValues);
#define NUMBER_OF_TEST_VALUES 10
int TestValues(NUMBER_OF_TEST_VALUES) = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
};
main()
{
printf("Среднее арифметическое равно: %f\n",
Average(TestValues, NUMBER_OF_TEST_VALUES));
}
float IntDivide(int Divedent, int Divisor)
}
return( (float) Divident / (float) Divisor );
}
а часть программы на Ассемблере (AVERAGE.ASM) имеет вид:
;
; Вызываемая из С++ функция с малой моделью памяти,
; которая возвращает среднее арифметическое последова-
; тельности целых чисел. Для выполнения завершающего
; деления вызывает функцию С++ IntDivide().
;
; Прототип функции:
; extern float Average(int far * ValuePtr,
; int NumberOfValues);
;
; Ввод:
; int far * ValuePtr: ; массив значений для
; ; вычисления среднего
; int NumberOfValues: ; число значений для
; ; вычисления среднего
.MODEL SMALL
EXTRN _IntDivide:PROC
.CODE
PUBLIC _Average
_Average PROC
push bp
mov bp,sp
les bx,[bp+4] ; ES:BX указывает на
; массив значений
mov cx,[bp+8] ; число значений, для
; которых нужно
; вычислить среднее
mov ax,0
AverageLoop:
add ax,es:[bx] ; прибавить текущее
; значение
add ax,2 ; ссылка на следующее
; значение
loop AverageLoop
push WORD PTR [bp+8] ; получить снова число
; значений, переданных
; в функцию IntDivide
; в правом параметре
push ax ; передать сумму в
; левом параметре
call _IntDivide ; вычислить среднее
; значение с плавающей
; точкой
add sp,4 ; отбросить параметры
pop bp
ret ; среднее значение в
; регистре вершины
; стека сопроцессора
; 8087
_Average ENDP
END
Основная функция (main) на языке С++ передает указатель на
массив целых чисел TestValues и длину массива в функцию на Ас-
семблере Average. Эта функция вычисляет сумму целых чисел, а за-
тем передает эту сумму и число значений в функцию С++ IntDivide.
Функция IntDivide приводит сумму и число значений к типу с плава-
ющей точкой и вычисляет среднее значение (делая это с помощью од-
ной строки на С++, в то время как на Ассемблере для этого потре-
бовалось бы несколько строк). Функция IntDivide возвращает сред-
нее значение (Average) в регистре вершины стека сопроцессора 8087
и передает управление обратно основной функции.
Программы CALCAVG.CPP и AVERAGE.ASM можно скомпилировать и
скомпоновать в выполняемую программу CALCAVG.EXE с помощью коман-
ды:
bcc calcavg.cpp average.asm
Отметим, что функция Average будет работать как с малой, так
и с большой моделью данных без необходимости изменения ее исход-
ного кода, так как во всех моделях передается указатель дальнего
типа. Для поддержки больших моделей кода (сверхбольшой, большой и
средней) пришлось бы только изменить соответствующую директиву
.MODEL.
Пользуясь преимуществами расширений, обеспечивающих незави-
симость Турбо Ассемблера от языка, ассемблерный код из предыдуще-
го примера можно записать более сжато (CONSISE.ASM):
.MODEL small,C
EXTRN C IntDivide:PROC
.CODE
PUBLIC C Average
Average PROC C ValuePtr:DWORD, NumberOfValues:WORD
les bx,ValuePtr
mov cx,NumberOfValues
mov ax,0
AverageLoop:
add ax,es:[bx]
add bx,2 ;установить указатель
;на следующее значение
loop AverageLoop
call _IntDivide C,ax,NumberOfValues
ret
Average ENDP
END
Назад | Содержание | Вперед