Как использовать ассемблер в c
Перейти к содержимому

Как использовать ассемблер в c

  • автор:

Взаимодействие с кодом на C/C++

Ассемблер MASM позволяет использовать гибридный подход, при котором часть программы пишется на ассемблере, а часть — на С/С++. Это позволяет сократить и упростить программу. Например, определим файл app.c с кодом на языке C:

А в файле app.c определим следующий код:

#include // подключаем внешнюю функцию hello из файла hello.asm extern void hello(void); int main()

Это обычная программа на языке С, в которой с помощью оператора extern подключаем некоторую внешнюю функцию hello, которая ничего не возвращает и не принимает никаких параметров. В функции main вызываем эту внешнюю функцию hello.

Теперь в той же папке определим файл hello.asm , который будет содержать следующий код на ассемблере:

.code option casemap:none ; определяем процедуру hello public hello hello proc ret ; возвращаемся в вызывающий код hello endp end

Секция .code здесь начинается с директивы option

option casemap:none

Директива option указывает MASM сделать все символы чувствительными к регистру. Это необходимо, потому что MASM по умолчанию нечувствителен к регистру и отображает все идентификаторы в верхнем регистре (поэтому hello() станет HELLO()). Но язык C чувствителен к регистру и рассматривает hello() и HELLO() как два разных идентификатора.

Далее идет определение функции hello. С помощью оператора public указываем, что функция hello будет видна вне исходного/объектного файла MASM:

public hello

Без этого оператора функция hello() была бы доступна только внутри текущего файла, и при компиляции компилятор не смог бы ее найти.

Затем между операторами hello proc и hello endp определяется функция hello, которая ничего не делает.

Скомпилируем оба файла — app.c и hello.asm в одну программу. Рассмотрим напримере ассемблера MASM64 и компилятора для Visual C++ в Visual Studio. Для компиляции откроем программу x64 Native Tools Command Prompt for VS 2022 , которая устанавливается вместе с Visual Studio, и перейдем в ней к папке, где располагаются файлы. Затем выполним следующюю команду

ml64 /c hello.asm

Опция /c говорит, что файл hello.asm надо только скомпилировать в объектный файл. И после этой команды в папке программы будет скомпилирован файл hello.obj :

********************************************************************** ** Visual Studio 2022 Developer Command Prompt v17.5.5 ** Copyright (c) 2022 Microsoft Corporation ********************************************************************** [vcvarsall.bat] Environment initialized for: 'x64' C:\Program Files\Microsoft Visual Studio\2022\Community>cd c:\asm c:\asm>ml64 /c hello.asm Microsoft (R) Macro Assembler (x64) Version 14.35.32217.1 Copyright (C) Microsoft Corporation. All rights reserved. Assembling: hello.asm c:\asm>

Далее используем скомпилируем всю программу с помощью команды:

cl app.c hello.obj

В итоге будет скомпилирован файл app.exe, который мы можем запустить и который при запуске выполнит ассемблерную функцию hello:

c:\asm>cl app.c hello.obj Microsoft (R) C/C++ Optimizing Compiler Version 19.35.32217.1 for x64 Copyright (C) Microsoft Corporation. All rights reserved. app.c Microsoft (R) Incremental Linker Version 14.35.32217.1 Copyright (C) Microsoft Corporation. All rights reserved. /out:app.exe app.obj hello.obj c:\asm>app Hello starts Hello ends c:\asm>

Вывод строки на консоль

Программа на C может вызывать код из файла на ассемблере, так и код на ассемблере может вызывать функции языка С. Например, используем стандартную функцию printf() для вывода строки на консоль в программе на ассемблере. Для этого определим в файле hello.asm следующий код:

option casemap:none .data text byte "Hello, world!", 10, 0 ; определяем выводимые данные .code ; подключаем определение функции printf() из C/C++ externdef printf:proc ; определяем процедуру hello public hello hello proc sub rsp, 40 ; резервируем в стеке 40 байт lea rcx, text ; в регистр rcx загружаем адрес строки text call printf ; вызываем функцию printf add rsp, 40 ; восстанавливаем значение в стеке ret ; возвращаемся в вызывающий код hello endp end

