Ассемблерные вставки в c visual studio

Обновлено: 06.07.2024

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

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




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

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

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


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

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

В дальнейшем речь пойдёт об архитектуре x86 (или, вернее, IA-32) — банально из-за того, что на тот момент именно с ней я был хоть как-то знаком, в отличие от, скажем, x86-64. Однако описанный выше способ передачи управления должен работать и для 64-битного кода.

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

Поскольку с точки зрения компилятора в коде творится не пойми что, он может ненароком выкинуть какой-то принципиально важный вызов/что-то заинлайнить/не сохранить какой-то «неиспользуемый» аргумент/ещё как-то помешать осуществлению нашего плана. Частично это решается атрибутом [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] , однако даже такие меры предосторожности не дают нужного эффекта: например, ключевая для всей функции локальная переменная i внезапно оказывается не стековой, а регистровой, что, очевидно, всё портит. Поэтому чтобы полностью исключить вероятность того, что что-то пойдёт не так, следует собирать библиотеку с отключенной оптимизацией (либо отключить её в свойствах проекта, либо использовать конфигурацию Debug). Следовательно, использоваться будет stdcall, поэтому в дальнейшем я буду исходить именно из этого соглашения о вызовах.

Улучшения

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

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

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

Возвращаясь к задаче написания безопасной обёртки для InvokeAsm() , следует отметить, что метод получения указателей с помощью __makeref() и __refvalue() , в отличие от использования GCHandle.Alloc(GCHandleType.Pinned) , не гарантирует того, что сборщик мусора никуда наш объект не передвинет. Поэтому обёртка должна начинаться с отключения сборщика мусора и заканчиваться восстановлением его функциональности. Решение довольно грубое, но эффективное.

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

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

Я не силён в парсинге строк, поэтому код транслятора… ну, неидеален, мягко говоря. Кроме того, не силён я и в регулярных выражениях, поэтому их там нет. И вообще — парсер итеративный.

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

Многие из нас изучали ассемблер в университете, но почти всегда это ограничивалось простыми алгоритмами под DOS. При разработке программ для Windows может возникнуть необходимость написать часть кода на ассемблер, в этой статье я хочу рассказать вам, как использовать ассемблер в ваших программах под Visual Studio 2005.

image

Создание проекта

В статье мы рассмотрим как вызывать ассемблер из С++ кода и обратно, передавать данные, а также использовать отладчик встроенный в Visual Studio 2005 для отладки кода на ассемблер.

Для начала нам нужно создать проект. Включаем Visual Studio, выбираем File > New > Project. В Visual Studio нет языка ассемблер в окне выбора типа проекта, поэтому создаем С++ Win32 проект. В окне настроек нового проекта выбираем «Empty Project».

image

По умолчанию Visual Studio не распознает файлы с кодом на ассемблер. Для того чтобы включить поддержку ассемблер нам необходимо настроить в проекте условия сборки указав какой программой необходимо компилировать файлы *.asm. Для этого выбираем пункт меню «Custom Build Rules. ».

image

В открывшемся окне мы можем указать специальные правила компиляции для различных файлов, Visual Studio 2005 уже имеет готовое правило для файлов *.asm, нам необходимо лишь включить его, установив напротив правила «Microsoft Macro Assembler» галочку.

image

Добавление исходного кода

Перейдем к написанию исходного кода нашего проекта. Начнем с добавления исходного кода на c++. Добавим новый файл в папку Source Files. В качестве Template выбираем C++ File и вводим желаемое имя файла, например main.cpp. Напишем функцию, которая будет считывать имя введенное пользователем, оформив это в виде функции readName() которая будет возвращать ссылку на считанное имя. Мы получим примерно следующее содержимое файла:

Теперь, когда мы знаем имя пользователя мы можем вывести приветствие, его будет выводить функция sayHello() которую мы напишем на ассемблер, чтобы использовать эту функцию сначала мы должны указать что она будет определена в другом файле, для этого добавим блок к main.cpp:

Этот блок говорит компилятору, что функция sayHello() будет объявлена в другом файле и будет иметь правила вызова «C». Компилятор C++ искажает имена функций так, что указание правил вызова обязательно. Кроме того мы хотим использовать функцию readName() из функции sayHello(), для этого необходимо добавить extern «C» перед определением функции readName(), это позволит вызывать эту функцию из других файлов используя правила вызова «C».

Пришло время добавить код на ассемблер, для этого добавим в Source Folder новый файл. Выбираем тип Text File (.txt) и в поле название заменяем .txt на .asm, назовем наш файл hello.asm. Объявим функцию sayHello() и укажем внешние функции, которые мы хотим использовать. Получим следующий код:

Теперь мы можем запустить проект, для этого просто выбираем Debug > Start Without Debugging или нажимаем комбинацию Ctrl-F5. Если все сделано верно, вы увидите окно программы:

image

Немного усложним задачу, попробуем написать на ассемблер функцию принимающую параметр и возвращающую значение. Для примера напишем функцию calcSumm() которая будет принимать целое число и возвращать сумму его цифр. Изменим наш код на С++ добавив в него информацию о функции calcSumm, ввод числа и собственно вызов функции. Добавим функцию в файл hello.asm, возвращаемое значение помещается в eax, параметры объявляются после ключевого слова PROC. Все параметры можно использовать в коде процедуры, они автоматически извлекутся из стека. Также в процедурах можно использовать локальные переменные. Вы не можете использовать эти переменные вне процедуры. Они сохранены в стеке и удаляются при возврате из процедуры:

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

