Try English version of Quizful



Раздаем бесплатные Q! подробности в группе Quizful.Alpha-test
Партнеры
Рекрутерам: Прескрининг кандидатов about
Топ контрибуторов
loading
loading
Знаете ли Вы, что

Список полученных сертификатов находится на странице Вашего профиля. Сертификаты можно распечатать или разместить на Вашем сайте.

Лента обновлений
ссылка Oct 22 23:32
Добавлен вопрос в тест Java - Эксперт
ссылка Oct 22 18:33
Комментарий от RomaOlya:
Идиома !=Стандарт!
ссылка Oct 22 16:49
Комментарий от merediq:
где тут дизлайк поставить за рекламу?
ссылка Oct 22 15:03
Комментарий от sertok:
Исправьте ответ на вопрос или сам вопрос(synhronyzed с ма...
ссылка Oct 22 10:42
Добавлен вопрос в тест JavaScript - Средний уровень
Статистика

Тестов: 152, вопросов: 8545. Пройдено: 383831 / 1861576.

Встроенный ассемблер для семейства процессоров x86 на платформе Linux

head tail Статья
категория
Администрирование
дата15.07.2009
авторcrypto
голосов15

[Disclaimer: Данная статья была переведена в рамках "Конкурса на лучший перевод статьи" на сервисе Quizful. Ссылка на оригинал находится внизу страницы.]

Если Вы являетесь разработчиком ядра Linux, то Вам, вероятно, очень часто приходится писать архитектурно-зависимые функции или оптимизировать код. И Вы, вероятно, делаете это путем вставки ассемблерных инструкций внутрь операторов на языке C (способ, известный как использование встроенного ассемблера). Давайте рассмотрим вопросы конкретного использования встроенного ассемблера в Linux. (Мы ограничимся обсуждением ассемблера для процессоров архитектуры IA32)

Краткий обзор синтаксиса ассемблера проекта GNU

Давайте сначала рассмотрим основы синтаксиса ассемблера, используемого в Linux. GCC, компилятор языка C проекта GNU, использует синтаксис ассемблера стандарта AT&T. Некоторые из основных правил данного синтаксиса перечислены ниже. (Перечень далеко не полный, в него включены только те правила, которые относятся к встроенному ассемблеру)

Именование регистров

Имена регистров имеют префикс %. Поэтому, если будет использован регистр eax, то он должен обозначаться как %eax.

Порядок следования операндов источника и приемника

В любой инструкции сначала указан операнд источника, за ним следует операнд приемника. В этом состоит отличие от синтаксиса фирмы Intel, где операнд источника следует после операнда приемника.

mov %eax, %ebx - пересылает содержимое регистра eax в регистр ebx

Размер операнда

Инструкции содержат суффиксы b, w или l, если операнд имеет размер 1 байт, 2 байта (слово) или 4 байта (длинное слово). Использование суффиксов не является обязательным – компилятор GCC ставит подходящий суффикс путем чтения операндов. Однако явное указание суффиксов улучшает читаемость кода и исключает возможность неправильных решений компиляторов.

movb %al, %bl – пересылка байта
movw %ax, %bx – пересылка слова
movl %eax, %ebx – пересылка длинного слова

Непосредственный операнд

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

movl $0xffff, %eax - пересылает значение 0xffff в регистр eax

Косвенная ссылка в память

Для задания косвенных ссылок в память используются скобки ( ).

movb (%esi), %al 

пересылает байт памяти по адресу, указанному в регистре esi, в регистр al

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

Компилятор GCC содержит специальную конструкцию "asm" для встроенного ассемблера, имеющую следующий формат:

asm (ассемблерный шаблон
: выходные операнды (не обязательные)
: входные операнды (не обязательные)
: список «затираемых» регистров (не обязательный)
);

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

asm ("movl %%cr3, %0\n" :"=r"(cr3val));
A %eax
b %ebx
c %ecx
d %edx
S %esi
D %edi

Ограничение операнда памяти (m)

Если операнды находятся в памяти, то любые операции, выполняемые с ними, будут выполняться непосредственно в памяти в отличии от регистровых ограничений, когда сначала значение сохраняется в регистре с целью модификации, а затем записывается обратно в память. Регистровые ограничения обычно используются, если они абсолютно необходимы для выполнения инструкции или они существенно ускоряют процесс вычислений. Ограничения памяти могут наиболее эффективно использоваться в случаях, когда переменную языка C необходимо изменить внутри конструкции "asm" и Вы не хотите использовать регистр для хранения значения переменной. В данном примере значение idtr сохраняется в переменной памяти loc:

("sidt %0\n" : :"m"(loc));

Ограничения совпадения (цифра)

В некоторых случаях одна переменная может служить в качестве входного и выходного операнда. Для этого в конструкции "asm" используются ограничения совпадения.

asm ("incl %0" :"=a"(var):"0"(var));

В приведенном примере ограничения совпадения регистр %eax используется в качестве входного и выходного операнда. Значение переменной var считывается в %eax, а модифицированное после инкремента значение %eax снова сохраняется в переменной var. Здесь цифра "0" указывает на то же ограничение, что и нулевой выходной операнд, т.е. он указывает, что выходное значение переменой var должно быть сохранено только в регистре %eax. Данное ограничение может быть использовано в случаях:

  • Если входной операнд считывается из переменной, или переменная модифицируется, и полученное значение записывается обратно в ту же самую переменную
  • Если не требуются отдельные экземпляры входных и выходных операндов

Наиболее существенно при использовании ограничений совпадения то, что при этом более эффективно используются доступные регистры.

Примеры общего использования встроенного ассемблера

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

Конструкция "asm" и регистровое ограничение "r"

Давайте сначала рассмотрим конструкцию "asm" с регистровым ограничением 'r'. Данный пример показывает, как компилятор GCC выбирает регистры и как он изменяет значение выходных переменных.

int main(void)
{
int x = 10, y;

asm ("movl %1, %%eax;
"movl %%eax, %0;"
:"=r"(y) /*y - выходной операнд*/
:"r"(x) /*x - входной операнд*/
:"%eax"); /*%eax - «затираемый» регистр*/
}

В примере значение x копируется в y внутри конструкции "asm". x и y переданы в конструкцию "asm", будучи сохраненными в регистрах. Ассемблерный листинг для данного примера выглядит следующим образом:

main:
pushl %ebp
movl %esp,%ebp
subl $8,%esp
movl $10,-4(%ebp)
movl -4(%ebp),%edx /*значение x=10 сохранено в регистре %edx*/
#APP /*начало конструкции asm*/
movl %edx, %eax /*значение x переслано в регистр %eax*/
movl %eax, %edx /*значение y помещено в регистр %edx*/

#NO_APP /*конец конструкции asm*/
movl %edx,-8(%ebp) /*значение y в стеке заменено значением в регистре %edx*/

Компилятор GCC может выбрать любой регистр в случае использования регистрового ограничения "r". В приведенном примере он выбрал регистр %edx для сохранения x. После чтения значения x в регистре %edx компилятор выбрал тот же самый регистр для y.

Поскольку переменная y указана в секции входных операндов, то модифицированное в регистре %edx значение сохраняется в -8(%ebp) - местонахождении переменной y в стеке. Если бы переменная y была указана в секции входных операндов, то значение переменной y в стеке не было бы обновлено, даже если бы оно было обновлено во временном регистровом хранилище переменной y (регистр %edx). А поскольку регистр %eax указан в списке «затираемых» регистров, компилятор GCC не использует данный регистр для хранения данных в других местах.

Как входной операнд x, так и выходной операнд y помещены в один и тот же регистр %edx, в предположении, что входные операнды используются до получения выходных значений. Заметим, что при наличии большого количества инструкций это предположение может быть неправильным. Чтобы гарантировать, что входные и выходные операнды помещены в различные регистры, можно указать модификатор ограничения &. Ниже приведен тот же пример с добавленным модификатором ограничения.

int main(void)
{
int x = 10, y;

asm ("movl %1, %%eax;
"movl %%eax, %0;"
:"=&r"(y) /*y - выходной операнд, отметим использование
модификатора ограничения &*/
:"r"(x) /*x - входной операнд*/
:"%eax"); /*%eax - «затираемый» регистр*/
}

Здесь показан ассемблерный листинг для данного примера, из которого ясно следует, что переменные x и y сохранены в различных регистрах в конструкции "asm".

main:
pushl %ebp
movl %esp,%ebp
subl $8,%esp
movl $10,-4(%ebp)
movl -4(%ebp),%ecx /*x, входной операнд находится в регистре %ecx*/
#APP
movl %ecx, %eax
movl %eax, %edx /* y, выходной операнд находится в регистре %edx*/

#NO_APP
movl %edx,-8(%ebp)

Использование конкретных регистровых ограничений

Давайте теперь рассмотрим, как указать конкретные регистры в качестве ограничений для операндов. В следующем примере инструкция cpuid берет входной параметр в регистре %eax и размещает выходные параметры в четырех регистрах: %eax, %ebx, %ecx, %edx. Входной операнд (переменная "op") передается в конструкцию "asm" в регистре eax, как того требует инструкция cpuid. Ограничения a, b, c и d используются в выходных операндах для соответствующей передачи значений в четыре регистра.

asm ("cpuid"
: "=a" (_eax),
"=b" (_ebx),
"=c" (_ecx),
"=d" (_edx)
: "a" (op));

Ниже приведен ассемблерный листинг для данной конструкции (предполагается, что переменные _eax, _ebx и т.д. сохранены в стеке):

        movl -20(%ebp),%eax       /*сохраняем переменную 'op' в %eax – входной операнд*/
#APP
cpuid
#NO_APP
movl %eax,-4(%ebp) /*сохраняем %eax в переменную _eax – выходной операнд*/
movl %ebx,-8(%ebp) /*сохраняем остальные регистры в
movl %ecx,-12(%ebp) соответствующих выходных переменных*/
movl %edx,-16(%ebp)

Функция strcpy может быть реализована с помощью ограничений "S" и "D"следующим образом:

asm ("cld\n
rep\n
movsb"
: /*входные операнды отсутствуют*/
:"S"(src), "D"(dst), "c"(count));

Указатель на источник данных src помещается в регистр %esi за счет использования ограничения "S", а указатель на приемник данных dst помещается в регистр %edi за счет использования ограничения "D". Значение count помещается в регистр %ecx, как того требует префикс rep. Здесь можно увидеть другое ограничение, использующее два регистра %eax и %edx для объединения двух 32-битных значений в 64-битное значение.

#define rdtscll(val) \
__asm__ __volatile__ ("rdtsc" : "=A" (val))

Ассемблерный листинг выглядит следующим образом (если переменная val имеет размер 64 бита).

#APP
rdtsc
#NO_APP
movl %eax,-8(%ebp) /*За счет ограничения A
movl %edx,-4(%ebp) регистры %eax и %edx служат в качестве выходных операндов*/

Отметим, что значения в паре регистров %edx:%eax служат в качестве 64-битного выходного операнда

Использование ограничений совпадения

Здесь приведен код для системного вызова с четырьмя параметрами:

#define _syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) \
type name (type1 arg1, type2 arg2, type3 arg3, type4 arg4) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
"d" ((long)(arg3)),"S" ((long)(arg4))); \
__syscall_return(type,__res); \
}

