Написание на языке Ассемблера функций-элементов С++
Хотя можно написать функцию-элемент класса С++ целиком на языке Ассемблера, это далеко не просто. Например, все функции-элементы классов С++ имеют "откорректированные" имена, что обеспечивает безопасную по согласованности типов компоновку функций и делает возможным переопределение функций, а ваша ассемблерная функция должна знать в точности, какое имя С++ ожидает для данной функции-элемента. Для доступа к переменным-элементам вы должны подготовить в ассемблерном коде определение STRUC, определяющее все переменные-элементы с точно совпадающими размерами и расположением. Если ваш класс является производным, то могут существовать и другие переменные-элементы, производные от базового класса. Даже если класс не является производным (порожденным), то расположение переменных-элементов в памяти изменяется в случае, если класс этот включает в себя какие-либо виртуальные функции.
Если вы пишете функцию на встроенном Ассемблере, Borland С++ может взять на себя эти вопросы. Однако если вы работаете на языке Ассемблера отдельно (например, переделываете уже имеющийся код), то существуют некоторые методы, позволяющие упростить эту работу.
Создайте определение фиктивной функции С++ для ассемблерной функции. Это определение удовлетворит компоновщик, так как будет содержать откорректированное имя функции-элемента. Эта фиктивная функция будет вызывать ассемблерную функцию и передавать ей переменные-элементы и прочие параметры. Так как ассемблерный код будет иметь все нужные ему параметры посредством аргументов, вы можете не заботиться об изменениях в определении класса. Ваша ассемблерная функция может быть описана в коде С++ как extern "C", что показано в примерах. Например (countadd.cpp):
class count_add { // Частные переменные-элементы (private) int access_count; // число обращений int count; // текущий счетчик public: count_add(void) { access_count=0; count=0; } int get_count (void) {return Count;}
// Две функции, которые будут фактически написаны на // Ассемблере:
void increment(void); void add(int what_to_add=-1); // Отметим, что умолчание влияет только // на вызовы add; оно не влияет на код add }
extern "C" { // Для создания уникальных и осмысленных имен // ассемблерных подпрограмм прибавим имя класса к // имени ассемблерной подпрограммы. В отличие от прочих // ассемблеров, Турбо Ассемблер не имеет проблем с // длиной имен. void count_add_increment(int *count); // Мы передадим указатель на переменную count. // Ассемблер выполнит увеличение. void count_add_add(int *count,int what_to_add); }
void count_add::increment(void) { count_add_increment(&count); }
void count_add(int what_to_add) { count_add(&count, int what_to_add); }
Ваш ассемблерный модуль, определяющий подпрограммы count_add _increment и count_add_add, должен иметь вид (COUNTADD.ASM):
.MODEL small ; выбор модели small (ближние код и данные) .CODE PUBLIC _count_add_increment _count_add_increment PROC ARG count_offset:word ; Адрес переменной-элемента push bp ; Сохранение записи активации вызывающей программы mov bp,sp ; Установка собственной записи активации mov bx,[count_offset] ; Загрузка указателя inc word ptr [bx] ; Увеличение переменной-элемента pop bp ; Восстановление записи активации вызывающей программы _count_add_increment ENDP
PUBLIC _count_add_add _count_add_add PROC ARG count_offset:word,what_to_add:word push bp mov bp,sp mov bx,[count_offset] ; Загрузка указателя mov ax,[what_to_add] add [bx],ax pop bp ret _count_add_add ENDP
end
Используя данный метод, вы можете не беспокоиться об изменениях в определении класса. Даже если вы добавляете или удаляете переменные-элементы, делаете этот класс производным или добавляете виртуальные функции, вам не требуется изменять ассемблерный модуль. Переассемблировать модуль нужно только в случае изменения структуры переменной-элемента count, либо если вы ходите сделать версию данного класса для модели памяти large. Переассемблирование в этих случаях необходимо, поскольку при обращении к переменной-элементу count вы имеете дело с сегментом и смещением.
Соглашения по вызовам, использующиеся в Паскале
Итак, теперь вы уже знаете, как обычно в С++ передаются параметры функциям: вызывающая программа заносит параметры (справа налево) в стек, вызывает функцию, и извлекает параметры из стека (отбрасывает их) после вызова. Borland C++ может также работать по соглашениям, принятым в Паскале. Согласно этим соглашениям параметры передаются слева направо, а отбрасывает параметры (из стека) вызываемая программа. Разрешить использование соглашений Паскаля в Borland C++ можно с помощью параметра командной строки -p или ключевого слова pascal.
Примечание: Более подробно соглашения о связях Паскаля рассматриваются в этом разделе.
Приведем пример функции на Ассемблере, в которой используются соглашения Паскаля:
; Вызывается, как: TEST(i, j ,k) ; i equ 8 ; левый параметр j equ 6 k equ 4 ; правый параметр ; .MODEL SMALL .CODE PUBLIC TEST TEST PROC push bp mov bp,sp mov ax,[bp+i] ; получить i add ax,[bp+j] ; прибавить к i j sub ax,[bp+k] ; вычесть из суммы k pop bp ret 6 ; возврат, отбросить 6 байт параметров (очистка стека) TEST ENDP END
Заметим, что для очистки стека от передаваемых параметров используется инструкция RET 6.
На Рисунке показано состояние стека после выполнения инструкции MOV BP,SP:
Соглашения по вызовам Паскаля требуют также, чтобы все внешние и общедоступные идентификаторы указывались в верхнем регистре и без предшествующих подчеркиваний. Зачем может потребоваться использовать в программе на С++ соглашения по вызовам Паскаля? Программа, использующая соглашения Паскаля, занимает обычно несколько меньше места в памяти и работает быстрее, чем обычная программа на языке С++, так как для очистки стека от параметров не требуется выполнять n инструкций ADD SP.
Вызов Borland C++ из Турбо Ассемблера
Хотя больше принято для выполнения специальных задач вызывать из С++ функции, написанные на Ассемблере, иногда вам может потребоваться вызывать из Ассемблера функции, написанные на языке С++. Оказывается, на самом деле легче вызвать функцию Borland C++ из функции Турбо Ассемблера, чем наоборот, поскольку со стороны Ассемблера не требуется отслеживать границы стека. Давайте рассмотрим кратко требования для вызова функций Borland C++ из Турбо Ассемблера.
Компоновка с кодом инициализации С++
Хорошим правилом является вызов библиотечных функций Borland C++ только из Ассемблера в программах, которые компонуются с модулем инициализации С++ (используя его в качестве первого компонуемого модуля). Этот "надежный" класс включает в себя все программы, которые компонуются с помощью командной строки TC.EXE или TCC.EXE, и программы, в качестве первого компонуемого файла которых используется файл C0T, C0S, C0C, C0M, C0L или C0H.
В общем случае вам не следует вызывать библиотечные функции Borland C++ из программ, которые не компонуются с модулем инициализации Borland C++, так как некоторые библиотечные функции Borland C++ не будут правильно работать, если не выполнялась компоновка с кодом инициализации. Если вы действительно хотите вызывать библиотечные функции Borland C++ из таких программ, мы предлагаем вам взглянуть на код инициализации (файл C0.ASM на дистрибутивных дисках Borland C++) и приобрести у фирмы Borland исходный код библиотеки языка С++, после чего вы сможете обеспечить правильную инициализацию для нужных библиотечных функций.
Вызов определяемых пользователем функций С++, которые в свою очередь вызывают библиотечные функции языка С++, попадают в ту же категорию, что и непосредственный вызов библиотечных функций С++. Отсутствие кода инициализации С++ может вызывать ошибки в любой программе Ассемблера, которая прямо или косвенно обращается к библиотечным функциям С++.
Задание сегмента
Как мы уже говорили ранее, необходимо обеспечивать, чтобы Borland C++ и Турбо Ассемблер использовали одну и ту же модель памяти, и чтобы сегменты, которые вы используете в Турбо Ассемблере, совпадали с теми сегментами, которые использует Borland C++. В Турбо Ассемблере имеется модель памяти tchuge,которая поддерживает модель huge Borland C++. Перечень моделей памяти и сегментов можно найти в предыдущем разделе. Нужно не забывать также помещать директиву EXTRN для внешних идентификаторов вне всех сегментов или внутри правильного сегмента.
1 2 3 4 5 6 7 8 9
8 8 8
| |