image

Отладка

Конечно в данной задаче нет ничего сложного и она вовсе не требует использования ассемблер. Более интересным будет рассмотреть, а что же нам дает Visual Studio для разработки на ассемблер. Попробуем включить режим отладки и установим точку остановки в hello.asm, запустим проект, мы увидим следующее:

image

Окно Disassembly (Debug > Windows > Disassembly) показываем команды ассемблер для данного объектного файла. Код который мы написали на С++ показывается черным цветом. Disassembled code показывается серым после соответствующего ему кода на C++/ассемблер. Окно Disassembly позволяет отлаживать код и осуществлять stepping по нему.

Окно регистров (Debug > Windows > Registers) позволяет посмотреть значение регистров.

Окно памяти (Debug > Windows > Memory) позволяет посмотреть дамп памяти, слева мы видим шестнадцатеричные адрес, справа шеснадцатеричные значения соответствующих ячеек памяти, можно перемещаться, вводя адрес в соответствующее поле в верху окна.

Здравствуйте. Я плохо разбираюсь в ассемблере, но в одной из лабораторных столкнулся с необходимостью использовать его в качестве ассемблерной вставки и начал с простого: решил написать маленький цикл..

И вот такое у меня получилось на ассемблере:

__________________
Помощь в написании контрольных, курсовых и дипломных работ здесь

Ассемблерная вставка в с++
Пытался сделать асм вставку для вычислений "Столкновений"(Collision). Вроде написал все как было в.

Вывести результат (С++ и ассемблерная вставка)
Доброго времени суток!С++ и Ассемблер изучаю недавно,прошу помощи с проверкой написанного и.


Ассемблерная вставка на windows form C++
Допустим у меня код с ассемблерной вставкой, как можно реализовать идею ввода данных(a,mask) и.

Ассемблерная вставка в C++: посчитать, сколько слов в строке заканчивается на гласные буквы: a, о, е, и
Дана строка. Необходимо написать ассемблерную вставку, которая посчитает сколько слов в строке.

vlad4475, на Асме у тебя, видимо, должно быть как-то так, например ;о):
Я понимаю, что можно писать и так, но дело в том, что мне придётся писать более сложные функции и желательно вообще переводить функции с С++ на ассемблер какими то средствами.. И мне важно понять, что именно я сделал не так

Решение

vlad4475, это уже третий вопрос ;о) Первый и второй ты сформулировал так, например:

(надеюсь, модератор в этом мрачном логове киборга тоже считает, например ;о)

А вообще, для начала можно, видимо, посмотреть, как переводит в Асм твой компилятор (для чего получить Асм-листинг, например ;о) А потом ручками рихтовать всю эту гнусность (доводя до бриллиантового Асм-совершенства, например ;о)

Для работы со стеком используется ЕВР, а не ЕВХ, разнница в сегментных регистрах подключенных по умолчанию - в первом случае SS, во втором DS.

Решение

Constantin Cat, замечание верное, но под виндой DS=SS всё равно, так что такой вариант хоть и не совсем корректный, но тоже рабочий.
А ещё почему-то идёт адресация в "свободную" область стека [ebx-4], а не [ebx+4]. Во-первых, X мы так не получим, а во-вторых, любая запись в стек потрёт эти значения (как минимум [ebx-4]).
Мне непонятно зачем тут edi и что такое [ebx-20] ?

Создание проекта консольного или оконного Windows-приложения не отличается от рассмотренного для языков программирования Си и C++.

После того, как в Visual Studio появилось окно проекта (в левой части появившегося окна отображается Обозреватель решений), для добавления нового файла программы в проект выбираем по правой кнопке мыши на папке Файлы исходного кода меню Добавить->Создать элемент.
В появившемся окне выбираем Файл C++ (.cpp), задаем имя файла и вручную добавляем к нему расширение asm. Нажимаем кнопку Добавить.

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


Далее необходимо сообщить среде разработки, что данный файл является программой на языке ассемблера, и для корректного включения его в проект требуется использовать Microsoft Macro Assembler. Для этого выбираем для проекта (по правой клавише мыши) опцию Настройки построения.

Настройки построения

Microsoft Macro Assembler

В появившемся окне ставим галочку для masm (Microsoft Macro Assembler) и нажимаем OK.
Теперь нужно проверить, что для файла на языке ассемблера установился соответствующий инструмент сборки. По правой кнопке мыши для файла с расширением .asm выбираем опцию Свойства.

Свойства проекта

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

Инструмент сборки

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

Построить решение

В случае успешного построения в нижней части окна отображается Построение: успешно 1.

Построить решение

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

Запуск приложения на выполнение

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

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

Изменить тип приложения с консольного на оконное

Изменение платформы приложения

Чтобы убрать консоль (поменять тип приложения с консольного на оконное, или наоборот) необходимо обратиться к меню Свойства проекта, вызванного по правой кнопке мыши.
В появившемся окне выбрать раздел Компоновщик->Система, и в разделе Подсистема поменять тип с Консоль на Windows (или наоборот).

Выбор консольного или оконного приложения

Запуск оконного приложения

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

Подсветка синтаксиса языка ассемблера

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

C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE

Для подключения подсветки синтаксиса выбираем меню Сервис->Параметры

После перезапуска Microsoft Visual Studio Express 2010 подсветка синтаксиса языка ассемблера будет активна.

Читайте также: