ошибка сегментации (дамп ядра) ошибка при использовании встроенной сборки

Я использую встроенную сборку в GCC. Я хочу повернуть содержимое переменной на 2 бита влево (я переместил переменную в регистр rax, а затем повернул ее 2 раза). Я написал приведенный ниже код, но столкнулся с ошибкой сегментации (дамп ядра). Буду признателен, если вы мне поможете.

uint64_t X = 13835058055282163712U;
 asm volatile(
            "movq %0 , %%rax\n"
            "rol %%rax\n"
            "rol %%rax\n"
            :"=r"(X)
            :"r"(X)
         );
printf("%" PRIu64 "\n" , X);

1 ответ

Решение

Ключом к пониманию встроенного asm является понимание того, что каждый оператор asm состоит из двух частей:

  1. Текст фактического материала ассемблера, в котором компилятор будет делать текстовые замены, но не понимает.

    Это AssemblerTemplate в документации (все до первого: в __asm__()).

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

    Это : OutputOperands : InputOperands : Clobbers в документации.

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

    Фактический ассемблер - это черный ящик, который принимает описанные здесь входные данные, производит описанные выходные данные и в качестве побочного эффекта может "затирать" некоторые регистры и / или память. Это должно быть полное описание того, что делает ассемблер... иначе сгенерированный компилятором asm вокруг вашего шаблона будет конфликтовать с ним и будет полагаться на ложные предположения.

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

Итак, ваш фрагмент:

 asm volatile(
            "movq %0 , %%rax\n"
            "rol %%rax\n"
            "rol %%rax\n"
            :"=r"(X)
            :"r"(X)
         );

