Сбой блоков кода Ver.16.01 во время цикла выполнения программы

У меня есть программа, которая, как было доказано, работает на более старой версии кодовых блоков (версия 13.12), но, кажется, не работает, когда я пробую ее на более новой версии (версия 16.01). Цель программы состоит в том, чтобы ввести два целых числа, которые затем будут добавлены, Mult и т. Д. Она использует асм-код, который я новичок. Мой вопрос, почему он говорит, что окна перестали отвечать после того, как я ввожу 2 целых числа и нажимаю ввод?

Вот код:

//Program 16

#include <stdio.h>
 #include <iostream>
 using namespace std;

int main() {

int arg1, arg2, add, sub, mul, quo, rem ;

cout << "Enter two integer numbers : " ;
cin >>  arg1 >> arg2 ;
cout << endl;

  asm ( "addl %%ebx, %%eax;" : "=a" (add) : "a" (arg1) , "b" (arg2) );
  asm ( "subl %%ebx, %%eax;" : "=a" (sub) : "a" (arg1) , "b" (arg2) );
 asm ( "imull %%ebx, %%eax;" : "=a" (mul) : "a" (arg1) , "b" (arg2) );

asm ( "movl $0x0, %%edx;"
"movl %2, %%eax;"
"movl %3, %%ebx;"
"idivl %%ebx;" : "=a" (quo), "=d" (rem) : "g" (arg1), "g" (arg2) );

cout<< arg1 << "+" << arg2 << " = " << add << endl;
 cout<< arg1 << "-" << arg2 << " = " << sub << endl;
cout<< arg1 << "x" << arg2 << " = " << mul << endl;
cout<< arg1 << "/" << arg2 << " = " << quo << "  ";
 cout<< "remainder " << rem << endl;

return 0;
}

2 ответа

Как сказал Майкл, ваша проблема, вероятно, связана с неправильным написанием вашего 4-го асма.

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

Получив это, вы должны понимать, что при запуске компилятора он использует эти регистры в коде, который он генерирует. Например, если вы делаете for (int x=0; x<10; x++) х (вероятно) собирается в конечном итоге в регистре. Так что же произойдет, если gcc решит использовать ebx для хранения значения 'x', а затем ваш оператор asm остановится на ebx, добавив в него другое значение? gcc не анализирует ваш ассемблер, чтобы выяснить, что вы делаете. Единственная подсказка о том, что делает ваша асма, это те ограничения, которые перечислены после инструкций ассма.

Это то, что имеет в виду Майкл, когда говорит: "4-й блок ASM не перечисляет"EBX"в списке клоббера (но его содержимое уничтожено)". Если мы посмотрим на ваш асм:

asm ("movl $0x0, %%edx;"
     "movl %2, %%eax;"
     "movl %3, %%ebx;"
     "idivl %%ebx;" 
  : "=a" (quo), "=d" (rem) 
  : "g" (arg1), "g" (arg2));

Вы видите, что 3-я строка перемещает значение в ebx, но в ограничениях, которые следуют, нет ничего, чтобы сказать, что оно будет изменено. Тот факт, что ваша программа дает сбой, вероятно, из-за того, что gcc использует этот регистр для чего-то другого. Самым простым решением может быть "перечисление EBX в списке клоббера":

asm ("movl $0x0, %%edx;"
     "movl %2, %%eax;"
     "movl %3, %%ebx;"
     "idivl %%ebx;" 
  : "=a" (quo), "=d" (rem) 
  : "g" (arg1), "g" (arg2)
  : "ebx");

Это говорит gcc, что ebx может быть изменен ассемблером (он же "обмывает" его), и что ему не нужно иметь какое-то конкретное значение, когда начинается оператор asm, и не будет иметь никакого конкретного значения в нем, когда асм выходит.

Однако, хотя это может быть "самым простым", оно не обязательно является лучшим. Например, вместо использования "g" ограничение для arg2, мы можем использовать "b" ограничение:

asm ("movl $0x0, %%edx;"
     "movl %2, %%eax;"
     "idivl %%ebx;" 
  : "=a" (quo), "=d" (rem) 
  : "g" (arg1), "b" (arg2));

