Rambler's
Top100

Inline Assembler в GCC

Автор: lonesome TSH/Digital Daemons
Дата: 29.04.2003
Раздел: Низкоуровневое программирование в Linux

Сам по себе асм конечно штука хорошая, но, к сожалению, под Unix и в частности Linux программировать на чистом ассемблере - занятие неблагодарное. Настолько неблагодарное, что "в промышленных масштабах" используется разве что inline assembler - т.е. вставки ассемблерного кода в код на языках высокого уровня. Сейчас я расскажу, как это можно делать в GCC.

Первым делом хотелось бы опровергнуть многих начинающих linux-программистов, которые считают, что, дескать, inline assembler - это просто вставка ассемблерного кода. Но inline assembler в GCC - штука гораздо более мощная, являющаяся по сути дела приближенным к ассемблеру языком программирования, который способен эффективно взаимодействовать с кодом на Си. Но обо всем по порядку

Для использования inline assembler'а в GCC используется ключевое слово asm или __asm__. Ассемблерный код необходимо указывать как строку, например:
asm("movl %eax, %ebx");
Напоминаю, что используемый x86 ассемблер - формата AT&T. Если вы еще с ним незнакомы, то рекомендую прочитать вот эти статьи:
Ассемблер для UNIX - Tutorial
Три простых примера использования ассемблера GAS

Допустим, что в ассемблерном коде необходимо использовать некоторую переменную var из кода на Си. Самый очевидный способ - обратиться к ней напрямую:
asm("movl var, %eax");

Этот способ прокатит если переменная объявлена как глобальная. В противном случае придется использовать т.н. вызов inline assembler'а с параметрами. Параметры указываются как %0, %1 и т.д. Примечание:если в коде указаны параметры, которые, как и регистры начинаются с префикса '%', то регистры указываются с префиксом '%%', например %%eax. Рассмотрим небольшой пример:

void main()
{
    int a=77, b;
    asm("movl %0, %%eax"::"d"(a));
    asm("movl %%eax, %0":"=d"(b));
    printf("Variable is %i\n", b);
}
Как видите, здесь мы используем inline assembler, чтобы скопировать содержимое локальной переменной a в локальную переменную b. Строго говоря, размещение двух директив asm(..) подряд не гарантирует, что они сохранят свой порядок после компиляции, но будем надеяться, что так и будет :)

После ассемблерного кода мы указываем откуда берутся операнды и куда помещаются вот таким образом:
asm("код":"=тип_результата"(результат):"тип_исходных_данных"(исходные данные));
Если что-нибудь из этих двух вариантов нас не интересует, то соответствующая секция просто пропускается.

В данном примере мы указываем тип исходных данных и тип результата как "d". Это означает, что вместо %0 GCC подставит регистр общего назначения (а конкретно EDX), не забыв позаботиться, чтобы в нем оказалось требуемое число в первом случае, и чтобы он после операции скопировался в нужную переменную - во втором случае. Посмотрим на интересующий нас участок результата (его можно получить, запустив GCC с опцией --save-temps):

movl-4(%ebp), %edx
#APP
movl %edx, %eax
movl %eax, %edx
#NO_APP
movl%edx, %eax
movl%eax, -8(%ebp)
Как видите, в обоих случаях GCC использовал регистр EDX в качестве операнда.

Теперь представим, что в Си-коде определена некоторая константа, которую нам нужно использовать в ассемблерном коде. Здесь выручает тип параметра "i":

#define PAGE_DIRECTORY 0x100000
void main()
{
    asm("movl %0, %%eax"::"i"(PAGE_DIRECTORY));
    asm("movl %eax, %cr3");
}

Вот что получится в результате компиляции:

#APP
movl $1048576, %eax
movl %eax, %cr3
#NO_APP

Существуют и другие типы параметра. Тип "a" - EAX, "b" - EBX, "m" означает адрес переменной в памяти, "y" - MMX-регистр и т.д.

В завершение статьи отмечу, что asm(..) не проверяет на ошибочность ни инструкции, ни типы операндов, оставляя это дело ассемблеру.

Rambler's Top100