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

Главная / Ассемблер / Ассемблер и языки высокого уровня /

Лучшие книги по Ассемблеру Лучшие книги по Ассемблеру

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

GCC - это бесплатный компилятор языка С++. Этот компилятор используется многими б есплатными средствами разработки, такими как Dev-C++. Это неплохие средства разработки, особенно для изучения языка С++.

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

Почему? Потому что компилятор GCC использует другой ассемблер и другой синтаксис вставки ассемблерного кода в программу на С++.

Поэтому я решил помочь в этом вопросе и перевёл оригинальную документацию на русский язык.

Скачать документ можно здесь.

  1. ВВЕДЕНИЕ
    1. Правообладание и лицензия
    2. Обратная связь и исправления
    3. Благодарности
  2. ОБЩИЕ СВЕДЕНИЯ
  3. СИНТАКСИС АССЕМБЛЕРА GCC
  4. ОСНОВЫ ВСТРАИВАНИЯ
  5. ДОПОЛНИТЕЛЬНЫЕ ВОЗМОЖНОСТИ
    1. Шаблон ассемблера
    2. Операнды
    3. Список регистров
    4. Volatile ...?
  6. ПОДРОБНЕЕ ОБ ОГРАНИЧИТЕЛЯХ
    1. Часто используемые ограничители
    2. Модификаторы ограничителей
  7. НЕСКОЛЬКО ПОЛЕЗНЫХ ПРИМЕРОВ
    1. Сложение двух чисел
    2. Атомарное сложение
    3. Инкремент и декремент
    4. Установка и сброс битов
    5. Копирование строки
    6. Копирование двойного слова
    7. Системные вызовы
  8. ЗАКЛЮЧИТЕЛЬНЫЕ ЗАМЕЧАНИЯ
  9. ССЫЛКИ

1. ВВЕДЕНИЕ

1.1. Правообладание и лицензия

Авторское Право © 2003 Сандип С (Sandeep S).

Этот документ является бесплатным. Вы можете распространять и/или изменять его в соответствии с условиями Генеральной общественной лицензии GNU (General Public License), опубликованной Фондом свободного программного обеспечения (Free Software Foundation), либо версии 2 лицензии, либо (по вашему выбору) любой более поздней версии.

Этот документ распространяется в надежде, что он будет полезным, но без каких-либо гарантий. Даже без подразумеваемых гарантий коммерческой ценности или пригодности для конкретной цели. Смотрите стандартную общественную лицензию GNU для получения дополнительной информации.

1.2. Обратная связь и исправления

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

1.3. Благодарности

Автор выражает искреннюю признательность людям GNU за предоставление такого замечательного объекта. Спасибо г-ну Pramode C.E. от всех людей, кому он помог. Спасибо друзьям из Правительственного Инженерного Училища (Govt Engineering College), Trichur за их моральную поддержку и сотрудничество, особенно Nisha Kurur и Sakeeb S. Спасибо моим дорогим учителям из Govt Engineering College.

Кроме того, благодаря Филиппу, Бреннан Андервуд и colin@nyx.net многие вещи здесь бессовестно украдены из их произведений.

2. ОБЩИЕ СВЕДЕНИЯ

Мы здесь для того, чтобы узнать о встроенном ассемблере GCC. Что означает встроенный?

Мы можем указать компилятору вставить код функции в тот участок кода, где она вызывается, то есть в то место, где выполняется вызов функции. Такая функция называется встроенной. Это похоже на вызов макроса.

В чём преимущества встроенных функций?

Такой способ встраивания снижает использование ресурсов при вызове функции. И если какие-либо фактические значения аргументов являются константами, их значения могут допускать упрощения во время компиляции, так что не весь код встроенной функции должен быть включен в программу. Влияние на размер кода менее предсказуемо, все зависит от конкретного случая. Для объявления встроенной функции мы должны использовать ключевое слово inline при её объявлении.

