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

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

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

  • Использование ассемблерных вставок (встроенный ассемблер, режим 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

Назад

21 комментарий к “Связь ассемблера с языками высокого уровня”

  1. mov eax, [ebp+8] ; a
    mov ebx, [ebp+12] ; b
    mov ecx, [ebp+16] ; c
    mov edx, [ebp+20] ; d
    Почему начинаем с +8, а не 0?

    1. Елена Вставская

      В первых ячейках — адрес возврата и текущая вершина стека (по 4 байта)

  2. Hello! My name is AnnaMarkova, our company need to advertise on your website. What is your prices? Thank you. Best regards, Mary.

  3. Все же проект видимо не собирается из-за разной структуры объектных файлов, полученных в masm и msvc, Юров в книге Ассемблер указывает на проблемы с совместимостью модулей в этой связке и как вариант решения этой проблемы предлагает создавать внешние подключаемые dll-библиотеки. Думал все же может существует вариант совмещения этих модулей без создания библиотек, инфы как-то немного, везде в основном предлагается связка для компоновки borland c++ и tasm, поскольку эти компиляторы одной фирмы, модули нормально компонуются.

    Еще вопрос по статье по поводу обратного взаимодействия, когда из ассемблерного модуля вызывается процедура на С/С++, не могли бы Вы осветить этот вопрос?

    1. Елена Вставская

      На этой странице внизу есть ссылка на конспект лекций по ассемблеру. Там в конце рассмотрены вопросы вызова API-функций из программы, написанной на языке ассемблера.

  4. Доброго времени суток.

    как собрать эти приложения не используя графической оболочки (сборки проекта), а средствами командной строки?

    Возможно ли собрать приложение, оттранслировав в MASM ассемблерный модуль и в Visual C++ с++ модуль? Пробовал это сделать, но компилятор Cl при попытке скомпоновать оба модуля ругается на поврежденный объектный модуль, полученный при трансляции asm-модуля.

    1. Елена Вставская

      Честно говоря, не пробовала. Не понимаю, зачем транслировать ассемблерный модуль без оболочки, если для С++ — модуля Вы всё равно используете оболочку.
      Возможно — проблема с несовместимостью разрядности файлов.

  5. Виктори я

    Есть ли возможность создавать внешний модуль на ассемблере для 64х-разрядных приложений?

     

    Спасибо

  6. push dpush сpush bpush acall some_proc@16add esp,16 ; освободить стек
    Вызванная таким образом процедура может инициализироваться так:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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 16
    some_proc endpp

    Зачем это add esp,16 если уже есть ret 16?

    1. Елена Вставская

      Согласна, исправила. В данном случае ret 16 не нужен, поскольку стек очищает вызывающая программа.

  7. Огромное спасибо^___^
    Отличная статья и мне очень помог пример для vs:)
    Еще раз спасибо!!)

  8. Super Mario Star Scramble 2

    Использование термина «язык ассемблера» также может вызвать ошибочное мнение о существовании некоего единого языка низкого уровня или хотя бы стандартов на такие языки. При именовании языка ассемблера желательно уточнять, ассемблер для какой архитектуры имеется в виду.

    1. Елена Вставская

      Одним из основных недостатков языка ассемблера является непереносимость его на другие платформы, поскольку каждая платформа характеризуется своей системой команд. В данном случае имеется в виду архитектура IA-32, более подробно рассмотренная в статье Программная модель центрального процессора.

  9. Спасибо за мгновенный ответ и выполненные полезные дополнения на сайте.

    В приведенной таблице типов, жаль, не хватает типа long double для полноты информации.

    Также возник вопрос. Почему в примерах на данной страничке в ассемблерных модулях, подключаемых к С-программе, отсутствует директива public для внешней процедуры? Работает, конечно, и без этого, но все же…

    И еще… Подскажите, пожалуйста…

    Если в С++ объявлена внешняя функция аналогичная следующей:

    extern "C" void F (double, double *);

    то, как обеспечить доступ в asm-программе ко второму параметру, дабы через него вернуть результат в С++-программу?

    Заранее спасибо за столь полезные консультации по весьма неосвещенной и малодоступной тематике! 🙂

    1. С типом long double, к сожалению, не все так тривиально, и разные компиляторы его трактуют по-разному. Например, в Visual Studio он приравнивается к типу double (qword), в других компиляторах занимает 10 байт (tbyte).
      Насчет public Вы сами ответили на свой вопрос. Можно описать функцию как public, ошибки не будет. Опять же, это зависит от используемой среды разработки.
      Обеспечить доступ к элементам массива типа double можно так же, как и к элементам массива типа int. Указатель на массив всегда будет 4-байтный (dword), независимо от типа его элементов:

      extern "C" void F (double n, double *d);
      ...
      F PROC C n:qword, d:dword

      Взять значение по адресу позволяют квадратные скобки.

  10. Алексей

    Зачем вы вводите, как в первом примере, библиотеки tchar.h и stdio.h. В языке С++ можно ввести так:

    1
    #include <iostream>

    Данная запись замещает две эти библиотеки.

    1. Библиотека tchar.h в <iostream> не входит. Возможно, это зависит от компилятора.
      А вообще, хотелось показать еще один способ перекодировки. Хотя, это, наверное, тема для отдельной статьи.

      1. Спасибо огромное за проделанную работу!

        Подскажите, пожалуйста…

        Если в С++ объявлена внешняя функция аналогичная следующей:

        extern "C" double F (double, double);

        а в самой asm-программе эта функция реализована через f-инструкции сопроцессора, как:

        1. Обеспечить доступ в asm-программе к параметрам (double)?

        2. Как вернуть в С-программу результат функции типа (double)?

        Заранее огромное спасибо за ответы!

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

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