46019 (665326), страница 36
Текст из файла (страница 36)
И наконец, getmodename и getdrivername возвращаютимя заданного режима драйвера и имя текущего графического драйвера, соответственно.
Глава 6
Интерфейс с языком ассемблера
В данной главе рассказывается, как написать ассемблерный код, который будет хорошо работать с Turbo C++. Предполагается, что вы знаете, как пишутсяподпрограммы на языке ассемблера икак определяются сегменты, константы данных и т. д. Если вы не знакомы с этими концепциями, почитайте руководство по Turbo Assembler, особенно главу "Интерфейс Turbo Assembler с Turbo C" в Руководстве пользователя. TurboAssembler версии 2.0включает несколько средств, делающих интерфейс с Turbo C++ более простым и прозрачным для программиста.
Смешанное программирование
Turbo C++ упрощает вызов из С-программ подпрограмм, написанных на языке ассемблера, и наоборот, вызов из ассемблерных программ подпрограмм на С. В данном разделе показано, насколько простинтерфейс между Turbo C++ и ассемблером; также приводится информация, помогающая на практике осуществить такой интерфейс.
Последовательности передачи параметров
Turbo C++ поддерживаетдва метода передачи параметров функции. Один из них является стандартным методом С, который мы рассмотрим первым; второй метод заимствован из Паскаля.
Последовательность передачи параметров в С
Предположим, вы объявили следующий прототип функции:
void funca(int p1, int p2, long p3);
По умолчанию Turbo C++ использует последовательность передачи параметров С, которая также называется соглашением о связях С. При вызове этой функции(funca) параметры помещаютсяв стек в последовательности справа-налево (p3,p2,p1), послечего в стек помещается адрес возврата. Таким образом, в случае вызова
main()
(*
int i,j;
long k;
...
i = 5; j = 7; k = 0x1407AA;
funca(i,j,k);
...
*)
то стек (непосредственно перед помещением в него адреса возврата) будет выглядеть следующим образом:
sp + 06: 0014
sp + 04: 07AA k = p3
sp + 02: 0007 j = p2
sp: 0005 i = p1
Вызываемой подпрограмме не требуется точно знать, сколько параметров помещено в стек. Она просто предполагает, что все нужные ей параметры находятся в стеке.
Кроме того - что очень важно - вызываемая подпрограмма не должна снимать параметры со стека. Почему? Дело в том, что это сделает вызывающая программа. Например, приведенная выше функция в ассемблерном виде, получаемом компилятором из исходного кода на С, будет выглядеть следующим образом:
mov WORD PTR [bp-8],5;установка i =5
mov WORD PTR [bp-6],7;установка j = 7
mov WORD PTR [bp-2],0014h;установка k = 0x1407AA
mov WORD PTR [bp-4],07AAh
push WORD PTR [bp-2];помещение в стек старшего слова k
push WORD PTR [bp-4];помещение в стек младшего слова k
push WORD PTR [bp-6];помещение в стек j
push WORD PTR [bp-8];помещение в стек i
call NEAR PTR funca ;вызов funca (помещение в стек
;адреса возврата)
add sp,8;настройка стека
Обратите внимание на последнюю команду, add sp,8. К этому моменту компилятору известно, сколько параметров было помещено в стек; компилятор также знает, что адрес возврата был помещен в стек при вызове funca и уже был снят оттуда командой ret в конце funca.
Последовательность передачи параметров Паскаля
Другим методом передачи параметров является стандартный метод передачи параметров Паскаля (называемый также соглашением о связях Паскаля). Это не значит, что вы можете вызывать из TurboC++ функции Turbo Pascal. Это невозможно. Если funca объявлена как
void pascal funca(int p1, int p2, long p3);
то при вызове этой функции параметры помещаются в стек в последовательности слева-направо (p1,p2,p3), после чего в стек помещается адрес возврата. Таким образом, при вызове:
main()
(*
int i,j;
long k;
...
i = 5; j = 7; k = 0x1407AA;
funca(i,j,k);
...
*)
то стек (непосредственно перед помещением в него адреса возврата) будет выглядеть следующим образом:
sp + 06: 0005 i = p1
sp + 04: 0007 j = p2
sp + 02: 0014
sp: 07AA k = p3
Итак, в чем здесь различие? Дело в том, что помимо изменения очередности помещения параметров в стек, последовательность передачи параметров Паскаля предполагает, что вызываемая функция (funca) знает, сколько параметров будет ей передано и соответственно настраиваетстек. Другимисловами, теперь в ассемблированном виде данная функция будет иметь вид:
push WORD PTR [bp-8];помещение в стек i
push WORD PTR [bp-6];помещение в стек j
push WORD PTR [bp-2];помещение в стек старшего слова k
push WORD PTR [bp-4];помещение в стек младшего слова k
call NEAR PTR funca ;вызов funca (помещение в стек
;адреса возврата)
Отметим, что теперь после вызова отсутствует командаadd sp,8. Вместо нее funca использует при окончании команду ret 8, при помощи которой очищает стек перед возвратом к main.
По умолчанию все функции, создаваемые в Turbo C++, используют способ передачи параметров С. Исключение делается при наличии опциикомпилятора -p (опция Pascal в диалоговомполе Code Generation); в этом случае все функции используют метод передачи параметров Паскаля. Тем не менее, вы можете задать для любой функции метод передачи параметров С при помощи модификатора cdecl:
void cdeclfunca(int p1, int p2, long p3);
Данное объявление переопределит директиву компилятора - p.
И однако, почему может возникнуть необходимость использовать соглашение о связях Паскаля вообще? Для этого есть две главных причины.
- Вам может понадобиться вызывать существующие подпрограммы на языке ассемблера, использующие соглашение о связях Паскаля.
- Получаемый в таком случае код несколько меньше по размеру, поскольку в этом случае не требуется в конце выполнять очистку стека.
Однако, использование соглашения о связях Паскаля может вызвать некоторые проблемы.
Прежде всего, соглашение о связях Паскаля дает меньше возможностей,чем для С. Вы не можете передавать переменное число параметров (как этодопускается всоглашении С), поскольку вызываемая подпрограмма должна знать число передаваемых ей параметров и соответственнымобразом настроить стек. Передача большего или меньшего числа параметроввызывает серьезные проблемы, тогда как в случае соглашения С ничего особенного в такихслучаях не происходит (кроме, возможно, того, что программа даст неправильный ответ).
Во-вторых, при использовании опции компилятора вы обязательно включить файлы заголовка длявсех вызываемых вашей программой стандартных функций С. Почему? Дело в том, что в противном случае Turbo C++ будет использовать для каждой из этих функций соглашение о связях (и именах) - и ваша программа не будет компоноваться.
В файле заголовка каждая из этих функций объявлена как cdecl, поэтому включение файловзаголовка заставит компилятор использовать для этих функций соглашение С.
Резюме:если вы собираетесь использовать вС-программе соглашение освязяхПаскаля, не забывайте о необходимости использовать прототипы функций везде, где это возможно, а каждую функцию явно объявляйте pascal или cdecl. Полезно также разрешить выдачу сообщения "Function call with no prototype" ("вызов функции без прототипа"), чтобы гарантировать наличие прототипов всех функций.
Подготовка к вызову .ASM из Turbo C++
При написании подпрограмм на языке ассемблера нужно принимать во внимание определенные соглашения для того, чтобы (1) обеспечить компоновщик нужной ему информацией и (2) обеспечить соответствие формата файла и модели памяти, используемой в программе на С. Упрощенные сегментные директивы
Обычно модули на языке ассемблера состоят из трех разделов: кода, инициализированных данных и неинициализированных данных. Каждый из этих типов информации организован в отдельный сегмент с использованием определенных имен, которые зависят от используемой в вашей С-программе модели памяти.
Turbo Assembler (TASM) предлагает вам три упрощенных сегментных директивы (.CODE, .DATA и .DATA?), которыемогут быть использованыпри определении этих сегментов. Они говорят компилятору о необходимостииспользовать имена сегментов по умолчанию для модели памяти, заданной вами при помощи директивы .MODEL. Например, если ваша программа на С использует модель памяти small, вы можете организовать каждый ассемблерный модуль с упрощенными сегментными директивами,как показано в следующей таблице:
-----------------------------------------------------------
.MODEL SMALL
.CODE
...кодовый сегмент...
.DATA
...сегмент инициализированных данных...
.DATA?
...сегмент неинициализированных данных...
Стандартные сегментные директивы
В некоторых случаях вам может понадобиться использовать другие имена сегментов, нежели те, что являются умолчаниямидля данной модели памяти. Для этого вы должны использовать стандартные сегментные директивы, как показано в Таблице 6.1.
Формат файла языка ассемблера Таблица 6.1
code SEGMENT BYTE PUBLIC 'CODE'
ASSUME CS:code, DS:dseg
...........кодовый сегмент.............
code ENDS
dseg GROUP _DATA,_BSS
data SEGMENT WORD PUBLIC 'DATA'
...инициализированный сегмент данных...
data ENDS
_BSS SEGMENT WORD PUBLIC 'BSS'
...неинициализированный сегмент данных...
_BSS ENDS
END
Идентификаторы code, data и dseg в данном макете имеют специальные заменители, зависящие от используемой модели памяти; в таблице 6.2 показано, какое имя должно использоваться для тойили иной модели. имя_файла в Таблице 6.2 - это имя модуля: оно должно быть тем же в директиве NAME и при заменах идентификаторов.
Отметим,что вслучаемоделипамятиhuge сегмент _BSS отсутствует, а определение GROUP опускается полностью. В целом, _BSS представляет собой опцию; определение ее необходимо только в случае использования.
Лучший способ создания "заготовки" для будущей ассемблерной программы состоит в том, чтобы скомпилировать пустую программу в .ASM-файл (при помощи опции TCC -S) и затем изучить сгенерированный таким образом ассемблерный код.
Замены идентификаторов и модели памяти Таблица 6.2
Модель | Замены идентификатораУказатели кода | и | данных |
Tiny,Small data dseg | code = _TEXTКод:DW _TEXT:xxx _DATAДанные: DW DGROUP:xxx DGROUP | ||
Compact data dseg | code = _TEXTКод:DW _TEXT:xxx _DATAДанные: DD DGROUP:xxx DGROUP | ||
Medium data dseg | code = имя_файла_TEXTКод:DD:xxx _DATAДанные: DW DGROUP:xxx DGROUP | ||
Large data dseg | code = имя_файла_TEXTКод:DD:xxx _DATAДанные: DD DGROUP:xxx DGROUP |
Huge code = имя_файла_TEXTКод:DD:xxx
data = имя_файла_DATAДанные: DD:xxx
Определение данных - констант и переменных
Модели памяти также влияют на то, каким образом вы определяете любые константы, являющиеся указателями кода, данных, либо того и другого. В таблице 6.2 показано, как должны выглядеть эти указатели, причем xxx - это адрес, на который устанавливается указатель.
Некоторые определения используют DW (определение слова), а некоторые - DD (определение двойного слова), что означает размер результирующего указателя. Числовые и текстовые константы определяются нормальным образом.
Переменные, разумеется, определяются так же, как и константы. Если вам нужны переменные,не инициализированныеконкретными значениями, вы можете объявить ихв сегменте _BSS, введя вопросительный знак(?) втом месте, где обычно находится значение.
Определение глобальных и внешних идентификаторов
После того, как вы создали модуль, вашей программе на Turbo
C++ требуется знать, какие функции она может вызывать и на
какие переменные ссылаться. Аналогичным образом, вам может
потребоваться иметь возможность вызывать функции Turbo C++из
подпрограмм на языке ассемблера, либо ссылаться оттуда напе-
ременные, определенные в программе на Turbo C++.
При выполнении таких вызовов вы должны хорошо представлять себе работу компилятора и компоновщика Turbo C++. При объявлении внешнего идентификатора компилятор автоматически добавляет к этому именисимволподчеркивания (_), прежде чем сохранить его в объектном модуле. Это означает, что вы должны поместить символ подчеркивания перед любыми идентификаторами вашего модуля на языке ассемблера, на которые вы хотите ссылаться из С-программы. Идентификаторы Паскаля обрабатываются иначе, чем идентификаторы С, - они состоят только из заглавных символов не имеют ведущего символа подчеркивания.
Символы подчеркивания в идентификаторах С необязательны, но по умолчаниюони помещаются передними. Их можно отменить при помощи командной строки -u-. Однако, при использовании стандартных библиотек Turbo C++ вы в таком случае столкнетесь с проблемами, и вам придется переделывать эти библиотеки. (Для этоговам понадобится другой продукт Turbo C++ - исходные тексты библиотек исполняющей системы;в этом случае за дополнительной информацией обращайтесь на фирму Borland).
Если какой-либоasm-код в исходном файле ссылается на идентификаторы С (данные или функции), эти идентификаторы должны начинаться знаком подчеркивания (если вы не используете один из описанных выше спецификаторов языка).
Turbo Assembler (TASM) не учитываетрегистры, которыми набраны символы идентификаторов; другими словами,при ассемблировании программы все идентификаторы записываются только заглавными буквами. Опция TASM /mx устанавливает учет регистра для общих и внешних имен. Компоновщик Turbo C++ также записывает идентификаторы extern заглавными буквами, поэтому тут все должно работать. В наших примерах ключевые слова и директивы записываются заглавными буквами, а все прочие идентификаторы и коды операций строчными; это соответствует стилю имен в справочном руководстве по TASM.Вам предоставляется свобода любых комбинаций заглавных и строчных букв в идентификаторах, по вашему усмотрению.