Это позволяет нам избавиться от movl %3, %%ebx оператор, так как gcc будет проверять значение в ebx перед вызовом asm, и нам больше не нужно его забивать.

Но зачем использовать ebx? idiv там не требует какой-либо конкретной регистрации, и, возможно, gcc уже использует ebx для чего-то другого. Как насчет того, чтобы позволить gcc просто выбрать регистр, который он не использует? Мы делаем это, используя "r" ограничение:

asm ("movl $0x0, %%edx;"
     "movl %2, %%eax;"
     "idivl %3;" 
  : "=a" (quo), "=d" (rem) 
  : "g" (arg1), "r" (arg2));

Обратите внимание, что теперь idiv использует%3, что означает "использовать то, что есть в (№ 0) параметре #3". В данном случае это регистр, содержащий arg2.

Тем не менее, мы все еще можем сделать лучше. Как вы уже видели в своих предыдущих инструкциях asm, вы можете использовать "a" ограничение на указание gcc поместить определенную переменную в регистр eax. Что означает, что мы можем сделать это:

asm ("movl $0x0, %%edx;"
     "idivl %3;" 
  : "=a" (quo), "=d" (rem) 
  : "a" (arg1), "r" (arg2));

Опять же, на 1 инструкцию меньше, поскольку нам больше не нужно перемещать значение в eax. Так как насчет этого movl $0x0, %%edx вещь? Ну, мы тоже можем от этого избавиться:

asm ("idivl %3"
  : "=a" (quo), "=d" (rem) 
  : "a" (arg1), "r" (arg2), "d" (0));

Это использует "d" ограничение для помещения 0 в edx перед выполнением asm. Это подводит нас к моей окончательной версии:

asm ("idivl %3"
  : "=a" (quo), "=d" (rem) 
  : "a" (arg1), "r" (arg2), "d" (0)
  : "cc");

Это говорит:

  • На входе поместите arg1 в eax, arg2 в какой-то регистр (который мы будем ссылаться на использование%3), а 0 в edx.
  • На выходе eax будет содержать частное, edx будет содержать остаток. Так работает инструкция idiv.
  • "Cc" clobber сообщает gcc, что ваш asm изменяет регистры флагов (eflags), что делает idiv как побочный эффект.

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

Это правда, что все это описано в документации gcc. Простые ограничения (как "r" а также "g") здесь документированы. Конкретные ограничения регистра для x86 находятся здесь в "семействе x86". И подробное описание всех возможностей ASM здесь. Так что, если вы должны использовать этот материал (например, если вы поддерживаете какой-то существующий код, который использует это), информация там.

Но здесь есть намного более короткое прочтение, которое дает вам целый список причин, по которым не стоит использовать встроенный ассм. Это чтение я бы порекомендовал. Придерживайтесь C, и пусть компилятор обрабатывает все, что регистрирует мусор для вас

PS Пока я на этом

asm ( "addl %2, %0" : "=r" (add) : "0" (arg1) , "r" (arg2) : "cc");
asm ( "subl %2, %0" : "=r" (sub) : "0" (arg1) , "r" (arg2) : "cc");
asm ( "imull %2, %0" : "=r" (mul) : "0" (arg1) , "r" (arg2) : "cc");

Посмотрите документы gcc, чтобы увидеть, что означает использование цифры во входном операнде.

Дэвид Волферд (David Wohlferd) дал очень хороший ответ о том, как лучше работать с расширенными шаблонами сборок GCC, чтобы выполнить работу существующего кода.

Может возникнуть вопрос, почему представленный код не работает с Codeblocks 16.01 с GCC, где, как это могло работать раньше. В нынешнем виде код выглядит довольно просто, так что же могло пойти не так?

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

Если вы использовали отладчик с Codeblocks 16.01, то при использовании стандартного консольного проекта C++ вы, возможно, обнаружили, что программа дает вам арифметическое исключение для инструкции IDIV в шаблоне сборки. Это то, что появляется в моей консоли вывод:

Программа получила сигнал SIGFPE, Арифметическое исключение.


Эти строки кода делают так, как вы ожидаете:

asm ( "addl %%ebx, %%eax;" : "=a" (add) : "a" (arg1) , "b" (arg2) );
asm ( "subl %%ebx, %%eax;" : "=a" (sub) : "a" (arg1) , "b" (arg2) );
asm ( "imull %%ebx, %%eax;" : "=a" (mul) : "a" (arg1) , "b" (arg2) );

Вот где были проблемы:

asm ( "movl $0x0, %%edx;"
      "movl %2, %%eax;"
      "movl %3, %%ebx;"
      "idivl %%ebx;" : "=a" (quo), "=d" (rem) : "g" (arg1), "g" (arg2) );

Одна вещь, которую Codeblocks может сделать для вас, это показать вам сгенерированный код сборки. Потяните вниз Debug меню, выберите Debugging Windows > а также Disassembly, Watches а также CPU Registers окна я очень рекомендую.

Если вы просмотрите сгенерированный код с помощью CodeBlocks 16.01 с GCC, вы можете обнаружить, что он произвел это:

/* Automatically produced by the assembly template for input constraints */
mov    -0x20(%ebp),%eax      /* EAX = value of arg1 */
mov    -0x24(%ebp),%edx      /* EDX = value of arg2 */

/* Our assembly template instructions */
mov    $0x0,%edx             /* EDX = 0 - we just clobbered the previous EDX! */
mov    %eax,%eax             /* EAX remains the same */
mov    %edx,%ebx             /* EBX = EDX = 0. */
idiv   %ebx                  /* EBX is 0 so this is division by zero!! *

/* Automatically produced by the assembly template for output constraints */
mov    %eax,-0x18(%ebp)      /* Value at quo = EAX */
mov    %edx,-0x1c(%ebp)      /* Value at rem = EDX */

Я прокомментировал код, и должно быть очевидно, почему этот код не будет работать. Мы фактически закончили тем, что поместили ноль в EBX и затем попытались использовать это как делитель с IDIV, и это произвело арифметическое исключение (деление на ноль в этом случае).

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

`&"

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

Изменяя операнды так, чтобы с ранними клобберами обращались, мы можем поместить & на обоих выходных ограничений, как это:

"idivl %%ebx;" : "=&a" (quo), "=&d" (rem) : "g" (arg1), "g" (arg2) );

В этом случае arg1 а также arg2 не будет передан через любой из операндов, отмеченных &, Это означает, что этот код не будет использовать EAX и EDX для входных операндов arg1 а также arg2,

Другая проблема заключается в том, что EBX модифицируется вашим кодом, но вы не говорите GCC. Вы можете просто добавить EBX в список clobber в шаблоне сборки следующим образом:

"idivl %%ebx;" : "=&a" (quo), "=&d" (rem) : "g" (arg1), "g" (arg2) : "ebx");

Так что этот код должен работать, но не эффективен:

asm ( "movl $0x0, %%edx;"
      "movl %2, %%eax;"
      "movl %3, %%ebx;"
      "idivl %%ebx;" : "=&a" (quo), "=&d" (rem) : "g" (arg1), "g" (arg2) : "ebx");

Сгенерированный код теперь будет выглядеть примерно так:

/* Automatically produced by the assembler template for input constraints */
mov    -0x30(%ebp),%ecx      /* ECX = value of arg1 */
mov    -0x34(%ebp),%esi      /* ESI = value of arg2 */

/* Our assembly template instructions */
mov    $0x0,%edx             /* EDX = 0 */
mov    %ecx,%eax             /* EAX = ECX = arg1 */
mov    %esi,%ebx             /* EBX = ESI = arg2 */
idiv   %ebx

/* Automatically produced by the assembler template for output constraints */
mov    %eax,-0x28(%ebp)      /* Value at quo = EAX */
mov    %edx,-0x2c(%ebp)      /* Value at rem = EDX */

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


Почему работают другие (включая старые) версии GCC?

Если GCC генерировал инструкции, используя регистры, отличные от EAX, EDX и EBX для arg1 а также arg2 Операнды тогда бы сработали. Но тот факт, что это сработало, был просто удачей. Чтобы увидеть, что происходит с более старыми Codeblocks и GCC, который поставляется с ним, я бы рекомендовал просмотреть код, сгенерированный в этой среде, так же, как я обсуждал выше.

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

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

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

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