Связь ассемблера с языками высокого уровня

Связь ассемблера с языками высокого уровня

Существуют следующие формы комбинирования программ на языках высокого уровня с ассемблером:

  • Использование ассемблерных вставок (встроенный ассемблер, режим inline). Ассемблерные коды в виде команд ассемблера вставляются в текст программы на языке высокого уровня. Компилятор языка распознает их как команды ассемблера и без изменений включает в формируемый им объектный код. Эта форма удобна, если надо вставить небольшой фрагмент.
  • Использование внешних процедур и функций. Это более универсальная форма комбинирования. У нее есть ряд преимуществ:
    - написание и отладку программ можно производить независимо;
    - написанные подпрограммы можно использовать в других проектах;
    - облегчаются модификация и сопровождение подпрограмм.
Встроенный ассемблер

При написании ассемблерных вставок используется следующий синтаксис:

_asm КодОперации операнды ; // комментарии

КодОперации задает команду ассемблера,
операнды – это операнды команды.
В конце записывается ;, как и в любой команде языка Си.
Комментарии записываются в той форме, которая принята для языка Си.
Если требуется в текст программы на языке Си вставить несколько идущих подряд команд ассемблера, то их объединяют в блок:

_asm
{
текст программы на ассемблере ; комментарии
}

Внутри блока текст программы пишется с использованием синтаксиса ассемблера, при необходимости можно использовать метки и идентификаторы. Комментарии в этом случае можно записывать как после ;, так и после //.

Пример Даны целые числа а и b. Вычислить выражение a+5b.
Для вывода приглашений Введите a: и Введите b: используем функцию CharToOem(_T("Введите "),s),
где s – указатель на строку, которая перекодирует русскоязычные сообщения.

#include <stdio.h>
#include <windows.h>
#include <tchar.h>
void main()
{
  char s[20];
  int a, b, sum;
  CharToOem(_T("Введите "),s);
  printf("%s a: ", s);
  scanf("%d",&a);
  printf("%s b: ",s);
  scanf("%d",&b);
  _asm
  {
    mov eax, a;
    mov ecx, 5
m: add eax, b
    loop m
    mov sum, eax
  }
  printf("\n %d + 5*%d = %d", a, b, sum);
  getchar(); getchar();
}

Для компоновки и запуска программы создаем проект как описано в разделе Создание консольных приложений.
Проект будет содержать 1 файл исходного кода с расширением cpp.
Результат выполнения программы:

Связь ассемблера и Си

Использование внешних процедур

Для связи посредством внешних процедур создается многофайловая программа. При этом в общем случае возможны два варианта вызова:

  • программа на языке высокого уровня вызывает процедуру на языке ассемблера;
  • программа на языке ассемблера вызывает процедуру на языке высокого уровня.

Рассмотрим более подробно первый вариант.
В таблице ниже представлены основные соглашения по передаче параметров в процедуру.
В программах, написанных на языке ассемблера, используется соглашение передачи параметров stdcall. Однако по сути получение и передача параметров в языке ассемблера производится явно, без помощи транслятора.
При связи процедуры, написанной на языке ассемблера, с языком высокого уровня, необходимо учитывать соглашение по передаче параметров.

Соглашение Параметры Очистка стека Регистры
Pascal (конвенция языка Паскаль) Слева направо Процедура Нет
C (конвенция С) Справа налево Вызывающая программа Нет
Fastcall (быстрый или регистровый вызов) Слева направо Процедура Задействованы три регистра (EAX,EDX,ECX), далее стек
Stdcall (стандартный вызов) Справа налево Процедура Нет

Конвенция Pascal заключается в том, что параметры из программы на языке высокого уровня передаются в стеке и возвращаются в регистре АХ/ЕАХ, — это способ, принятый в языке PASCAL (а также в BASIC, FORTRAN, ADA, OBERON, MODULA2), — просто поместить параметры в стек в естественном порядке. В этом случае запись

some_proc(a,b,c,d);

запишется как

push a
push b
push с
push d
call some_proc@16

Процедура some_proc, во-первых, должна очистить стек по окончании работы (например, командой ret 16) и, во-вторых, параметры, переданные ей, находятся в стеке в обратном порядке:

some_proc proc
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.
Однако можно использовать упрощенную форму, которую поддерживают все современные ассемблеры:

some_proc proc PASCAL, а:dword, b:dword, с:dword, d:dword
...
ret
some_proc endp

Главный недостаток этого подхода — сложность создания функции с изменяемым числом параметров, аналогичных функции языка С printf. Чтобы определить число параметров, переданных printf, процедура должна сначала прочитать первый параметр, но она не знает его расположения в стеке. Эту проблему решает подход, используемый в С, где параметры передаются в обратном порядке.

