Встроенная сборка GCC: ограничения

Мне сложно понять роль ограничений в встроенной сборке GCC (x86). Я читал руководство, в котором точно объясняется, что делает каждое ограничение. Проблема в том, что, хотя я понимаю, что делает каждое ограничение, я мало понимаю, почему вы должны использовать одно ограничение над другим или какие последствия могут быть.

Я понимаю, что это очень широкая тема, поэтому небольшой пример должен помочь сузить фокус. Ниже приведена простая процедура asm, которая просто добавляет два числа. Если происходит переполнение целых чисел, он записывает значение 1 в выходную переменную C.

 int32_t a = 10, b = 5;
 int32_t c = 0; // overflow flag

 __asm__
 (
  "addl %2,%3;"        // Do a + b (the result goes into b)
  "jno 0f;"            // Jump ahead if an overflow occurred
  "movl $1, %1;"       // Copy 1 into c
  "0:"                 // We're done.

  :"=r"(b), "=m"(c)    // Output list
  :"r"(a), "0"(b)     // Input list
 );

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

  :"=r"(b), "=m"(c)    // Output list
  :"r"(a), "m"(b)     // Input list

Обратите внимание, что вместо "0" я использую ограничение "m" для b. У этого был странный побочный эффект: если я скомпилировал флаги оптимизации и дважды вызвал функцию, по какой-то причине результат операции добавления также будет сохранен в c. В конце концов я прочитал " соответствия ограничений", который позволяет указать, что переменная должна использоваться как как входной, так и выходной операнд. Когда я изменил "m"(b) на "0"(b), он сработал.

Но я не понимаю, почему вы использовали бы одно ограничение над другим. Я имею в виду, да, я понимаю, что "r" означает, что переменная должна находиться в регистре, а "m" означает, что она должна быть в памяти, - но я действительно не понимаю, какие последствия выбора одного над другим, или почему добавление операция работает некорректно, если я выбираю определенную комбинацию ограничений.

Вопросы: 1) В приведенном выше примере кода почему ограничение "m" на b вызывает c для записи? 2) Есть ли какой-либо учебник или онлайн-ресурс, который более подробно описывает ограничения?

Ответ 1

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

bool add_and_check_overflow(int32_t& a, int32_t b)
{
    bool result;
    __asm__("addl %2, %1; seto %b0"
            : "=q" (result), "+g" (a)
            : "r" (b));
    return result;
}

Таким образом, использовались следующие ограничения: q, r и g.

  • q означает, что можно выбрать только eax, ecx, edx или ebx. Это связано с тем, что инструкции set* должны записывать в 8-битный адресный регистр (al, ah,...). Использование b в %b0 означает, что используется самая низкая 8-битная часть (al, cl,...).
  • Для большинства инструкций с двумя операндами по крайней мере один из операндов должен быть регистром. Поэтому не используйте m или g для обоих; используйте r для хотя бы одного из операндов.
  • Для конечного операнда не имеет значения, зарегистрирован ли он или память, поэтому используйте g (общий).

В приведенном выше примере я решил использовать g (а не r) для a, потому что ссылки обычно реализуются как указатели на память, поэтому использование ограничения r потребовало бы копирования референта в сначала зарегистрируйтесь, а затем скопируйте назад. Используя g, референт может быть обновлен напрямую.


Что касается того, почему ваша оригинальная версия перезаписала ваш c с добавлением, это потому, что вы указали =m в выходном слоте, а не (скажем) +m; это означает, что компилятору разрешено повторно использовать одну и ту же ячейку памяти для ввода и вывода.

В вашем случае это означает два результата (поскольку для b и c) использовалось то же расположение памяти:

  • Добавление не было переполнено: тогда c получил перезапись со значением b (результат добавления).
  • Добавление переполнено: тогда c стал 1 (и b может стать 1 также, в зависимости от того, как был сгенерирован код).