Введение в язык ассемблера. Размещение данных и операторов в тексте программы

высокого уровня

Несмотря на огромные возможности языков высокого уровня, иногда возникает необходимость применения Ассемблера в программах на ЯВУ. Наиболее распространены два подхода:

1) в тексте программы, написанной на языке высокого уровня, делаются ассемблерные вставки на встроенном ассемблере;

2) подключаются внешние ассемблерные модули: файл с процедурами пишется на внешнем ассемблере, компилируется в объектный файл OBJ, который подключается к проекту на ЯВУ.

Ассемблерные вставки применяются:

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

Для вызова команд, не используемых компилятором ЯВУ. С появлением новых поколений процессоров добавляются новые, более эффективные, машинные команды. Однако компиляторы ЯВУ при переводе программы с языка высокого уровня в машинные коды стараются использовать устаревшие команды i386-го, чтобы обеспечить максимальную совместимость со всеми, даже старыми, компьютерами. А если нужны специализированные команды новых процессоров (MMX, XMM, SSE, SSE II), приходится писать на ассемблере.

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

3.3. Встроенный ассемблер

Системы программирования Delphi, C++ Builder, Visual C++ позволяют вставлять в текст программы на языке высокого уровня участки кода, написанные на ассемблере. Этот ассемблер называется встроенным, и имеет незначительные синтаксические отличия от ассемблера TASM или MASM. Подробнее про него можно прочитать в справочной системе ЯВУ. Здесь приводятся краткие сведения.

    C ++ Builder, Visual C++ :

Ассемблерный текст заключается в блок _asm{…}

cmp eax, SomeVariable

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

    Delphi :

Ассемблерные команды заключаются между словами asm end ;

cmp eax, SomeVariable

Внутри ассемблерного блока можно обращаться по именам к переменным, функциям, процедурам и меткам. Переменные, объявленные внутри блока директивами DB, DW и т.п. будут размещены в сегменте кода, а не данных. Это нужно учесть, чтобы компилятор не стал исполнять их значения как машинные коды – это может привести к ошибке исполнения программы.

Если внутри ассемблерного блока нужны переходы по меткам, их делают локальными – имена таких меток следует начинать с символа @. Область действия локальной метки ограничена ассемблерным блоком:

test SomeVariable , 0Fh

... // какие-то команды

@M1: // это локальная метка

3.4. Подключение внешних ассемблерных модулей в программы на языках высокого уровня

Этап 1 . Создание ассемблерного файла с экспортируемой процедурой. Чтобы подключение ассемблерного модуля к программе на ЯВУ было корректным, он должен удовлетворять правилам, в соответствии с которыми создает программу компилятор ЯВУ. Так, должны совпадать имена сегментов, конвенции вызова и т.д. Каркас ассемблерного модуля для подключения к Delphi, C++ Builder и Visual C++, приведен в листинге 1.

Листинг 1 – Каркас ассемблерного модуля для подключения к программам Win32.

.486 ; 32-разрядные приложения

. model flat ; в программах Win32 используется линейная модель

; памяти (flat)

. bss

; в этом сегменте описываются неинициализированные данные

. const

; в этом сегменте описываются типизированные константы

. data

; в этом сегменте описываются переменные с начальными значениями

. code

PUBLIC Имя_процедуры ; чтобы процедуру можно было вызывать из

; программы на ЯВУ, её нужно объявить экспортируемой

; реализация процедуры

Имя_процедуры proc near ; все процедуры – ближние

push ebp ; если процедура с параметрами, то в начале процедуры

; регистр

mov ebp , esp ; затем установить ebp = esp для обращения к параметрам

; здесь тело процедуры

pop ebp ; в конце восстанавливаем ebp

ret N ; если процедура освобождает стек из-под параметров сама, то

; N – число байтов, которое занимает стековый кадр,

; N всегда кратно 4.

Имя_процедуры endp

end ; конец модуля

Директива PUBLIC применяется, чтобы сделать имя процедуры «видимой» за пределами модуля, чтобы её «нашел» компилятор ЯВУ.

Компиляторы С++ различают в именах функций большие и маленькие буквы. Delphi, обычно не чувствительная к регистру, при импорте процедур из внешних OBJ и DLL файлов, буквы большие и малые различает. Поэтому при объявлении процедуры в ассемблерном модуле её имя следует писать так, как оно будет вызываться в ЯВУ.

Замечания по написанию тела процедуры – в п.3.5.

Этап 2 . Компиляция ассемблерного модуля в файл OBJ.

При компиляции следует использовать ключ /ml , чтобы заглавные и строчные буквы различались:

TASM.EXE /ml имя_файла .asm

Если этого не сделать, то процедуру MyProc язык высокого уровня увидит как MYPROC , поскольку TASM по умолчанию все экспортируемые имена записывает заглавными буквами.

Этап 3 . Подключение объектного файла к программе на ЯВУ.

!!! Предварительно объектный файл скопируйте в папку с проектом программы на ЯВУ.

    C ++ Builder :

Объектный модуль можно подключить тремя способами:

1) добавить к проекту через меню Project / Add To Project, тип файлов "OBJ".

2) директивой

#pragma link "имя_файла.obj"

3) директивой

USEOBJ("имя_файла.obj")

    Visual C++

В VC++ 6.0 имя OBJ-файла нужно вписать в параметры командной строки компоновщика: для этого выбрать меню Project / Settings, открыть закладку Link и вписать имя OBJ-файла в параметры командной строки “Object/Library modules”.

В VC++.NET выбирается меню Project/Properties, в дереве свойств папка C/C++ → Command Line и имя OBJ-файла вписывается в строку “Addition options”.

    Delphi :

Для подключения объектного файла в начало программы (между строками Program … или Unit … и строкой Uses …) добавляется директива:

{$LINK имя_файла.obj}

или {$L имя_файла.obj}

Этап 4 . Объявление в программе импортируемой процедуры.

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

    C ++ Builder , Visual C++ :

Заголовок (прототип) ассемблерной функции объявляется с квалификаторами extern C ” :

extern "C" int __stdcall MyFunc(int x,y);

Если внешних функций несколько, их можно перечислить внутри секции extern :

extern "C"{

int __stdcall MyFunc(int x,y);

int __cdecl YourFunc(void* param);

Опция extern указывает компилятору, что тело функции следует искать в объектных файлах или библиотеках. Квалификатор "С" нужен, чтобы компилятор С++ не искажал имена функций (иначе он «от-себя» добавляет в имя @, подчеркивания и пр., и в таком виде пытается найти имя в подключенных модулях).

    Delphi :

В секции описания процедур и функций помещается заголовок ассемблерной подпрограммы, а вместо тела пишется зарезервированное слово external . Так компилятор понимает, что тело подпрограммы нужно искать во внешнем OBJ-файле. Например

Procedure MyProc(X,Y:integer); stdcall; external ;

Function MyFunc(X,Y:integer):integer; cdecl; external ;

Если предполагается, что внешняя подпрограмма имеет переменное количество аргументов, то записывается директива varargs . Эту директиву можно использоваться только совместно с конвенцией cdecl .


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

Дело в том, что инструкции процессора Интел и синтаксис вставок ассемблерного кода в программы на 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("Здесь код на ассемблере" );

/* помещает содержимое ecx в eax */ asm("movl %ecx %eax"); /* помещает байт из bh в память, на которую указывает eax */ __asm__("movb %bh (%eax)");

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

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

Asm__ ("movl %eax, %ebx\n\t" "movl $56, %esi\n\t" "movl %ecx, $label(%edx,%ebx,$4)\n\t" "movb %ah, (%ebx)");

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

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

Asm (assembler template: output operands /* не обязательно */ : input operands /* не обязательно */ : list of clobbered registers /* не обязательно */);

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

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

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

Int x = 0, y = 0; cout

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

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

Int y = 15, z = 10; cout

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

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

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

6 ответов

Вы можете получить доступ к переменным по их имени и скопировать их в регистры. Вот пример из MSDN:

Int power2(int num, int power) { __asm { mov eax, num ; Get first argument mov ecx, power ; Get second argument shl eax, cl ; EAX = EAX * (2 to the power of CL) } // Return with result in EAX }

Компилятор microsoft очень плохо оптимизируется при подключении встроенной сборки. Он должен создавать резервные копии регистров, потому что если вы используете eax, тогда он не переместит eax в другой свободный регистр, он будет продолжать использовать eax. Ассемблер GCC намного продвинулся вперед на этом фронте.

Чтобы обойти это, Microsoft предложила intrinsics . Это гораздо лучший способ сделать вашу оптимизацию, поскольку она позволяет компилятору работать с вами. Поскольку Крис упоминал, что встроенная сборка не работает под x64 с компилятором MS, поэтому на этой платформе вы ДЕЙСТВИТЕЛЬНО лучше просто используете встроенные средства.

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

В реестрах ничего нет. как выполняется блок _asm. Вам нужно переместить материал в регистры. Если есть переменная: "a", тогда вам нужно

Asm { mov eax, [a] }

Стоит отметить, что VS2010 поставляется с ассемблером Microsoft. Щелкните правой кнопкой мыши по проекту, перейдите к правилам сборки и включите правила сборки ассемблера, а среда IDE обработает файлы.asm.

это несколько лучшее решение, так как VS2010 поддерживает 32-битные и 64-битные проекты, а ключевое слово __asm ​​НЕ работает в 64-битных сборках. Вы ДОЛЖНЫ использовать внешний ассемблер для 64-битного кода:/

Я предпочитаю писать целые функции в сборке, а не с помощью сборки inline . Это позволяет поменять языковые функции высокого уровня на сборку в процессе сборки. Кроме того, вам не нужно беспокоиться о том, что оптимизация компилятора мешает.

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

Если вам нужно вставить встроенную сборку для большой функции, создайте новую функцию для кода, который необходим для встроенного. Снова замените на С++ или сборку во время сборки.

Это мои предложения, ваш пробег май-Вар (YMMV).

Мне очень нравится сборка, поэтому я не собираюсь быть здесь. Похоже, что вы профилировали свой код и нашли "горячую точку", что является правильным способом запуска. Я также предполагаю, что 200 строк, о которых идет речь, не используют много конструкций высокого уровня, таких как vector .

Все, что сказал: если бы я был вами, я бы прошел через этот код в отладчике VS, используя представление "Разборка". Если вам комфортно читать код, когда вы идете вперед, это хороший знак. После этого выполните компиляцию Release (Debug отключит оптимизацию) и создайте список ASM для этого модуля. Тогда, если вы думаете, что видите место для улучшения... у вас есть место для начала. Ответы других людей связаны с документацией MSDN, которая действительно очень скудная, но все же разумное начало.

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

Мы рассмотрим две возможности стыковки Си и Ассемблера: использование команд на языке Ассемблера прямо в тексте программы, написанной на языке Си, и вызов из программы на языке Си подпрограммы, написанной на языке ассемблера.

Встроенный ассемблерный код.

Рассмотрим самый простой пример

#include

Void main()

{ int TestValue;

printf("Input TestValue\n");

scanf("%d", &TestValue);

asm inc word ptr TestValue

printf("Incremented %d\n",TestValue);

Ключевое слово asmозначает, что за ней следует строка на языке Ассемблера. Точку с запятой - разделитель операторов в языке Си - ставить не нужно. Вызывает удивление присутствие атрибутного оператораword ptr . Зачем он нужен, если в тексте программы указано, что TestValue имеет типint.

Воспользуемся компилятором командной строки.

Ключ -BозначаетCompileviaassemble- компиляция посредством ассемблирования. Файлincr.cпреобразуется во временный файлincr.asm. Далееtccвызывает ассемблерtasm.exe, который создаёт объектный файл. Далее вызывается компоновщик.tccдолжен знать, где находитсяtasm. Поэтому, если кtasm.exeне "проложено дорожки" (path), то её нужно явно указать в файлеturboc.cfg, расположенном в текущей директории. Для нашего ВЦ этот файл должен быть таким

Вместо ключа -Bможно было вставить в текст программы в качестве первой строки директиву #pragmainline.

Как посмотреть сгенерированный ассемблерный код. Для этого укажем ключ -S - produce assemble output.

Тогда на диске создаётся файл incr.asm. В нём находим строку

inc word ptr

Переменная TestValueсоздаётся в автоматической памяти, т.е. в стеке. Как мы видели ранее, такие переменные адресуются с помощьюbp, причём отсчёт идёт в сторону уменьшения адресов. В приведенной выше команде атрибутный оператор необходим, т.к. неясно, на что ссылаетсяbp-2 - на слово или байт.

Ключ -Sполезен для изучения ассемблерного аналога исходного текста на языке Си. Но можно обойтись и без него.

В BorlandC++ 3.1 появился встроенный (built-in) ассемблер. Если не указать ключ -Bпри вызовеbcc, то используется именно он. Встроенный ассемблер не использует макросов, режимаIDEAL, инструкций 386-го процессора (впрочем, уже естьBorlandC++ 5.01).

Начиная с BorlandC++ 3.1 можно заключать группу ассемблерных команд в фигурные скобки и помещать перед ними ключевое словоasm.

Ограничения на встроенное ассемблирование.

    Команды перехода могут ссылаться только на метки Си

    Остальные команды могут иметь любые операнды кроме меток Си

    В начале ассемблерного фрагмента нужно сохранять, а в конце восстанавливать регистры BP,SP,CS,DS,SS(разумеется, если они претерпевают изменения). Если возникают сомнения, полезно использовать ключ -Sи смотреть ассемблерный код в целом.

Недостатки встроенного ассемблерного кода

    компилятор не оптимизирует код текста программы на Си,

    нет мобильности (нельзя перенести программу на другой тип процессора),

    медленнее выполняется компиляция,

    затруднена отладка.

В VisualC++ 6.0 используется ключевое слово__ asm (обратите внимание, чтоasm предшествует два символа подчёркивания).

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

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

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

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

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

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

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

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

#include void main () { printf("Hello, what is your name?\n"); } void* readName() { char name; scanf("%s", &name); return &name; }

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

Extern "C" { void sayHello(); }

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

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

686 .MODEL FLAT, C .STACK .DATA helloFormat BYTE "Hello %s!",10,13,0 .CODE readName PROTO C printf PROTO arg1:Ptr Byte, printlist: VARARG sayHello PROC invoke readName invoke printf, ADDR helloFormat, eax ret sayHello ENDP END

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

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

686 .MODEL FLAT, C .STACK .DATA helloFormat BYTE "Hello %s!",10,13,0 .CODE readName PROTO C printf PROTO arg1:Ptr Byte, printlist: VARARG sayHello PROC invoke readName invoke printf, ADDR helloFormat, eax ret sayHello ENDP calcSumm PROC a:DWORD xor esi, esi mov eax, a mov bx, 10 @div: xor edx, edx div bx add esi, edx cmp ax, 0 jne @div mov eax, esi ret calcSumm ENDP END

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

Отладка

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

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

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

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