Теперь мы в состоянии предположить, что такое встроенный ассемблер. Это просто некоторые ассемблерные сборки, написанные, как встроенные функции. Они удобны, быстры и очень полезны в системе программирования. Нашим основным направлением является изучение основных форматов и использования (GCC) ассемблерной функции. Для объявления ассемблерной функции мы используем ключевое слово asm.

Встроенный ассемблер важен, в первую очередь, из-за его способности выполнять операции и быть видимым для переменных C/С++. Из-за этой возможности "asm" работает в качестве интерфейса между инструкциями ассемблера и программой на С/С++, в которой он содержится.

3. СИНТАКСИС АССЕМБЛЕРА GCC

GCC (GNU компилятор C для Linux), использует синтаксис ассемблера AT&T/UNIX. Здесь мы будем использовать синтаксис AT&T для кодирования на ассемблере. Не переживайте, если Вы не знакомы с синтаксисом AT&T, мы научим вас. Он сильно отличается от синтаксиса Intel. Здесь будут рассмотрены основные отличия:

  1. Порядок ИСТОЧНИК-ПРИЁМНИК.
    1. Направление операндов в синтаксисе AT&T является противоположным тому, как это принято в Интел. В Intel синтаксисе первый операнд является приёмником, а второй операнд является источником, тогда как в АТ&Т-синтаксисе первый операнд является источником, а второй операнд приёмником, то есть

      "Команда ПРИЁМНИК ИСТОЧНИК" в синтаксисе Intel меняется на

      "Команда ИСТОЧНИК ПРИЁМНИК" в синтаксисе AT&T.

  2. Имена регистров.
    1. Имена регистров имеют префикс %, то есть если в Интел это ЕАХ, то в AT&T это %eax.

  3. Непосредственное значение.
    1. В AT&T операнды, которые содержат непосредственное значение (число, константу) имеют префикс ’$’. Для статических переменных "C" также нужен префикс ’$’. В синтаксисе Intel для шестнадцатеричных констант используется суффикс ’h’, в то время как в С шестнадцатеричное значение имеет префикс ’0x’. В AT&T для шестнадцатеричных констант мы сначала пишем ’$’, затем ’0x’, а затем константу.

  4. Размер операнда.
    1. В синтаксисе AT&T размер в памяти операнда определяется последним символом в имени команды. Суффиксы в имени команды ’b’, ’w’ и ’l’ определяют соответственно байт (8-бит), слово (16-бит) и двойное слово (32-бита) в области памяти. Синтаксис Intel подразумевает в таких случаях добавление префикса к операндам памяти (не к именам команд), таких как ’byte ptr’, ’word ptr’ и ’dword ptr’. Таким образом, запись в синтаксисе Интел "mov al, byte ptr foo" эквивалентна "movb foo, %al" в синтаксисе AT&T.

  5. Операнды памяти.
    1. В синтаксисе Intel базовый регистр заключается в квадратные скобки ’[’ и ’]’, в то время как в AT&T используются круглые скобки ’(’ и ’)’. Кроме того, в синтаксисе Intel при косвенной адресации такой код section:[base + index*scale + disp] будет земенён на section:disp(base, index, scale) в AT&T. Здесь следует учесть, что если константа используется для disp/scale, то будет нужен префикс ’$’.

Теперь вы знаете основные отличия между синтаксисом Intel и AT&T. Но это лишь несколько отличий. Подробности см. в документации на GNU Assembler. Для лучшего понимания ниже приведены несколько примеров использования, где сравнивается синтаксис Intel и AT&T.

