Бесконечная рекурсия в C

учитывая программу C с бесконечной рекурсией:

int main() {

    main();

    return 0;
}

Почему это приведет к переполнению стека. Я знаю, что это приводит к неопределенному поведению в C++ из следующих нити это бесконечная рекурсия UB? (и как боковой узел нельзя вызвать main() В C++). Однако, Valgrind говорит мне, что это приводит к переполнению стека:

Stack overflow in thread 1: can't grow stack to 0x7fe801ff8

и, наконец, программа заканчивается из-за ошибки сегментации:

==2907== Process terminating with default action of signal 11 (SIGSEGV)
==2907==  Access not within mapped region at address 0x7FE801FF0

это также неопределенное поведение в C, или это действительно должно привести к переполнению стека, а затем почему это приводит к переполнению стека?

редактировать

1 я хотел бы знать, разрешена ли бесконечная рекурсия в C?
2 должно ли это привести к переполнению стека? (получил достаточный ответ)

13 ответов


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

в вашем примере это означает, что никакие аргументы не используются, поэтому единственное, что нажимается, - это обратный адрес, который довольно мал( 4 байта на x86-32 architexture), и дополнительно настраивается stackframe, который занимает еще четыре байта на этой архитектуре.

из этого следует, что, как только сегмент стека исчерпан, функция не может быть вызвана aynmore и исключение вызывается в ОС. Теперь могут произойти две вещи. Либо ОС перенаправляет исключение обратно в ваше приложение, которое вы увидите как переполнение стека. Или ОС может попытаться выделить дополнительное пространство для стека segemnt, до определенного предела, после чего приложение увидит стек переполнение.

поэтому этот код (я переименовал его в infinite_recursion () как main () не может быть вызван)...

int inifinite_recursion(void)
{
    inifinite_recursion();
    return 0;
}

... выглядит так:

_inifinite_recursion:
    push    ebp                    ; 4 bytes on the stack
    mov ebp, esp

    call    _inifinite_recursion   ; another 4 bytes on the stack
    mov eax, 0                 ; this will never be executed.

    pop ebp
    ret 

обновление

Что касается стандарта C99 для определения рекурсии, лучшее, что я нашел до сих пор, находится в разделе 6.5.2.2 пункт 11:

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

конечно, это не отвечает, определено ли, что происходит, когда стек переполняется. Однако, по крайней мере, это позволяет main вызывается рекурсивно, хотя это явно запрещено в C++ (раздел 3.6.1 пункт 3 и раздел 5.2.2 пункт 9).


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


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

void iteration_2 (int x) {
    /* ... */
}

void iteration_1 (int x) {
    if (x > 0) return;
    iteration_2(x + 1);
}

void iteration_0 (int x) {
    if (x > 0) return;
    iteration_1(x + 1);
}

каждого iteration_#() в основном идентичны друг другу, но каждый из них имеет свои собственные x, и каждый помнит, какая функция вызвала его, поэтому он может правильно вернуться к вызывающему абоненту, когда функция, которую он вызывает, выполнена. Этот понятие не меняется, когда программа преобразуется в рекурсивную версию:

void iteration (int x) {
    if (x > 0) return;
    iteration(x + 1);
}

итерация становится бесконечной, если условие остановки (if проверьте return из функции) удаляется. Из рекурсии возврата нет. Таким образом, информация, которая запоминается для каждого последующего вызова функции (local x и адрес вызывающего абонента) продолжает накапливаться, пока в ОС не закончится память для хранения этой информации.

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

int iteration () {
    return iteration();
}

при компиляции с gcc -O0 в:

iteration:
.LFB2:
        pushq   %rbp
.LCFI0:
        movq    %rsp, %rbp
.LCFI1:
        movl    , %eax
        call    iteration
        leave
        ret

но при компиляции с gcc -O2, рекурсивный вызов удаляется:

iteration:
.LFB2:
        .p2align 4,,7
.L3:
        jmp     .L3

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

ваша программа, однако, не является кандидатом на оптимизацию хвостового вызова, поскольку рекурсивный вызов-это не последнее, что делает ваша функция. Ваша функция все еще имеет return оператор, который следует за рекурсивным вызовом. Поскольку есть еще код, который необходимо выполнить после возвращения рекурсивного вызова, оптимизатор не может удалить накладные расходы рекурсивного вызова. Он должен позволить вызову вернуться нормально, чтобы код после него мог выполняться. Таким образом, ваша программа всегда будет платить штраф за хранение обратного адреса вызывающего кода.

стандарт не говорит о "бесконечной рекурсии" в каких-либо конкретных терминах. Я собрал все, что, на мой взгляд, имеет отношение к вашему вопросу.

  • рекурсивный вызов функции разрешен (C. 11 §6.5.2.2 ¶11)

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

  • рекурсивная запись в оператор создает новые экземпляры локальных переменных (C. 11 §6.2.4 ¶5,6,7)