В данном примере четыре аргумента системного вызова помещены в регистры %ebx, %ecx, %edx и %esi за счет использования ограничений b, c, d и S. Заметим, что ограничение "=a" используется в выходном операнде, поэтому возвращаемое значение системного вызова, находящееся в регистре %eax, помещается в переменную __res. За счет использования ограничения совпадения "0" в качестве первого ограничения операнда в секции входных операндов номер системного вызова __NR_##name помещается в регистр %eax и используется в качестве входного аргумента системного вызова. Тем самым, регистр %eax служит здесь в качестве как входного регистра, так и выходного регистра. Никаких отдельных регистров для этой цели не используется. Отметим также, что входной аргумент (номер системного вызова) используется до получения выходного операнда (возвращаемое значение системного вызова).

Использование ограничения операнда памяти

Рассмотрим следующую атомарную операцию декремента:

__asm__ __volatile__(
"lock; decl %0"
:"=m" (counter)
:"m" (counter));

Ассемблерный листинг будет в этом случае выглядеть примерно следующим образом:

#APP
lock
decl -24(%ebp) /*переменная counter модифицирована в своей локации памяти*/
#NO_APP.

Вы можете захотеть использовать здесь регистровое ограничение для переменной counter. В этом случае значение переменной counter должно сначала быть скопировано в регистр, декрементировано, а затем помещено в память. Но тогда не будут иметь смысла блокировка и атомарность, которые ясно показывают необходимость использования ограничения операнда памяти.

