Автор: 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(..) не проверяет на ошибочность ни
инструкции, ни типы операндов, оставляя это дело ассемблеру.