Хотя некоторые программисты могут разрабатывать программы целиком на языке Ассемблера (и делают это), другие предпочитают писать основную часть программы на языке высокого уровня, обраща- ясь к языку Ассемблера только для осуществления управления нижне- го уровня, или когда требуется высокая производительность. Неко- торые предпочитают писать преимущественно на Ассемблере, только иногда используя конструкции и библиотечные средства языков высо- кого уровня. Данная глава объясняет использование Турбо Ассемблера с ком- пиляторами С++. В этой главе они называются компиляторами семейс- тва 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
Назад | Содержание | Вперед