+------------------------------+------------------------------------+
|       Intel код              |      AT&T код                      |
+------------------------------+------------------------------------+
| mov     eax,1                |  movl    $1,%eax                   |   
| mov     ebx,0ffh             |  movl    $0xff,%ebx                |   
| int     80h                  |  int     $0x80                     |   
| mov     ebx, eax             |  movl    %eax, %ebx                |
| mov     eax,[ecx]            |  movl    (%ecx),%eax               |
| mov     eax,[ebx+3]          |  movl    3(%ebx),%eax              | 
| mov     eax,[ebx+20h]        |  movl    0x20(%ebx),%eax           |
| add     eax,[ebx+ecx*2h]     |  addl    (%ebx,%ecx,0x2),%eax      |
| lea     eax,[ebx+ecx]        |  leal    (%ebx,%ecx),%eax          |
| sub     eax,[ebx+ecx*4h-20h] |  subl    -0x20(%ebx,%ecx,0x4),%eax |
+------------------------------+------------------------------------+

4. ОСНОВЫ ВСТРАИВАНИЯ

Основной формат встраивания ассемблера показан ниже:

asm("assembly code");

Пример

/* помещает содержимое ecx в eax */
asm("movl %ecx %eax");

/* помещает байт из bh в память, на которую указывает eax */
__asm__("movb %bh (%eax)");

Как вы могли заметить, здесь используются два варианта встраивания ассемблера: asm и __asm__. Оба варианта правильные. Мы можем использовать __asm__, если ключевое слово asm конфликтует с каким-либо участком вашей программы.

Если встраивание кода на ассемблере содержит более одной инструкции, то мы пишем по одной инструкции в строке в двойных кавычках, а также суффикс ’\n’ и ’\t’ для каждой инструкции. Это потому, что gcc отправляет каждую инструкцию в виде строки для функции as(GAS), а при использовании символов новой строки и табуляции (’\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 не имеет представления о том, какие изменения в регистрах произошли, и это может привести к неприятностям, особенно когда компилятор выполняет какие-либо оптимизации.

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

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

5. ДОПОЛНИТЕЛЬНЫЕ ВОЗМОЖНОСТИ

В основном варианте вставки ассемблера мы могли добавлять только инструкции. В расширенном варианте мы можем также добавлять операнды. Это позволяет нам указывать входные регистры, выходные регистры и список используемых регистров.

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

asm (assembler template 
     : output operands                  /* не обязательно */
     : input operands                   /* не обязательно */
     : list of clobbered registers      /* не обязательно */
     );

Шаблон ассемблера (assembler template) состоит из инструкций ассемблера.

Каждый операнд представляет собой содержащую операнд строку, за которой следует в скобках выражение на С/С++.

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

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

Если нет выходных операндов, но есть входные операнды, то вы должны поместить подряд два двоеточия вокруг того места, где должны быть выходные операнды.

Пример:

asm ("cld\n\t"
     "rep\n\t"
     "stosl"
     : /* нет выходных регистров */
     : "c" (count), "a" (fill_value), "D" (dest)
     : "%ecx", "%edi" 
     );

Что делает этот код? Он заполняет переменной fill_value count раз место, указанное в регистре edi. Он также указывает компилятору gcc, что содержимое регистров eax и edi больше не является действенным. Давайте посмотрим ещё один пример, чтобы всё стало более понятно:

         
int a=10, b;
asm ("movl %1, %%eax; 
      movl %%eax, %0;"
      :"=r"(b)        	/* выход */
      :"r"(a)         	/* вход */
      :"%eax"         	/* используемые регистры */
      );

Что мы здесь сделали? Мы сделали значение переменной ’b’ равным значению переменной ’a’, используя инструкции ассемблера. Некоторые интересные моменты:

  • "b" - это выходной операнд (операнд вывода), связанный с %0 в коде ассемблера, а "a" - это входной операнд, связанный с %1.
  • "r" - это ограничитель операндов. Ограничения мы рассмотрим позже. А пока только скажем, что "r" указывает компилятору GCC, что можно использовать любой регистр для хранения операндов. Выходной операнд ограничителя должен содержать модификатор "=". И этот модификатор говорит о том, что это выходной операнд, доступный только для записи.
  • Имеется два префикса % перед именем регистра. Это помогает компилятору GCC различать операнды и регистры. Операнды имеют один символ % в качестве префикса.
  • %eax в списке используемых регистров после третьего двоеточия указывает компилятору GCC, что значение регистра %eax должно быть изменено внутри блока "asm", так что GCC не будет использовать этот регистр для записи других значений.

Когда выполнение блока "asm" будет завершено, переменная "b" будет содержать обновлённое значение, потому что она указана как операнд вывода. Иными словами, изменение, сделанные для "b" внутри блока "asm", будут видны за пределами блока "asm".

А теперь можно рассмотреть каждый элемент этого блока подробно.

5.1. Шаблон ассемблера

Шаблон ассемблера (assembler template) содержит набор инструкций ассемблера, которые вставляются в исходные коды программы на С/С++.

Формат такой: либо каждая инструкция должна заключаться в двойные кавычки, либо все инструкции должны быть в двойных кавычках.

Каждая инструкция также должна заканчиваться разделителем. Допустимые разделители - это символ новой строки (\n) и точка с запятой (;). За символом ’\n’ может следовать символ табуляции (\t). Мы знаем причины связки newline/tab, верно?

Операнды, связанные с выражениями C/С++, представлены с помощью %0, %1 ... и т.д.

5.2. Операнды

Выражения C/С++ служат как операнды для инструкций ассемблера внутри блока "asm". Каждый операнд, записанный первым, должен содержать ограничитель операндов в двойных кавычках. Для операндов вывода в ограничителе также будет модификатор в двойных кавычках, за которым следует выражение С/С++, связанное с операндом. То есть

"ограничитель" (выражение C/С++) - это общая форма записи.

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

Если мы используем более одного операнда, то они разделяются запятой.

В шаблоне ассемблера каждый операнд ссылается на цифры. Нумерация выполняется следующим образом. Если есть n операндов (всего входных и выходных включительно), то первый операнд вывода имеет номер 0, а далее нумерация продолжается с увеличением на единицу, последний операнд имеет номер n-1. О максимальном количестве операндов мы говорили в предыдущем разделе.

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

Расширенные функции asm компилятор сам не знает, как существуют ;-). Если выходное выражение не может быть адресовано напрямую (например, это бит), то наши ограничители должны позволить использовать регистр. В этом случае GCC будет использовать регистр как выход для asm, а затем сохранит содержимое регистра в выход.

