Использование базового указателя в С++ inline asm

Я хочу использовать регистр базового указателя (%rbp) внутри встроенного asm. Пример игрушки выглядит так:

void Foo(int &x)
{
    asm volatile ("pushq %%rbp;"         // 'prologue'
                  "movq %%rsp, %%rbp;"   // 'prologue'
                  "subq $12, %%rsp;"     // make room

                  "movl $5, -12(%%rbp);" // some asm instruction

                  "movq %%rbp, %%rsp;"  // 'epilogue'
                  "popq %%rbp;"         // 'epilogue'
                  : : : );
    x = 5;
}

int main() 
{
    int x;
    Foo(x);
    return 0;
}

Я надеялся, что, поскольку я использую обычный метод пролог/эпилог, вызывающий вызов и выскакивающий старый %rbp, это было бы нормально. Тем не менее, это seg ошибки, когда я пытаюсь получить доступ к x после встроенного asm.

Сводный код, созданный GCC (слегка урезанный):

_Foo:
    pushq   %rbp
    movq    %rsp, %rbp
    movq    %rdi, -8(%rbp)

    # INLINEASM
    pushq %rbp;          // prologue
    movq %rsp, %rbp;     // prologue
    subq $12, %rsp;      // make room
    movl $5, -12(%rbp);  // some asm instruction
    movq %rbp, %rsp;     // epilogue
    popq %rbp;           // epilogue
    # /INLINEASM

    movq    -8(%rbp), %rax
    movl    $5, (%rax)      // x=5;
    popq    %rbp
    ret

main:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp
    leaq    -4(%rbp), %rax
    movq    %rax, %rdi
    call    _Foo
    movl    $0, %eax
    leave
    ret

Может ли кто-нибудь сказать мне, почему этот seg неисправен? Кажется, я как-то испортил %rbp, но я не понимаю, как это сделать. Спасибо заранее.

Я запускаю GCC 4.8.4 на 64-битном Ubuntu 14.04.

Ответ 1

Смотрите в нижней части этого ответа коллекцию ссылок на другие вопросы и ответы inline-asm.

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


Что вы надеетесь научиться делать с помощью встроенного ассема? Если вы хотите изучить встроенный asm, научитесь использовать его для создания эффективного кода, а не таких ужасных вещей, как этот. Если вы хотите написать пролог функции и нажать/щелкнуть для сохранения/восстановления регистров, вы должны написать целые функции в asm. (Тогда вы можете легко использовать nasm или yasm вместо менее предпочтительного синтаксиса AT & T с директивами ассемблера GNU 1.)

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

Если вы просто пытаетесь изучать asm, GNU inline asm - ужасный выбор. Вы должны полностью понять почти все, что происходит с ассемблером, и понять, что должен знать компилятор, чтобы написать правильные ограничения ввода/вывода и получить все правильно. Ошибки приведут к разбиванию вещей и трудно отлаживаемым поломкам. Вызов функции ABI намного проще и проще отслеживать границы между вашим кодом и кодом компилятора.


Почему это ломается

Вы скомпилировали с -O0, поэтому код gcc проливает параметр функции из %rdi в какое-либо место в стеке. (Это может произойти в нетривиальной функции даже с -O3).

Поскольку целевым ABI является ABI SysV x86-64, он использует "красную зону" (128 байт ниже %rsp что даже асинхронным обработчикам сигналов не разрешено блокировать) вместо того, чтобы тратить инструкцию, уменьшающую указатель стека на резервное пространство,

Он хранит функцию указателя 8B arg в -8(rsp_at_function_entry). Затем ваш встроенный asm выдвигает %rbp, который уменьшает значение% rsp на 8, а затем записывает туда, что приводит к засорению младшего 32b символа &x (указатель).

Когда ваш встроенный ассм закончен,

  • gcc перезагружает -8(%rbp) (который был перезаписан %rbp) и использует его в качестве адреса для хранилища 4B.
  • Foo возвращается к main с %rbp = (upper32)|5 (значение orig с минимальным 32, установленным в 5).
  • leave main трассы: %rsp = (upper32)|5
  • main запускает ret с %rsp = (upper32)|5, считывая адрес возврата с виртуального адреса (void*)(upper32|5), который из вашего комментария равен 0x7fff0000000d.

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

