Существуют следующие формы комбинирования программ на языках высокого уровня с ассемблером:
- Использование ассемблерных вставок (встроенный ассемблер, режим inline). Ассемблерные коды в виде команд ассемблера вставляются в текст программы на языке высокого уровня. Компилятор языка распознает их как команды ассемблера и без изменений включает в формируемый им объектный код. Эта форма удобна, если надо вставить небольшой фрагмент.
- Использование внешних процедур и функций. Это более универсальная форма комбинирования. У нее есть ряд преимуществ:
— написание и отладку программ можно производить независимо;
— написанные подпрограммы можно использовать в других проектах;
— облегчаются модификация и сопровождение подпрограмм.
Встроенный ассемблер
При написании ассемблерных вставок используется следующий синтаксис:
КодОперации задает команду ассемблера,
операнды – это операнды команды.
В конце записывается ;, как и в любой команде языка Си.
Комментарии записываются в той форме, которая принята для языка Си.
Если требуется в текст программы на языке Си вставить несколько идущих подряд команд ассемблера, то их объединяют в блок:
{
текст программы на ассемблере ; комментарии
}
Внутри блока текст программы пишется с использованием синтаксиса ассемблера, при необходимости можно использовать метки и идентификаторы. Комментарии в этом случае можно записывать как после ;, так и после //.
Пример Даны целые числа а и b. Вычислить выражение a+5b.
Для вывода приглашений Введите a: и Введите b: используем функцию CharToOem(_T(«Введите «),s),
где s – указатель на строку, которая перекодирует русскоязычные сообщения.
#include <windows.h>
#include <tchar.h>
void main()
{
}
Для компоновки и запуска программы создаем проект как описано в разделе Создание консольных приложений.
Проект будет содержать 1 файл исходного кода с расширением cpp.
Результат выполнения программы:
Использование внешних процедур
Для связи посредством внешних процедур создается многофайловая программа. При этом в общем случае возможны два варианта вызова:
- программа на языке высокого уровня вызывает процедуру на языке ассемблера;
- программа на языке ассемблера вызывает процедуру на языке высокого уровня.
Рассмотрим более подробно первый вариант.
В таблице ниже представлены основные соглашения по передаче параметров в процедуру.
В программах, написанных на языке ассемблера, используется соглашение передачи параметров stdcall. Однако по сути получение и передача параметров в языке ассемблера производится явно, без помощи транслятора.
При связи процедуры, написанной на языке ассемблера, с языком высокого уровня, необходимо учитывать соглашение по передаче параметров.
Соглашение | Параметры | Очистка стека | Регистры |
Pascal (конвенция языка Паскаль) | Слева направо | Процедура | Нет |
C (конвенция С) | Справа налево | Вызывающая программа | Нет |
Fastcall (быстрый или регистровый вызов) | Слева направо | Процедура | Задействованы три регистра (EAX,EDX,ECX), далее стек |
Stdcall (стандартный вызов) | Справа налево | Процедура | Нет |
Конвенция Pascal заключается в том, что параметры из программы на языке высокого уровня передаются в стеке и возвращаются в регистре АХ/ЕАХ, — это способ, принятый в языке PASCAL (а также в BASIC, FORTRAN, ADA, OBERON, MODULA2), — просто поместить параметры в стек в естественном порядке. В этом случае запись
запишется как
push b
push с
push d
call some_proc@16
Процедура some_proc, во-первых, должна очистить стек по окончании работы (например, командой ret 16) и, во-вторых, параметры, переданные ей, находятся в стеке в обратном порядке:
push ebp
mov ebp,esp ; пролог
mov eax, [ebp+20] ; a
mov ebx, [ebp+16] ; b
mov ecx, [ebp+12] ; c
mov edx, [ebp+8] ; d
…
pop ebp ; эпилог
ret 16
some_proc endp
Этот код в точности соответствует полной форме директивы proc.
Однако можно использовать упрощенную форму, которую поддерживают все современные ассемблеры:
…
ret
some_proc endp
Главный недостаток этого подхода — сложность создания функции с изменяемым числом параметров, аналогичных функции языка С printf. Чтобы определить число параметров, переданных printf, процедура должна сначала прочитать первый параметр, но она не знает его расположения в стеке. Эту проблему решает подход, используемый в С, где параметры передаются в обратном порядке.
Конвенция С используется, в первую очередь, в языках С и C++, а также в PROLOG и других. Параметры помещаются в стек в обратном порядке, и, в противоположность PASCAL-конвенции, удаление параметров из стека выполняет вызывающая процедура.
Запись some_proc(a,b,c,d)
будет выглядеть как
push с
push b
push a
call some_proc@16
add esp,16 ; освободить стек
Вызванная таким образом процедура может инициализироваться так:
push ebp
mov ebp,esp ; пролог
mov eax, [ebp+8] ; a
mov ebx, [ebp+12] ; b
mov ecx, [ebp+16] ; c
mov edx, [ebp+20] ; d
…
pop ebp
ret
some_proc endp
Трансляторы ассемблера поддерживают и такой формат вызова при помощи полной формы директивы proc с указанием языка С:
…
ret
some_proc endp
Регистр EВР используется для хранения параметров, и его нельзя изменять программно при использовании упрощенной формы директивы proc.
Преимущество по сравнению с PASCAL-конвенцией заключается в том, что освобождение стека от параметров в конвенции С возлагается на вызывающую процедуру, что позволяет лучше оптимизировать код программы. Например, если необходимо вызвать несколько функций, принимающих одни и те же параметры подряд, можно не заполнять стек каждый раз заново, и это — одна из причин, по которой компиляторы с языка С создают более компактный и быстрый код по сравнению с компиляторами с других языков.
Смешанные конвенции
Существует конвенция передачи параметров STDCALL, отличающаяся и от C, и от PASCAL-конвенций, которая применяется для всех системных функций Win32 API. Здесь параметры помещаются в стек в обратном порядке, как в С, но процедуры должны очищать стек сами, как в PASCAL.
Еще одно отличие от С-конвенции – это быстрое или регистровое соглашение FASTCALL. В этом случае параметры в функции также передаются по возможности через регистры. Например, при вызове функции с шестью параметрами
первые три параметра передаются соответственно в ЕАХ, EDX, ЕСХ, а только начиная с четвертого, параметры помещают в стек в обычном обратном порядке:
mov e, [ebp+12]
mov f, [ebp+16]
В случае если стек был задействован, освобождение его возлагается на вызываемую процедуру.
В случае быстрого вызова транслятор Си добавляет к имени значок @ спереди, что искажает имена при обращении к ним в ассемблерном модуле.
Возврат результата из процедуры
Чтобы возвратить результат в программу на С из процедуры на ассемблере, перед возвратом управления в вызываемой процедуре (на языке ассемблера) необходимо поместить результат в соответствующий регистр:
Тип возвращаемого значения | Регистр |
unsigned char | al |
char | al |
unsigned short | ax |
short | ax |
unsigned int | eax |
int | eax |
unsigned long int | edx:eax |
long int | edx:eax |
Пример Умножить на 2 первый элемент массива (нумерация элементов ведется с 0).
using namespace std;
extern «C» int MAS_FUNC (int *, int);
int main() {
}
.586
.MODEL FLAT, C
.CODE
MAS_FUNC PROC C mas:dword, n:dword
mov esi,mas
mov eax, [esi+4]
shl eax, 1
ret
MAS_FUNC ENDP
END
Чтобы построить проект в Microsoft Visual Studio Express 2010, совместив в нем файлы, написанные на разных языках программирования, выполняем следующие действия.
Создание проекта начинается с выбора меню Файл -> Создать -> Проект.
Аналогично проекту, содержащему 1 язык программирования, создаем пустой проект консольного приложения и задаем имя проекта.
Добавляем в дерево проекта два файла исходного кода:
- вызывающая процедура на языке C++;
- вызываемая процедура на языке ассемблера.
Для этого выбираем по правой кнопке мыши Файлы исходного кода -> Добавить -> Создать элемент и задаем имя файла программы на языке С++.
Второй добавляемый файл исходного кода будет иметь расширение .asm, которое необходимо указать явно.
Важно, чтобы файлы программ на C++ и ассемблере имели не только разные расширения, но и имена. В случае совпадающих имен файлов возникнет ошибка при компоновке проекта, поскольку оба файла будут иметь одно и то же имя объектного файла.
Набираем код программы для файлов вызывающей и вызываемой процедур соответственно на C++ и ассемблере.
Подключаем инструмент Microsoft Macro Assembler. По правой кнопке мыши для проекта выбираем Настройки построения.
В появившемся окне ставим галочку в строке masm.
Выбираем меню Свойства для файла на языке ассемблера по правой кнопке мыши и выбираем для этого файла инструмент Microsoft Macro Assembler.
Выполняем построение проекта, выбрав меню Отладка -> Построить решение.
Результат построения отображается в нижней части окна проекта.
Запуск проекта на выполнение осуществляется через меню Отладка -> Начать отладку.
Результат выполнения программы
Перед вызовом процедуры всегда нужно сохранять содержимое регистров ebp, esp, а перед выходом из процедуры – восстанавливать содержимое этих регистров. Это делается компилятором языка Си. Остальные регистры нужно сохранять при необходимости (если содержимое регистра подвергается изменению в вызванной процедуре, а далее может использоваться в вызывающей программе) Это может быть сделано с помощью команды pusha.
Передача аргументов в процедуру на ассемблере из программы на Си осуществляется через стек. При этом вызывающая программа записывает передаваемые параметры в стек, а вызываемая программа извлекает их из стека.
Работа с аргументами вещественного типа
При вызове функции с аргументами вещественного типа конфигурация проекта ничем не отличается от описанной выше. Для передачи аргументов необходимо указать их тип.
тип Си | Количество байт | Тип аргумента ассемблера |
float | 4 | dword |
double | 8 | qword |
Возвращаемое вещественное значение по умолчанию располагается в вершине стека сопроцессора st(0).
Пример. Найти разность двух чисел
using namespace std;
extern «C» double func(double, double);
int main() {
double x, y, r;
cout << «x= «;
cin >> x;
cout << «y= «;
cin >> y;
r = func(x, y);
cout << r;
cin.get(); cin.get();
return 0;
}
.MODEL FLAT, C
.CODE
func PROC C x:qword, y:qword
fld x
fsub y
ret
func ENDP
END
Назад: Язык ассемблера
2
3
4
5
6
7
8
9
10
11
push ebp
mov ebp,esp ; пролог
mov eax, [ebp+8] ; a
mov ebx, [ebp+12] ; b
mov ecx, [ebp+16] ; c
mov edx, [ebp+20] ; d
...
pop ebp
ret 16
some_proc endpp