Блоки кода Ver.16.01 сбой во время цикла запуска программы

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

Вот код:

//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;
}

Ответ 1

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

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

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

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

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

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

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

Это говорит gcc, что ebx может быть изменен asm (иначе он "clobbers" ) и что ему не нужно иметь какое-либо конкретное значение, когда инструкция asm начинается, и не будет иметь никакого конкретного значения в это когда 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, что означает "использовать вещь, которая находится в (нулевом) параметре № 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.
  • Clobber "cc" сообщает gcc, что ваш asm изменяет регистры флагов (eflags), которые idiv делает как побочный эффект.

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

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

Но здесь гораздо меньше читать здесь, который дает вам целый список причин не использовать inline asm. То, что прочитал, я бы рекомендовал. Stick с 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 docs, чтобы увидеть, что означает использование цифры во входном операнде.

Ответ 2

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

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

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

Если вы использовали отладчик с Codeblocks 16.01, с консольным проектом С++, вы, возможно, обнаружили, что программа дает вам арифметическое исключение в команде 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 w/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 считает эту ситуацию Early Clobber. Он предоставляет механизм для отметки ограничения на выход как ранний clobber с использованием модификатора & (амперсанд):

`& '

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

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

"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, которые пришли с ним, я бы рекомендовал просмотреть код, сгенерированный в этой среде, так же, как я обсуждал выше.

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

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

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