Даже добавление "памяти" clobber не дает gcc избежать использования красной зоны, поэтому похоже, что выделение собственной памяти стека из встроенного asm - просто плохая идея. (Подстановка памяти означает, что вы, возможно, записали некоторую память, в которую вам разрешено писать, например, глобальную переменную или что-то, на которое указывает глобальная переменная, а не то, что вы могли перезаписать то, что не должны были делать.)

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

AFAIK, нет синтаксиса для объявления, что вы изменяете красную зону, поэтому ваши единственные варианты:

  • используйте выходной операнд "=m" (возможно, массив) для пустого пространства; компилятор, вероятно, заполнит этот операнд режимом адресации относительно RBP или RSP. Вы можете индексировать его с помощью констант, таких как 4 + %[tmp] или чего-либо еще. Вы можете получить предупреждение ассемблера от 4 + (%rsp) но не ошибку.
  • пропустите красную зону с add $-128, %rsp/sub $-128, %rsp вокруг вашего кода. (Необходим, если вы хотите использовать неизвестное количество дополнительного стекового пространства, например, вставить цикл или сделать вызов функции. Еще одна причина, чтобы разыменовать указатель функции в чистом C, а не в встроенном asm.)
  • компилировать с -mno-red-zone (я не думаю, что вы можете включить это для каждой функции, только для каждого файла)
  • Во-первых, не используйте пустое место. Сообщите компилятору, какие регистры вы записываете, и дайте ему сохранить их.

Вот что вы должны были сделать:

void Bar(int &x)
{
    int tmp;
    long tmplong;
    asm ("lea  -16 + %[mem1], %%rbp\n\t"
         "imul $10, %%rbp, %q[reg1]\n\t"  // q modifier: 64bit name.
         "add  %k[reg1], %k[reg1]\n\t"    // k modifier: 32bit name
         "movl $5, %[mem1]\n\t" // some asm instruction writing to mem
           : [mem1] "=m" (tmp), [reg1] "=r" (tmplong)  // tmp vars -> tmp regs / mem for use inside asm
           :
           : "%rbp" // tell compiler it needs to save/restore %rbp.
  // gcc refuses to let you clobber %rbp with -fno-omit-frame-pointer (the default at -O0)
  // clang lets you, but memory operands still use an offset from %rbp, which will crash!
  // gcc memory operands still reference %rsp, so don't modify it.  Declaring a clobber on %rsp does nothing
         );
    x = 5;
}

Обратите внимание на push/pop %rbp в коде вне #APP/#NO_APP, #NO_APP gcc. Также обратите внимание, что чистая память, которую он вам дает, находится в красной зоне. Если вы скомпилируете с -O0, вы увидите, что он находится не в том месте, где он разлит &x.

Чтобы получить больше чистых регистров, лучше просто объявить больше выходных операндов, которые никогда не используются окружающим не-asm-кодом. Это оставляет распределение регистров для компилятора, поэтому оно может быть различным, если встроено в разные места. Выбор заблаговременно и объявление Clobber имеет смысл, только если вам нужно использовать определенный регистр (например, число сдвигов в %cl). Конечно, входное ограничение, такое как "c" (count) заставляет gcc поместить счет в rcx/ecx/cx/cl, поэтому вы не создадите потенциально избыточный mov %[count], %%ecx.

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

При использовании встроенного asm, сохраняйте его как можно меньше: в идеале это всего лишь одна или две инструкции, которые gcc не генерирует самостоятельно, с ограничениями ввода/вывода, которые сообщают ему, как вводить/выводить данные из оператора asm. Это то, для чего он предназначен.

Основное правило: если ваш встроенный ассемблер GNU C начинается или заканчивается mov, вы обычно делаете это неправильно и вместо этого должны были использовать ограничение.


Сноски:

  1. Вы можете использовать Intel-синтаксис GAS в inline-asm, построив с помощью -masm=intel (в этом случае ваш код будет работать только с этой опцией), или используя альтернативные диалекты, чтобы он работал с компилятором в синтаксисе вывода Intel или AT & T asm, Но это не меняет директив, и Intel-синтаксис GAS плохо документирован. (Это как MASM, а не NASM, хотя.) Я действительно не рекомендую это, если вы действительно не ненавидите синтаксис AT & T.

Встроенные ссылки asm:

Некоторые из них повторяют некоторые из тех вещей, которые я объяснил здесь. Я не перечитывал их, чтобы избежать избыточности, извините.

Ответ 2

В x86-64 указатель стека должен быть выровнен с 8 байтами.

Это:

subq $12, %rsp;      // make room

должен быть:

subq $16, %rsp;      // make room