Как сказано выше, обычные выходные операнды должны иметь доступ только для записи. GCC будет считать, что значения в этих операндах перед выполнением инструкций отсутствуют и не нуждаются в генерации кода для них. Расширенный asm также поддерживает входные-выходные операнды (или операнды для чтения-записи).

Сейчас рассмотрим некоторые примеры. Допустим, мы хотим умножить число на 5. Для этого мы используем инструкцию lea.

asm ("leal (%1,%1,4), %0"
     : "=r" (five_times_x)
     : "r" (x) 
     );

Здесь на входе переменная ’x’. Мы не указываем используемые регистры. GCC сам выберет какой-нибудь регистр для входа, один для вывода, и сделает то, что мы хотели.

Если мы хотим, чтобы и для входа и для выхода использовался один регистр, мы можем указать компилятору GCC, чтобы он сделал это. Здесь мы используем эти типы для операндов чтения-записи. Мы делаем это путём указания соответствующего ограничителя.

asm ("leal (%0,%0,4), %0"
     : "=r" (five_times_x)
     : "0" (x) 
     );

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

asm ("leal (%%ecx,%%ecx,4), %%ecx"
     : "=c" (x)
     : "c" (x) 
     );

Во всех трёх примерах выше мы мы не указывали список используемых регистров. Почему? В первых двух примерах компилятор GCC сам решает, какие регистры использовать и он знает, какие изменения при этом происходят. В последнем случае мы не указываем в списке используемых регистров ecx, потому что gcc знает, что он используется для работы с переменной x. Потому что это мы указали в ограничителе операндов.

5.3. Список регистров