объект, идентификатор которого объявлен без привязки и без класса хранения спецификатор static имеет автоматическую продолжительность хранения, как и некоторые составные литералы. ...

для такого объекта, который не имеет типа массива переменной длины, его время жизни продлевается от входа в блок, с которым он связан, до завершения выполнения этого блока в любой способ. (Ввод закрытого блока или вызов функции приостанавливается, но не завершается, выполнение текущего блока.) Если блок вводится рекурсивно, то новый экземпляр объект создается каждый раз. ...

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

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


всякий раз, когда вы делаете вызов функции (включая main ()), вызов функции "info" (например, аргументы) нажимается поверх стека. Эта информация выводится из стека при возврате функции. Но, как вы можете видеть в своем коде, вы делаете рекурсивный вызов main до вы вернетесь, поэтому стек продолжает расти, пока не достигнет своего предела и, следовательно, ошибка сегментации.

размер стека часто ограничен и определяется до выполнения (например, вашей операционной система.)

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


даже если функция не использует пространство стека для локальных переменных или передачи аргументов, ей все равно нужно сохранить обратный адрес и (возможно) базовый указатель фрейма (с gcc это можно отключить через -fomit-frame-pointer).

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


вопросы 1:

я хотел бы знать, разрешена ли бесконечная рекурсия в C?

в этой статье Compilers and Termination Revisited by John Regehr является ответом на вопрос C standard позволяет бесконечную рекурсию или нет, и после прочесывания стандарта меня не слишком удивляет, что выводы являются неоднозначными. Основная тяга статьи о бесконечных петлях и поддерживается ли она стандартом различных языки (включая C и C++), чтобы иметь не прекращающиеся исполнения. Насколько я могу судить, обсуждение относится и к бесконечной рекурсии, конечно, если мы можем избежать переполнения стека.

в отличие от C++ который говорит в разделе 1.10 Multi-threaded executions and data races абзац 24:

The implementation may assume that any thread will eventually do one of the
following:
  — terminate,
  [...]

что, похоже, исключает бесконечную рекурсию в C++. The draft C99 standard написано в разделе 6.5.2.2 Function calls абзац 11:

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

который не ставит никаких ограничений на рекурсию и говорит об этом в разделе 5.1.2.3 Program execution абзац 5:

The least requirements on a conforming implementation are:
— At sequence points, volatile objects are stable in the sense that previous 
  accesses are complete and subsequent accesses have not yet occurred.
— At program termination, all data written into files shall be identical to the
  result that execution of the program according to the abstract semantics would
  have  produced.
— The input and output dynamics of interactive devices shall take place as
  specified in 7.19.3. The intent of these requirements is that unbuffered or     
  line-buffered output appear as soon as possible, to ensure that prompting
  messages actually appear prior to a program waiting for input.

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

если речь идет о завершении программы, запущенной на абстрактной машине, то она бессмысленно выполняется, потому что наша программа не завершается. Если речь идет о завершении фактической программы, сгенерированной компилятором, то реализация C глючит, потому что данные, записанные в файлы (stdout-это файл), отличаются от данных, записанных абстрактной машиной. (Это чтение принадлежит Гансу Бему; мне не удалось вывести эту тонкость из стандарта.)

Итак, у вас есть это: поставщики компиляторов читают стандарт в одну сторону, а другие (например, я) читают его в другую сторону. Совершенно ясно, что стандарт ошибочен: он должен, как C++ или Java, быть однозначным относительно того, разрешено ли это поведение.

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


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


важно понять, как выглядит вызывающий стек функций в C:

function stack


бесконечная рекурсия разрешено в C? Простой ответ: да. Компилятор позволит вам вызывать функцию бесконечно, пока у вас не закончится пространство стека; это не помешает вам сделать это.

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

возможна ли поддельная бесконечная рекурсия? Да. Можно создать функцию, которая вызывает себя 1000 раз, а затем позволяет выйти из 1000 вызовов функций, так что стек имеет только исходный вызов функции в стеке... а затем повторите весь процесс снова и снова в бесконечном цикле. Однако я не считаю эту реальную бесконечную рекурсию.


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


это разрешено в c, так как стандарт говорит ->

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

в 6.5.2.2 -> 11

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


причина стека ограничена, и всякий раз, когда вы вызываете функцию, она сохраняет вызываемого (путем нажатия базового указателя на стек и копирования текущего указателя стека в качестве нового значения базового указателя), поэтому потребляет стек будет переполняться бесконечным количеством вызовов. См. соглашение о вызовах и как стек реагирует здесь (http://www.csee.umbc.edu / ~chang / cs313.S02 / stack.shtml)


Я просто посмотрел на копию недавней проект стандартов C doc и ни одна из ссылок рекурсии не говорит о бесконечной рекурсии.

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