Здесь для хранения данных определяем секцию .data

.data text byte "Hello, world!", 10, 0 ; определяем выводимые данные

Здесь определена строка text, которая представляет набор байт. Причем предпоследний байт — представляет число 10 — это числовой код перевода строки. Последний байт — 0, так как строки в Си должны завершаться нулевым байтом.

Для использования функции printf подключаем ее с помощью директивы.

externdef printf:proc

Директива externdef имеет следующую форму

externdef symbol:type

Где symbol — это внешный идентификатор, который мы хотим определить — в данном случае функция printf . А type — тип этого идентификатора (для функций это тип proc ).

Стоит отметить, что директива externdef должна применяться в секции .code

Функция printf() принимает как минимум один параметр — выводимую строку. Эта строка передается в функцию через регистр RCX. Для загрузки адреса строки в регистр rcx применяем инструкцию lea и затем вызываем функцию printf.

lea rcx, text call printf

Снова сначала скомпилируем код ассемблера из файла hello.asm с помощью команды:

ml64 /c hello.asm

И скомпилируем файл app.c в файл приложения с помощью команды:

cl app.c hello.obj

После компиляции запустим файл app.exe:

c:\asm>ml64 /c hello.asm Microsoft (R) Macro Assembler (x64) Version 14.35.32217.1 Copyright (C) Microsoft Corporation. All rights reserved. Assembling: hello.asm c:\asm>cl app.c hello.obj Microsoft (R) C/C++ Optimizing Compiler Version 19.35.32217.1 for x64 Copyright (C) Microsoft Corporation. All rights reserved. app.c Microsoft (R) Incremental Linker Version 14.35.32217.1 Copyright (C) Microsoft Corporation. All rights reserved. /out:app.exe app.obj hello.obj c:\asm>app Hello starts Hello, world! Hello ends c:\asm>

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

Пытаюсь разобраться, как вставить код на ассемблере в Си код. Беглый поиск по гугл лишь запутал. Попытка что-то скомпилировать из написанного не увенчалась успехом. Может ли кто-нибудь мне привести два варианта рабочего кода, для linux/windows (x64), который бы выражал следующие идеи:

#include int main(int argc, char *argv[]) < int a = 1; int b = 2; int c; __assembly__ < mov eax, a mov ebx, b add eax, ebx mov c, eax >printf("%d + %d = %d\n", a, b, c); return 0; > 

п.с. Я правильно понимаю, что для этих целей нужно использовать GAS? Нет никакой возможности заставить компилятор понимать вставки на NASM?

Отслеживать
8,992 2 2 золотых знака 19 19 серебряных знаков 34 34 бронзовых знака
задан 18 мая 2018 в 14:21
143 2 2 золотых знака 2 2 серебряных знака 10 10 бронзовых знаков

Для этих целей нужно использовать ГОЛОВУ и кучу разной документации, начиная от понимания что такое CPU architectural ABI и как его готовить.

18 мая 2018 в 15:12

@0andriy хм, дерзко. А вот Крис Касперски так не считал, когда советовал новичкам (в своих статьях) изучать ассемблер с привязкой к Си, ибо так куда проще и понятней. Там же и привёл рабочий код подобной программы, но видать рабочий для того времени. Наверно он просто баран. да? п.с. что-то мне подсказывает, что мне если покажут правильный синтаксис, то этот код магическим образом заработает. Без кучи разной докуметации, CPU, ABI, и их приготовления. И да, без головы тоже. Догадка.

18 мая 2018 в 16:10
18 мая 2018 в 16:14
18 мая 2018 в 16:14
Для GNU читайте тут (также обратите внимание на раздел Multiple assembler dialects in asm templates)
18 мая 2018 в 18:17

2 ответа 2

Сортировка: Сброс на вариант по умолчанию