Это список некоторых аппаратных регистров. Мы имеем список этих регистров в списке используемых регистров, то есть в поле после третьего двоеточия (’:’) в функции asm. Тем самым мы сообщаем компилятору gcc, что мы их будем использовать и изменять их содержимое. Таким образом компилятор gcc предполагает, что значения, которые он загружает в эти регистры, могут быть неправильными. Мы не должны указывать в этом списке входные или выходные регистры. Потому что компилятор gcc знает, что блок "asm" использует их (потому что они явно указаны в ограничителях).

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

Если наша инструкция может изменить состояние регистра кода, то мы должны добавить "cc" в список ограничителей регистров.

Если наша инструкция изменяет память в непредсказуемом месте, надо добавить "memory" в список используемых регистров. Это приведёт к тому, что GCC не будет сохранять кэшированные значения в регистрах через инструкции ассемблера. Мы также должны добавить ключевое слово volatile, если памяти ("memory") нет в списке входов или выходов в/из asm.

Мы можем читать и записывать используемые регистры любое количество раз. Далее на примере мы рассмотрим несколько инструкций в шаблоне ассемблера. Мы предполагаем, что подпрограмма _foo принимает параметры в регистры eax и ecx.

asm ("movl %0,%%eax;
      movl %1,%%ecx;
      call _foo"
      : /* нет выходов */
      : "g" (from), "g" (to)
      : "eax", "ecx"
      );

5.4. Volatile ...?

Если вы знакомы с исходными кодами ядра или какими-либо другими красивыми исходными кодами, то вы наверняка видели множество функций, объявленных с квалификатором volatile или __volatile__, которое следует за ключевым словом asm или __asm__. О ключевых словах asm и __asm__ мы уже говорили. Но что такое volatile?

Квалификатор volatile запрещает выполнять оптимизацию кода.

То есть если мы хотим, чтобы наш код компилировался именно так, как мы его создали (то есть если мы считаем себя умнее разработчиков компилятора)))), то мы должны поместить ключевое слово volatile после asm и перед скобками ().

Таким образом, если мы хотим не выполнять оптимизацию для нашего кода, то есть сохранить его от перемещения, удаления и т.п., мы объявим его так:

asm volatile ( ... : ... : ... : ...);

Используйте __volatile__, если вы не уверены, что использование volatile не вызовет конфликтов.

Если наш ассемблерный код просто выполняет какие-то расчёты и не имеет никаких побочных эффектов, то слово volatile лучше не использовать, так как неумелое применение этого ключевого слова может привести к труднонаходимым ошибкам. Избежать таких ошибок помогает оптимизация кода компилятором gcc, которая делает код более красивым и правильным.

В разделе “Несколько полезных советов” (Some Useful Recipes) приведены примеры встроенных функций asm. Там мы можем увидеть применение списка используемых регистров во всех подробностях.

6. ПОДРОБНЕЕ ОБ ОГРАНИЧИТЕЛЯХ

К этому времени, вы, возможно, уже поняли, что ограничители имеют много общего со встроенным ассемблером. Но мы пока мало говорили об ограничителях. Ограничители могут указать, может ли операнд располагаться в регистре и какого вида может быть регистр. Должен ли операнд располагаться в памяти, и по какому адресу. Является ли операнд непосредственным значением (константой), и каким может быть это значение (диапазон значений). А также указать другие параметры.

6.1. Часто используемые ограничители

Имеется набор ограничителей, из которых наиболее часто используются лишь несколько. Рассмотрим их.

6.1.1. Ограничитель операнда регистра (r).

Если операнды задаются с помощью этого ограничителя, то они сохраняются в регистры общего назначения (РОН) - General Purpose Registers (GPR). Возьмём следующий пример:

asm ("movl %%eax, %0\n" :"=r"(myval));

Здесь переменная myval сохраняется в регистр, значение из регистра eax копируется в этот регистр, и значение переменной myval обновляется из этого регистра. Если указан ограничитель "r", то компилятор gcc может сохранить значение в любом доступном регистре общего назначения. Чтобы указать конкретный регистр, вы должны использовать специфический для этого регистра ограничитель. Вот эти ограничители:

+---+--------------------+
| r |    Регистр(ы)      |
+---+--------------------+
| a |   %eax, %ax, %al   |
| b |   %ebx, %bx, %bl   |
| c |   %ecx, %cx, %cl   |
| d |   %edx, %dx, %dl   |
| S |   %esi, %si        |
| D |   %edi, %di        |
+---+--------------------+

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

Когда операнды находятся в памяти, какие-либо выполняемые операции будут происходить непосредственно в том месте памяти (в отличие от использования ограничителей регистра, которые сначала сохраняют значения в регистры, чтобы изменить их, а потом записывают его обратно в память).

То есть операции в этом случае не записываются в регистры, а используют память, выделенную для операнда.

Но ограничители регистра обычно используются только тогда, когда они совершенно необходимы для инструкций или если они могут существенно ускорить процесс. Ограничители памяти наиболее эффективно могут быть использованы в тех случаях, когда переменная С/С++ нуждается в обновлении внутри блока "asm", и вы действительно не хотите использовать регистр, для хранения её значения. Например, значение idtr сохраняется в памяти, которая выделена для переменной loc:

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

6.1.3. Связывающие (Digit) ограничители

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

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

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

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

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

6.1.4. Некоторые другие ограничители

  1. "m" : Допускается использовать операнд памяти с адресацией любого вида, которая поддерживается аппаратной частью в целом.
  2. "o" : Допускается использовать операнд памяти, но только если адрес в формате сегмент-смещение, то есть добавляя смещения для адреса, дающего правильный адрес.
  3. "V" : Операнд памяти НЕ в формате сегмент-смещение. Иными словами, это всё, что не попадает под определение ограничителей 'm' и 'o'.
  4. "i" : Непосредственное значение - целочисленный операнд (константа). Включает в себя символьные константы, значение которых будет известно только во время сборки.
  5. "n" : Допускается непосредственное целочисленное значение операнда с известным числовым значение. Многие системы не поддерживают ассемблерные временные константы для операндов размером более слова. Ограничитель для таких операндов должен использовать ’n’, а не ’i’.
  6. "g" : Допускаются любые регистры, память или непосредственное значение, кроме регистров, которые не являются регистрами общего назначения.

Следующие ограничители являются специфическими для платформы x86.

  1. "r" : Ограничитель регистра операнда (см. таблицу выше).
  2. "q" : Регистры a, b, c или d.
  3. "I" : Константа в диапазоне от 0 до 31 (для 32-разрядного сдвига).
  4. "J" : Константа в диапазоне от 0 до 63 (для 64-разрядного сдвига).
  5. "K" : 0xff.
  6. "L" : 0xffff.
  7. "M" : 0, 1, 2 или 3 (сдвиг для инструкции lea).
  8. "N" : Константа в диапазоне от 0 до 255 (для инструкции out).
  9. "f" : Регистр плавающей точки.
  10. "t" : Первый регистр (вершина стека) плавающей точки.
  11. "u" : Второй регистр плавающей точки.
  12. "A" : Определяет регистр 'a' или 'd'. Это, прежде всего, полезно для 64-разрядных целочисленных значений, которые возвращаются в регистр `d’, хранящем старшие значащие биты, и в регистр `a’, в котором находятся младшие значащие биты.

6.2. Модификаторы ограничителей

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

  1. "=" : Означает, что операнд для данной инструкции предназначен только для записи. Предыдущее значение операнда отбрасывается и заменяется выходными данными.
  2. "&" : Означает, что этот операнд является первым используемым операндом, который изменяется перед тем, как инструкция завершит использование входных операндов. Таким образом, этот операнд не может помещаться в регистр, который используется как входной операнд или как часть какого-либо адреса в памяти. Входной операнд может быть связан с первым используемым операндом, если он используется только как вход перед началом записи результата.

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

