Как перевести ассемблер в си
Связь ассемблера с языками высокого уровня
Существуют следующие формы комбинирования программ на языках высокого уровня с ассемблером:
Встроенный ассемблер
При написании ассемблерных вставок используется следующий синтаксис:
КодОперации задает команду ассемблера,
операнды – это операнды команды.
В конце записывается ;, как и в любой команде языка Си.
Комментарии записываются в той форме, которая принята для языка Си.
Если требуется в текст программы на языке Си вставить несколько идущих подряд команд ассемблера, то их объединяют в блок:
Внутри блока текст программы пишется с использованием синтаксиса ассемблера, при необходимости можно использовать метки и идентификаторы. Комментарии в этом случае можно записывать как после ;, так и после //.
Использование внешних процедур
Для связи посредством внешних процедур создается многофайловая программа. При этом в общем случае возможны два варианта вызова:
| Соглашение | Параметры | Очистка стека | Регистры |
| Pascal (конвенция языка Паскаль) | Слева направо | Процедура | Нет |
| C (конвенция С) | Справа налево | Вызывающая программа | Нет |
| Fastcall (быстрый или регистровый вызов) | Слева направо | Процедура | Задействованы три регистра (EAX,EDX,ECX), далее стек |
| Stdcall (стандартный вызов) | Справа налево | Процедура | Нет |
Конвенция Pascal заключается в том, что параметры из программы на языке высокого уровня передаются в стеке и возвращаются в регистре АХ/ЕАХ, — это способ, принятый в языке PASCAL (а также в BASIC, FORTRAN, ADA, OBERON, MODULA2), — просто поместить параметры в стек в естественном порядке. В этом случае запись
Конвенция С используется, в первую очередь, в языках С и C++, а также в PROLOG и других. Параметры помещаются в стек в обратном порядке, и, в противоположность PASCAL-конвенции, удаление параметров из стека выполняет вызывающая процедура.
Запись some_proc(a,b,c,d)
будет выглядеть как
Вызванная таким образом процедура может инициализироваться так:
Трансляторы ассемблера поддерживают и такой формат вызова при помощи полной формы директивы proc с указанием языка С:
В случае если стек был задействован, освобождение его возлагается на вызываемую процедуру.
В случае быстрого вызова транслятор Си добавляет к имени значок @ спереди, что искажает имена при обращении к ним в ассемблерном модуле.
Возврат результата из процедуры
Чтобы возвратить результат в программу на С из процедуры на ассемблере, перед возвратом управления в вызываемой процедуре (на языке ассемблера) необходимо поместить результат в соответствующий регистр:
| Тип возвращаемого значения | Регистр |
| 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).
Чтобы построить проект в Microsoft Visual Studio Express 2010, совместив в нем файлы, написанные на разных языках программирования, выполняем следующие действия.



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

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

