Многие программисты могут — и действительно так делают —
разрабатывать на языке ассемблера всю программу целиком, однако
многие также предпочитают писать основное тело программы на языке
высокого уровня, углубляясь в язык ассемблера только в тех
случаях, когда им требуется выйти на низкий уровень управления
аппаратным обеспечением, либо получить код с высокой скоростью
выполнения. Существует и третья группа программистов, которые
предпочитают писать всю программу в основном на языке ассемблера,
но в некоторых специальных случаях пользуются готовыми средствами,
предоставляемыми библиотеками и конструкциями языков высокого
уровня.
Turbo C дает хорошие возможности поддержки смешанного
программирования на Си и на языке ассемблера на произвольной
основе и предоставляет не один, а целых два механизма
интегрирования модулей на ассемблере и на Си. Имеющееся в Си
средство встроенного ассемблирования позволяет быстро и просто
вставить текст на языке ассемблера прямо в функцию Си. Те, кто
предпочитает держать ассемблерные части программы в отдельных
модулях, написанных целиком на языке ассемблера, может
ассемблировать их в Turbo Assembler и затем скомпоновать с
модулями Си.
Сначала мы рассмотрим использование встроенного в Turbo C
ассемблера. Далее мы подробно обсудим компоновку отдельно
ассемблированных в Turbo Assembler модулей с Turbo C и исследуем
процесс вызова функций, написанных в Turbo Assembler, из
программы, созданной в Turbo C. И наконец, мы рассмотрим вызов
функций Turbo C из программы Turbo Assembler. (Примечание: когда
речь идет о Turbo C, имеется в виду версия 1.5 и старше).
Приступим к изучению материала.
Использования средства встроенного ассемблирования Turbo C
——————————————————————
Если вы желаете использовать ассемблер в качестве идеального
средства для расширения возможностей программы на языке Си, то
вам, безусловно, понадобится средство включения строк на
ассемблера в тех местах программы на Си, где скорость выполнения и
возможности управления аппаратным обеспечением на низком уровне
ассемблера позволят получить значительный выигрыш в
характеристиках качества программы. Использование данной
возможности позволяет обойти традиционные сложности, связанные с
организацией интерфейса между Си и ассемблером. В результате вы
сможете выполнить эту задачу, на на один бит не изменив программу
на Си, а это означает, что уже существующую и работающую программу
на Си можно связать с ассемблером, не изменяя ее.
Turbo C выполнит все ваши требования при помощи средства
встроенного ассемблирования. Встроенное ассемблирование это ни что
иное, как возможность произвольно помещать в любое место программы
на Си строки на языке ассемблера, причем ассемблерная часть имеет
возможности полного доступа к константам, переменным и даже
функциям Си. Действительно, встроенное ассемблирование может
служить не только для точной настройки программы для конкретных
целей, но и фактически является столь же мощным средством, как и
программирование на самом ассемблере; например, высокоэффективные
моули библиотек Си написаны именно на встроенном в Си ассемблере.
Встроенное ассемблирование дает возможность сколько угодно
пользоваться в программе на Си командами ассемблера, не заботясь о
том, как именно происходит в этом случае связь языков.
Быстродействующие коды в библиотеках Turbo C написаны именно
с использованием встроенного ассемблера.
Рассмотрим следующую программу на Си, в которой приводится
пример встроенного ассемблирования:
.
.
.
i = 0; /* установить i равной 0 (на Си) */
asm dec WORD PTR i; /* декремент i (на ассемблере) */
i++; /* инкремент i (на Си) */
.
.
.
Первая и последняя строки выглядят достаточно обычно, но что
из себя представляет средняя строка? Как вы возможно, догадались,
строка, начинающаяся с asm, является строкой, подлежащей
встроенному ассемблированию. Если вы захотите посмотреть при
помощи отладчика, как будет выглядеть соответствующий загрузочный
код, то увидите следующее:
.
.
.
mov WORD PTR [bp-02],0000
dec WORD PTR [bp-02]
inc WORD PTR [bp-02]
.
.
.
где строка для встроенного ассемблирования будет находиться
между компилированными кодами, полученными для строк Си
i = 0;
и
i++;
В целом, всякий раз, как компилятор Turbo C встречает
ключевое слово asm, обозначающее необходимость встроенного
ассемблирования данной строки, он помещает ее непосредственно в
ассемблированный код с одним изменением: ссылки на переменные Си
преобразуются в соответствующие ассемблерные эквиваленты, как в
приведенном примере, где ссылка на переменную Си i была
преобразована в WORD PTR [BP-02]. Короче говоря, ключевое слово
asm позволяет произвольно вставлять строки на языке ассемблера в
любые места программы на Си.
(Существуют некоторые ограничения на то, что разрешено делать
с помощью ассемблерных вставок; мы рассмотрим их в разделе
«Ограничения встроенного ассемблирования» на стр.298).
Средство вставки ассемблерных частей в программу на Си может
показаться несколько опасным, и действительно, при нем возникает
некоторый риск. Хотя сам Turbo C при компиляции заботится о том,
чтобы избежать потенциально опасных взаимодействий со встроенными
ассемблерными частями, безусловно, неправильное поведение
последних может привести к серьезным ошибкам.
С другой стороны, любая слабо составленная ассемблерная
программа, будь то встроенная часть или же отдельный ассемблерный
модуль, всегда потенциально опасна с точки зрения ошибок; это
цена, которую приходится платить за высокую скорость выполнения и
возможность управления на низком уровне, предоставляемые языком
ассемблера. Вообще, ошибки во встроенном ассемблере случаются
реже, чем в чисто ассемблерной программе, поскольку Turbo C
проверяет многие детали программы, как то ввод и выход из функций,
передача параметров и распределение переменных. В конечном счете,
возможность легко настраивать программу под конкретные условия,
вставляя туда части на ассемблере и расширяя тем самым возможности
Turbo C, вполне стоит того, чтобы потратить усилия на отладку
возможных ошибок в ассемблере.
Важные замечания относительно встроенного ассемблирования:
1. Для того, чтобы воспользоваться средством встроенного ас-
семблирования, вы должны запускать TCC.EXE, то есть вер-
сию Turbo C, работающую из командной строки. TC.EXE, вер-
сия Turbo C, обеспечивающая интегрированную среду интер-
фейса пользователя, не поддерживает средство встроенного
ассемблирования.
2. Весьма вероятно, что версия TLINK, поставленная с вашей
копией Turbo Assembler, не является той же самой, что была
поставлена с вашей версией Turbo C. Вследствие того, что
для поддержки Turbo Assembler в TLINK были внесены значи-
тельные усовершенствования и поскольку, без сомнений, улу-
чшения TLINK будут происходить и далее, важно, чтобы ком-
поновка модулей Turbo C, содержащих встроенные ассемблер-
ные части, выполнялась при помощи самой последней имеющей-
ся у вас версии TLINK. Самый надежный способ здесь состоит
в том, чтобы держать на диске, с которого вы запускаете
компоновщик, только один файл TLINK.EXE; этот файл TLINK.EXE
должен иметь последний номер версии из всех файлов TLINK.EXE,
полученных вами вместе с прочими продуктами Borland.
Как работает встроенный ассемблер
Обычно Turbo Assembler компилирует каждый файл исходной
программы на Си прямо в объектный файл, а затем запускает TLINK
для связи всех объектных файлов в одну выполняемую программу.
Такой цикл компиляции-компоновки показан на рис.7.1. Для того,
чтобы войти в цикл, введите в командной строке:
tcc filename
сообщив тем самым Turbo C, что сначала требуется
компилировать FILENAME.C в FILENAME.OBJ, а затем запустить TLINK
для компоновки FILENAME.OBJ в FILENAME.EXE.
—————————-
| Исходный файл Си |
| FILENAME.C |
—————————-
?
————
| Turbo C | Компиляция
————
?
—————————-
| Объектный файл Си |
| FILENAME.OBJ |
—————————-
?
————
| TLINK | Компоновка
————
?
—————————-
| Выполняемый файл Си |
| FILENAME.EXE |
—————————-
Рис.7.1 Цикл компиляции и компоновки Turbo C
Однако, при использовании встроенного ассемблирования Turbo C
автоматически добавляет в цикл компиляции-компоновки один
дополнительный шаг.
Turbo C обрабатывает каждый модуль, содержащий встроенный
ассемблерный код, компилируя сперва целый модуль в исходный файл
на языке ассемблера, а затем запуская Turbo Assembler, который
ассемблирует получившийся ассемблерный код в объектный файл, и
наконец запуская TLINK для компоновки объектных файлов. На рис.6.2
показан этот процесс, и из него видно, каким образом Turbo C
создает выполняемый файл из исходного файла Си, содержащего
встроенные ассемблерные коды. Цикл этот начинается из командной
строки:
tcc -b filename
которая говорит Turbo C о том, что сначала нужно
компилировать FILENAME.ASM, а затем запустить Turbo Assembler для
ассемблирования FILENAME.ASM в FILENAME.OBJ, и наконец запустить
TLINK для компоновки FILENAME.OBJ в FILENAME.EXE.
Встроенные ассемблерные коды просто передаются Turbo C в файл
на языке ассемблера. Такая система замечательна тем, что сам Turbo
C не должен заботиться об ассемблировании встроенных кодов; вместо
этого Turbo C компилирует исходные коды Си к тому же уровню —
уровню ассемблерных кодов — что и встроенные ассемблерные коды, а
само ассемблирование оставляет для Turbo Assembler.
Для того, чтобы в точности посмотреть, как именно Turbp C
обрабатывает встроенные ассемблерные части программы, рассмотрим
программу, которая называется PLUSONE.C (ее можно загрузить с
диска):
#include
int main(void)
{
int TestValue;
scanf(«%d»,&TestValue); /* прием инкрементируемого
значения */
asm inc WORD PTR TestValue; /* его инкрементирование
в ассемблере */
printf(«%d»,TestValue); /* печать инкрементированного
значения */
}
и компилируем ее командной строкой
tcc -s plusone
—————————-
| Исходный файл Си |
| FILENAME.C |
—————————-
?
————
| Turbo C | Компиляция
————
?
——————————
|Исходный файл на ассемблере|
| FILENAME.ASM |
——————————
?
————
| Turbo | Ассемблирование
|Assembler |
————
?
—————————-
| Объектный файл |
| FILENAME.OBJ |
—————————-
?
————
| TLINK | Компоновка
————
?
—————————-
| Выполняемый файл Си |
| FILENAME.EXE |
—————————-
Рис.7.2 Цикл компиляции, ассемблирования и компоновки Turbo C
Опция -s сообщает Turbo C о том, что необходимо выполнить
компиляцию в ассемблерный код и затем остановиться, тем самым
создав на диске файл PLUSONE.ASM. PLUSONE.ASM будет иметь вид:
ifndef ??version
?debug macro
endm
endif
name Plusone Этот код позволит
_TEXT SEGMENT BYTE PUBLIC ‘CODE’ вам оценить объем
DGROUP GROUP _DATA,_BSS работы, которую
ASSUME cs:_TEXT,ds:DGROUP,ss:DGROUP экономит для вас
_TEXT ENDS Turbo C благодаря
_DATA SEGMENT WORD PUBLIC ‘DATA’ встроенному
_d@ label BYTE ассемблированию
_d@w label WORD
_DATA ENDS
_BSS SEGMENT WORD PUBLIC ‘BSS’
_b@ label BYTE
_b@w label WORD
?debug C E90156E11009706C75736F6E652E63
?debug C E90009B9100F696E636C7564655C737464696F2E68
?debug C E90009B91010696E636C7564655C7374646172672E68
_BSS ENDS
_TEXT SEGMENT BYTE PUBLIC ‘CODE’
; ?debug L 3
_main PROC NEAR
push bp
mov bp,sp
dec sp
dec sp
; ?debug L 8
lea ax,WORD PTR [bp-2]
push ax
mov ax,OFFSET DGROUP:_s@
push ax
call NEAR PTR_scanf Здесь идет ассемблерный
pop cx код для вызова scanf,
pop cx затем встроенная
; ?debug L 9 ассемблерная
inc WORD PTR [bp-2] команда инкремента
; ?debug L 10 TestValue и затем код
push WORD PTR [bp-2] для вызова printf
mov ax,OFFSET DGROUP:_s@+3
push ax
call NEAR PTR -printf
pop cx
pop cx
@l:
; ?debug L 12
mov sp,bp
pop bp
ret
_main ENDP
_TEXT ENDS
_DATA SEGMENT WORD PUBLIC ‘DATA’
_s@ label BYTE
db 37 Turbo C автоматически
db 100 транслирует переменную
db 0 Си TestValue в
db 37 ассемблерный эквивалент
db 100 адресации этой
db 0 переменной (BP-2)
_DATA ENDS
_TEXT SEGMENT BYTE PUBLIC ‘CODE’
EXTRN _printf:NEAR
EXTRN _scanf:NEAR
_TEXT ENDS
PUBLIC _main
END
(Данный пример программы хорошо показывает, какую работу
проделывает за вас Turbo C, поддерживая средство встроенного
ассемблирования!) Под комментарием
; ?debug L 8
вы увидите ассемблерный код, соответствующий обращению в Си к
scanf, и далее
; ?debug L 9
inc WORD PTR [bp-2]
что собственно и есть встроенная ассемблерная команда,
выполняющая операцию инкремента TestValue. (Отметим, что Turbo C
автоматически транслирует имя переменной в Си TestValue в
эквивалентную ассемблерную адресацию данной переменной, [BP-2]).
Вслед за встроенной ассемблерной командой идут ассемблерные коды,
соответствующие обращению в Си к printf.
Смысл здесь в том, что Turbo C скомпилировал обращение к
scanf на язык ассемблер, затем непосредственно после этого
поместил в выходной файл на ассемблере встроенный ассемблерный код
и далле скомпилировал обращение к printf. Получившийся файл
является допустимым исходным файлом на языке ассемблера, готовый
для ассемблирования при помощи Turbo Assembler.
Если вы не задавали опцию -s, то Turbo C перейдет к запуску
Turbo Assembler, ассемблирующего PLUSONE.ASM, а затем к запуску
TLINK, компонующего результирующий объектный файл PLUSONE.ASM в
выполняемый файл PLUSONE.EXE. Это нормальный режим работы Turbo C
со встроенным ассемблером; опция -s в данном случае использовалась
исключительно в целях объяснения, с тем, чтобы мы могли
рассмотреть промежуточный шаг получения файла на языке ассемблера,
который Turbo C использует для поддержки средства встроенного
ассемблирования. Опция -s не имеет особого смысла при компиляции,
после которой будет непосредственно получена выполняемая
программа, однако она представляет собой удобный способ,
позволяющий рассмотреть, в какой среде оказался ваш встроенный
ассемблерный код, а также какие ассемблерные коды генерируются
Turbo C в целом. Если вы не уверены точно, какие коды генерируются
при использовании встроенного ассемблирования, вам следует
исследовать файл, полученный с опцией -S.
Как Turbo C распознает режим встроенного ассемблирования
———————————————————
Обычно Turbo C прямо компилирует исходную программы на Си в
объектные коды. Существует несколько способов, которыми можно
сообщить Turbo C, что требуется поддержка встроенного
ассемблирования, при котором исходные тексты компилируются на язык
ассемблера, и затем запускается Turbo Assembler.
Опция командной строки -B сообщает Turbo C о том, что
требуется генерировать объектные файлы методом промежуточной
компиляции исходных в ассемблерный вид с последующей обработ кой
их запуском Turbo Assembler.
Опция командной строки -S сообщает Turbo C о том, что
требуется выполнить компиляцию в ассемблерный вид и затем
остановиться. При заданной опции -s генерируемый Turbo C файл .ASM
может быть раздельно ассемблирован и скомпонован с другими
модулями Си или на ассемблере. За исключением отладки или просто
изучения программы нет никакого смысла предпочитать опцию -S опции
-B.
Директива #pragma
#pragma inline
имеет тот же эффект, что и опция командной строки -B и
сообщает Turbo C, что сперва нужно выполнить компиляцию в
ассемблерный вид, а затем запустить Turbo Assembler, который
ассемблирует результат. Когда Turbo C встретит #pragma inline,
компиляция будет начата сначала, в режиме вывода в файл исходного
текста на языке ассемблера. Следовательно, имеет смысл помещать
директиву #pragma inline как можно ближе к началу исходного текста
программы на Си, поскольку все предшествующие директиве #pragma
inline строки программы на Си будут компилироваться дважды, один
раз в нормальном режиме «Си — объектный файл», а затем в режиме
«Си — ассемблер». Хотя это ни на что не влияет, однако позволяет
экономить время.
И наконец, если Turbo C встречает встроенные ассемблерные
коды, а опции -B, -S или директива #pragma inline отсутствуют, то
выдается предупреждение вида:
Warning test.c 6: Restarting compile using assembly
in function main
(Предупреждение test.c 6: Повторная компиляция в ассемблерный
вид в функции main)
и затем повторяет компиляцию в режиме вывода в файл на языке
ассемблера, как если бы в этой точке ему встретилась директива
#pragma inline. Старайтесь избежать появления таких
предупреждений, используя для этого опцию -B или директиву #pragma
inline, так как рестарт компиляции с точки, где встретились
команды для встроенного ассемблирования относительно замедляет
работу.
Запуск Turbo Assembler для встроенного ассемблирования
——————————————————
Для того, чтобы Turbo C мог запустить Turbo Assembler, Turbo
C должен сначала иметь возможность найти Turbo Assembler. Как это
происходит,зависит от конкретной версии Turbo C.
Версии Turbo C старше 1.5 предполагают, что Turbo Assembler
должен иметь имя TASM.EXE и находиться либо в текущей директории,
либо в одной из директорий, заданных в переменной контекста DOS
PATH. В целом, Turbo C может запустить Turbo Assembler по тем же
правилам, что выполняются, если вы просто наберете команду
TASM
и выполните Turbo Assembler из командной строки по
приглашению DOS. Таким образом, если Turbo Assembler находится в
текущей директории или в любой директории, заданной в маршруте
поиска команды, Turbo C автоматически отыщет его и запустит для
выполнения встроенного ассемблирования.
Версии Turbo C 1.0 и 1.5 работают несколько иначе. Поскольку
эти версии Turbo C были созданы до появления Turbo As- sembler, то
для выполнения встроенного ассемблирования они запускают MASM,
Microsoft Macro Assembler. Следовательно, эти версии Turbo C
просматривают текущую директорию и маршрут поиска, чтобы найти не
TASM.EXE, а MASM.EXE, и не могут использовать Turbo Assembler
автоматически.
Информацию о том, как сделать, чтобы эти версии TCC
использовали TASM, см. в файле README на дистрибутивной дискете с
Turbo Assembler.
Куда Turbo C ассемблирует встроенные ассемблерные команды
———————————————————
Встроенный ассемблерный код может заканчиваться либо в
кодовом сегменте Turbo C, либо в сегменте данных Turbo C.
Встроенный ассемблерный код, находящийся внутри функции,
ассемблируется в кодовый сегмент Turbo C, а расположенный вне
функции ассемблируется в сегмент данных Turbo C.
Например, Си-программа
/* Таблица квадратов */
asm SquareLookUpTable label word;
asm dw 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100;
/* Функция просмотра квадратов значений от 0 до 10 */
int LookUpSquare(int Value)
{
asm mov bx,Value; /* прием возводимого в квадрат
значения */
asm shl bx,1; /* умножение его на 2 для просмотра
в таблице элементов размером
в слово */
asm mov ax,[SquareLookUpTable+bx];
/* поиск квадрата */
return(_AX); /* возврат результата */
помещает данные для таблицы SquareLookUpTable в сегмент
данных Turbo C, а встроенные ассемблерные коды, находящиеся внутри
LookUpSquare в кодовый сегмент Turbo C. Данные равным образом
могут быть помещены и в кодовый сегмент; рассмотрим следующую
версию программы LookUpSquare, в которой таблица SquareLookUpTable
располагается в кодовом сегменте Turbo C:
/* Функция просмотра квадратов значений от 0 до 10 */
int LookUpSquare(int Value)
{
asm jmp SkipAroundData /*переход через таблицу
данных */
/* Таблица квадратов */
asm SquareLookUpTable label word;
asm dw 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100;
SkipArounddata:
asm mov bx,Value; /* прием возводимого в квадрат
значения */
asm shl bx,1; /* умножение его на 2 для просмотра
в таблице элементов размером
в слово */
asm mov ax,[SquareLookUpTable+bx];
/* поиск квадрата */
return(_AX); /* возврат результата */
Поскольку SquarelookUpTable находится в кодовом сегменте
Turbo C, может показаться, что для считывания оттуда значений
понадобится использовать префикса переопределения сегмента CS:. На
самом деле данный код будет автоматически ассемблирован для
доступа к SquareLookupTable с префиксом CS:; Turbo C генерирует
правильный ассемблерный код, из которого Turbo Assembler узнает, в
каком сегменте находится SquareLookUpTable, после чего Turbo
Assembler сможет при необходимости генерировать правильные
префиксы переопределения сегмента.
Использование ключа -l для ассемблирования команд 80186/80286
————————————————————-
Если вы желаете использовать ассемблерные команды, являющиеся
уникальными командами процессора 80186 типа
shr ax,3
и
push 1
то проще всего задать опцию командной строки -l, как в
следующем примере:
tcc -l -b heapmgr
где HEAPMGR.C это программа, в которой находятся встроенные
ассемблерные команды, уникальные для 80186.
Главное назначение опции -l состоит в том, чтобы сообщить
Turbo C о необходимости использовать при компиляции все
возможности, предоставляемые полным набором команд для 80186, и
кроме того, данная опция заставляет Turbo C вставить в
началовыходного ассемблерного файла директиву .186; тем самым он
сообщит Turbo Assembler, что тот при ассемблировании должен
использовать полный набор команд для 80186. Без директивы 80186
Turbo Assembler отметит все команды встроенного ассемблирования,
уникальные для 80186, как ошибочные. Если вам нужно ассемблировать
команды для 80186 без применения Turbo C полного набора команд
данного процессора, вставьте в начало каждого модуля Turbo C,
содержащего команды для 80186, строку
asm .186;
Данная строка будет передана через ассемблерный файл, через
который Turbo Assembler узнает о необходимости ассемблировать
команды 80186.
Хотя Turbo C не обеспечивает встроенной поддержки процессоров
80286, 80386, 80287 и 80387, средство встроенного ассемблирования,
поддерживающее 80286, 80287 и 80386, может быть включено
аналогичным образом, при помощи ключевого слова asm и директив
Turbo Assembler .286, .286C, .286P, .386, .386C, .287 и .387.
Строка
asm .186;
иллюстрирует важный момент, касающийся встроенного
ассемблирования: в ассемблерный файл при помощи префикса asm может
быть передана любая допустимая ассемблерная строка, включая
сегментныедирективы, присвоения, макросы и т.д.
Формат операторов встроенного ассемблирования ——————
Операторы встроенного ассемблирования во многом похожи на
обычные ассемблерные строки, однако при некоторых различиях.
Формат операторов встроенного ассемблирования таков:
asm [<метка>] <команда/директива><операнд><; или новая строка>
где
— Ключевое слово asm должно начинать каждый оператор
встроенного ассемблирования.
— [<метка>] — это любая допустимая в языке ассемблера метка.
Квадратные скобки указывают, что метка является опцией, как
в ассемблере.
(Информацию об использовании в Си ассемблерных меток
приводится в разделе «Ограничения на память и адреса операндов».)
— <команда/директива> — это любая допустимая ассемблерная
команда или директива.
— <операнды> представляют собой операнд(ы), приемлемые для
данной команды или директивы; здесь могут также содержаться
ссылки на константы, переменные и метки Си, с учетом ограни-
чений, описанных в разделе «Ограничения на память и адреса
операндов».
— <; или новая строка> — это точка с запятой или новая строка,
сообщающие об окончании оператора asm.
Использование во встроенном ассемблере двоеточий
————————————————
Один из аспектов встроенного ассемблирование, на который не
может не обратить внимание ни один знаток Си, состоит в том, что
из всех операторов Си только операторы встроенного ассемблирования
не требуют наличия оконечной точки с запятой в обязательном
порядке. Точку с запятой ставить в конце каждого такого оператора
можно, но совсем не обязательно, поскольку конец строки будет
означать для Turbo C то же самое. Поэтому, если вы не собираетесь
помещать в одной строке несколько операторов встроенного
ассемблирования (что не является хорошим тоном программирования по
причинам недостаточной ясности программы), точки с запятой
являются необязательными элементами синтаксиса таких операторов.
Хотя такой подход и не в традициях Си, он тем не менее
соответствует соглашениям, принятым в нескольких компиляторах на
базе UNIX.
Комментарии во встроенном ассемблере
————————————
В предыдущем описании формата оператора встроенного ас
семблировании отсутствует один ключевой элемент — поле
комментария. Точки с запятыми, которые могут быть помещены в конце
операторов встроенного ассемблирования, лишь отмечают окончание
этих операторов, как и в случае всех прочих операторов Си; они не
являются началом поля комментария собственно текста команд
встроенного ассемблера.
Каким же образом комментируется исходный текст встроенного
ассемблера? На первый взгляд, достаточно странно — при помощи
комментариев Си. В действительности же ничего странного здесь нет,
так как препроцессор Си обрабатывает строки встроенного
ассемблирования вместе с остальной частью программы Си. Тем самым
вам дается преимущество одинакового стиля выполнения комментариев
по всей программе Си, содержащей операторы встроенного
ассемблирования, и вы получаете возможность использовать
определенные в Си символические имена как собственно в операторах
Си, так и в операторах встроенного ассемблирования. Например, в
.
.
.
#define CONSTANT 51
int i;
.
.
.
i = CONSTANT; /* установить i равной
значению константы */
asm sub WORD PTR i,CONSTANT; /* вычесть значение
константы из i *?
.
.
.
Как операторы Си, так и встроенный ассемблер используют
определенное в Си символическое имя CONSTANT, и i приравнивается
нулю.
В последнем примере показано одно замечательное свойство
встроенного ассемблирования, заключающееся в том, что поле
операнда может содержать прямые ссылки не только на определенные в
Си символические имена, но и на переменные Си. Как будет показано
далее в этой главе, доступ к переменным Си из ассемблера обычно
является довольно сложной задачей, и удобная возможность доступа к
переменным Си служит главной причиной, заставляющей в большинстве
прикладных задач предпочитать встроенный ассемблер стандартному
методу интегрирования раздельных модулей на Си и на языке
ассемблера.
Доступ к элементам структур/объединений
—————————————
Часть программы на встроенном ассемблере может
непосредственно обращаться к элементам структур. Например,
.
.
.
struct Student {
char Teacher[30];
int Grade;
} JohnQPublic;
.
.
.
asm mov ax,JohnQPublic.Grade;
.
.
.
загружает в AX содержимое члена Grade структуры JohnQPublic
типа Student.
Часть программы на встроенном ассемблере может также
выполнять доступ к элементам структуры методом адресации
относительно базового или индексного регистров. Например,
.
.
.
asm mov bx,OFFSET JohnQPublic;
asm mov ax,[bx].Grade;
.
.
.
также загрузит AX членом Grade структуры JohnQPublic.
Поскольку Grade располагается в структуре типа Student со
смещением 30, то последний пример фактически можно переписать как:
.
.
.
asm mov bx,OFFSET JohnQPublic;
asm mov ax,[bx]+30;
.
.
.
Средство доступа к элементам структуры отностительно
регистра-указателя является очень мощным, так как оно позволяет
встроенному ассемблеру обрабатывать массивы структур и переданные
указатели структур.
Однако, если две или более структуры, к которым вы выполняете
доступ из встроенного ассемблера, имеют одиноковое имя члена, вы
должны вставить в программу следующее:
asm mov bx,[di].(struct tm) tm_hour > alt
Например,
.
.
.
struct Student {
char Teacher[30];
int Grade;
} JohnQPublic;
.
.
.
struct Teacher {
int Grade;
long Income;
};
.
.
.
asm mov ax,JohnQPublic.(struct Student) Grade;
.
.
.
Пример встроенного ассемблирования —————————-
Итак, вы рассмотрели множество фрагментов программ, в которых
использовалось средство встроенного ассемблирования, но ни одной
практически работающей программы. В данном разделе эта ситуация
разрешается, так как здесь представлена программа, в которой для
обеспечения высокой скорости преобразования символов к верхнему
регистру применяется средство встроенного ассемблирования.
Представленная программа может служить как примером возможностей
встроенного ассемблера, так и шаблоном, к которому вы сможете
обратиться при разработки своих собственных программ с
использованием встроенного ассемблера.
Сперва рассмотрим саму задачу, решаемую в данном примере
программы. Мы хотим разработать функцию с именем StringToUpper,
которая будет копировать одну строку в другую, преобразовывая в
процессе копирования все строчные буквы в заглавные. Мы также
хотим, чтобы эта функция одинаково хорошо работала со всеми
строками и со всеми моделями памяти. Хороший способ реализации
такой задачи состоит в использовании передаваемых функции дальних
указателей на строку, поскольку ближние указатели всегда могут
быть получены из дальних, а вот обратное не всегда верно.
К сожалению, здесь нам придется столкнуться с вопросом
производительности программы. Хотя Turbo C обрабатывает дальние
указатели достаточно хорошо, тем не менее, скорость работы с
такими указателями существенно ниже, нежели с ближними. Это
является не столько узким местом Turbo C, сколько неизбежным
эффектом, возникающим при программировании 8086 на языках высокого
уровня.
И напротив, обработка строк и дальних указателей являются
именно теми областями, в которых ассемблер позволяет получить
максимальную выгоду. Здесь логичное решение состоит в том, чтобы
для копирования строк и обработки дальних ссылок использовать
встроенный ассемблер, а все остальные функции реализовать с
помощью Turbo C. Следующая программа, STRINGUP.C, устроена именно
так:
/* Программа, демонстрирующая использование StringToUpper().
Она вызывает StringToUpper для преобразования символов
строки TestString к верхнему регистру и помещения их в
строку UpperCasestring, а затем для распечатки
UpperCaseString и ее длины. */
#pragma inline
#include
/* Прототип функции для StringToUpper() */
extern unsigned int StringToUpper(
unsigned char far * DestFarstring,
unsigned char far * SourceFarString);
#define MAX_STRING_LENGTH 100
char *TestString = «This Started Out As Lowecase!»;
char UpperCaseString[MAX_STRING_LENGTH];
main()
{
unsigned int StringLength;
/* Копирование версии строки TestString, состоящей из
заглавных букв, в строку UpperCaseString */
StringLength = StringToUpper(UpperCaseString, TestString);
/* Вывод на дисплей результата преобразования */
printf(«Исходная строка:\n%s\n\n», TestString);
printf(«Строка из заглавных букв:\n%s\n\n»,UpperCaseString);
printf(«Число символов: %d\n\n»,StringLength);
}
/* Функция, выполняющая высокопроизводительное
преобразование строчных букв в заглавные из одной
дальней строки в другую
Вход:
DestFarString — массив, в который будет помещена
строка из заглавных букв (будет
оканчиваться нулем)
SourceFarString — строка, содержащая символы, кото-
рые должны быть преобразованы в за-
главные (должна оканчиваться нулем)
Возвращает:
длину исходной строки в символов, не считая оконеч-
ного нуля. */
unsigned int StringToUpper(unsigned char far * DestFarString,
unsigned char far * SourceFarstring)
{
unsigned int CharacterCount;
#define LOWER_CASE_A ‘a’
#define LOWER_CASE_Z ‘z’
asm ADJUST_VALUE EQU 20h; /* Число, вычитаемое из
кода буквы для преобра-
зования ее в заглавную */
asm cld;
asm push ds; /* Сохранить сегмент
данных Си */
asm lds si,SourceFarString; /* Загрузить дальний ука-
затель исходной строки */
asm les di,DestFarString; /* Загрузить дальний ука-
затель строки назначения */
CharacterCount = 0; /* Счетчик символов */
StringToUpperLoop:
asm lodsb; /* Прием следующего
символа */
asm cmp al,LOWER_CASE_A; /* Если < a, то это не
строчная буква */
asm jb SaveCharacter;
asm cmp al,LOWER_CASE_Z; /* Если > z, то это не
строчная буква */
asm ja SaveCharacter;
asm sub al,ADJUST_VALUE; /* Это строчная буква,
выполнить ее преобразо-
вание в заглавную */
SaveCharacter:
asm stosb; /* Сохранить символ */
CharacterCount++; /* Посчитать данный
символ */
asm and al,al; /* Это оконечный ноль?*/
asm jnz StringToUpperLoop; /* Нет, переход к обра-
ботке следующего символа,
если они еще остались */
CharacterCount—; /* Оконечный ноль не
подсчитывается */
asm pop ds; /* Восстановить сегмент
данных Си */
return(CharacterCount);
}
При запуске STRINGUP.C выведет на дисплей:
Исходная строка:
This Started Out As Lowercase!
Строка из заглавных букв:
THIS STARTED OUT AS LOWERCASE!
Число символов: 30
продемонстрировав, что она действительно преобразовывает
строчные буквы в заглавные.
В основе программы StringUp.C лежит функция StringToUpper,
которая собственной и выполняет весь процесс копирования строки и
преобразования строчных букв в заглавные. StringToUpper написана
частично на Си и частично на встроенном ассемблере, а в качестве
параметров она принимает два дальних указателя. Один из этих
дальних указателей указывает на строку, содержащую исходный текст;
а второй — на другую строку, в которую этот текст будет
скопирован, с преобразованием всех строчных букв в заглавные.
Объявление функции и определение параметров выполняются в Си, и
действительно, в начале программы находится прототип функции для
StringToUpper. Главная программа вызывает StringToUpper, как если
бы та была целиком написана на Си. Короче говоря, такой подход
позволяет воспользоваться всеми преимуществами, предоставляемыми
Си, даже если StringToUpper содержит ряд строк для встроенного
ассемблирования.
Тело StringToUpper написано частично на Си, а частично на
встроенном ассемблере. Ассемблер используется для чтения из
исходной строки каждого символа, проверки его и при необходимости
для преобразования его в заглавную букву, а затем для записи
символа в строку назначения. Встроенный ассемблер позволяет
StringToUpper использовать для чтения и записи символов мощные
строковые команды языка ассемблера LODSB и STOSB.
При написании StringTo Upper мы знали, что доступ к
каким-либо данным из сегмента данных Turbo C нам не понадобится,
поэтому в начале функции мы просто поместили DS в стек, после чего
установили DS как указатель исходной строки и оставили его в этом
состоянии до конца функции. Одно из больших преимуществ
встроенного ассемблера над чистой программой на Си состоит в том,
что он позволяет загрузить дальние указатели один раз в начале
функции и затем не перезагружать их до самого конца функции.
Напротив, Turbo C и другие языки высокого уровня обычно
перезагружают дальние указатели всякий раз при их использовании.
Возможность загружать дальние указатели только один раз означает,
что StringToUpper обрабатывает дальние строки с такой же
скоростью, как если бы это были ближние строки.
Еще один интересный момент, связанный с StringToUpper,
заключается в способе чередования строк на Си и на встроенном
ассемблере. Для установки LOWER_CASE_A и LOWER_CASE_Z применяется
оператор #define, а для установки ADJUST_VALUE применяется
директива ассемблера EQU, однако в операторах встроенного
ассемблера все три символических имени используются одинаковым
образом. Подстановки символических имен, определенных в Си,
выполняются препроцессором Си, а подстановка для ADJUST_ VALUE
выполняется в Turbo Assembler, однако в части программы,
написанной на встроенном ассемблере, можно использовать все эти
имена.
Операторы Си, манипулирующие CharacterCount, разбросаны по
функции StringToUpper, по разным ее частям. Это сделано
исключительно для того, чтобы продемонстрировать, что операторы Си
и операторы встроенного ассемблера могут чередоваться в смешанной
программе произвольным образом. С CharacterCount можно было бы
манипулировать непосредственно операторами встроенного ассемблера
в свободном регистре, например CX или DX; при этом и функция
StringToUpper работала бы быстрее.
Однако, если вы не знаете в точности, какие коды генерирует
Turbo C между операторами встроенного ассемблера, то свободное
чередование операторов Си и встроенного ассемблера содержит риск
ошибки. Для изучения результата смешивания в программе операторов
Си и встроенного ассемблера удобно пользоваться опцией компилятора
Turbo C -s. Например, вы можете в точности узнать, что происходит
при смешивании Си со встроенным ассемблером в функции
StringToupper, откомпилировав STRINGUP.C с опцией -s и затем
исследовав получившийся в результате файл STRINGUP.ASM.
Программа STRINGUP.C явно демонстрирует замечательный выигрыш
от правильного применения встроенного ассемблера. В функции
StringToUpper вставка 15 операторов встроенного ассемблера
приблизительно вдвое увеличивает скорость обработки строк по
сравнению с аналогичной программой на Си.
Ограничения на встроенное ассемблирование ———————-
На использование встроенного ассемблирования существует
небольшое количество ограничений, поскольку в целом операторы
встроенного ассемблера просто передаются Turbo Assembler без
всяких изменений. Однако имеется ряд достойных упоминания
ограничений, связанных с использованием конкретных операндов
памяти и адресов, а также ряд других, касающихся правил
использования регистров и отсутствия установки размеров по
умолчанию используемых в операторах встроенного ассемблера
автоматических переменных Си.
Ограничения на операнды памяти и адреса
—————————————
Единственные изменения, вносимые Turbo C в операторы
встроенного ассемблера, заключаются в преобразованиях ссылок на
память и адреса памяти, например имен переменных и назначений
перехода, из формы их представления в Си в их ассемблерные
эквиваленты. Эти изменения вносят следующие ограничения: команды
перехода встроенного ассемблера могут ссылаться только на метки
Си, тогда как остальные команды встроенного ассемблера могут
ссылаться на любые значения, за исключением меток Си.
Например, фрагмент
.
.
.
asm jz NoDec;
asm dec cx;
NoDec:
.
.
.
является допустимым, тогда как фрагмент
.
.
.
asm jnz NoDec;
asm dec cx;
asm NoDec:
.
.
.
правильно скомпилирован быть не может. Аналогичным образом,
команды перехода встроенного ассемблера не могут иметь в качестве
операндов имен функций.
Команды встроенного ассемблирования, за исключением команд
перехода, могут иметь любые операнды, кроме меток Си. Например,
фрагмент
.
.
.
asm BaseValue db ‘0’;
.
.
.
asm mov al,BYTE PTR BaseValue;
.
.
.
является допустимым, тогда как фрагмент
.
.
.
BaseValue:
asm db ‘0’;
.
.
.
asm mov al,BYTE PTR BaseValue;
.
.
.
правильно скомпилирован быть не может. Отметим, что вызов не
рассматривается как переход, поэтому правильные операнды вызова на
встроенном ассемблере могут включать в себя имена функций Си и
метки ассемблера, но не метки Си. Если в части программы на
встроенном ассемблере содержится обращение к имени функции Си, то
это имя должно начинаться со знака подчеркивания; подробности см.
в разделе «Подчеркивания».
Отсутствия установки размеров по умолчанию
используемых в операторах встроенного ассемблера
автоматических переменных
——————————————
Когда Turbo C заменяет в операторе встроенного ассемблера
ссылку на автоматическую переменную на операнд типа [BP-02], он не
помещает в изменяемый оператор оператор размера, например, WORD
PTR или BYTE PTR. Это означает, что
.
.
.
int i;
.
.
.
asm mov ax,i;
.
.
.
при выводе в файл на языке ассемблера будет иметь вид
mov ax,[bp-02]
В данном случае никаких проблем не возникает, так как
использование имя регистра AX сообщает Turbo Assembler, что это
ссылка на 16-битовую память. Более того, отсутствие оператора
размера дает вам полную гибкость управления размером оператора во
встроенном ассемблере. Однако, рассмотрим фрагмент
.
.
.
int i;
.
.
.
asm mov i,0;
asm inc i;
.
.
.
преобразуемый в
mov [bp-02],0
inc [bp-02]
Ни одна из этих команд не имеет присвоенного ей размера
операндов, поэтому Turbo Assembler не сможет выполнить их
ассемблирование. Следовательно, когда вы обращаетесь в Turbo
Assem- bler к автоматической переменной без использования в
качестве исходного оператора или оперетора назначения имени
регистра, не забывайте использовать оператор размера. Последний
пример будет правильно ассемблирован, если переписать его в виде:
.
.
.
int i;
.
.
.
asm mov WORD PTR i,0;
asm inc BYTE PTR i;
.
.
.
Необходимость сохранения регистров
———————————-
В конце любой части программы на встроенном ассемблере
следующие регистры должны снова принимать те же значения, что и к
началу соответствующей части: BP, SP, CS, DS и SS. Несоблюдение
этого правила может привести к частым сбоям программы и
необходимости перезагрузки системы. AX, BX, CX, DX, SI, DI, ES и
флаги можно встроенный ассемблер может изменять свободно.
Сохранение при вызове функций переменных регистров
—————————————————
Turbo C требует, чтобы SI и DI, используемые в качестве
переменных регистров, не разрушались при вызове функций. К
счастью, при использовании регистров SI или DI во встроенном
ассемблере вам нет необходимости самому явно заботиться об их
сохранении. Как только Turbo C обнаруживает их использовании в
части программы на встроенном ассемблере, он автоматически
сохраняет их в начале функции и восстанавливает в конце — что
является еще одним из удобств пользования встроенным ассемблером.
Подавление внутренних регистровых переменных
———————————————
Поскольку переменные регистры хранятся в SI и DI, может
показаться, что возникает конфликт между переменными регистрами в
данном модуле и части программы на встроенном ассемблере в том же
модуле, использующей SI и DI. Однако Turbo C сам разрешает эту
проблему; любое использование SI или DI во встроенном ассемблере
отменяет использование этих регистров для хранения переменных.
Turbo C версии 1.0 не гарантировал разрешения конфликта между
переменными регистрами и встроенными ассемблерными кодами. Если вы
работаете с версией 1.0, вы должны либо явно сохранять SI и DI
перед использованием их во встроенном ассемблере, либо перейти на
более позднюю версию компилятора.
Недостатки использования встроенного ассемблера по сравнению
с чистой программой на Си
————————————————————
Мы потратили много времени на объяснение работы встроенного
ассемблера и исследование потенциальных преимуществ его
использования. Хотя для многих прикладных задач встроенный
ассемблер является прекрасным средством, он имеет и некоторые
недостатки. Сделаем краткий обзор этих недостатков, чтобы вы могли
квалифицированно судить о том, стоит ли в конкретных случаях
использовать в ваших программах встроенное ассемблирование.
Уменьшенные мобильность и удобство сопровождения
————————————————
Самая главная причина, делающая применение встроенных
ассемблерных кодов столь эффективным, — возможность прямого
программировани 8086, — одновременно приводит и к потере основного
достоинства Си, мобильности написанного на этом языке программного
обеспечения. При использовании встроенного ассемблера вы должны
учитывать, что практически невозможно будет перенести вашу
программу на другой процессор или компилятор Си без внесения в нее
изменений.
Аналогичным образом, встроенные ассемблерные коды не обладают
ясностью и сжатостью формата, обеспечиваемыми Си, и часто плохо
структурированы. Следовательно, коды встроенного ассемблера
труднее читать и сопровождать, чем коды Си.
При использовании встроенных ассемблерных кодов хорошим тоном
является изолирование их в отдельных самодостаточных модулях с
тщательным структурированием и множеством комментариев. Тем самым
улучшаются возможности сопровождения этих частей программы, а при
необходимости перенести программу в другую среду такая организация
позволяет быстрее найти соответствующие коды на встроенном
ассемблере и переписать их операторами Си.
Более медленная компиляция
—————————
Компиляция модулей на Си, содержащих встроенные ассемблерные
коды, выполняется более медленно, нежели чистая программа на Си, в
основном из-за того, что встроенные ассемблерные коды фактически
компилируются дважды, первый раз при помощи Turbo C и второй раз
при помощи Turbo Assembler. Если Turbo C вынужден повторно начать
компиляцию в случае, когда не были использованы ни опции -B, -S,
ни 3pragma inline, время компиляции таких программ еще более
увеличивается. К счастью, в настоящее время более медленная
компиляция модулей со встроенным ассемблером составляет меньшую
проблему, нежели ранее, так как Turbo Assem- bler работает
существенно быстрее предыдущих ассемблеров.
Возможность работы с программой только при помощи TCC
——————————————————
Как было отмечено выше, средство встроенного ассемблирования
является уникальным средством, присущим только TCC.EXE, версии
Turbo C, запускаемой из командной строки. Интегрированная же
версия среды разработки программ на Turbo C, а именно TC.EXE, не
поддерживает средства встроенного ассемблирования.
Ухудшение оптимизации программы
——————————-
При использовании встроенного ассемблирования Turbo C в
некотором смысле теряет управление программными кодами, так как вы
напрямую вставляете в программу на Си команды на языке ассемблера.
Программируя на встроенном ассемблере, вы в какойто степени должны
компенсировать это ухудшение, избегая явно ошибочных действий,
например разрушения содержимого регистра DS или ошибочной записи
данных в неверную область памяти.
С другой стороны, Turbo C не требует, чтобы вы, программируя
на встроенном ассемблере, соблюдали все внутренние правила Turbo
C; если бы это было так, то вряд ли бы вы смогли получить
какие-либо преимущества перед простым программированием на Си и
генерированием кода самим Turbo C. В случае, когда функции на Си
содержат встроенные ассемблерные коды, Turbo C выключает некоторые
свои средства оптимизации, позволяя тем самым относительно
свободное программирование на встроенном ассемблере. Например, в
таких случаях отключаются некоторые части оптимизатора переходов,
а при использовании встроенным ассемблером регистров SI или DI не
работают переменные регистры. Такую частичную потерю оптимизации
стоит учитывать, особенно если считать, что вы используете
встроенный ассемблер именно для того, чтобы добиться максимального
качества кодов.
Если вы желаете при помощи встроенного ассемблера получить
наиболее быстродействующие и компактные коды, вам может
понадобиться написать на встроенном ассемблере целые функции
полностью — то есть не смешивая в пределах одной и той же функции
операторы Си с операторами встроенного ассемблера. Тем самым вы
обеспечите качество управления кодами встроенного ассемблера,
Turbo C обеспечит качество кодов функций, написанных на Си, и
суммарно будет получен наиболее эффективный код, на который не
будут влиять никакие ограничения.
Ограничения возможностей отслеживания ошибок
———————————————
Поскольку Turbo C имеет слабые средства контроля ошибок в
операторах встроенного ассемблера, эти ошибки часто обнаруживает
не Turbo C, а Turbo Assembler. К сожелению, часто бывает
затруднительным соотнести сообщение об ошибках, выдаваемые Turbo
Assembler, с исходным текстом на Си, так как выводимые на дисплей
сообщение и номера строк относятся не к самому тексту программы на
Си, а к создаваемому Turbo C файлу .ASM.
Например, если во время компиляции TEST.C, программы на Си,
содержащей встроенные ассемблерные коды, Turbo Assembler сообщит о
том, что в строке 23 указан операнд, имеющий неправильный размер,
то к сожалению, ’23’ будет относиться к номеру вызвавшей ошибку
строки в файле TEST.ASM, промежуточном ассемблерном файле,
созданном Turbo C для последующего ассемблирования в Turbo
Assembler. В конечном счете, определить, какая именно строка в
TEST.C ответственна за эту ошибку, должны будете вы сами.
Вашим лучшее решение в таком случае будет заключаться в том,
чтобы сначала по сообщению Turbo Assembler определить, какая
строка в промежуточном файле .ASM, помещаемом Turbo C на диск,
вызвала данную ошибку. Файл .ASM содержит специальные комментарии,
идентифицирующие строку в исходном файле Си, из которой был
сгенерирован тот или иной блок ассемблерных операторов; например,
ассемблерные строки, следующие после
; Line 15
были сгенерированы из строки 15 исходного текста на Си.
Обнаружив строку в файле .ASM, вызвавшую ошибку, вы можете по
данному содержащему номер строки комментарию вернуться к исходному
файлу Си и найти ошибочную строку там.
Отладочные ограничения
———————-
Версии Turbo C до 1.5 включительно не могут генерировать
отладочную информацию уровня исходного текста (информацию,
позволяющую при отладке видеть исходный текст программы) для
модулей, содержащих встроенные ассемблерные коды. При
использовании встроенного ассемблирования Turbo C версии 1.5 и
более ранних генерируют плоский ассемблерный код без включенной в
него отладочной информации. Средства отладки на уровне исходных
текстов отсутствуют, и отладка модулей Си, содержащих операторы
встроенного ассемблирования, возможна исключительно на уровне
текста на языке ассемблера.
Последние версии Turbo c используют преимущества специальных
средств Turbo Assembler, обеспечивающих при работе совместно с
Turbo Debugger самую совершенную отладку на уровне исходных
текстов и позволяющих выполнять отладку модулей, содержащих
встроенные ассемблерные коды (и конечно, модулей, целиком
написанных на Си).
Первоначальная разработка программы на Си и последующая
компиляция ее конечной версии с использованиемв средства
встроенного ассемблирования
——————————————————-
В свете только что рассмотренных нами недостатков встроенного
ассемблирования может показаться, что это средство нужно применять
как можно реже. Однако это не так. Важно использовать встроенное
ассемблирование в нужной точке цикла разработки — в его конце.
Большинство недостатков встроенного ассемблирования сводится
к одному и тому же — встроенное ассемблирование относительно
замедляет цикл редактирования/компиляции/отладки. Более медленная
компиляция, невозможность воспользоваться преимуществами
интегрированной среды разработки с удобным пользовательским
интерфейсом, а также сложности в обнаружении ошибок означают, что
разработка программы, содержащей операторы встроенного ассемблера,
будет выполняться медленнее, нежели разработка программы чисто на
Си. Однако правильное применение встроенного ассемблера дает
резкое улучшение характеристик программы. Что же делать?
Ответ прост. В начале каждую такую программу следует
разрабатывать целиком на Си, пользуясь всеми преимуществами
замечательной интегрированной среды разработки, обеспечиваемыми
TC.EXE. Когда программа достигнет уровня полной функциональной
завершенности, а выполняемые коды будут целиком отлажены и
выполняться без сбоев, следует перейти к работе с TCC.EXE и начать
перевод критических частей программы на встроенный ассемблер.
Такой подход обеспечит эффективную разработку и отладку программы
в целом и позволит затем изолировать и улучшить некоторые ее
участки, требующие тонкой подстройки для получения максимального
качества.
Вызов функций на Turbo Assembler из Turbo C
——————————————————————
Традиционно смешанное программирование на Си и языке
ассемблера состоит в написании раздельных модулей на этих языках,
компиляции модулей на Си и ассемблировании модулей на языке
ассемблера с последующей компоновкой раздельно скомпилированных
модулей. Модули на Turbo C легко компонуются в этом режиме с
модулями на Turbo Assembler. этот процесс показан на рис.7.3.
——————— ——————————
| Исходный файл Си | |Исходный файл на ассемблере|
| FILENAM1.C | | FILENAM2.ASM |
——————— ——————————
? ?
———— ————-
| Turbo C | Компиляция | Turbo | Ассембли-
———— | Assembler | рование
| ————-
? ?
——————— ———————
| Объектный файл | | Объектный файл |
| FILENAM1.OBJ | | FILENAM2.OBJ |
——————— ———————
| |
————— —————
? ?
———
| TLINK | Компоновка
———
?
———————
| Выполняемый файл |
| FILENAM1.EXE |
———————
Рис.7.3 Компиляция, ассемблирование и компоновка при помощи
Turbo C, Turbo Assembler и TLINK
Выполняемый файл создается при объединении исходных файлов на
Си и на языке ассемблера. Цикл этот начинается командой
tcc filenam1 filenam2.asm
Тем самым Turbo C получает команду сначала скомпилировать
файл FILENAM1.C в FILENAM1.OBJ, затем запустить Turbo Assembler
для ассемблирования FILENAM2 в FILENAM2.OBJ, и наконец запустить
TLINK, который скомпонует FILENAM1.OBJ и FILENAm2.OBJ в файл
FILЕNAM2.EXE.
Раздельная компиляция весьма полезна для программ, содержащих
значительные количества ассемблерных кодов, поскольку она дает
доступ к мощным средствам Turbo Assembler и позволяет
программирование на языке ассемблера в чисто ассемблерной среде,
без ключевых слов asm, дополнительного времени на компиляцию и
необходимости при использовании встроенного ассемблирования
ориентироваться на особенности Си.
Имеется и цена, которую приходится платить за возможность
раздельной компиляции: программист, работающий на ассемблере,
должен входить во все детали интерфейса Си и ассемблера. Если
Turbo C для встроенного ассемблера обрабатывает спецификации
сегмента, передачу параметров, обращения к переменным Си,
сохранение переменных регистров, то в случае раздельно
компилируемых ассемблерных функций все это, а также многое другое,
должно быть выполнено явно самим программистом.
При интерфейсе Turbo C и Turbo Assembler существует два
основных аспекта. Во-первых, различные части программы на Си и на
ассемблере должны быть правильно скомпонованы друг с другом, а
функции и переменные в каждой из частей программы должны быть
сделаны доступными в остальных частях, где они нужны. Во-вторых,
ассемблерная программа должна правильно обрабатывать вызовы к
фуннкциям Си согласно принятым для Си правилам. Сюда входит доступ
к передаваемым параметрам, возвращаемым значениям и следование
правилам сохранения регистров, установленным для функций Си.
Рассмотрим правила компоновки частей программы на Turbo C и
Turbo Assembler.
Вопросы организации Turbo C и Turbo Assembler ——————
Для компоновки модулей Turbo C и Turbo Assembler должны
выполняться следующие моменты:
— модули Turbo Assembler обязаны соблюдать соглашения об именах,
принятые в Turbo C
— модули Turbo C и Turbo Assembler должны разделять
соответствующие имена функций и переменных в форме, приемлемой
для Turbo C
— для объединения нескольких модулей в одну выполняемую программу
должен использоваться компоновщик TLINK
Пока ничего не говорилось о том, что в действительности
делают модули Turbo Assembler; в этой точке только очерчиваются
рамки, в пределах которых должны создаваться функции Turbo
Assembler, совместимые с Turbo C.
Модели памяти и сегменты
————————
Для того, чтобы та или иная ассемблерная функция могла быть
вызвана из Си, она должна использовать ту же модель памяти, что и
программа на Си, а также совместимый с Си кодовый сегмент.
Аналогичным образом, для того, чтобы можно было из программы на Си
обращаться к данным, определенным в ассемблерном модуле (или для
доступа данным в Си из ассемблерной части программы), ассемблерная
часть программы должна следовать соглашениям о наименованиях
сегмента данных, принятым в Си.
Модели памяти и обработка сегментов могут быть достаточно
сложными для реализации на ассемблере. К счастью, Turbo assem-
bler может проделать для вас всю работу по реализации совместимых
с Си моделей памяти и сегментов в форме упрощенных сегментных
директив.
(См. раздел «Стандартные сегментные директивы» в главе 4, где
дается введение в упрощенные сегментные директивы.)
Упрощенные сегментные директивы и Turbo C
——————————————
Директива DOSSEG говорит Turbo Assembler о том, что сегменты
должны упорядочиваться согласно соответствующим соглашениям Intel,
выполняемым также и Turbo C (а также многими другими программными
продуктами, включая продукты Microsoft).
Директива .MODEL сообщает Turbo Assembler о том, что
сегменты, создаваемые упрощенными сегментными директивами, должны
быть совместимы с выбранной моделью памяти (tiny, small, compact,
medium, large или huge), а также управляет типом по умолчанию
(ближний или дальний) процедур, создаваемых директивой PROC.
Модели памяти, определяемые директивой .MODEL, совместимы с
эквивалентно называющимися моделями Turbo C.
И наконец, упрощенные сегментные директивы .CODE, .DATA,
.DATA?, .FARDATA, .FARDATA? и .CONST генерируют сегменты,
совместимые с Turbo C.
Например, рассмотрим следующий модуль Turbo Assembler с
именем DOTOTAL.ASM:
;выбор метода упорядочения сегментов по соглашениям Intel
.MODEL small ;выбор модели памяти Small
;(ближние коды и данные
.DATA ;инициализированный сегмент
;данных, совместимый с TC
EXTRN _REPETITIONS:WORD ;определяется как внешняя
PUBLIC _StartingValue ;доступна другим модулям
_StartingValue DW 0
.DATA? ;неинициализированный сегмент
;данных, совместимый с TC
RunningTotal DW ?
.CODE ;кодовый сегмент, совместимый
;с TC
PUBLIC _DoTotal
_DoTotal PROC ;функция (с ближним вызовом по
;малой модели памяти)
mov cx,[_Repetitions] ;число циклов выполнения
mov ax,[_StartingValue]
mov [RunningTotal],ax ;установка исходного
;значения
TotalLoop:
inc [RunningTotal] ;RunningTotal++
loop TotalLoop
mov ax,[RunningTotal] ;возврат конечного итога
ret
_DoTotal ENDP
END
(Префикс подчеркивания (_) часто встречается в процедуре
DoTotal перед метками, поскольку они обычно требуются в Turbo C.
Этот вопрос подробнее рассматривается в разделе «Подчеркивания»).
Ассемблерная процедура _DoTotal может быть всегда вызвана из
программы на Turbo C с моделью памяти small оператором:
DoTotal();
Отметим, что _DoTotal ожидает определения внешней переменной
Repetitions в другой части программы. Аналогичным образом,
переменная StartingValue объявляется общей, что позволяет
обращение к ней из других частей программы. Следующий модуль Turbo
C, SHOWTOT.C, обращается к общим данным из DOTOTAL.ASM и
обеспечивает для последнего внешние по отношению к нему данные:
extern int StartingValue;
extern int DoTotal(void);
int Repetitions;
main()
{
int i;
Repetitions = 10;
StartingValue = 2;
printf(«%d\n», DoTotal());
}
Для создания из SHOWTOT.C и DOTOTAL.ASM выполняемой программы
SHOWTOT.EXE введите командную строку
tcc showtot dototal.asm
Если вы хотите скомпоновать _DoTotal с программой Си с
моделью памяти compact, вам следует просто заменить директиву
.MODEL на .MODEL COMPACT. Если вы хотите использовать в
DOTOTAL.ASM дальний сегмент, то вы должны использовать директиву
.FARDATA.
Короче говоря, генерирование правильного упорядочения
сегментов, модели памяти и имен сегментов для компоновки с Turbo C
при помощи упрощенных сегментных дирекив выполняется легче.
Сегментные директивы более старых версий и Turbo C
—————————————————
Предположим, что у вас возникла необходимость интерфейса
части программы на Turbo Assembler с частью на Turbo C с
использованием сегментных директив старых версий. Например, если
вы заменяете упрощенные сегментные директивы в DOTOTAL.ASM
сегментными директивами старых версий, то вы получите:
DGROUP group _DATA,_BSS
_DATA segment word public ‘DATA’
EXTRN _REPETITIONS:WORD
;определяется как внешняя
PUBLIC _StartingValue ;доступна другим модулям
_StartingValue 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,[_StartingValue]
mov [RunningTotal],ax ;установка исходного
;значения
TotalLoop:
inc [RunningTotal] ;RunningTotal++
loop TotalLoop
mov ax,[RunningTotal] ;возврат конечного итога
ret
_DoTotal ENDP
_TEXT ENDS
END
Программа с сегментными директивами старых версий не только
длиннее, но и гораздо труднее читается, и кроме того, ее сложнее
читать и модифицировать для других моделей памяти Си. Для
интерфейса с Turbo C использовать директивы старых версий смысла
не имеет. Однако если вы все же хотите их использовать, следует
идентифицировать нужные сегменты в соответствии с моделью памяти,
принятой в программе на Си.
Обзор применения сегментов в Turbo C см. в Руководстве
пользователя Turbo C.
Простейший способ определения, какие сегментные директивы
старых версий должны выбираться для компоновки с той или иной
программой Turbo C, заключается в компиляции главного модуля
программы на Turbo C для желаемой модели памяти с опцией -S, что
тем самым заставит Turbo C сгенерировать ассемблерную версию
соответствующей программы на Turbo C. В этой версии кодов Си вы
сможете найти все старые сегментные директивы, исполь зуемые turbo
C; просто скопируйте их в вашу ассемблерную часть программы.
Например, если ввести команду:
tcc -s showtot.c
то будет сгенерирован файл SHOWTOT.ASM, содержащий:
ifndef ??version
?debug macro
endm
endif
name showtot
_TEXT segment byte public ‘CODE’
DGROUP group _DATA,_BSS
assume cs:_TEXT,ds:DGROUP,ss:DGROUP
_TEXT ends
_DATA segment word public ‘DATA’
_d@ label byte
_d@w label word
_DATA ends
_BSS segment word public ‘BSS’
_b@ label byte
_b@w label word
?debug C E91481D5100973686F77746F742E63
_BSS ends
_TEXT segment byte public ‘CODE’
; ?debug L 3
_main proc near
; ?debug L 6
mov word ptr Dgroup:_Repetitions,10
; ?debug L 7
mov word ptr StartingValue,2
; ?debug L 8
call near ptr_DoTotal
push ax
mov ax,offset DGROUP:_s@
push ax
call near ptr_printf
pop cx
pop cx
@l:
; ?debug L 9
ret
_main endp
_TEXT ends
_BSS segment word public ‘BSS’
_Repetitions label word
db 2 dup (?)
?debug C E9
_BSS ends
_DATA segment word public ‘DATA’
_s@ label byte
db 37
db 100
db 10
db 0
_DATA ends
extrn _StartingValue:word
_TEXT segment byte public ‘CODE’
extrn _DoTotal:near
extrn _printf:near
_TEXT ends
public _Repetitions
public _main
end
Сегментные директивы для _DATA (инициализированный сегмент
данных), _TEXT (кодовый сегмент) и BSS (неинициализированный
сегмент данных), а также директивы GROUP и ASSUME, находятся в
готовом к ассемблированию виде, поэтому их можно использовать в
том виде, в каком они есть.
(В главе 10 сегментные директивы рассматриваются подробно).
Сегментны умолчания: когда требуется загружать сегменты?
———————————————————
При некоторых обстоятельствах вызываемые из Си ассемблерные
модули могут для доступа к данным иметь необходимость загружать
регистры DS и/или ES. Также полезно знать о взаимосвязях между
установками сегментных регистров при вызовах из Turbo C, поскольку
иногда в ассемблерном коде можно получить выгоду от
эквивалентности двух сегментных регистров. Давайте рассмотрим
установки сегментных регистров при вызове ассемблерной функции из
Turbo C, взаимоотношения между сегментными регистрами и случаи, в
которых ассемблерной функции может понадобиться загрузка одного
или более сегментных регистров.
При входе из Turbo c в ассемблерную функцию регистры CS и DS
имеют следующие установки, в зависимости от используемой модели
памяти (SS всегда используется для стекового сегмента, а ES всегда
используется для регистра стираемого регистра):
Установки регистров Таблица 7.1
при входе из Turbo C в ассемблер
————————————————————
Модель CS DS
————————————————————
Tiny _TEXT DGROUP
Small _TEXT DGROUP
Compact _TEXT DGROUP
Medium имяфайла_TEXT DGROUP
Large имяфайла_TEXT DGROUP
Huge имяфайла_TEXT имявызывающего файла_DGROUP
————————————————————
Имяфайла здесь — это имя ассемблерного модуля, а
имявызывающего файла — это имя модуля, вызывающего ассемблерный
модуль.
В случае модели памяти tiny _TEXT и DGROUP одинаковы, и
поэтому при входе в функцию CS равен DS. Аналогичным образом, для
моделей tiny, small и medium при входе в функцию SS равен DS.
Итак, когда в вызываемом из Си ассемблерном модуле требуется
загружать сегментный регистр? Начинающим программистам не следует
пытаться (или даже желать) прямо загрузить регистры CS или SS. CS
при дальних вызовах, переходах и возвратах устанавливается в
правильное значение автоматически и не требует каких-либо
корректировок. SS всегда указывает на стековый сегмент, который не
должен изменяться во время работы программы (если создаваемая вами
программа не предназначена именно для переключения сегментов, а в
этом случае вы должны абсолютно точно представлять себе, что вы
делаете!).
ES всегда доступен вам при необходимости. Его можно
использовать как указатель на дальние данные, либо в него можно
загрузить сегмент назначения при работе со строковыми командами.
Остается регистр DS. Во всех моделях памяти Turbo C, за
исключением huge, 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 es,ax ;DS указывает на дальний
;сегмент данных
inc [Counter] ;инкремент переменной-счетчика
assume ds:@Data
mov ax,@Data
mov ds,ax ;DS снова указывает на DGROUP
.
.
.
_AsmFunction ENDP
.
.
.
Преимущество второй версии перед первой состоит в том, что
здесь при каждом доступе к памяти в дальнем сегменте данных
задавать ES:переопределение_сегмента не требуется. Если вы
загрузите Ds так, чтобы он указывал на дальний сегмент, не
забывайте о необходимости его восстановления, как в преыдущем
примере, прежде чем пытаться обращаться к каким-либо переменным в
DGROUP. Даже если в данной ассемблерной функции вы не обращаетесь
в DGROUP, не забудьте перед выходом восстановить DS, поскольку
Turbo C предполагает, что функции не изменяют DS.
Работа с DS в вызываемых из Си функциях с моделью памяти huge
несколько отлична от описанной выше. В случае модели huge Turbo C
не использует DGROUP вообще. Вместо этого каждый модуль имеет свой
собственный сегмент данных, относительно любых других модулей в
программе являющийся дальним; общего разделяемого ближнего
сегмента данных не существует. При входе в функцию в модели huge
DS должен быть установлен как указатель на соответствующий дальний
сегмент модуля и остается в этом значении до выхода из функции,
как показано в следующем примере:
.
.
.
.FARDATA
.
.
.
.CODE
PUBLIC _AsmFunction
_AsmFunction PROC
push ds
mov ax,@FarData
mov ds,ax
.
.
.
pop ds
ret
_AsmFunction ENDP
.
.
.
Отметим, что исходное состояние DS сохраняется при входе в
AsmFunction при помощи команды PUSH и затем восстанавливается при
помощи POP перед выходом; даже в случае модели huge Turbo C
требует, чтобы все функции сохраняли DS.
Общие и внешние символические имена
————————————
Коды на Turbo Assembler могут вызывать функции Си и
обращаться к внешним переменным Си, и наоборот, коды на Turbo C
могут вызывать общие функции Turbo Assembler и обращаться к общим
переменным Turbo Assembler. Если в Turbo Assembler установлены,
как было описано в предыдущих разделах, сегменты, совместимые с
Turbo C, следует соблюдать только несколько описываемых ниже
простых правил, позволяющих разделять функции между Turbo C и
Turbo Assembler.
Подчеркивания
————-
Обычно Turbo C предполагает, что все внешние метки должны
начинаться с символа подчеркивания (_). Turbo C вставляет символы
подчеркивания перед всеми именами внешних функций и переменных при
их использовании в программе на Си автоматически, поэтому вам
требуется вставить их самим только в ассемблерных кодах. Вы должны
убедиться, что все ассемблерные обращения к функциям и переменным
Turbo C начинаются с символа подчеркивания, и кроме того, вы
должны вставить его перед именами всех ассемблерных функций и
переменных, которые делаются общими и вызываются из Turbo C.
Например, следующая программа на Си:
extern int ToggleFlag();
int Flag;
main()
{
ToggleFlag();
}
правильно скомпонуется со следующей ассемблерной программой:
.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
Отметим, что метки, на которые из кодов на turbo C ссылок не
имеется, например SetFlag, могут не иметь ведущего символа
подчеркивания.
При использовании спецификатора языка С в директивах EXTRN и
PUBLIC
DOSSEG
.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
Turbo Assembler автоматически при записи имен Flag и
ToggleFlag в объектный файл поместит перед ними символ
подчеркивания.
Между прочим, можно сообщить Turbo C, что он не должен
использовать подчеркивания, задав для этого опцию командной строки
-u. Однако, несмотря на то, что использование этой опции может
показаться привлекательным, все библиотеки исполняющей системы
Turbo C компилируются при включенном средстве вставки
подчеркиваний. Следовательно, чтобы пользоваться опцией -u, вам
необходимо приобрести у фирмы Borland исходные тексты библиотек
исполняющей системы и затем перекомпилировать их с выключенным
средством вставки подчеркиваний. (Информацию об опции -p,
отменяющей использование подчеркиваний и чувствительность к
регистру, в котором набраны символические имена, см. в разделе
«Соглашения Паскаля о связях»).
Учет регистра, в котором набраны символические имена
—————————————————-
Обычно Turbo Assembler, работая с символическими именами,
нечувствителен к регистру, в котором они набраны, и не делает
различия между заглавными и строчными буквами. Поскольку в Си
такое различие делается, то желательно, чтобы это требование
выполнялось и в Turbo Assembler, по крайней мере касательно тех
символических имен, разделяемых ассемблером и Си. Это можно
выполнить, задав /ml и /mx.
Ключ командной строки /ml делает Turbo Assembler
чувствительным к регистру для всех символических имен. Ключ
командной строки /mx делает Turbo Assembler чувствительным к
регистру для общих (PUBLIC), внешних (EXTRN), глобальных (GLOBAL)
и коммуникационных (COMM) символических имен.
Типы меток
———-
Поскольку ассемблерная программа имеет возможность обращаться
к переменным как к данным любого размера (8 бит, 16 бит, 32 бита и
т.д.), имеет смысл обращаться к данным в их естественном формате.
Например, обычно вызывает проблемы запись слова в байтовую
переменную:
.
.
.
SmallCount DB 0
.
.
.
mov WORD PTR [SmallCount],0ffffh
.
.
.
Следовательно, важным является, чтобы ассемблерные операторы
EXTRN, объявляющие внешние переменные Си, задавали для этих
переменных правильный размер, так как Turbo Assembler, решая, с
каким размером сгенерировать доступ к переменной Си,
руководствуется исключительно этими объявлениями. Если в программе
на Си задан оператор
char c
то ассемблерные коды
.
.
.
EXTRN c:WORD
.
.
.
inc [c]
.
.
.
могут вызвать серьезные проблемы, так как через каждые 256
выполнений в ассемблерных кодах инкрементирования переменной c эта
переменная циклически переходит в исходное значение, поскольку она
ошибочно объявлена как имеющая размер в слово, и байт по адресу
OFFSET+1 будет тем самым инкрементирован ошибочно, что приведет к
непредсказуемым результатам.
Между типами данных в Си и в ассемблере существует следующее
соответствие:
————————————————————
Типы данных в Си Типы данных в ассемблере
————————————————————
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
————————————————————
Дальние внешние символические имена
Если вы используете упрощенные сегментные директивы, то
объявления при помощи EXTRN символических имен в дальних сегментах
не должны быть помещены ни в один сегмент, поскольку Turbo
Assembler рассматривает символические имена, объявленные в данном
сегменте, связанными с этим сегментом. Это свойство имеет свои
негативные последствия; Turbo Assembler не может проверить
адресование символических имен, объявленных EXTRN вне любого
сегмента, и не может ни генерировать переопределения сегментов,
как это необходимо, ни информировать вас о недопустимой попытке
обращения к этой переменной, если не загружен правильный сегмент.
Turbo Assembler тем не менее может сгенерировать для ссылок на
такие внешние символические имена правильные коды, но не может
обеспечить нормальную степень контроля адресации сегмента.
Если вы хотите (несмотря на то, что мы вас отговаривали), то
вы можете использовать старые сегментные директивы для явного
объявления того, в каком сегменте находится внешнее символическое
имя, и затем поместить для этого символического имени внутри
объявления сегмента директиву EXTRN. Однако, это весьма трудоемкий
прием; если вас не пугает необходимость самому отвечать за
загрузку при доступе к дальним данным правильного сегмента, проще
поместить объявления EXTRN дальних символических имен вне всех
сегментов. Например, предположим, что FILE1.ASM содержит
.
.
.
.FARDATA
FileVariable DB 0
.
.
.
Тогда при компоновке FILE1.ASM с FILE2.ASM, который содержит
.
.
.
.DATA
EXTRN File1Variable:BYTE
.CODE
Start PROC
mov ax,SEG File1Variable
mov ds,ax
.
.
.
то SEG File1Variable не вернет правильного сегмента.
Директива EXTRN помещена в области действия директивы DATA в
FILE2.ASM, поэтому Turbo assembler рассматривает File1Variable как
находящуюся в ближнем сегменте DATA в FILE2.ASM, а не в дальнем
сегменте данных.
Следующий фрагмент FILE2.ASM позволяет SEG File1Variable
возвратить правильный сегмент:
.
.
.
.DATA
@curseg ENDS
EXTRN File1Variable:BYTE
.CODE
Start PROC
mov ax,SEG File1Variable
mov ds,ax
.
.
.
Самое интересное здесь в том, что сегмент .DATA заканчивается
директивой @curseg ENDS, и поэтому при объявлении File1Variable
внешней переменной ни одна сегментная директива не действует.
Командная строка компоновщика
——————————
Простейший способ компоновки модулей Turbo C с модулями Turbo
Assembler заключается в том, чтобы ввести одну командную строку
Turbo C и предоставить Turbo C сделать всю остальную работу
самому. При правильно составленной командной строке Turbo C
скомпилирует код Си, запустит Turbo Assembler на ассемблирование и
запустит TLINK для компоновки объектных файлов в один выполняемый
файл. Предположим, например, что у вас имеется программа,
состоящая из файлов Си MAIN.C и STAT.C и ассемблерных файлов
SUMM.ASM и DISPLAY.ASM. Командная строка
tcc main stat summ.asm display.asm
компилирует MAIN.C и STAT.C, ассемблирует SUMM.ASM и
DISPLAY.ASM и компонует все четыре объектных файла с кодами
загрузки Си и требуемыми библиотечными функциями в MAIN.EXE. Вы
должны только помнить о том, что при вводе имен ассемблерных
файлов требуется указывать расширения .ASM.
Если вы используете TLINK в автономном режиме, то объектные
файлы, генерируемые Turbo Assembler, являются стандартными
объектными модулями и затем рассматриваются как объектные модули
Си.
Взаимодействие между Turbo Assembler и Turbo C —————-
Теперь, когда вы понимаете, как строить и компоновать
ассемблерные модули, совместимые с Си, вам следует узнать, какого
рода коды можно помещать в вызываемые из Си ассемблерные функции.
Существует три области, которые в этой связи нужно рассмотреть:
прием переданных параметров, использование регистров и возврат
значений в вызывающую программу.
Передача параметров
——————-
Turbo C передает функциям параметры через стек. Перед вызовом
функции Turbo C сначала помещает параметры, которые требуется
передать этой функции, в стек, начиная с самого правого параметра
и кончая левым. Вызов функции в Си
.
.
.
Test(i, j, l);
.
.
.
после компиляции принимает вид
mov ax,l
push ax
push word ptr DGROUP:_j
push word ptr DGROUP:_i
call near ptr _Test
add sp,6
где вы ясно можете видеть, что самый правый параметр, l,
помещается в стек первым, а за ним j, и наконец, i.
После возврата из функции параметры, помещенные в стек, все
еще находятся там, но никак не используются. Следовательно,
непосредственно после каждого вызова функции Turbo C возвращает
указатель стека назад, на значение, в котором он находился до
помещения в стек параметров, и тем самым параметры уничтожаются. В
предыдущем примере три параметра, по 6 байт каждый, занимают
суммарно 6 байт стековой памяти, поэтому Turbo C прибавит к
указателю стека число 6, обеспечив тем самым уничтожение
параметров после вызова Test. Важным здесь является то, что по
соглашениям о связях Си за уничтожение параметров в стеке отвечает
вызывающая программа.
(См. раздел «Соглашения Паскаля о связях»)&
Ассемблерные функции могут обращаться к параметрам,
переданным через стек, относительно регистра BP.
Например,предположим, что функция Test в предыдущем примере имеет
следующий вид:
.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
END
Вы видите, что Test принимает параметры, переданные
программой на Си через стек относительно BP. (Помните, что BP
адресует стековый сегмент.) При этом возникает вопрос, откуда вы
узнаете, где находятся параметры относительно BP?
На рис.7.4 показано, как выглядит стек перед выполнением
первой команды в Test:
i = 25;
j = 4;
Test(i, j, l);
| |
——————
| |
——————
SP —> | Адрес возврата |
——————
SP+2 | 25(i) |
——————
SP+4 | 4(j) |
——————
SP+6 | l |
——————
| |
——————
| |
Рис.7.4 Состояние стека непосредственно перед выполнением
первой команды Test
Параметры, передаваемые в Test, являются фиксированными
адресами относительно SP, начиная с адреса стека, на 2 байта выше
адреса возврата, помещенного в стек при вызове. После загрузки SP
в BP вы можете обращаться к параметрам относительно BP. Однако
сначала вы должны сохранить значение BP, так как вызывающая
программа на Си ожидает, что при возврате из функции BP останется
без изменений. Помещение BP в стек изменяет все смещения в стеке.
На рис. 7.5 показан стек после выполнения следующих строк
программы:
.
.
.
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 | l | BP+8
------------------
| |
------------------
| |
Рис.7.5 Состояние стека после выполнения команд PUSH и MOV
Таковы стандартный фрейм стека Си, организация передачи
параметров в функцию и автоматические переменные в стеке. Как
видите, независимо от того, сколько параметров может иметь
программа на Си, самый левый параметр всегда помещается в адрес
стека непосредственно над помещенным в стек адресом возврата,
следующий параметр вправо будет храниться над самым левым
параметром и т.д. Если вы знаете порядок следования и тип
передаваемых параметров, то вы всегда будете знать, где искать их
в стеке.
Область для автоматических переменных можно зарезервировать,
вычев требуемое количество байтов из SP. Например, для
100-байтового автоматического массива можно зарезервировать
область, начав Test с
.
.
.
push bp
mov bp,sp
sub sp,100
.
.
.
как показано на рис.7.6.
| |
------------------
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 | l | BP+8
------------------
| |
------------------
| |
Рис.7.5 Состояние стека после выполнения команд PUSH
и MOV и SUB
Поскольку часть стека, в которой находятся автоматические
переменные, имеет младший адрес, чем BP, то для их адресации
используются отрицательные смещения относительно BP. Например,
mov byte ptr [bp-100],0
установит первый байт ранее зарезервированного вами
100-байтового массива в ноль. С другой стороны, переданные
параметры всегда адресуются с положительными смещениями
относительно BP.
Для того, чтобы вы могли при желании распределить область для
автоматических переменных, как было показано выше, Turbo Assembler
обеспечивает специальную версию директивы 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 ;установить локальный счетчик
;равным 10.
;(Фактически LocalCount равен
;[BP-102])
.
.
.
mov cx,[LocalCount] ;прием значения локального счетчика
mov al,'A' ;заполнение массива будет выполнено
;символом 'A'
lea bx,[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 элементов размером
в байт, а LocalCount из 1 элемента размером в слово.
Отметим также, что в предыдущем примере строка с директивой
LOCAL оканчивается =AUTO_SIZE. Это поле, начиная со знака
равенства, является необязательным; если же оно присутствует, то
устанавливает метку после знака равенства равной числу байтов,
требуемых для хранения автоматических переменных, поскольку
директива LOCAL лишь генерирует метки и фактически не генерирует
никаких программных кодов или областей данных. Можно изложить
сказанное иначе: директива LOCAL не распределяет автоматических
переменных, а просто генерирует метки, которые вы затем сможете
использовать либо для распределения области памяти, либо для
доступа к автоматическим переменным.
Очень удобное свойство директивы LOCAL состоит в том, что
метки как для автоматических переменных, так и для общего размера
автоматических переменных ограничены по своей области определения
процедурой, в которой они используются, что позволяет вам свободно
давать в других процедурах другим автоматическим переменным эти же
имена.
Как вы могли видеть, LOCAL существенно упрощает определение и
использование автоматических переменных. Отметим, что директива
LOCAL имеет полностью иной смысл при использовании ее в макросах;
этот вопрос рассматривается в главе 9.
(Дополнительную информацию об обеих формах директивы LOCAL
см. также в главе 3 Справочного руководства.)
Между прочим, Turbo C работает со стековыми фреймами именно
тем способом, который мы с вами только что обсудили. Может
оказаться для вас полезным откомпилировать несколько модулей Turbo
C с опцией -s и посмотреть, какие ассемблерные коды генерирует
Turbo C для создания и использования стековых фреймов.
До сих пор все шло хорошо, но здесь начинают возникать
сложности. Во-первых, такого рода организация доступа к параметрам
по смещениям, являющимся константами, несколько неудобен; он не
только позволяет делать ошибки - при добавлении еще одного
параметра требуется изменить все остальные смещения стековых
фреймов в функции. Например, предположим, что вы изменили функцию
Test таким образом, что она стала принимать четыре параметра:
Test(Flag, i, j ,l);
Теперь i имеет смещение 6 вместо 4, j - смещение 8 вместо 6 и
т.д. Для задания смещений параметров можно использовать директивы
присвоения:
.
.
.
Flag EQU 4
AddParm1 EQU 6
AddParm2 EQU 8
SubParm1 EQU 10
mov ax,[bp+AddParm1]
add ax,[bp+AddParm2]
sub ax,[bp+SubParm1]
.
.
.
однако и это неудобно, поскольку требуется вычислять смещения
и вносить в директивы возникающие изменения. Имеется и более
серьезная проблема: для моделей памяти с дальней адресацией кодов
размер помещаемого в стек адреса возврата увеличивается на 2
байта, как и размеры переданных указателей кодов и указателей
данных для моделей с дальней адресацией кодов и данных,
соответственно. Написать функцию, которая бы легко
ассенблировалась и правильно обращалась к стековому фрейму при
любой модели памяти, становится тем самым затруднительно.
Однако не пугайтесь. Turbo Assembler дает вам директиву ARG,
которая упрощает обработку в ассемблерных подпрограммах переданных
параметров.
Директива ARG автоматически генерирует для заданных вами
переменных правильные смещения в стеке. Например,
arg FillArray:WORD,Count:WORD,FillValue:BYTE
задает три параметра: FillArray, параметр с размером в слово,
Count, параметр с размером в слово, и FillValue, параметр с
размером в байт. ARG фактически устанавливает метку FillArray в
[BP+4] (предполагая при этом, что коды данного примера
располагаются в ближней процедуре), метку Count в [BP+6], а метку
FillValue в [BP+8]. Однако, особенная ценность директивы ARG
состоит в том, что вы можете использовать определенные в ней
метки, не зная, в какие значения они фактически установлены.
Например, предположим, что у вас имеется функция FillSub,
вызываемая из Си следующим образом:
main()
{
#define ARRAY_LENGTH 100
char TestArray[Array_LENGTH];
FillSub(TestArray,Array_Length,'*');
}
Для обработки параметров вы можете в FillSub использовать
директиву ARG следующим образом:
_FillSub PROC NEAR
ARG FillArray:BYTE:100,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 автоматически
расчитывает различные размеры адресов для ближних или дальних
возвратов. Другое удобство состоит в том, что область определения
заданных в этой директиве меток ограничена процедурой, в которой
они использованы, и вы можете не беспокоиться о том, что возникнет
ошибка, связанная с использованием в разных процедурах одних и тех
же имен.
Дополнительную информацию о директиве ARG см. в главе 3
Справочного руководства.
Сохранение регистров
--------------------
Что касается Turbo C, то вызуваемые из него ассемблерные
функции могут делать все, что угодно, при условии, что они
обеспечат сохранность следующих регистров: BP, SP, CS, DS и SS.
Поскольку состояния этих регистров во время выполнения
ассемблерной функции могут меняться, то при возврате в вызывающую
программу они должны иметь в точности то же самое значение, что и
при входе в ассемблерную функцию. AX, BX, CX, DX, ES и флаги могут
меняться любым образом.
Специальные случаи представляют собой SI и DI, поскольку они
используются в Turbo C как переменные-регистры. Если в модуле Си,
вызвавшем вашу ассемблерную функцию, переменные-регистры
разрешены, то вы должны позаботиться о сохранении SI и DI, а если
не разрешены, то об их сохранении можно не заботиться.
Хорошей практикой является сохранение в вызываемых из Си
ассемблерных функциях состояний регистров SI и DI независимо от
того, разрешено или нет использование в вызывающем модуле
переменных-регистров. Нельзя знать заранее, придется или не
придется когда-нибудь компоновать данную ассемблерный модуль с
другим модулем Си, либо с этим же, но при разрешенном
использовании переменных-регистров, и такое независимое сохранение
SI и DI позволит не вносить затем изменения в ассемблерные коды.
Возврат значений
----------------
Вызываемая из Си ассемблерная функция может возвращать
значения, так же , как и функция на Си. Значения возвращаются из
функции следующим образом:
------------------------------------------------------------
Тип возвращаемого Адрес возвращаемого
значения значения
------------------------------------------------------------
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 регистр (ST(0)) вершины стека (TOS) 8087
double регистр (ST(0)) вершины стека (TOS) 8087
long double регистр (ST(0)) вершины стека (TOS) 8087
near* AX
far* DX:AX
------------------------------------------------------------
В целом, 8- и 16-битовые значения возвращаются через AX, а
32-битовые значения возвращаются через DX:AX, где старшие 16 битов
находятся в DX. Значения с плавающей точкой возвращаются через
регистр ST(0), являющийся регистром вершины стека 9TOS) процессора
8087, либо, при использовании эмулятора операций с плавающей
точкой, через регистр TOS этого эмулятора 8087.
Возврат структур организован несколько сложнее. Структуры,
имеющие длину в 1 или 2 байта, возвращаются через AX, а структуры
длиной в четыре байта возвращаются через DX:AX. Структуры длиной
три байта или свыше четырех байт должны возвращаться через область
статических данных, и при этом возвращаемое значение должно
являться указателем статических данных. Как и для всех указателей,
ближние указатели структур возвращаются через AX, а дальние -
через DX:AX.
Рассмотрим вызываемую из Си ассемблерную функцию с моделью
памяти small, 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 байтах
repnz scasb ;выполнение поиска нуля
dec di ;возврат указателя к нулю
dec di ;возврат указателя к последнему
;символу просмотренной строки
mov ax,di ;возврат ближнего указателя через AX
pop bp
ret
_FindLastChar ENDP
END
Конечный результат, а именно ближний указатель на последний
символ в переданной строке, возвращается в вызывающий модуль через
AX.
Вызов функции на Turbo Assembler из Turbo C -------------------
Рассмотрим пример программы на Turbo C, вызывающей функцию на
turbo Assembler. Следующий модуль Turbo Assembler, COUNT.ASM,
содержит функцию LineCount, которая подсчитывает число строчек и
символов в переданной строке символов:
; Вызываемая из Си ассемблерная функция с моделью памяти
; small, которая подсчитывает число строчек и символов в
; переданной ей строке символов.
;
; Прототип функции:
; extern unsigned int LineCount(char * near StringToCount,
; unsigned int near * CharacterCountPtr);
; Вход:
; char near * StringToCount: указатель на строку, для
; которой должно быть подсчитано число строчек
;
; unsigned int near * CharacterCountptr: указатель на
; переменную типа int, в которую должен быть
; помещен счетчик символов
;
NEWLINE EQU 0ah ;символ перевода строки является
;в Си символом новой строки
DOSSEG
.MODEL SMALL
.CODE
PUBLIC _LineCount
_LineCount PROC
push bp
mov bp,sp
push si ;сохранить переменные-реги-
;стры вызывающей программы,
;если они имеются
mov si,[bp+4] ;SI указывает на строку
sub cx,cx ;обнуление счетчика символов
mov dx,cx ;обнуление счетчика строчек
LineCountLoop:
lodsb ;прием следующего символа
and al,al ;это ноль, оканчивающий
;строку?
jz EndLineCount ;да, подсчет закончен
inc cx ;нет подсчет этого символа
cmp al,NEWLINE ;это новая строчка?
jnz LineCountLoop ;нет, переход к проверке
;следующего символа
inc dx ;да, подсчет этой строчки
jmp LineCountLoop
EndLineCount:
inc dx ;подсчет строчки, в которой
;находится нулевой символ
mov bx,[bp+6] ;указатель на адрес, через
;который должен быть возвра-
;щен счетчик символов
mov [bx],cx ;установка переменной счет-
;чика символов
mov ax,dx ;возврат числа строчек как
;значение самой функции
pop si ;восстановить переменную-
;регистр вызывающей програм-
;мы, если она была
pop bp
ret
_LineCount ENDP
END
Следующий модуль Си, CALLCT.C, является примером программы,
из которой функция LineCount вызывается:
char * TestString="Line 1\nline 2\nline 3";
extern 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);
}
Эти два модуля компилируются и компонуются вместе при помощи
командной строки
tcc -ms callct count.asm
Как было сказано, LineCount будет работать только при
компоновке с программами на Си с моделью памяти small, поскольку в
случае других моделей изменятся размеры указателей и адреса в
стековом фрейме. Ниже приводится версия LineCount, COUNTLG.ASM,
которая может работать с программами на Си с моделью памяти large
(но не с моделью памяти small, если только ей не будут
передаваться дальние указатели и LineCount не будет объявлена как
дальняя процедура):
; Вызываемая из Си ассемблерная функция с моделью памяти
; large, которая подсчитывает число строчек и символов в
; переданной ей строке символов, оканчивающейся нулем.
;
; Прототип функции:
; extern unsigned int LineCount(char * far StringToCount,
; unsigned int * far CharacterCountPtr);
; Вход:
; char far * StringToCount: указатель на строку, для
; которой должно быть подсчитано число строчек
;
; unsigned int far * CharacterCountptr: указатель на
; переменную типа int, в которую должен быть
; помещен счетчик символов
;
NEWLINE EQU 0ah ;символ перевода строки является
;в Си символом новой строки
DOSSEG
.MODEL LARGE
.CODE
PUBLIC _LineCount
_LineCount PROC
push bp
mov bp,sp
push si ;сохранить переменные-реги-
;стры вызывающей программы,
;если они имеются
push ds ;сохранить стандартный
;сегмент данных Си
lds si,[bp+6] ;DS:SI указывает на строку
sub cx,cx ;обнуление счетчика символов
mov dx,cx ;обнуление счетчика строчек
LineCountLoop:
lodsb ;прием следующего символа
and al,al ;это ноль, оканчивающий
;строку?
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.C при помощи
следующей командной строки:
tcc -ml callct countlg.asm
Соглашения Паскаля о связях -----------------------------------
До сих пор вы видели, как Си в нормальном режиме передает
функциям параметры, помещая их справа-налево в стек, вызывая
функцию и уничтожая параметры при выходе из функции. Turbo C имеет
также возможность следовать соглашениям о связях, принятым в
Паскале, когда параметры передаются слеванаправо, и уничтожение
переданных параметров в стеке выполняет при выходе сама вызываемая
программа. В Turbo C соглашения Паскаля о связях выполняются либо
при задании опции командной строки -p, либо при включении
ключевого слова pascal.
Ниже приводится пример ассемблерной функции, использующей
соглашения Паскаля о связях:
;
; Вызывается в виде: 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
ENDP
На рис.7.7 показан стековый фрейм после выполнения команды
MOV BP,SP.
Отметим, что очистки стека от переданных параметров
вызываемая функция использует команду RET 6.
Соглашения Паскаля о связях требуют также, чтобы все внешние
и общие символические имена были набраны заглавными буквами и не
имели ведущего символа подчеркивания. Зачем может понадобиться
использовать соглашения Паскаля о связях в программе на Си? Коды,
использующие соглашения Паскаля о связях, как правило несколько
компактнее и работают быстрее, чем обчные коды Си, поскольку
отсутствует необходимость уничтожать параметры после каждого
вызова при помощи команды ADD SP n.
| |
------------------
SP --> |BP в вызывающей | <-- BP
| программе |
------------------
SP+2 | Адрес возврата | BP+2
------------------
SP+4 | k | BP+4
------------------
SP+6 | j | BP+6
------------------
SP+8 | i | BP+8
------------------
| |
------------------
| |
Рис.7.7 Состояние стека непосредственно после выполнения
команды MOV BP,SP
Вызов Turbo C из Turbo Assembler
-----------------------------------------------------------------
Хотя наибольшее распространение получили обращения из
программы на Си к ассемблерным функциям, выполняющим
специализированные задачи, иногда вам может понадобиться вызвать
из ассемблера функции, написанные на Си. Оказывается, фактически
легче вызвать функцию на Turbo C из функции на Turbo Assembler,
нежели наоборот, поскольку со стороны ассемблерного кода не
требуется никакой обработки стековых фреймов. Кратко рассмотрим
требования, имеющиеся при вызове из ассемблера функций на Turbo C.
Компоновка со стартовыми кодами Си ----------------------------
Можно взять за общее правило вызывать из ассемблера
библиотечные функции Turbo C только в таких программах, где первым
из компонуемых модулей будет являться стартовый модуль на Си. В
этот "безопасный" класс входят все программы, которые компонуются
командной строкой, запускающей TC.EXE или TCC.EXE, а также
программы, прямо компонуемые TLINK таким образом, что первым
компонуемым файлом является файл C0T, C0S, C0C, C0M, C0L или C0H.
Как правило, не следует вызывать библиотечные функции Turbo C
из программ, в которых не компонуется стартовый модуль Си,
поскольку при отсутствии стартовых кодов Си некоторые библиотечные
функции не смогут работать правильно. Если вы действительно хотите
вызывать библиотечные функции Turbo C из таких программ, то мы
советуем вам ознакомиться с исходными стартовыми кодами Си (файл
C0.ASM, находящийся на дистрибутивной дискете с Turbo C) и
приобрести у Borland исходные тексты библиотек Си, и тогда вы
будете уверены, что инициализация нужных вам библиотечных функций
выполнится правильно. Другой возможный подход состоит в том, чтобы
просто скомпоновать каждую желаемую библиотечную функцию с
ассемблерной программой, например с X.ASM, в которой не
выполнялось бы никаких других действий, кроме вызова каждой из
этих функций; для этого компоновщик можно запустить командной
строкой типа:
tlink x,x,,cm.lib
где m - это первая буква названия желаемой модели памяти (t
для tiny, s для small и т.д.) Если TLINK выдает сообщения о
каких-либо неопределенных символических именах, то такая
библиотечная функция не может быть вызвана без компоновки в
программу стартовых кодов Си.
Примечание: вызов функций Си, определяемых пользователем,
которые в свою очередь вызывают библиотечные функции Си, попадают
в ту же категорию, что и при вызове библиотечных функций
непосредственно; отсутствие стартовых кодов Си потениально может
вызвать проблемы для любой ассемблерной программы, вызывающей
библиотечные функции, прямо или косвенно.
Убедитесь в правильности сегментных установок -----------------
Как мы узнали ранее, сперва необходимо убедиться, что Turbo C
и Turbo Assembler имеют одну и ту же модель памяти, и что
сегменты, используемые в Turbo Assembler, соответствуют
используемым в Turbo C. Если вам необходимо освежить в памяти
сведения о согласовании используемых моделей памяти и сегментов,
см. предыдущий раздел, "Вопросы интерфейса Turbo C и Turbo
Assembler" . Кроме того, не забывайте, что для дальних
символических имен требуется помещать соответствующие директивы
EXTRN, либо вне всех сегментов, либо внутри правильного сегмента.
Выполнение вызова --------------------------------------------
В разделе "Вызов функций на Turbo Assembler из Turbo C" мы
уже изучили, каким образом Turbo C подготавливает и выполняет
вызов функции. Теперь кратко рассмотрим механизм вызова функций на
Си, на этот раз с точки зрения их вызова из Turbo Assembler.
Все, что необходимо сделать для передачи параметров в функцию
на Turbo C, это поместить первым в стек самый правый параметр и
т.д., до тех пор, пока в стеке не окажутся все параметры. После
этого можно просто вызвать функцию. Например, программируя на
Turbo C, вы можете вызвать библиотечную функцию Turbo C strcpy для
копирования SourceString в DestString, записав:
strcpy(DestString, SourceString);
Для выполнения же такого вызова на ассемблере следует
записать:
lea ax,SourceString ;самый правый параметр
push ax
lea ax,DestString ;самый левый параметр
push ax
call _strcpy ;копирование строки
add sp,4 ;уничтожение параметров в стеке
Не забывайте о том, что после вызова необходимо уничтожить
параметры в стеке, изменив соответствующим образом SP.
Можно упростить ваш код и сделать его независимым от языка,
воспользовавшись расширением команды Turbo Assembler CALL:
call назначение [язык [,арг1] ...]
где язык - это C, PASCAL, BASIC, FORTRAN, PROLOG или NO LANGUAGE,
а "арг" это любой допустимый аргумент программы, который может
быть прямо помещен в стек процессора.
Используя данное средство, можно записать:
lea ax,SourceString
lea bx,DestString
call strcpy c,bx,ax
Turbo Assembler автоматически вставит команды помещения
аргументов в стек в последовательности, принятой в Си (сначала AX,
затем BX), выполнит вызов _strcopy (перед именами Си Turbo
Assembler автоматически вставляет символ подчеркивания), и очищает
стек после вызова.
Если вы вызываете функцию Си, использующую соглашения Паскаля
о связях, параметры следует поместить в стек слева-направо и после
вызова не трогать SP:
lea ax,DestString ;самый левый параметр
lea ax,SourceString ;самый правый параметр
push bx
push ax
call STRCPY ;копирование строки
;стек остается без изменений
Можно опять упростить ваш код, воспользовавшись расширением
команды Turbo Assembler CALL:
lea bx,DestString ;самый левый параметр
lea ax,SourceString ;самый правый параметр
call strcpy pascal,bx,ax
Turbo Assembler автоматически вставит команды помещения
аргументов в стек в последовательности, принятой в Паскале
(сначала BX, затем AX), и выполнит вызов STRCPY (преобразовывая
имя к верхн ему регистру, как принято в соглашениях Паскаля).
Безусловно, в последнем примере предполагается, что вы
рекомпилировали strcpy с ключом -p, поскольку версия strcpy в
стандартной библиотеке использует соглашения о связях не Паскаля,
а Си. Функции на Си выполняют возврат значений, как описано в
разделе "Возврат значений"; 8- и 16-битовые значения через AX,
32-битовые значения через DX:AX, значения с плавающей точкой через
регистр 8087 TOS, а структуры различными способами, в зависимости
от их размера.
Функции на Си сами сохраняют следующие (и только эти)
регистры: SI, DI, BP, DS, SS, SP и CS. Регистры Ax, BX, CX, DX, ES
и флаги могут быть изменены функциями произвольным образом.
Вызов функции на Turbo C из Turbo Assembler -------------------
Одной из причин, когда вам может понадобиться вызвать функцию
на Turbo C из Turbo Assembler, является необходимость выполнить
сложные вычисления, поскольку реализовать их на Си значительно
легче, нежели на ассемблере. Это особенно важно в случае смешанных
целочисленных вычислениях и вычислениях с плавающей точкой; хотя,
конечно, можно реализовать их и на ассемблере, гораздо проще
предоставить Си все действия по преобразованиям типов данных и
выполнению арифметических операций с плавающей точкой.
Рассмотрим пример ассемблерной программы, которая вызывает
функцию на Turbo C для выполнения вычислений с плавающей точкой.
На самом деле в этом примере функция на Turbo C передает в функцию
на Turbo Assembler последовательность целых чисел, где они
суммируются и оттуда же происходит вызов другой функции на Turbo
C, где выполняются вычисления с плавающей точкой, с помощью
которых получается среднее значение переданной последовательности
целых чисел.
Часть программы на Си в файле CALCAVG.C имеет вид:
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 Divident, int Divisor)
{
return( (float) Divident / (float) Divisor );
}
а ассемблерная часть программы в файле AVERAGE.ASM имеет вид:
;
;Вызываемая из Turbo C функция с моделью памяти small,
;возвращающая среднее значение последовательности целочис-
;ленных значений. Вызывает в свою очередь функцию на
;Turbo C 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 bx,2 ;установить указатель
;на следующее значение
loop AverageLoop
push WORD PTR [bp+8];
;передать в IntDivide количество
;значений, как самый правый
;параметр
push ax ;передать итог, как самый левый
;параметр
call _IntDivide ;вычислить среднее значение с
;плавающей точкой
add sp,4 ;уничтожить параметры в стеке
pop bp
ret ;среднее значение находится в
;регистре 8087 TOS
_Average ENDP
END
Функция Си main передает указатель массива целых чисел
TestValues и длину этого массива в ассемблерную функцию Average.
Average суммирует целые числа и передает вычисленную сумму вместе
с количеством значений в функцию Си IntDivide. IntDivide переводит
сумму и количество значений в форму с плавающей точкой и вычисляет
среднее значение; все это на Си занимает одну строку программы,
тогда как на ассемблере заняло бы несколько. IntDivide возвращает
среднее значение в Average через регистр 8087 TOS, а Average
просто оставляет это значение в TOS и выполняет возврат в main.
CALCAVG.C и AVERAGE.ASM можно скомпилировать и скомпоновать в
одну выполняемую программу CALCAVG.EXE командой
tcc calcavg average.asm
Отметим, что Average может работать с моделями данных как
small, так и large, без необходимости в изменениях программы,
поскольку в обеих этих моделях передается дальний указатель. Для
того, чтобы функция поддерживала работу с большими моделями памяти
для кодов (huge, large и medium), нужно просто использовать
соответствующую директиву .MODEL.
Пользуясь преимуществами расширений, обеспечивающих
независимость Turbo Assembler от языка, ассемблерный код из
предыдущего примера можно записать более сжато:
DOSSEG
.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