Начинается оператор ассемблерной вставки с ключевого слова asm или __asm__ , после чего в круглых скобках следует ее описание.

Синтаксис оператора следующий:

asm ( текст_вставки : список_выходных_параметров : список_входных_параметров : список_разрушаемых_регистров ) 

Текст вставки представляет собой строковую константу с ассемблерными инструкциями. В нем могут находиться не только ассемблерные инструкции, но и любые директивы ассемблера GAS.

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

Операнд имеет следующий вид:

ограничение_типа (имя_переменной) 

имя_переменной — ни что иное, как имя C-переменой, значение которой вы хотите использовать в ассемблерном коде.

ограничение_типа — строковая константа, описывает допустимый тип операнда.

Для выходных операндов строка ограничения типа должна начинаться с символа = .

Итак, этого достаточно, чтобы написать ваш пример:

int main() < int a = 1; int b = 2; int c; asm(".intel_syntax noprefix\n\t" // директива GAS, включаем Intel синтаксис. "mov eax, %1\n\t" // перемещаем в eax значение переменной a. "add eax, %2\n\t" // прибавляем значение переменной b к eax. "mov %0, eax\n\t" // перемещаем в переменную c значение eax. :"=r"(c) // список выходных параметров. :"r"(a), "r"(b) // список входных параметров. : "eax" // список разрушаемых регистров. ); printf("%d + %d = %d\n", a, b, c); return 0; >

В общем-то писал по этому документу. Неплохая информация представлена здесь.

Как использовать ассемблер в c

Иногда полезно некоторые куски кода писать на ассемблере, и при этом весь основной код должен быть написан на C. К примеру, некоторая часть программы очень критична ко времени выполнения, и нужно четко контролировать её выполнение. Компилятор C не может учесть все нюансы программы и архитектуры микроконтроллера, и не в состоянии сгенерировать максимально оптимальный код, поэтому приходится писать такой код вручную на ассемблере. Очень часто на ASM пишут обработчики прерывания для ускорения их выполнения. Другие же части программы, которые имеют сложный алгоритм, и скорость их выполнения некритична, рационально писать на C.

Общие причины, по которым приходится иногда писать части кода на ассемблере:

— Мало свободного места для переменных в RAM или для кода FLASH.
— Приложения, очень критичные ко времени выполнения.
— Специальное программирование, которое не может быть реализовано на C (что бывает очень редко).

Есть два метода встроить код ассемблера в программу на C. Один метод — прямое встраивание операторов ассемблера в код C. Метод называется in-line ASM, описание этого метода приведено в документации на используемый компилятор (для каждого компилятора это может осуществляться по-разному). Как использовать inline ассемблер для компилятора GCC — см. в статье [4]. Как использовать inline ассемблер для компилятора IAR — см. в статье [5]. Второй метод — написать код ассемблера в отдельном файле и скомпилировать его отдельно в объектный код, а затем присоединить (link) объектный код к основной программе на этапе линковки. Второй метод и будет рассматриваться в этой статье.

Хороший пример использования ассемблера в виде отдельного модуля — код библиотеки V-USB и примеры firmware USB-устройств на микроконтроллерах AVR, которые используют эту библиотеку [2]. Код ассемблера здесь сосредоточен в модуле usbdrv\usbdrvasm.asm -> usbdrvasm.S. Он необходим для очень быстрой обработки прерывания INT0, используемого для отслеживания событий перепада сигнала шины данных USB (обычно это сигнал D+). Удивительно, что микроконтроллер AVR успевает обработать перепады сигнала с частотой шины USB и декодировать их на физическом уровне — и все это благодаря кодированию на языке ассемблера!

Оба метода вставки кода ассемблера — in-line и в виде отдельного ASM модуля — требуют тщательного изучения документации на компилятор. Во-первых, нужно знать синтаксис in-line ассемблера, а во-вторых, нужно знать, какие регистры и каким образом использует компилятор C для временных переменных, для передачи переменных в функции и возврата значений из функций (см. далее раздел «Какие регистры использует компилятор AVR GCC?»). Это нужно для того, чтобы программа на ассемблере правильно восстанавливала значение используемых регистров, и могла корректно принимать и возвращать значения данных. Упростить написание модуля на ассемблере может написание болванки на C и компиляция её в ассемблер (см. раздел «Как скомпилировать модуль C в модуль на ASM?»).

[Как скомпилировать модуль C в модуль на ASM?]

Хороший метод написания шаблона для модуля ASM — сделать модуль на C и скомпилировать его в код ассемблера. Все компиляторы умеют это делать, в том числе и компилятор AVR GCC. После того, как будет получена болванка модуля ASM, в ней уже будут корректно передаваться нужные параметры и возвращаться значения через нужные регистры. Нужно будет только вручную поправить код, добившись требуемой оптимизации.

Для компиляции в ассемблер используется опция -S (avr-gcc . -S). За подробностями обратитесь к документации на компилятор avr-gcc [3]. Для вызова подсказки по командной строке выполните avr-gcc —help. Пример:

avr-gcc -S -mmcu=at90usb1287 -I. -Os -Wall -DF_CPU=16000000UL timerint.c -o timerint.S

[Какие регистры использует компилятор AVR GCC?]

Data types. Типы данных: char занимает 8 бит (1 байт), int 16 бит (2 байта), long 32 бита (4 байта), long long 64 бита (8 байт), float и double 32 бита (4 байта, поддерживается только формат с плавающей точкой, floating point format), указатели 16 бит (указатели функций адресуют не байты, а слова, т. е. реальный адрес нужно умножать на 2. Это для того, чтобы 16-битное число могло позволить обращаться к любому месту в 128K памяти программ). Имеется опция -mint8 (см. Options for the C compiler avr-gcc) чтобы сделать int размерностью 8 бит, однако это не поддерживается avr-libc и нарушает стандарты C (int должен быть как минимум 16-битным). Опция -mint8 может быть удалена в последующих версиях 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 для локальных данных, но часто используются для определенных целей:

r0 — временный регистр, может использоваться любым кодом на C (за исключением обработчиков прерываний, которые сохраняют значение этого регистра). Обычно используется для запоминания чего-нибудь в пределах кусков кода ассемблера.

r1 — подразумевается, что всегда 0 в любом коде C. Может использоваться для запоминания чего-нибудь в пределах кусков кода ассемблера, однако после использования должен очищаться (clr r1). Это относится к любому использованию инструкций [f]mul[s[u]], которые возвращают результат в паре регистров r1:r0. Обработчики прерываний сохраняют и очищают r1 на входе, и восстанавливают r1 на выходе (в том случае, если r1 не ноль).

Function call conventions, соглашения о вызовах функций. Аргументы выделяются слева направо, в регистрах от r25 до r8. Все аргументы выравниваются для начала с четно нумерованных регистров (нечетные по размеру аргументы, включая char, имеют один свободный регистр перед собой). Это позволяет эффективнее использовать инструкцию movw на расширенном ядре. Если переменных слишком много, то те что не влезли в регистры, передаются через стек.

Возвращаемые значения: 8 бит в r24 (не в r25!), 16 бит в r25:r24, до 32 бит в r22-r25, до 64 бит в r18-r25. 8-битные возвращаемые значения расширяются до нуля/знака к 16 битам вызванной функцией (unsigned char более эффективен, чем signed char — просто clr r25). Аргументы функций с переменным количеством аргументов (например printf и т. п.) все передаются через стек, и тип char расширяется до int.

Внимание: такого выравнивания не было до 2000-07-01, включая старые патчи для gcc-2.95.2. Проверьте Ваши старые подпрограммы на ассемблере, и сделайте соответствующие исправления.

[Ссылки]

Ассемблер в 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(«Здесь код на ассемблере»);

/* помещает содержимое 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 в переменную х. Но при этом мы не заботимся о том, что состояние этих регистров может быть изменено где-то в другом месте. Так что это может привести к потенциальным неожиданностям.

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

Добавить комментарий

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