Результат построения отображается в нижней части окна проекта.
Работа с аргументами вещественного типа
При вызове функции с аргументами вещественного типа конфигурация проекта ничем не отличается от описанной выше. Для передачи аргументов необходимо указать их тип.
| тип Си | Количество байт | Тип аргумента ассемблера |
| float | 4 | dword |
| double | 8 | qword |
Возвращаемое вещественное значение по умолчанию располагается в вершине стека сопроцессора st(0).
Ассемблерная вставка в Си
Пытаюсь разобраться, как вставить код на ассемблере в Си код. Беглый поиск по гугл лишь запутал. Попытка что-то скомпилировать из написанного не увенчалась успехом. Может ли кто-нибудь мне привести два варианта рабочего кода, для linux/windows (x64), который бы выражал следующие идеи:
п.с. Я правильно понимаю, что для этих целей нужно использовать GAS? Нет никакой возможности заставить компилятор понимать вставки на NASM?
2 ответа 2
По-моему, более правильный путь использования ассемблера в Си, не вставками и смешиванием Си и ассемблера, а линковкой скомпилированного ассемблера.
Т.е. пишите на любимом NASM (к примеру) в файл test.asm :
Далее, вызываете эту функцию из Си:
При сборке не забывайте указать линковщику, чтобы он линковался с test.o :
Чтобы собрать тоже самое под Linux, вам нужно будет привести ассемблерный код в соответствие с соглашением о передаче параметров в Unix 64-bit (отличается от соглашения для Win64) и указать NASM другой выходной формат:
О соглашениях передачи параметров и об особенностях написания 64-битного кода, в NASM посвящена отдельная глава в документации: Writing 64-bit Code (Unix, Win64)
Синтаксис оператора следующий:
Текст вставки представляет собой строковую константу с ассемблерными инструкциями. В нем могут находиться не только ассемблерные инструкции, но и любые директивы ассемблера GAS.
Операнд имеет следующий вид:
имя_переменной — ни что иное, как имя C-переменой, значение которой вы хотите использовать в ассемблерном коде.
ограничение_типа — строковая константа, описывает допустимый тип операнда.
Итак, этого достаточно, чтобы написать ваш пример:
В общем-то писал по этому документу. Неплохая информация представлена здесь.
Постигаем Си глубже, используя ассемблер
Вдохновением послужила эта статья: Разбираемся в С, изучая ассемблер. Продолжение так и не вышло, хотя тема интересная. Многие бы хотели писать код и понимать, как он работает. Поэтому я запущу цикл статей о том, как выглядит Си-код после декомпиляции, попутно разбирая основные структуры кода.
От читающих потребуются хотя бы базовые знания в следующих вещах:
Что будем использовать?
При более основательном подходе к изучению, лучше пользоваться оффлайн версиями компиляторов, можете взять связку из актуального gcc, OllyDbg и NASM. Отличия должны быть минимальны.
Простейшая программа
Эта статья не стремится повторить ту, которую я приводил в самом начале. Но начинать нужно с азов, поэтому часть материала будет вынуждено пересекаться. Надеюсь на понимание.
Первое, что нужно усвоить, компилятор даже при оптимизации нулевого уровня (-O0), может вырезать код, написанный программистом. Поэтому код следующего вида:
Ничем не будет отличаться от:
Поэтому придется писать таким образом, чтобы при декомпиляции мы, все же, увидели превращение нашего кода во что-то осмысленное, поэтому примеры могут выглядеть, как минимум странно.
Второе, нам нужны флаги компиляции. Достаточно двух: -O0 и -m32. Этим мы задаем нулевой уровень оптимизации и 32-битный режим. С оптимизаций должно быть очевидно: нам не хочется видеть интерпретацию нашего кода в asm, а не оптимизированного. С режимом тоже должно быть очевидно: меньше регистров — больше внимания к сути. Хотя эти флаги я буду периодически менять, чтобы углубляться в материал.
Таким образом, если вы пользуетесь gcc, то компиляция может выглядеть так:
Соответственно, если вы пользуетесь godbolt, то вам нужно указать эти флаги в строку ввода рядом с выбором компилятора. (Первые примеры я демонстрирую на gcc 4.4.7, потом поменяю на более поздний)
Теперь, можно посмотреть первый пример:
Итак, следующий код соответствует этому:
Первые две строчки соответствую прологу функции (точнее три, но третью хочу пояснить сейчас), и мы их разберем в статье о функциях. Сейчас просто не обращайте на них внимание, тоже самое касается последних 3х строчек. Если вы не знаете asm, давайте смотреть, что означают эти команды.
Инструкции ассемблера имеют вид:
mnemonic dst, src
т. е.
инструкция получатель, источник
Тут нужно оговориться, что AT&T-синтаксис имеет другой порядок, и потом мы к нему еще вернемся, но сейчас нас интересует синтаксис схожий с NASM.
Начнем с инструкции mov. Эта инструкция перемещает из памяти в регистры или из регистров в память. В нашем случае она перемещает число 1 в регистр ebx.
Давайте кратко о регистрах: в архитектуре x86 восемь 32х битных регистров общего назначения, это значит, что эти регистры могут быть использованы программистом (в нашем случае компилятором) при написании программ. Регистры ebp, esp, esi и edi компилятор будет использовать в особых случаях, которые мы рассмотрим позже, а регистры eax, ebx, ecx и edx компилятор будет использовать для всех остальных нужд.
Таким образом mov ebx, 1, прямо соответствует строке register int a = 1;
И означает, что в регистр ebx было перемещено значение 1.
А строчка mov eax, ebx, будет означать, что в регистр eax будет перемещено значение из регистра ebx.
Есть еще две строчки push ebx и pop ebx. Если вы знакомы с понятием «стек», то догадываетесь, что сначала компилятор поместил ebx в стек, тем самым запомнил старое значение регистра, а после окончания работы программы, вернул из стека это значение обратно в регистр ebx.
Почему компилятор помещает значение 1 из регистра ebx в eax? Это связано с соглашением о вызовах функций языка Си. Там несколько пунктов, все они нас сейчас не интересуют. Важно то, что результат возвращается в eax, если это возможно. Таким образом понятно, почему единица в итоге оказывается в eax.
Но теперь логичный вопрос, а зачем понадобился ebx? Почему нельзя было написать сразу mov eax, 1? Все дело в уровне оптимизации. Я же говорил: компилятор не должен вырезать наш код, а мы написали не return 1, мы использовали регистровую переменную. Т. е. компилятор сначала поместил значение в регистр, а затем, следуя соглашению, вернул результат. Поменяйте уровень оптимизации на любой другой, и вы увидите, что регистр ebx, действительно, не нужен.
Кстати, если вы пользуетесь godbolt, то вы можете наводить мышкой на строку в Си, и вам подсветится соответствующий этой строке код в asm, при условии, что эта строка выделена цветом.
Усложним пример и перестанем пользоваться регистровыми переменными (Вы же их нечасто используете?). Посмотрим во что превратится такой код:
Опять же, пропустим верхние 3 строчки и нижние 2. Теперь у нас переменная а локальная, следовательно память ей выделяется на стеке. Поэтому мы видим следующую магию: DWORD PTR [ebp-8], что же она означает? DWORD PTR — это переменная типа двойного слова. Слово — это 16 бит. Термин получил распространение в эпоху 16-ти битных процессоров, тогда в регистр помещалось ровно 16 бит. Такой объем информации стали называть словом (word). Т. е. в нашем случае dword (double word) 2*16 = 32 бита = 4 байта (обычный int).
В регистре ebp содержится адрес на вершину стека для текущей функции (мы к этому еще вернемся, потом), поэтому он смещается на 4 байта, чтобы не затереть сам адрес и дописывает значение нашей переменной. Только, в нашем случае он смещается на 8 байт для переменной a. Но если вы посмотрите на код ниже, то увидите, что переменная b лежит со смещением в 4 байта. Квадратные скобки означают адрес. Т. е. это строка работает следующим образом: на основе адреса, хранящегося в ebp, компилятор помещает значение 1 по адресу ebp-8 размера 4 байта. Почему минус восемь, а не плюс. Потому что плюсу бы соответствовали параметры, переданные в эту функцию, но опять же, обсудим это позже.
Следующая строка перемещает значение 1 в регистр eax. Думаю, это не нуждается в подробных объяснениях.
Далее у нас новая инструкция add, которая осуществляет добавление (сложение). Т. е. к значению в eax (1) добавляется 5, теперь в eax находится значение 6.
После этого нужно переместить значение 6 в переменную b, что и делается следующей строкой (переменная b находится в стеке по смещению 4).
Наконец, нам нужно вернуть значение переменной b, следовательно нужно переместить
значение в регистр eax (mov eax, DWORD PTR [ebp-4]).
Если с предыдущим все понятно, то можно переходить, к более сложному.
Интересные и не очень очевидные вещи.
Что произойдет, если мы напишем следующее: int var = 2.5;
Каждый из вас, я думаю, ответит верно, что в var будет значение 2. Но что произойдет с дробной частью? Она отбросится, проигнорируется, будет ли преобразование типа? Давайте посмотрим:
Компилятор сам отбросил дробную часть за ненадобностью.
Что произойдет, если написать так: int var = 2 + 3;
И мы узнаем, что компилятор сам способен вычислять константы. А в данном случае: так как 2 и 3 являются константами, то их сумму можно вычислить на этапе компиляции. Поэтому можно не забивать себе голову вычислением таких констант, компилятор может сделать работу за вас. Например, перевод в секунды из часов можно записать, как hours * 60 * 60. Но скорее, в пример тут стоит поставить операции над константами, которые объявлены в коде.
Что произойдет, если напишем такой код:
Интересно, не правда ли? Компилятор решил не пользоваться операцией умножения, а просто сложил два числа, что и есть — умножить на 2. (Я уже не буду подробно описывать эти строки, вы должны понять их, исходя из предыдущего материала)
Вы могли слышать, что операция «умножение» выполняется дольше, чем операция «сложение». Именно по этим соображениям компилятор оптимизирует такие простые вещи.
Но усложним ему задачу и напишем так:
Пусть вас не вводит в заблуждение использование нового регистра edx, он ничем не хуже eax или ebx. Может понадобиться время, но вы должны увидеть, что единица попадает в регистр edx, затем в регистр eax, после чего значение eax складывается само с собой и после уже добавляется еще одна единица из edx. Таким образом, мы получили 1+1+1.
Знаете, бесконечно он так делать не будет, уже на *4, компилятор выдаст следующее:
Итак, у нас новая инструкция sal, что же она делает? Это двоичный сдвиг влево. Эквивалентно следующему коду в Си:
Для тех, кто не очень понимает, как работает этот оператор:
0001 сдвигаем влево (или добавляем справа) на два нуля: 0100 (т. е. 4 в 10ой системе счисления). По своей сути сдвиг влево на 2 разряда — это умножение на 4.
Забавно, что если вы умножите на 5, то компилятор сделает один sal и один add, можете сами потестировать разные числа.
На 22, компилятор на godbolt.org сдается и использует умножение, но до этого числа он пытается выкрутиться самыми разными способами. Даже вычитание использует и еще некоторые инструкции, которые мы еще не обсуждали.
Ладно, это были цветочки, а что вы думаете по поводу следующего кода:
Если вы ожидаете вычитания, то увы — нет. Компилятор будет выдавать более изощренные методы. Операция «деление» еще медленнее умножения, поэтому компилятор будет также выкручиваться:
Следует сказать, что для этого кода я выбрал компилятор существенно более поздней версии (gcc 7.2), до этого я приводил в пример gcc 4.4.7. Для ранних примеров существенных отличий не было, для этого примера они используют разные инструкции в 5ой строчке кода. И пример, сгенерированный 7.2, мне сейчас легче вам объяснить.
Стоит обратить внимание, что теперь переменная a находится в стеке по смещению 4, а не 8 и сразу же забыть об этом незначительном отличии. Ключевые моменты начинаются с mov edx, eax. Но пока пропустим значение этой строки. Инструкция shr осуществляет двоичный сдвиг вправо (т. е. деление на 2, если бы было shr edx, 1). И тут некоторые смогут подумать, а почему, действительно, не написать shr edx, 1, это же то, что делает код в Си? Но не все так просто.
Давайте проведем небольшую оптимизацию и посмотрим на что это повлияет. В действительности, мы нашим кодом выполняем целочисленное деление. Так как переменная «a» является целочисленным типом и 2 константа типа int, то результат никак не может получиться дробным по логике Си. И это хорошо, так как делить целочисленные числа быстрее и проще, но у нас знаковые числа, а это значит, что отрицательное число при делении инструкцией shr может отличаться на единицу от правильного ответа. (Это все из-за того, что 0 влезает по середине диапазона для знаковых типов). Если мы заменим знаковое деление на unsigned:
То получим ожидаемое. Стоит учесть, что godbolt опустит единицу в инструкции shr, и это не скомпилируется в NASM, но она там подразумевается. Измените 2 на 4, и вы увидите второй операнд в виде 2.
Теперь посмотрим на предыдущий код. В нем мы видим sar eax, это то же самое, что и shr, только для знаковых чисел. Остальной же код просто учитывает эту единицу, когда мы делим отрицательное число (или на отрицательное число, хотя код немного изменится). Если вы знаете, как представляются отрицательные числа в компьютере, вам будет не трудно догадаться, почему мы делаем сдвиг вправо на 31 разряд и добавляем это значение к исходному числу.
С делением на большие числа, все еще проще. Там деление заменяется на умножение, в качестве второго операнда вычисляется константа. Если вам будет интересно как, можете поломать над этим голову самостоятельно, там нет ничего сложного. Нужно просто понимать, как представляются вещественные числа в памяти.
Заключение
Для первой статьи материала уже больше, чем достаточно. Пора закруглятся и подводить итоги. Мы ознакомились с базовым синтаксисом ассемблера, выяснили, что компилятор может брать на себя простейшие оптимизации при вычислениях. Увидели разницу между регистровыми и стековыми переменными. И некоторые другие вещи. Это была вводная статья, пришлось много времени уделять очевидным вещам, но они очевидны не для всех, в будущем мы постигнем больше тонкостей языка Си.
Как перевести ассемблер в си
Иногда полезно некоторые куски кода писать на ассемблере, и при этом весь основной код должен быть написан на C. К примеру, некоторая часть программы очень критична ко времени выполнения, и нужно четко контролировать её выполнение. Компилятор C не может учесть все нюансы программы и архитектуры микроконтроллера, и не в состоянии сгенерировать максимально оптимальный код, поэтому приходится писать такой код вручную на ассемблере. Очень часто на ASM пишут обработчики прерывания для ускорения их выполнения. Другие же части программы, которые имеют сложный алгоритм, и скорость их выполнения некритична, рационально писать на C.
Общие причины, по которым приходится иногда писать части кода на ассемблере:
— Мало свободного места для переменных в RAM или для кода FLASH.
— Приложения, очень критичные ко времени выполнения.
— Специальное программирование, которое не может быть реализовано на C (что бывает очень редко).
[Как скомпилировать модуль C в модуль на ASM?]
[Какие регистры использует компилятор AVR GCC?]
Call-used registers (r18-r27, r30-r31). Регистры, используемые при вызовах функций. Могут быть заняты компилятором gcc для локальных данных (переменных). Вы свободно можете их использовать в подпрограммах на ассемблере, без необходимости сохранения и восстановления (не нужно их сохранять в стек командой push и извлекать из стека командой pop). Если же из кода ASM вызываются подпрограммы на C, то эти регистры могут портиться произвольным образом, так что вызывающий код ассемблера должен отвечать за сохранение и восстановление данных в этих регистрах.
Call-saved registers (r2-r17, r28-r29). Регистры, сохраняемые при вызовах функций. Могут быть выделены gcc для локальных данных (переменных). Вызовы подпрограмм C оставляют эти регистры неизменными. Подпрограммы на языке ассемблера также их должны сохранять при входе и восстанавливать при выходе (обычно с помощью стека операциями push и pop). Если эти регистры изменяются в коде, то r29:r28 (Y pointer) используется при необходимости как указатель на фрейм (frame pointer, указывает на локальные данные в стеке). Требования системы вызова для сохранения/восстановления содержимого этих регистров также относится и к ситуациям, когда компилятор использует эти регистры для передачи аргументов в функцию.
Fixed registers (r0, r1), фиксированные регистры. Никогда не выделяются gcc для локальных данных, но часто используются для определенных целей:
Function call conventions, соглашения о вызовах функций. Аргументы выделяются слева направо, в регистрах от r25 до r8. Все аргументы выравниваются для начала с четно нумерованных регистров (нечетные по размеру аргументы, включая char, имеют один свободный регистр перед собой). Это позволяет эффективнее использовать инструкцию movw на расширенном ядре. Если переменных слишком много, то те что не влезли в регистры, передаются через стек.
Внимание: такого выравнивания не было до 2000-07-01, включая старые патчи для gcc-2.95.2. Проверьте Ваши старые подпрограммы на ассемблере, и сделайте соответствующие исправления.
Совместное использование ассемблера и Си для AVR
При программировании вещей, критичных к быстродействию и размеру кода хорошо использовать ассемблер. При этом обычно не обязательно писать на нем весь код, достаточно реализовать наиболее “чувствительные” подпрограммы. Компилятор GCC и среда Atmel Studio позволяют использовать в проекте ассемблер и С одновременно. При этом возникает вопрос организации взаимодействия между подпрограммами на разных языках: вызова методов с передачей им параметров и доступа к переменным.
При совмещении языков Си и ассемблера в одном проекте могут возникнуть следующие вопросы:
Видимость функций
Чтобы Си-функции были видны из ассемблерного кода нужно использовать директиву .extern
Для того, чтобы видеть ассемблерные функции из Си, служит директива .global:
При этом, в Си-коде надо объявить эту ассемблерную функцию с ключевым словом extern:
Глобальные переменные
Для того, чтобы использовать общие глобальные переменные коде, их следует объявить в Си коде:
В ассемблерном коде доступ к переменным организуется аналогично доступу к функциям, директивой .extern:
Использование регистров
Написание ассемблерного кода для совместного использования с Си-кодом требует знаний о том, каким образом компилятор Си использует регистры.
Регистры r18-r27, r30, r31 могут свободно использоваться в коде. Компилятор Си не беспокоится о их сохранении. Поэтому, если ассемблерный код использует эти регистры и вызывает Си-функции, то он должен сам заботится о сохранении этих регистров перед вызовом и восстановлении после вызова.
Регистры r2-r17, r28, r29 не изменяются из Си-функций. Если ассемблерная функция, вызываемая из Си-кода использует регистры из этой группы, то она сама должна заботится о сохранении и восстановлении их содержимого. Регистры r29:r28 образуют регистровую пару Y, которая часто может не использоваться в Си-коде (т.е., если не используется C++). Точнее, они используются в процедуре инициализации __ctors_end для настройки указателя стека SP.
Регистр r1 всегда содержит ноль. Если ассемблерный метод модифицирует этот регистр, то он должен всегда занулять его перед возвращением в Си-код (например, командой ”clr r1”).
Регистр r0 используется компилятором в качестве временного хранилища. Если ассемблерный код использует этот регистр, и вызывает Си-методы, то он обязан сохранить и восстановить этот регистр, посколько тот может быть использован компилятором
Передача параметров
Если число параметров у метода фиксировано, то для передачи аргументов используются регистры r25-r8. Аргументы передаются слева-направо, каждый использует четное количество регистров, т.е., для передачи аргумента типа char/uint8_t будет зарезервировано два регистра (это сделано для того, чтобы компилятор мог использовать команду movw). Если регистров не хватит, часть аргументов будет переданы через стек. Последнее не лучшим образом сказывается на производительности, поэтому передачи больших объемов данных в функцию следует избегать.
Если параметр имеет переменное число аргументов, то они передаются через стек, справа-налево. Однобайтовые аргументы так же передаются как двубайтовые (при этом старший байт для них зануляется командой eor r25, r25).
Лучше всего понять, как передаются параметры и результат для функций можно рассмотрев примеры генерируемого компилятором кода. Например, для функции
Параметры a1, a2, a3, a4 будет передаваться в регистрах r24, r22, r20 и r18 соответственно.
Параметры передадуться через регистры r25:r24:r23:r22, r21:r20:r19:r18, r17:r16:r15:r14, r13:r12:r11:r10 соответственно.