Использование "затираемых" регистров

Рассмотрим элементарную реализацию процедуры копирования памяти.

   asm ("movl $count, %%ecx;
up: lodsl;
stosl;
loop up;"
: /*выходные операнды отсутствуют*/
:"S"(src), "D"(dst) /*входные операнды*/
:"%ecx", "%eax" ); /*список «затираемых» регистров*/

Несмотря на то, что инструкция lodsl изменяет регистр %eax, инструкции lodsl и stosl используют его неявно. Регистр %ecx явно загружает переменную count. Однако компилятор GCC не будет знать об этом, пока мы не проинформируем его путем включения регистров %eax и %ecx в список «затираемых» регистров. До тех пор, пока это не сделано, компилятор GCC предполагает, что регистры %eax и %ecx свободны и может принять решение использовать их для хранения других данных. Отметим здесь, что регистры %esi и %edi используются конструкцией "asm" и не включены в список "затираемых" регистров, поскольку было объявлено, что конструкция "asm" будет использовать их в списке входных операндов. Последняя строка примера указывает на то, что если регистр используется внутри конструкции "asm" (неявно или явно) и не представлен ни в списке входных операндов, ни в списке выходных операндов, то этот регистр должен быть перечислен в качестве "затираемого" регистра.

Заключение

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

----------
Оригинальный текст статьи: Inline assembly for x86 in Linux

Если Вам понравилась статья, проголосуйте за нее

Голосов: 15  loading...
pasis   svarog   googperson   googman   googler   admin   rubynovich   yohan   crypto   globus   neepolas   c0nst   Ghosting   kite   debian