Конвенция С используется, в первую очередь, в языках С и C++, а также в PROLOG и других. Параметры помещаются в стек в обратном порядке, и, в противоположность PASCAL-конвенции, удаление параметров из стека выполняет вызывающая процедура.
Запись some_proc(a,b,c,d)
будет выглядеть как

push d
push с
push b
push a
call some_proc@16
add esp,16 ; освободить стек

Вызванная таким образом процедура может инициализироваться так:

some_proc proc
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 с указанием языка С:

some_proc proc С, а:dword, b:dword, с:dword, d:dword
...
ret
some_proc endp

Регистр EВР используется для хранения параметров, и его нельзя изменять программно при использовании упрощенной формы директивы proc.
Преимущество по сравнению с PASCAL-конвенцией заключается в том, что освобождение стека от параметров в конвенции С возлагается на вызывающую процедуру, что позволяет лучше оптимизировать код программы. Например, если необходимо вызвать несколько функций, принимающих одни и те же параметры подряд, можно не заполнять стек каждый раз заново, и это — одна из причин, по которой компиляторы с языка С создают более компактный и быстрый код по сравнению с компиляторами с других языков.

Смешанные конвенции
Существует конвенция передачи параметров STDCALL, отличающаяся и от C, и от PASCAL-конвенций, которая применяется для всех системных функций Win32 API. Здесь параметры помещаются в стек в обратном порядке, как в С, но процедуры должны очищать стек сами, как в PASCAL.
Еще одно отличие от С-конвенции – это быстрое или регистровое соглашение FASTCALL. В этом случае параметры в функции также передаются по возможности через регистры. Например, при вызове функции с шестью параметрами

some_proc(a,b,с,d,e,f);

первые три параметра передаются соответственно в ЕАХ, EDX, ЕСХ, а только начиная с четвертого, параметры помещают в стек в обычном обратном порядке:

mov a, eax
mov b, edx
mov c, ecx
mov d, [ebp+8]
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).

//Вызывающая программа file1.cpp
#include <iostream>
using namespace std;
extern "C" int MAS_FUNC (int *, int);
int main() {
  int *mas, n, k;
  system("chcp 1251");
  system("cls");
  cout << "Введите размер массива: ";
  cin >> n;
  mas = new int[n];
  cout << "Введите элементы массива: " << endl;
  for(int i=0; i<n; i++) {
    cout << "mas[" << i <<"]= ";
    cin >> mas;
  }
  k = MAS_FUNC(mas, n);
  cout << mas[1] << "*2= " << k;
  cin.get(); cin.get();
  return 0;
}
;Вызываемая функция file2.asm
.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, которое необходимо указать явно.
casmprj7
Важно, чтобы файлы программ на C++ и ассемблере имели не только разные расширения, но и имена. В случае совпадающих имен файлов возникнет ошибка при компоновке проекта, поскольку оба файла будут иметь одно и то же имя объектного файла.

Набираем код программы для файлов вызывающей и вызываемой процедур соответственно на C++ и ассемблере.
Файл исходного кода
Файл исходного кода
Подключаем инструмент Microsoft Macro Assembler. По правой кнопке мыши для проекта выбираем Настройки построения.
Подключение Microsoft MacroAssembler
В появившемся окне ставим галочку в строке masm.

masm
Выбираем меню Свойства для файла на языке ассемблера по правой кнопке мыши и выбираем для этого файла инструмент Microsoft Macro Assembler.
casmprj12
casmprj13
Выполняем построение проекта, выбрав меню Отладка -> Построить решение.

Запуск построения проекта
Результат построения отображается в нижней части окна проекта.

Результат построения проекта
Запуск проекта на выполнение осуществляется через меню Отладка -> Начать отладку.

Результат выполнения программы
Результат выполнения
Перед вызовом процедуры всегда нужно сохранять содержимое регистров ebp, esp, а перед выходом из процедуры – восстанавливать содержимое этих регистров. Это делается компилятором языка Си. Остальные регистры нужно сохранять при необходимости (если содержимое регистра подвергается изменению в вызванной процедуре, а далее может использоваться в вызывающей программе) Это может быть сделано с помощью команды pusha.
Передача аргументов в процедуру на ассемблере из программы на Си осуществляется через стек. При этом вызывающая программа записывает передаваемые параметры в стек, а вызываемая программа извлекает их из стека.

Работа с аргументами вещественного типа

При вызове функции с аргументами вещественного типа конфигурация проекта ничем не отличается от описанной выше. Для передачи аргументов необходимо указать их тип.

тип Си Количество байт Тип аргумента ассемблера
float 4 dword
double 8 qword

Возвращаемое вещественное значение по умолчанию располагается в вершине стека сопроцессора st(0).

Пример. Найти разность двух чисел

#include <iostream>
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;
}
; Функция на ассемблере
.586
.MODEL FLAT, C
.CODE
func PROC C x:qword, y:qword
fld x
fsub y
ret
func ENDP
END

Назад

Прокрутить вверх