Существуют следующие формы комбинирования программ на языках высокого уровня с ассемблером:
- Использование ассемблерных вставок (встроенный ассемблер, режим 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
Как подключить к ассемблеру библиотеку C?
mov eax, [ebp+8] ; a
mov ebx, [ebp+12] ; b
mov ecx, [ebp+16] ; c
mov edx, [ebp+20] ; d
Почему начинаем с +8, а не 0?
В первых ячейках — адрес возврата и текущая вершина стека (по 4 байта)
Hello.
I need to contact admin.
Thank you.
Hello! My name is AnnaMarkova, our company need to advertise on your website. What is your prices? Thank you. Best regards, Mary.
Все же проект видимо не собирается из-за разной структуры объектных файлов, полученных в masm и msvc, Юров в книге Ассемблер указывает на проблемы с совместимостью модулей в этой связке и как вариант решения этой проблемы предлагает создавать внешние подключаемые dll-библиотеки. Думал все же может существует вариант совмещения этих модулей без создания библиотек, инфы как-то немного, везде в основном предлагается связка для компоновки borland c++ и tasm, поскольку эти компиляторы одной фирмы, модули нормально компонуются.
Еще вопрос по статье по поводу обратного взаимодействия, когда из ассемблерного модуля вызывается процедура на С/С++, не могли бы Вы осветить этот вопрос?
На этой странице внизу есть ссылка на конспект лекций по ассемблеру. Там в конце рассмотрены вопросы вызова API-функций из программы, написанной на языке ассемблера.
Доброго времени суток.
как собрать эти приложения не используя графической оболочки (сборки проекта), а средствами командной строки?
Возможно ли собрать приложение, оттранслировав в MASM ассемблерный модуль и в Visual C++ с++ модуль? Пробовал это сделать, но компилятор Cl при попытке скомпоновать оба модуля ругается на поврежденный объектный модуль, полученный при трансляции asm-модуля.
Честно говоря, не пробовала. Не понимаю, зачем транслировать ассемблерный модуль без оболочки, если для С++ — модуля Вы всё равно используете оболочку.
Возможно — проблема с несовместимостью разрядности файлов.
Есть ли возможность создавать внешний модуль на ассемблере для 64х-разрядных приложений?
Спасибо
push dpush сpush bpush acall some_proc@16add esp,16 ; освободить стек
Вызванная таким образом процедура может инициализироваться так:
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
Зачем это add esp,16 если уже есть ret 16?
Согласна, исправила. В данном случае ret 16 не нужен, поскольку стек очищает вызывающая программа.
Огромное спасибо^___^
Отличная статья и мне очень помог пример для vs:)
Еще раз спасибо!!)
Использование термина «язык ассемблера» также может вызвать ошибочное мнение о существовании некоего единого языка низкого уровня или хотя бы стандартов на такие языки. При именовании языка ассемблера желательно уточнять, ассемблер для какой архитектуры имеется в виду.
Одним из основных недостатков языка ассемблера является непереносимость его на другие платформы, поскольку каждая платформа характеризуется своей системой команд. В данном случае имеется в виду архитектура IA-32, более подробно рассмотренная в статье Программная модель центрального процессора.
Спасибо за мгновенный ответ и выполненные полезные дополнения на сайте.
В приведенной таблице типов, жаль, не хватает типа long double для полноты информации.
Также возник вопрос. Почему в примерах на данной страничке в ассемблерных модулях, подключаемых к С-программе, отсутствует директива public для внешней процедуры? Работает, конечно, и без этого, но все же…
И еще… Подскажите, пожалуйста…
Если в С++ объявлена внешняя функция аналогичная следующей:
то, как обеспечить доступ в asm-программе ко второму параметру, дабы через него вернуть результат в С++-программу?
Заранее спасибо за столь полезные консультации по весьма неосвещенной и малодоступной тематике! 🙂
С типом long double, к сожалению, не все так тривиально, и разные компиляторы его трактуют по-разному. Например, в Visual Studio он приравнивается к типу double (qword), в других компиляторах занимает 10 байт (tbyte).
Насчет public Вы сами ответили на свой вопрос. Можно описать функцию как public, ошибки не будет. Опять же, это зависит от используемой среды разработки.
Обеспечить доступ к элементам массива типа double можно так же, как и к элементам массива типа int. Указатель на массив всегда будет 4-байтный (dword), независимо от типа его элементов:
Взять значение по адресу позволяют квадратные скобки.
Зачем вы вводите, как в первом примере, библиотеки tchar.h и stdio.h. В языке С++ можно ввести так:
Данная запись замещает две эти библиотеки.
Библиотека tchar.h в <iostream> не входит. Возможно, это зависит от компилятора.
А вообще, хотелось показать еще один способ перекодировки. Хотя, это, наверное, тема для отдельной статьи.
Спасибо огромное за проделанную работу!
Подскажите, пожалуйста…
Если в С++ объявлена внешняя функция аналогичная следующей:
а в самой asm-программе эта функция реализована через f-инструкции сопроцессора, как:
1. Обеспечить доступ в asm-программе к параметрам (double)?
2. Как вернуть в С-программу результат функции типа (double)?
Заранее огромное спасибо за ответы!
Ответила в конце статьи