Встроенный ассемблер для семейства процессоров x86 на платформе Linux
Если Вы являетесь разработчиком ядра Linux, то Вам, вероятно, очень часто приходится писать архитектурно-зависимые функции или оптимизировать код. И Вы, вероятно, делаете это путем вставки ассемблерных инструкций внутрь операторов на языке C (способ, известный как использование встроенного ассемблера). Давайте рассмотрим вопросы конкретного использования встроенного ассемблера в Linux. (Мы ограничимся обсуждением ассемблера для процессоров архитектуры IA32)
Краткий обзор синтаксиса ассемблера проекта GNU
Давайте сначала рассмотрим основы синтаксиса ассемблера, используемого в Linux. GCC, компилятор языка C проекта GNU, использует синтаксис ассемблера стандарта AT&T. Некоторые из основных правил данного синтаксиса перечислены ниже. (Перечень далеко не полный, в него включены только те правила, которые относятся к встроенному ассемблеру)
Именование регистровИмена регистров имеют префикс %. Поэтому, если будет использован регистр eax, то он должен обозначаться как %eax.
Порядок следования операндов источника и приемникаВ любой инструкции сначала указан операнд источника, за ним следует операнд приемника. В этом состоит отличие от синтаксиса фирмы Intel, где операнд источника следует после операнда приемника.
Размер операндаИнструкции содержат суффиксы b, w или l, если операнд имеет размер 1 байт, 2 байта (слово) или 4 байта (длинное слово). Использование суффиксов не является обязательным – компилятор GCC ставит подходящий суффикс путем чтения операндов. Однако явное указание суффиксов улучшает читаемость кода и исключает возможность неправильных решений компиляторов.
Непосредственный операндНепосредственный операнд указывается с помощью символа $.
Косвенная ссылка в памятьДля задания косвенных ссылок в память используются скобки ( ).
пересылает байт памяти по адресу, указанному в регистре esi, в регистр al
Встроенный ассемблер
Компилятор GCC содержит специальную конструкцию "asm" для встроенного ассемблера, имеющую следующий формат:
В данном примере ассемблерный шаблон состоит из ассемблерных инструкций. Входные операнды являются выражениями на языке C, которые служат в качестве входных параметров для инструкций. Выходные операнды являются выражениями на языке C, в которые будет выполнен вывод ассемблерных инструкций.
Ограничение операнда памяти (m)Если операнды находятся в памяти, то любые операции, выполняемые с ними, будут выполняться непосредственно в памяти в отличии от регистровых ограничений, когда сначала значение сохраняется в регистре с целью модификации, а затем записывается обратно в память. Регистровые ограничения обычно используются, если они абсолютно необходимы для выполнения инструкции или они существенно ускоряют процесс вычислений. Ограничения памяти могут наиболее эффективно использоваться в случаях, когда переменную языка C необходимо изменить внутри конструкции "asm" и Вы не хотите использовать регистр для хранения значения переменной. В данном примере значение idtr сохраняется в переменной памяти loc:
Ограничения совпадения (цифра)В некоторых случаях одна переменная может служить в качестве входного и выходного операнда. Для этого в конструкции "asm" используются ограничения совпадения.
В приведенном примере ограничения совпадения регистр %eax используется в качестве входного и выходного операнда. Значение переменной var считывается в %eax, а модифицированное после инкремента значение %eax снова сохраняется в переменной var. Здесь цифра "0" указывает на то же ограничение, что и нулевой выходной операнд, т.е. он указывает, что выходное значение переменой var должно быть сохранено только в регистре %eax. Данное ограничение может быть использовано в случаях:
- Если входной операнд считывается из переменной, или переменная модифицируется, и полученное значение записывается обратно в ту же самую переменную
- Если не требуются отдельные экземпляры входных и выходных операндов
Наиболее существенно при использовании ограничений совпадения то, что при этом более эффективно используются доступные регистры.
Примеры общего использования встроенного ассемблера
Следующие примеры иллюстрируют использование различных ограничений. Существует слишком много ограничений, чтобы дать примеры для каждого из них, поэтому приведенные примеры относятся к наиболее часто используемым типам ограничений.
Конструкция "asm" и регистровое ограничение "r"Давайте сначала рассмотрим конструкцию "asm" с регистровым ограничением 'r'. Данный пример показывает, как компилятор GCC выбирает регистры и как он изменяет значение выходных переменных.
В примере значение x копируется в y внутри конструкции "asm". x и y переданы в конструкцию "asm", будучи сохраненными в регистрах. Ассемблерный листинг для данного примера выглядит следующим образом:
Компилятор GCC может выбрать любой регистр в случае использования регистрового ограничения "r". В приведенном примере он выбрал регистр %edx для сохранения x. После чтения значения x в регистре %edx компилятор выбрал тот же самый регистр для y.
Поскольку переменная y указана в секции входных операндов, то модифицированное в регистре %edx значение сохраняется в -8(%ebp) - местонахождении переменной y в стеке. Если бы переменная y была указана в секции входных операндов, то значение переменной y в стеке не было бы обновлено, даже если бы оно было обновлено во временном регистровом хранилище переменной y (регистр %edx). А поскольку регистр %eax указан в списке «затираемых» регистров, компилятор GCC не использует данный регистр для хранения данных в других местах.
Как входной операнд x, так и выходной операнд y помещены в один и тот же регистр %edx, в предположении, что входные операнды используются до получения выходных значений. Заметим, что при наличии большого количества инструкций это предположение может быть неправильным. Чтобы гарантировать, что входные и выходные операнды помещены в различные регистры, можно указать модификатор ограничения &. Ниже приведен тот же пример с добавленным модификатором ограничения.
Здесь показан ассемблерный листинг для данного примера, из которого ясно следует, что переменные x и y сохранены в различных регистрах в конструкции "asm".
Использование конкретных регистровых ограничений
Давайте теперь рассмотрим, как указать конкретные регистры в качестве ограничений для операндов. В следующем примере инструкция cpuid берет входной параметр в регистре %eax и размещает выходные параметры в четырех регистрах: %eax, %ebx, %ecx, %edx. Входной операнд (переменная "op") передается в конструкцию "asm" в регистре eax, как того требует инструкция cpuid. Ограничения a, b, c и d используются в выходных операндах для соответствующей передачи значений в четыре регистра.
Ниже приведен ассемблерный листинг для данной конструкции (предполагается, что переменные _eax, _ebx и т.д. сохранены в стеке):
Функция strcpy может быть реализована с помощью ограничений "S" и "D"следующим образом:
Указатель на источник данных src помещается в регистр %esi за счет использования ограничения "S", а указатель на приемник данных dst помещается в регистр %edi за счет использования ограничения "D". Значение count помещается в регистр %ecx, как того требует префикс rep. Здесь можно увидеть другое ограничение, использующее два регистра %eax и %edx для объединения двух 32-битных значений в 64-битное значение.
Ассемблерный листинг выглядит следующим образом (если переменная val имеет размер 64 бита).
Отметим, что значения в паре регистров %edx:%eax служат в качестве 64-битного выходного операнда
Использование ограничений совпадения
Здесь приведен код для системного вызова с четырьмя параметрами:
В данном примере четыре аргумента системного вызова помещены в регистры %ebx, %ecx, %edx и %esi за счет использования ограничений b, c, d и S. Заметим, что ограничение "=a" используется в выходном операнде, поэтому возвращаемое значение системного вызова, находящееся в регистре %eax, помещается в переменную __res. За счет использования ограничения совпадения "0" в качестве первого ограничения операнда в секции входных операндов номер системного вызова __NR_##name помещается в регистр %eax и используется в качестве входного аргумента системного вызова. Тем самым, регистр %eax служит здесь в качестве как входного регистра, так и выходного регистра. Никаких отдельных регистров для этой цели не используется. Отметим также, что входной аргумент (номер системного вызова) используется до получения выходного операнда (возвращаемое значение системного вызова).
Использование ограничения операнда памяти
Рассмотрим следующую атомарную операцию декремента:
Ассемблерный листинг будет в этом случае выглядеть примерно следующим образом:
Вы можете захотеть использовать здесь регистровое ограничение для переменной counter. В этом случае значение переменной counter должно сначала быть скопировано в регистр, декрементировано, а затем помещено в память. Но тогда не будут иметь смысла блокировка и атомарность, которые ясно показывают необходимость использования ограничения операнда памяти.
Использование "затираемых" регистров
Рассмотрим элементарную реализацию процедуры копирования памяти.
Несмотря на то, что инструкция lodsl изменяет регистр %eax, инструкции lodsl и stosl используют его неявно. Регистр %ecx явно загружает переменную count. Однако компилятор GCC не будет знать об этом, пока мы не проинформируем его путем включения регистров %eax и %ecx в список «затираемых» регистров. До тех пор, пока это не сделано, компилятор GCC предполагает, что регистры %eax и %ecx свободны и может принять решение использовать их для хранения других данных. Отметим здесь, что регистры %esi и %edi используются конструкцией "asm" и не включены в список "затираемых" регистров, поскольку было объявлено, что конструкция "asm" будет использовать их в списке входных операндов. Последняя строка примера указывает на то, что если регистр используется внутри конструкции "asm" (неявно или явно) и не представлен ни в списке входных операндов, ни в списке выходных операндов, то этот регистр должен быть перечислен в качестве "затираемого" регистра.
Заключение
В целом встроенный ассемблер необъятен и обеспечивает разнообразные возможности, которые здесь даже не затронуты. Однако, поняв основы материала данной статьи, Вы будете в состоянии начать самостоятельно кодировать с помощью встроенного ассемблера.