7. НЕСКОЛЬКО ПОЛЕЗНЫХ ПРИМЕРОВ

Мы рассмотрели основные теоретические моменты использования встроенного ассемблера компилятора GCC, теперь можно рассмотреть несколько простых примеров. Всегда удобно писать функции на встроенном ассемблера в виде макросов. Мы также можем увидеть много функций на ассемблере в коде ядра Линукс (/usr/src/linux/include/asm/*.h).

7.1. Сложение двух чисел

Начнём с простого примера. Напишем программу сложения двух чисел.

 int main(void)
{
  int foo = 10, bar = 15;
  __asm__ __volatile__("addl  %%ebx,%%eax"
                       :"=a"(foo)
                       :"a"(foo), "b"(bar)
                       );
  printf("foo+bar=%d\n", foo);
  return 0;
}

Здесь мы настраиваем GCC для записи значения из переменной foo в регистр %eax, а bar в %ebx, а результат будет в регистре %eax. Знак ’=’ указывает, что это выходной регистр.

7.2. Атомарное сложение

Теперь можно добавить целое число к переменной каким-нибудь другим способом.

 __asm__ __volatile__("   lock       ;\n"
                      "   addl %1,%0 ;\n"
                      : "=m"  (my_var)
                      : "ir"  (my_int), "m" (my_var)
                      : /* нет списка используемых регистров */
                      );

Это атомарное сложение (atomic addition). Мы можем удалить инструкцию ’lock’, чтобы удалить атомарность. В выходном поле мы указали "=m", чтобы дать понять компилятору, что my_var является выходом и находится в памяти. Аналогично, "ir" указывает на то, что my_int является целым числом и должна находиться в каком-либо регистре (см. таблицу выше). В списке используемых регистров не указаны никакие регистры.

7.3. Инкремент и декремент

Далее мы выполним некоторые действия с некоторыми регистрами/переменными и сравним значения.

 __asm__ __volatile__("decl %0; sete %1"
                      : "=m" (my_var), "=q" (cond)
                      : "m" (my_var) 
                      : "memory"
                      );

Здесь значение my_var уменьшится на единицу, и если в результате будет ноль, то переменная cond будет установлена. Мы можем добавить атомарность, добавив инструкцию "lock;\n\t" как первую инструкцию в шаблоне ассемблера.

Подобным образом мы можем использовать "incl %0" вместо "decl %0" для увеличения значения my_var.

Учтите, что здесь, во-первых, переменная my_var хранится в памяти, а во-вторых, переменная cond может быть в любом из регистров eax, ebx, ecx и edx. Ограничитель "=q" гарантирует это. В третьих, мы видим, что память есть в списке используемых, то есть код изменяет содержимое памяти.

7.4. Установка и сброс битов

Как установить/сбросить бит в регистре? В следующем примере это показано.

__asm__ __volatile__("btsl %1,%0"
                     : "=m" (ADDR)
                     : "Ir" (pos)
                     : "cc"
                     );

Здесь бит в позиции ’pos’ переменной ADDR (переменная в памяти) устанавливается в 1. Мы можем использовать ’btrl’ вместо ’btsl’ для сброса бита. Ограничитель "Ir" перед pos указывает на то, что pos записывается в регистр, и это значение в диапазоне 0-31 (зависит от ограничителя x86). То есть мы можем установить/сбросить любой бит от 0 до 31 в переменной ADDR. Так как условия кодов будут изменены, мы добавляем в список используемых "cc".

7.5. Копирование строки

Теперь рассмотрим более сложные, но полезные функции. Копирование строки.

static inline char * strcpy(char * dest,const char *src)
{
int d0, d1, d2;
__asm__ __volatile__("1:\tlodsb\n\t"
                     "stosb\n\t"
                     "testb %%al,%%al\n\t"
                     "jne 1b"
                     : "=&S" (d0), "=&D" (d1), "=&a" (d2)
                     : "0" (src),"1" (dest) 
                     : "memory");
return dest;
}

