Подтвердить что ты не робот

Функция, оптимизированная для бесконечного цикла в 'gcc -O2'

Контекст
Меня спросила следующая головоломка одного из моих друзей:

void fn(void)
{
  /* write something after this comment so that the program output is 10 */
  /* write something before this comment */
}

int main()
{
  int i = 5;
  fn();
  printf("%d\n", i);
  return 0;
}

Я знаю, что может быть несколько решений, некоторые из которых связаны с макросом, а некоторые - что-то о реализации и нарушении C.

Единственное конкретное решение, которое меня интересовало, - это сделать некоторые предположения о стеке и написать следующий код: (Я понимаю, что это поведение undefined, но может работать как ожидается во многих реализациях)

void fn(void)
{
  /* write something after this comment so that the program output is 10 */
  int a[1] = {0};
  int j = 0;
  while(a[j] != 5) ++j;  /* Search stack until you find 5 */
  a[j] = 10;             /* Overwrite it with 10 */
  /* write something before this comment */
}

Проблема
Эта программа отлично работала в MSVC и gcc без оптимизации. Но когда я скомпилировал его с флагом gcc -O2 или попытался на ideone, он бесконечно петлит в функции fn.

Мое наблюдение
Когда я скомпилировал файл с gcc -S vs gcc -S -O2 и сравнил, он ясно показывает, что gcc хранит бесконечный цикл в функции fn.

Вопрос
Я понимаю, потому что код вызывает поведение undefined, его нельзя назвать ошибкой. Но почему и как компилятор анализирует поведение и оставляет бесконечный цикл в O2?


Многие люди прокомментировали это поведение, если некоторые переменные изменены на изменчивые. Ожидаемый результат:

  • Если i или j изменено на volatile, поведение программы остается таким же.
  • Если массив a сделан volatile, программа не терпит бесконечного цикла.
  • Кроме того, если я применил следующий патч
-  int a[1] = {0};
+  int aa[1] = {0};
+  int *a = aa;

Поведение программы остается таким же (бесконечный цикл)

Если я скомпилирую код с помощью gcc -O2 -fdump-tree-optimized, я получаю следующий промежуточный файл:

;; Function fn (fn) (executed once)

Removing basic block 3
fn ()
{
<bb 2>:

<bb 3>:
  goto <bb 3>;

}



;; Function main (main) (executed once)

main ()
{
<bb 2>:
  fn ();

}
Invalid sum of incoming frequencies 0, should be 10000

Это проверяет утверждения, сделанные после ответов ниже.

4b9b3361

Ответ 1

Это поведение undefined, поэтому компилятор действительно может что-то сделать вообще, мы можем найти аналогичный пример в GCC pre-4.8 Breaks Broken SPEC 2006 Тесты, где gcc принимает цикл с поведением undefined и оптимизирует его для:

L2:
    jmp .L2

В статье говорится (внимание мое):

Конечно, это бесконечный цикл. Поскольку SATD() безоговорочно выполняет undefined поведение (его функция типа 3), любой перевод (или вообще ничего) является вполне приемлемым поведением для правильный компилятор C. Поведение undefined доступно для доступа к d [16] перед выходом из цикла. В C99 законно создавать указатель на элемент на одну позицию за конец массива, но этот указатель не должно быть разыменовано. Аналогично, элемент ячейки массива прошёл конец массива не должен быть доступен.

который, если мы рассмотрим вашу программу с godbolt, мы видим:

fn:
.L2:
    jmp .L2

Логика, используемая оптимизатором, вероятно, выглядит примерно так:

  • Все элементы a инициализируются нулем
  • a никогда не изменяется до или внутри цикла
  • Итак a[j] != 5 всегда истинно → бесконечный цикл
  • Из-за бесконечности a[j] = 10; недоступен и поэтому может быть оптимизирован, поэтому можно a и j, так как они больше не нужны для определения условия цикла.

что аналогично случаю в статье, которая дана:

int d[16];

анализирует следующий цикл:

for (dd=d[k=0]; k<16; dd=d[++k]) 

вот так:

при просмотре d [++ k], допускается считать, что приращение of k находится в пределах массива, так как в противном случае поведение undefinedимеет место. Для кода здесь GCC может сделать вывод, что k находится в диапазоне 0..15. Немного позже, когда GCC видит k < 16, он говорит себе: "Aha- that выражение всегда истинно, поэтому мы имеем бесконечный цикл".

Возможно, интересная вторичная точка, является ли бесконечный цикл рассмотренным поведением (w.r.t. к правилу as-if) или нет, что влияет на то, можно ли оптимизировать бесконечный цикл. Мы можем видеть из C Compilers Disprove Fermats Последняя теорема, что до C11 было хотя бы место для интерпретации:

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

C11 добавляет пояснения к разделу 6.8.5 Итерационные утверждения и более подробно рассматривается в этом ответе.

Ответ 2

В оптимизированной версии компилятор решил несколько вещей:

  • Массив a не изменяется перед этим тестом.
  • Массив a не содержит 5.

Поэтому мы можем переписать код как:

void fn(void) {
  int a[1] = {0};
  int j = 0;
  while(true) ++j;
  a[j] = 10;
}

Теперь мы можем принимать дальнейшие решения:

  • Весь код после цикла while является мертвым кодом (недостижимым).
  • j написано, но никогда не читается. Поэтому мы можем избавиться от него.
  • a никогда не читается.

В этот момент ваш код был уменьшен до:

void fn(void) {
  int a[1] = {0};
  while(true);
}

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

void fn(void) {
  while(true);
}

Теперь неоптимизированный код:

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

Вот почему неоптимизированная версия иногда не сбой и запись.

Ответ 3

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

  • not volatile

  • содержит только 0

  • никогда не записывается на

и, следовательно, не может содержать число 5. Это означает бесконечный цикл.

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

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

void fn(void)
{
  /* write something after this comment so that the program output is 10 */
  printf("10\n"); /* Output 10 */
  while(1); /* Endless loop, function won't return, i won't be output */
  /* write something before this comment */
}

или это (если включено stdlib.h):

void fn(void)
{
  /* write something after this comment so that the program output is 10 */
  printf("10\n"); /* Output 10 */
  exit(0); /* Exit gracefully */
  /* write something before this comment */
}