Tooprogram.ru

Компьютерный справочник
0 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

Ассемблерная вставка в си

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Этот код в точности соответствует полной форме директивы proc .
Однако можно использовать упрощенную форму, которую поддерживают все современные ассемблеры:

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

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

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

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

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

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

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

В случае если стек был задействован, освобождение его возлагается на вызываемую процедуру.
В случае быстрого вызова транслятор Си добавляет к имени значок @ спереди, что искажает имена при обращении к ним в ассемблерном модуле.

Возврат результата из процедуры

Чтобы возвратить результат в программу на С из процедуры на ассемблере, перед возвратом управления в вызываемой процедуре (на языке ассемблера) необходимо поместить результат в соответствующий регистр:

Тип возвращаемого значенияРегистр
unsigned charal
charal
unsigned shortax
shortax
unsigned inteax
inteax
unsigned long intedx:eax
long intedx:eax

Пример Умножить на 2 первый элемент массива (нумерация элементов ведется с 0).

Чтобы построить проект в Microsoft Visual Studio Express 2010, совместив в нем файлы, написанные на разных языках программирования, выполняем следующие действия.

Создание проекта начинается с выбора меню Файл -> Создать -> Проект.

Аналогично проекту, содержащему 1 язык программирования, создаем пустой проект консольного приложения и задаем имя проекта.




Добавляем в дерево проекта два файла исходного кода:

  • вызывающая процедура на языке C++;
  • вызываемая процедура на языке ассемблера.

Для этого выбираем по правой кнопке мыши Файлы исходного кода -> Добавить -> Создать элемент и задаем имя файла программы на языке С++.

Второй добавляемый файл исходного кода будет иметь расширение .asm, которое необходимо указать явно.

Важно, чтобы файлы программ на C++ и ассемблере имели не только разные расширения, но и имена. В случае совпадающих имен файлов возникнет ошибка при компоновке проекта, поскольку оба файла будут иметь одно и то же имя объектного файла.

Набираем код программы для файлов вызывающей и вызываемой процедур соответственно на C++ и ассемблере.


Подключаем инструмент Microsoft Macro Assembler. По правой кнопке мыши для проекта выбираем Настройки построения.

В появившемся окне ставим галочку в строке masm.


Выбираем меню Свойства для файла на языке ассемблера по правой кнопке мыши и выбираем для этого файла инструмент Microsoft Macro Assembler.


Выполняем построение проекта, выбрав меню Отладка -> Построить решение.


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


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

Результат выполнения программы

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

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

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

тип СиКоличество байтТип аргумента ассемблера
float4dword
double8qword

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

Ассемблерные вставки в GCC

Статья является попыткой систематизировать brain-dump известной автору информации по ассемблерным вставкам в GCC.

Автор не считает себя авторитетом в данном вопросе, и статья может содержать неточности. Замечания, дополнения и исправления приветствуются.
Вопрос, зачем вообще использовать встроенный ассемблер, выходит за рамки данной статьи.
Предполагается, что читатель знаком с каким-либо ассемблером: учебником по языку per se статья не является.
За исключением случаев, когда явно оговорено обратное, информация в равном мере применима к C и к C++.
Все примеры в статье — для архитектуры x86 (и x86_64), но сама поддержка ассемблерных вставок в GCC не ограничивается этой архитектурой, и большинство информации применимо и для других.
Все отрывки кода, если явно не оговорено обратное и не указано авторство, находятся в общем доступе (public domain).

Общие сведения

Язык C++ поддерживает ассемблерные вставки, что отражено в Стандарте языка.
Однако данная возможность глубоко специфична индивидуальным компилятору/архитектуре, поэтому Стандарт немногословен и ограничивается следующим текстом:

Стандарт языка C ассемблерные вставки вообще не определяет, и упоминает их только в списке распространённых расширений:

Под GCC здесь и далее (если не оговорено обратное) подразумевается компилятор, поддерживающий набор расширений GCC. Это, с некоторых пор, официальное определение макро __GNUC__ (см. https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html , https://sourceforge.net/p/predef/wiki/Compilers/ и далее по ссылкам). В частности компиляторы clang и icc определяют __GNUC__; оба этих компилятора (так же как и сам GCC) поддерживают ассемблерные вставки в синтаксисе GCC.

Концептуальная модель

Типично, компилятор превращает исходный код в объектный код, который потом сборщиком (линкером) собирается в исполняемый файл.
GCC использует несколько необычное решение: компиляторы C и C++ во многом близки к языковым трансляторам ( https://en.wikipedia.org/wiki/Source-to-source_compiler ), и результатом их работы является файл на языке ассемблера (именно на текстовом (мнемоническом) ассемблере, а не в машинном коде), который далее преобразуется в объектный код штатным ассемблером (утилита as ; ассемблер, входящий в состав GNU, обычно в документации называют GNU Assembler, сокращённо gas, несмотря на то, что сама утилита называется именно as).
Обычно при вызове компилятор GCC сам производит соответствующие действия (вызов as и т. д.) «под капотом», и для создания объектного файла никаких дополнительных указаний не требуется.
Можно указать компилятору опцию -S (большая буква S; у малой s другое значение), для того чтобы произвести только перевод программы на ассемблер.
Рассмотрим пример. Создадим файл test_asm.cpp со следующим кодом:

При желании, можно получить несколько более подробный код (с дополнительными комментариями), воспользовавшись опцией -fverbose-asm.
Построчный листинг (в файле test_asm.lst) C++ и ассемблера можно получить, воспользовавшись командой (подробнее см. http://www.delorie.com/djgpp/v2faq/faq8_20.html ):

Данная модель достаточно естественным образом приводит к логике ассемблерных вставок, которую и использует GCC.
GCC вставляет, после возможных макроподстановок, текст ассемблерной вставки непосредственно в выходной файл на языке ассемблера.
Пример (о конкретном синтаксисе ассемблерных вставок см. ниже):

Модель, используемая GCC имеет несколько важных свойств.

Во-первых, компилятор почти не «подглядывает» в указанный программистом код ассемблерной вставки. Он при необходимости проводит макроподстановки (см. ниже), но в общем передаёт код ассемблеру почти «как есть», и почти не имеет представления о происходящем внутри. В частности, обнаружением ошибок занимается именно ассемблер (GCC передаёт программисту сообщения об ошибках от ассемблера).

Во-вторых, как следствие, ассемблерная вставка является для компилятора (и в частности оптимизатора) единой непрозрачной командой (чёрным ящиком). Всё нужную ему информацию о том, как этот блок взаимодействует с окружающим миром, компилятор получает напрямую от программиста, из явно указанных операндов ассемблерной вставки (задающих связи ассемблерного кода с переменными C++ и список задействованных ресурсов (регистров и т. д.) и изменений (состояния флагов, памяти и т. д.) в ассемблерной вставке), а не из детального рассмотрения текста ассемблерной вставки (которого он не производит). Технически говоря, GCC предоставляет программисту интерфейс к Register Transfer Language. Ответственность о соответствии действительности информации, указанной в операндах, целиком лежит на программисте.
В рамках этих ограничений, компилятор волен обращаться с ассемблерной вставкой (как и с другими командами) так, как ему вздумается: перемещать, дублировать (напр. при подстановке inline-функций), или вообще выбросить, если оптимизатор придёт к такому решению.

Ассемблерные вставки… в C#?

Итак, эта история началась с совпадения трёх факторов. Я:

  1. в основном писал на C#;
  2. лишь примерно представлял, как он устроен и работает;
  3. заинтересовался ассемблером.

Эта, на первый взгляд, невинная смесь породила странную идею: а можно ли как-то совместить эти языки? Добавить в C# возможность делать ассемблерные вставки, примерно как в C++.

Если вам интересно, к каким последствиям это привело, — добро пожаловать под кат.

Первые сложности

Уже в тот момент я понимал, что очень вряд ли есть стандартные инструменты для вызова ассемблерного кода из кода C# — это слишком сильно противоречит одной из важных концепций языка: безопасности работы с памятью. После поверхностного изучения вопроса (которое, кроме всего прочего, подтвердило изначальную догадку — «из коробки» такая возможность отсутствует) стало понятно, что кроме проблемы идейного характера есть и проблема чисто техническая: C#, как известно, компилируется в промежуточный байт-код, который в дальнейшем интерпретируется виртуальной машиной CLR. И как раз здесь перед нами в полный рост встаёт та самая проблема: с одной стороны, компилятор (здесь и дальше я буду подразумевать Roslyn от Microsoft, поскольку он де-факто является стандартом в области C#-компиляторов), очевидно, не умеет распознавать и транслировать ассемблерные команды из текстового вида в какое-либо двоичное представление, а значит, в качестве вставки мы должны использовать непосредственно машинные команды в их бинарном виде, а с другой — виртуальная машина имеет свой собственный байт-код и не может распознать и выполнить тот набор команд, который предложим ей мы.

Теоретическое решение этой проблемы очевидно — нужно сделать так, чтобы двоичный код вставки выполнялся процессором, минуя интерпретацию виртуальной машиной. Самое простое, что приходит в голову, — хранить двоичный код в виде массива байт, на который в нужный момент каким-то образом будет передано управление. Отсюда вырисовывается первая задача: нужно придумать способ передачи управления тому, что содержится в произвольной области памяти.

Первый прототип: «вызов» массива

Задача эта — пожалуй, самое серьёзное препятствие на пути к вставкам. Средствами языка несложно получить указатель на наш массив, но в мире C# указатели существуют только на данные и превратить его в указатель на, скажем, функцию, чтобы её потом вызвать, невозможно (ну или, по крайней мере, мне не удалось придумать, как это сделать).

К счастью (или к сожалению), ничто не ново под луной и быстрый поиск в Яндексе по словам «C#» и «ассемблерные вставки» привёл меня к статье в декабрьском выпуске журнала «][акер» за 2007 год. Честно скопипастив оттуда функцию и подогнав её к своим нуждам, получил

Основная идея этого кода — подменить в стеке адрес возврата из функции InvokeAsm() на адрес массива байт, которому требуется передать управление. Тогда после выхода из функции вместо продолжения выполнения программы начнётся выполнение нашего двоичного кода.

Разберёмся с магией, творящейся в InvokeAsm() , поподробнее. Сначала мы объявляем локальную переменную, которая, разумеется, оказывается в стеке, потом получаем её адрес (получая тем самым адрес верхушки стека). Дальше добавляем к нему некую магическую константу, полученную путём кропотливого подсчёта в отладчике смещения адреса возврата относительно верхушки стека, сохраняем адрес возврата и записываем вместо него адрес нашего массива байт. Сакральный смысл сохранения адреса возврата очевиден — нам же нужно продолжить выполнение программы после нашей вставки, а значит, надо знать, куда после неё передавать управление. Дальше идёт вызов WinAPI-функции из библиотеки kernel32.dll — VirtualProtect() . Он нужен для того, чтобы изменить атрибуты страницы памяти, на которой находится код вставки. Он, разумеется, при компиляции программы оказывается в секции данных, а соответствующая страница памяти имеет доступ для чтения и записи. Нам же нужно добавить ещё и разрешение на исполнение её содержимого. Наконец, мы возвращаем сохранённый настоящий адрес возврата. Разумеется, в код, вызвавший InvokeAsm() , этот адрес не вернётся, т.к. выполнение сразу же после return (void*)i; «провалится» во вставку. Однако, используемые виртуальной машиной соглашения о вызове (stdcall с отключенной оптимизацией и fastcall со включенной) подразумевают возврат значения через регистр EAX, т.е. для возврата из вставки нам понадобится выполнить две инструкции: push eax (код 0x50) и ret (код 0xC3).

Наконец, стоит обратить внимание на два неиспользуемых аргумента: void* firstAsmArg и void* secondAsmArg . Они нужны для передачи произвольных пользовательских данных в ассемблерную вставку. Расположены эти аргументы будут либо в известном месте стека (stdcall), либо в, опять же, известных регистрах (fastcall).

Улучшения

«Безопасное» лучше небезопасного

Разумеется, ни о какой безопасности (в том смысле, с котором это слово используется в C#) здесь не может быть и речи. Однако вышеописанный метод InvokeAsm() оперирует указателями, а значит, вызываться может только из блока, помеченного ключевым словом unsafe , что не всегда удобно — как минимум, это требует компиляции с ключом /unsafe (ну или соответствующей галочки в свойствах проекта в VS). Поэтому логичным кажется предоставить оболочку, оперирующую хотя бы IntPtr (на худой конец), а в идеале — и вовсе позволяющую пользователю указывать передаваемые и возвращаемые типы. Что ж, это звучит как generic, пишем generic, о чём тут ещё, спрашивается, говорить? На самом деле — есть о чём.

Самое очевидное: а как получить указатель на аргумент, тип которого неизвестен? Конструкции типа T* ptr = &arg в C# не допускаются, и, в общем-то, несложно понять причину: пользователь вполне может в качестве параметра типа использовать один из управляемх типов, указатель на который получить невозможно. Решением могло бы стать ограничение параметра типа unmanaged , но оно, во-первых, появилось только в C# 7.3, а во-вторых — не позволяет передавать в качестве аргументов строки и массивы, хотя оператор fixed их использовать позволяет (указатель при этом получаем на первый символ или элемент массива соответственно). Ну и, к тому же, хотелось бы дать пользователю возможность оперировать в том числе и управляемыми типами — раз уж мы взялись нарушать правила языка, то будем их нарушать до конца!

Получение указателя на управляемый объект и объекта по указателю

И вновь после не особо плодотворных раздумий я начал искать готорвые решения. На этот раз помогла мне статья на Хабре. Если вкратце, один из предлагаемых в ней методов заключается в том, чтобы написать вспомогательную библиотеку, причём не на C#, а непосредственно на IL. Её задача — помещать в стек виртуальной машины объект (фактически — ссылку на объект), передаваемый в качестве аргумента, после чего извлекать из стека что-то другое — например, число или IntPtr . Проделав те же действия в обратной последовательности, можно преобразовать указатель (например, возвращённый из ассемблерной вставки) в объект. Этот способ хорош тем, что всё происходящее понятно и прозрачно. Но есть и минус: я хотел обойтись как можно меньшим количеством файлов, поэтому вместо написания отдельной библиотеки решил встроить IL-код в основную. Единственный найденный мной способ — написать на C# методы-заглушки, собрать проект, дизассемблировать бинарник с помошью ildasm, переписать код методов-заглушек и собрать всё это обратно с помощью ilasm. Это довольно много дополнительных действий, а если учесть, что проделывать их нужно при каждой сборке после внесения в код любых изменений… В общем, довольно быстро мне это надоело, и я начал искать альтернативы.

Как раз в это время мне в руки попала замечательная книга, благодаря которой я узнал для себя много нового — «CLR via C#» Джеффри Рихтера. В ней, где-то в районе двадцатой главы, речь зашла про структуру GCHandle , у которой есть метод Alloc() , принимающий объект и один из элементов перечисления GCHandleType . Так вот, если вызвать этот метод передав ему желаемый объект и GCHandle.Pinned , то можно получить адрес этого объекта в памяти. Более того, до вызова GCHandle.Free() объект фиксируется, т.е. полностью защищается от воздействия сборщика мусора. Однако и тут есть определённые проблемы. Во-первых, GCHandle никоим образом не помогает совершить преобразование «указатель → объект», только «объект → указатель». Что важнее, для использования GCHandleType.Pinned класс или структура объекта, адрес которого мы хотим получить, должны иметь атрибут [StructLayout(LayoutKind.Sequential)] , в то время как по умолчанию используется LayoutKind.Auto . Так что такой способ подойдёт лишь для некоторых стандартных типов и для тех пользовательских типов, которые изначально проектировались с учётом этой особенности. Не совсем тот универсальный метод, который мы хотели бы найти, верно?

Что ж, попробуем ещё раз. Теперь обратим внимание на две недокументированные функции, которые, тем не менее, поддерживаются Roslyn-ом: __makeref() и __refvalue() . Первая из них принимает объект и возвращает экземпляр структуры TypedReference , хранящей ссылку на объект и его тип, вторая же извлекает объект из передаваемого экземпляра typedReference . Почему эти функции так для нас важны? Потому что TypedReference — структура! В контексте обсуждения это значит, что мы можем получить указатель на неё, который, по совместительству, будет являться указателем на первое поле этой структуры. А именно в нём хранится та самая интересующая нас ссылка на объект. Тогда для получения указателя на управляемый объект нам нужно прочитать значение по указателю на то, что вернёт __makeref() и сконвертировать это в указатель. Для получения же объекта по указателю необходимо вызвать __makeref() от, условно, пустого объекта требуемого типа, получить указатель на возвращаемый экземпляр TypedReference , записать по нему указатель на объект, после чего вызвать __refvalue() . В итоге получился примерно такой код:

Для тех, кто не помнит опкоды

Итак, мы узнали, как вызывать двоичный код, научились передавать ему в качестве аргументов не только непосредственные значения, но и указатели на что угодно… Осталась лишь одна проблема. Где взять тот самый двоичный код? Можно вооружиться карандашом, блокнотом и таблицей опкодов (например, этой) или взять шестнадцатеричный редактор с поддержкой ассемблера x86 или даже полноценный транслятор, но все эти варианты подразумевают, что пользователю придётся использовать ещё что-то кроме библиотеки. Это — не совсем то, чего мне хотелось, поэтому я решил включить в состав библиотеки свой транслятор, который по устоявшейся традиции был назван SASM (сокращение от Stack Assembler; никакого отношения к IDE не имеет).

Рассказывать о процессе создания этого «чуда» я, пожалуй, не буду — нет в этой истории ничего интересного, а вот основные возможности кратко опишу. На данный момент поддерживается большинство инструкций x86. Инструкции математического сопроцессора для работы с числами с плавающей точкой и из расширений (MMX, SSE, AVX) пока не поддерживаются. Есть возможность объявления констант, процедур, локальных стековых переменных, глобальных переменных, память под которые выделяется в процессе трансляции непосредственно в массиве с двоичным кодом (если эти переменные именованы при помощи меток, то их значение можно получить и из C# после выполнения вставки путём вызова методов GetBYTEVariable() , GetWORDVariable() , GetDWORDVariable() , GetAStringVariable() и GetWStringVariable() объекта SASMCode ), присутствуют макросы addr и invoke . Одной из важных особенностей является поддержка импорта функций из внешних библиотек с помощью конструкции extern lib .

Отдельного абзаца достоин макрос asmret . В процессе трансляции он разворачивается в 11 инструкций, образующих эпилог. В начало же транслируемого кода по умолчанию добавляется пролог. Их задача — сохранение/восстановление состояния процессора. Кроме того, пролог добавляет четыре константы — $first , $second , $this и $return . В процессе трансляции эти константы заменяются адресами в стеке, по которым находятся, соответственно, первый и второй аргументы, переданные ассемблерной вставке, адрес первой команды вставки и адрес возврата.

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

Если же всё же попытаться как-то обобщить всё сделанное — то, на мой взгляд, получился интересный и даже, в какой-то степени, небесполезный проект. Например, идентичные алгоритмы сортировки вставками на C# и с помощью ассемблерной вставки по скорости отличаются более чем в два раза (разумеется, в пользу ассемблера). В серьёзных проектах, конечно, получившуюся библиотеку использовать не рекомендуется (возможны, хоть и не слишком вероятны, непредсказуемые побочные эффекты), но для себя — вполне можно.

Ассемблер в Dev-C++

Как выучить английский

В наше время любой человек должен знать английский язык. А тем более программист. Потому что довольно большая часть документации существует только на английском. А профессионал не может ждать, пока кто-то переведёт ему документацию. Он должен уметь быстро получать нужную инфорамцию и использовать её, независимо от того, на каком она языке — на английском или на русском. Ссылки на курсы по английскому.

Давно хотел разобраться с этой темой. И вот наконец собрался.

Дело в том, что инструкции процессора Интел и синтаксис вставок ассемблерного кода в программы на Visual C++ не будут работать в Dev-C++.

Потому что Dev-C++ использует компилятор GCC (бесплатный компилятор языка С++). Этот компилятор имеет встроенный ассемблер, но это не MASM и не TASM с привычным набором команд. Это ассемблер AT&T, синтаксис которого очень сильно отличается от синтаксиса MASM/TASM и подобных.

Кроме того, если в Паскале или Visual C++ вы просто используете ключевые слова — операторные скобки (в Паскале это asm. end, в Visual C++ это __asm <. >), и между этими скобками пишите инструкции ассемблера как вы привыкли, то с компилятором GCC это не проканает.

Я сначала никак не мог понять, почему. Но когда немного познакомился с документацией, то понял.

Оказывается, в компиляторе GCC, как и в Паскале и в Visual C++, есть ключевые слова asm и __asm. Вот только это вовсе не операторные скобки.

По сути это функции, которые вызываются с определённым набором параметров. И в эти функции в качестве параметров передаются инструкции ассемблера!

Вот уж воистину — зачем просто, если можно сложно!

В общем, использование встроенного ассемблера GCC — это целая наука. Если интересно её освоить, то можете начать вот с этой статьи (это мой перевод английского оригинала).

А здесь я просто в самых общих чертах покажу, как можно использовать вставки на ассемблере в Dev-C++ (это будет также справедливо для других средств разработки, использующих компилятор GCC).

Ассемблер AT&T

Как я уже сказал, этот ассемблер сильно отличается от привычных нам инструкций процессора Интел. Здесь я не буду об этом говорить. Если кому интересно, то основные отличия описаны здесь.

Вставка на ассемблере в Dev-C++

Основной формат вставки кода ассемблера показан ниже:

asm( «Здесь код на ассемблере»);

Как вы могли заметить, здесь используются два варианта встраивания ассемблера: asm и __asm__. Оба варианта правильные. Следует использовать __asm__, если ключевое слово asm конфликтует с каким-либо участком вашей программы (например, в вашей программе есть переменная с именем asm).

Если встраивание кода на ассемблере содержит более одной инструкции, то мы пишем по одной инструкции в строке в двойных кавычках, а также суффикс ’n’ и ’t’ для каждой инструкции.

Однако в большинстве случаев требуется обмен данными между кодом на ассемблере и переменными, которые объявлены в исходных кодах на языке высокого уровня.

Это тоже возможно. Общий формат ассемблерной вставки для компилятора GCC такой:

Не буду здесь подробно всё это расписывать, так как это уже сделано здесь. Там же вы найдёте все подробности использования встроенного ассемблера компилятора GCC (ну хотя не все, а основные).

Я же здесь приведу пример, и на этом успокоюсь.

Для начала не очень хороший пример.

Не очень хороший он потому, что мы изменяем значения регистров, а потом получаем значение регистра eax в переменную х. Но при этом мы не заботимся о том, что состояние этих регистров может быть изменено где-то в другом месте. Так что это может привести к потенциальным неожиданностям.

Теперь попробуем сделать всё чуть более правильно (хотя и не идеально).

Здесь в ассемблерный код мы передаём значения переменных y и z. Значение у помещается в регистр еах (на это указывает буква “a”), а значение z помещается в регистр ebx (на это указывает буква “b”).

Сам ассемблерный код выполняет сложение значений регистров eax и ebx, и помещает результат в eax. А уже этот результат выводится в переменную y. То, что у — это выходная переменная, определяет модификатор “=”.

Ну вот как-то так. Это, конечно, в самых общих чертах. Если кого интересуют подробности, то см. здесь.

Читать еще:  Безопасный переход по ссылке
Ссылка на основную публикацию
Adblock
detector