Адрес источника записывается в esi, приёмника - в edi, а затем начинается копирование. Когда мы считываем 0, копирование завершается. Ограничители "&S", "&D", "&a" указывают, что регистры esi, edi и eax являются ранними используемыми регистрами, то есть их содержимое будет изменено перед завершением функции. Здесь также понятно, почему память в списке используемых.

7.6. Копирование двойного слова

Подобная функция, которая перемещает блок двойных слов. Обратите внимание, что функция определена как макрос.

#define mov_blk(src, dest, numwords) 
__asm__ __volatile__ (                                          
                       "cld\n\t"                                
                       "rep\n\t"                                
                       "movsl"                                  
                       :                                        
                       : "S" (src), "D" (dest), "c" (numwords)  
                       : "%ecx", "%esi", "%edi"                 
                       )

Здесь у нас нет выходов, поэтому изменения, которые происходят с содержимым регистров ecx, esi и edi, имеют побочные эффекты блока перемещения. Таким образом, мы должны добавить их в список используемых регистров.

7.7. Системные вызовы

В Linux системные вызовы выполняются с использованием встроенного ассемблера GCC. Давайте посмотрим, как реализован системный вызов. Все системные вызовы записываются как макросы (linux/unistd.h). Например, системный вызов с тремя параметрами определён как макрос, показанный ниже:

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

Каждый раз, когда выполняется системный вызов, макрос, показанный выше, используется для выполнения вызова. Номер системного вызова помещается в eax, затем каждый параметр помещается в ebx, ecx, edx. В конце выполняется инструкция "int 0x80", которая запускает обработку вызова. Возвращаемое значение может быть взято из eax.

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

{
  asm("movl $1,%%eax;    /* SYS_exit равно 1 */
  xorl %%ebx,%%ebx;      /* Параметр в ebx, он равен 0 */
  int  $0x80"            /* Вход в режим ядра */
  );
}

Число выхода равно "1", и здесь параметр равен 0. Мы делаем так, что eax содержит 1, а ebx содержит 0, а при помощи int $0x80 мы выполняем выход exit(0) с нулевым результатом. Так это работает.

8. ЗАКЛЮЧИТЕЛЬНЫЕ ЗАМЕЧАНИЯ

Этот документ содержит базовую информацию о встроенном ассемблере компилятора GCC. После того, как вы поняли суть, не сложно (при желании) проделать остальные шаги по дальнейшему изучению. Мы рассмотрели несколько примеров, которые будут полезны для понимания основных используемых функций встроенного ассемблера GCC (GCC Inline Assembly).

Встроенный ассемблер GCC - это очень большая тема. Данная статья отнюдь не является полным раскрытием этой темы. Многие подробности о синтаксисе, который мы рассмотрели в общих чертах, можно более глубоко изучить в документации для GNU Assembler. Также полный список ограничителей см. в официальной документации по GCC.

И конечно, ядро Linux использует встроенный ассемблер GCC в полном масштабе. Поэтому вы можете найти множество примеров в исходных кодах ядра. Они могут вам очень сильно помочь.

Если вы нашли какие-то опечатки или устаревшую информацию, то сообщите об этом автору этого документа (не переводчику)))). Хотя и переводчику тоже можете сообщить - с удовольствием исправлю косяки, которые я наверняка в спешке допустил )))

9. ССЫЛКИ

Ссылки см. в оригинальном документе.



Первые шаги в программирование Первые шаги в программирование

Главный вопрос начинающего программиста – с чего начать? Вроде бы есть желание, но иногда «не знаешь, как начать думать, чтобы до такого додуматься». У человека, который никогда не имел дело с информационными технологиями, даже простые вопросы могут вызвать большие трудности и отнять много времени на решение. Подробнее...

Инфо-МАСТЕР ®
Все права защищены ©
e-mail: mail@info-master.su