есть несколько "проблем":

  • вы могли выбрать %rax для результата на основании того, что asm() подобен функции, и можно ожидать, что он вернет результат в %rax - но это не так.
  • вы пошли дальше и использовали %rax, который компилятор может (хорошо) уже выделил для чего-то еще... так что вы, по сути, `` затираете '' %rax но вы не смогли сообщить об этом компилятору!
  • вы указали =r(X)(OutputOperand), который сообщает компилятору ожидать вывода в некотором регистре, и этот вывод будет новым значением переменной.X. В%0в AssemblerTemplate будет заменен регистром, выбранным для вывода. К сожалению, ваша сборка лечит%0 как вход:-(А выход, собственно, в %rax - о чем, как и выше, компилятор не знает.
  • вы также указали r(X)(InputOperand), который сообщает компилятору организовать текущее значение переменнойXбыть помещенным в какой-либо регистр для использования ассемблером. Это было бы%1в AssemblerTemplate. К сожалению, ваша сборка не использует этот ввод.

    Несмотря на то, что операнды вывода и ввода относятся к X, компилятор может не сделать %0 тот же регистр, что и %1. (Это позволяет использовать блок asm в качестве неразрушающей операции, которая оставляет исходное значение ввода неизменным. Если ваш шаблон работает не так, не пишите его таким образом.

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

Кроме того, все было замечательно. Следующее безопасно и позволяет избежатьmov инструкция:

 asm("rol %0\n"
     "rol %0\n"   : "+r"(X));

где "+r"(X) говорит, что требуется один комбинированный регистр ввода и вывода, принимая старое значение X и вернув новый.

Теперь, если вы не хотите заменять X, то предполагая Y в результате вы можете:

 asm("mov %1, %0\n"
     "rol %0\n"
     "rol %0\n"   : "=r"(Y) : "r"(X));

Но лучше предоставить компилятору решать, нужно ли ему mov или может ли он просто позволить уничтожить ввод.


Есть несколько правил относительно InputOperands, о которых стоит упомянуть:

  • Ассемблер не должен перезаписывать ни один из InputOperands - компилятор отслеживает, какие значения он имеет в каких регистрах, и ожидает сохранения InputOperands.

  • Компилятор ожидает, что все InputOperand будут прочитаны перед записью любого OutputOperand. Это важно, когда компилятор знает, что данный InputOperand больше не используется послеasm(), и поэтому он может выделить регистр InputOperand для OutputOperand. Есть штука под названием Earlyclobber (=&r(foo)), чтобы избавиться от этой маленькой морщинки.

В приведенном выше примере, если вы на самом деле не используете X снова компилятор мог выделить %0 а также %1в тот же реестр! Но (лишний)movвсе равно будет собрана - помня, что компилятор действительно не понимает AssemblerTemplate. Итак, вам в целом лучше перемешивать значения в C, а не вasm(). См. https://gcc.gnu.org/wiki/DontUseInlineAsm и Лучшие практики для операций кругового сдвига (поворота) в C++


Итак, вот четыре варианта темы и сгенерированный код (gcc -O2):

// (1) uses both X and Y in the printf() -- does mov %1, %0 in asm()
void Never_Inline footle(void)               Dump of assembler code for function footle:
{                                              mov    $0x492782,%edi   # address of format string
  unsigned long  X, Y ;                        xor    %eax,%eax
                                               mov    $0x63,%esi       # X = 99
  X = 99 ;                                     rol    %rsi            # 1st asm
  __asm__("\t rol  %0\n"                       rol    %rsi
          "\t rol  %0\n" : "+r"(X)             mov    %rsi,%rdx       # 2nd asm, compiler using it as a copy-and-rotate
      ) ;                                      rol    %rdx
                                               rol    %rdx
  __asm__("\t mov  %1, %0\n"                   jmpq   0x4010a0 <printf@plt>  # tailcall printf
          "\t rol  %0\n"
          "\t rol  %0\n" : "=r"(Y) : "r"(X)
      ) ;

  printf("%lx %lx\n", X, Y) ;
}

// (2) uses both X and Y in the printf() -- does Y = X in 'C'
void Never_Inline footle(void)               Dump of assembler code for function footle:
{                                              mov    $0x492782,%edi
  unsigned long  X, Y ;                        xor    %eax,%eax
                                               mov    $0x63,%esi
  X = 99 ;                                     rol    %rsi       # 1st asm
  __asm__("\t rol  %0\n"                       rol    %rsi
          "\t rol  %0\n" : "+r"(X)             mov    %rsi,%rdx  # compiler-generated mov
      ) ;                                      rol    %rdx       # 2nd asm
                                               rol    %rdx
  Y = X ;                                      jmpq   0x4010a0 <printf@plt>
  __asm__("\t rol  %0\n"
          "\t rol  %0\n" : "+r"(Y)
      ) ;

  printf("%lx %lx\n", X, Y) ;
}

// (3) uses only Y in the printf() -- does mov %1, %0 in asm()
void Never_Inline footle(void)               Dump of assembler code for function footle:
{                                              mov    $0x492782,%edi
  unsigned long  X, Y ;                        xor    %eax,%eax
                                               mov    $0x63,%esi
  X = 99 ;                                     rol    %rsi
  __asm__("\t rol  %0\n"                       rol    %rsi
          "\t rol  %0\n" : "+r"(X)             mov    %rsi,%rsi   # redundant instruction because of mov in the asm template
      ) ;                                      rol    %rsi
                                               rol    %rsi
  __asm__("\t mov  %1, %0\n"                   jmpq   0x4010a0 <printf@plt>
          "\t rol  %0\n"
          "\t rol  %0\n" : "=r"(Y) : "r"(X)
      ) ;

  printf("%lx\n", Y) ;
}

// (4) uses only Y in the printf() -- does Y = X in 'C'
void Never_Inline footle(void)               Dump of assembler code for function footle:
{                                              mov    $0x492782,%edi
  unsigned long  X, Y ;                        xor    %eax,%eax
                                               mov    $0x63,%esi
  X = 99 ;                                     rol    %rsi
  __asm__("\t rol  %0\n"                       rol    %rsi
          "\t rol  %0\n" : "+r"(X)             rol    %rsi    # no wasted mov, compiler picked %0=%1=%rsi
      ) ;                                      rol    %rsi
                                               jmpq   0x4010a0 <printf@plt>
  Y = X ;
  __asm__("\t rol  %0\n"
          "\t rol  %0\n" : "+r"(Y)
      ) ;

  printf("%lx\n", Y) ;
}

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

Итак, фокус в том, чтобы работать с компилятором, понимая, что :OutputOperands:InputOperands:Clobbers - это то место, где вы описываете, что делает ассемблер.

Другие вопросы по тегам