Если вы используете GDB, довольно легко пропустить тот факт, что новый кадр стека используется при каждом вызове. По умолчанию GDB показывает вам только один кадр стека для main
, независимо от того, сколько рекурсий выполняется:
$ cat recursive_main.c
#include <stddef.h>
int main(int argc, char* argv[]) {
for(int i = 0; i < argc; i++) {
main(argc, NULL);
}
}
$ clang-9 -o recursive_main -Wall -g recursive_main.c
$ ./recursive_main
Segmentation fault (core dumped)
$ gdb -q ./recursive_main
Reading symbols from ./recursive_main...done.
(gdb) break main
Breakpoint 1 at 0x4004b6: file recursive_main.c, line 4.
(gdb) commands
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>bt
>end
(gdb) r
Starting program: /home/rici/src/tmp/recursive_main
Breakpoint 1, main (argc=1, argv=0x7fffffffdec8) at recursive_main.c:4
4 for(int i = 0; i < argc; i++) {
#0 main (argc=1, argv=0x7fffffffdec8) at recursive_main.c:4
(gdb) c
Continuing.
Breakpoint 1, main (argc=1, argv=0x0) at recursive_main.c:4
4 for(int i = 0; i < argc; i++) {
#0 main (argc=1, argv=0x0) at recursive_main.c:4
(gdb)
Continuing.
Breakpoint 1, main (argc=1, argv=0x0) at recursive_main.c:4
4 for(int i = 0; i < argc; i++) {
#0 main (argc=1, argv=0x0) at recursive_main.c:4
(gdb)
Continuing.
Но если мы распечатаем указатель стека на каждую запись, мы можем видеть, что он уменьшается каждый раз:
$ gdb -q ./recursive_main
Reading symbols from ./recursive_main...done.
(gdb) break main
Breakpoint 1 at 0x4004b6: file recursive_main.c, line 4.
(gdb) commands
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>info r esp
>end
(gdb) r
Starting program: /home/rici/src/tmp/recursive_main
Breakpoint 1, main (argc=1, argv=0x7fffffffdec8) at recursive_main.c:4
4 for(int i = 0; i < argc; i++) {
esp 0xffffddc0 -8768
(gdb) c
Continuing.
Breakpoint 1, main (argc=1, argv=0x0) at recursive_main.c:4
4 for(int i = 0; i < argc; i++) {
esp 0xffffdd90 -8816
(gdb)
Continuing.
Breakpoint 1, main (argc=1, argv=0x0) at recursive_main.c:4
4 for(int i = 0; i < argc; i++) {
esp 0xffffdd60 -8864
(gdb)
Continuing.
Breakpoint 1, main (argc=1, argv=0x0) at recursive_main.c:4
4 for(int i = 0; i < argc; i++) {
esp 0xffffdd30 -8912
(gdb)
Таким образом, стек расширяется на 0x30 (48) байтов при каждом рекурсивном вызове.
Причина этого любопытного поведения заключается в том, что GDB преднамеренно завершает обратную трассировку при обращении к нему main
. Это происходит потому, что реальной точкой входа исполняемого файла является не main
, а некоторый платформо-зависимый код, который все настраивает так, чтобы можно было вызвать main
, а затем вызвать main
. Как следствие, GDB на самом деле не знает, где «начинается» стек. Точнее, он знает, где начинается стек исполняемого файла, но не знает, где начинается стек программы. Было бы немного странно включать функции в код установки исполняемого файла в каждую обратную трассировку, поэтому по умолчанию GDB просто прекращает обход стека, когда он попадает в кадр, точка входа которого main
. Если вы знаете об этой опции, вы можете управлять ей:
(gdb) help set backtrace past-main
Set whether backtraces should continue past "main".
Normally the caller of "main" is not of interest, so GDB will terminate
the backtrace at "main". Set this variable if you need to see the rest
of the stack trace.
И с установленной опцией вы можете видеть различные кадры стека, соответствующие рекурсивным вызовам main
:
$ gdb -q ./recursive_main
Reading symbols from ./recursive_main...done.
(gdb) set backtrace past-main 1
(gdb) break main
Breakpoint 1 at 0x4004b6: file recursive_main.c, line 4.
(gdb) commands
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>bt
>end
(gdb) r
Starting program: /home/rici/src/tmp/recursive_main
Breakpoint 1, main (argc=1, argv=0x7fffffffdec8) at recursive_main.c:4
4 for(int i = 0; i < argc; i++) {
#0 main (argc=1, argv=0x7fffffffdec8) at recursive_main.c:4
#1 0x00007ffff7a05b97 in __libc_start_main (main=0x4004a0 <main>, argc=1, argv=0x7fffffffdec8, init=<optimized out>,
fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdeb8) at ../csu/libc-start.c:310
#2 0x00000000004003da in _start ()
(gdb) c
Continuing.
Breakpoint 1, main (argc=1, argv=0x0) at recursive_main.c:4
4 for(int i = 0; i < argc; i++) {
#0 main (argc=1, argv=0x0) at recursive_main.c:4
#1 0x00000000004004d5 in main (argc=1, argv=0x7fffffffdec8) at recursive_main.c:5
#2 0x00007ffff7a05b97 in __libc_start_main (main=0x4004a0 <main>, argc=1, argv=0x7fffffffdec8, init=<optimized out>,
fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdeb8) at ../csu/libc-start.c:310
#3 0x00000000004003da in _start ()
(gdb)
Continuing.
Breakpoint 1, main (argc=1, argv=0x0) at recursive_main.c:4
4 for(int i = 0; i < argc; i++) {
#0 main (argc=1, argv=0x0) at recursive_main.c:4
#1 0x00000000004004d5 in main (argc=1, argv=0x0) at recursive_main.c:5
#2 0x00000000004004d5 in main (argc=1, argv=0x7fffffffdec8) at recursive_main.c:5
#3 0x00007ffff7a05b97 in __libc_start_main (main=0x4004a0 <main>, argc=1, argv=0x7fffffffdec8, init=<optimized out>,
fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdeb8) at ../csu/libc-start.c:310
#4 0x00000000004003da in _start ()
(gdb)
Continuing.
Breakpoint 1, main (argc=1, argv=0x0) at recursive_main.c:4
4 for(int i = 0; i < argc; i++) {
#0 main (argc=1, argv=0x0) at recursive_main.c:4
#1 0x00000000004004d5 in main (argc=1, argv=0x0) at recursive_main.c:5
#2 0x00000000004004d5 in main (argc=1, argv=0x0) at recursive_main.c:5
#3 0x00000000004004d5 in main (argc=1, argv=0x7fffffffdec8) at recursive_main.c:5
#4 0x00007ffff7a05b97 in __libc_start_main (main=0x4004a0 <main>, argc=1, argv=0x7fffffffdec8, init=<optimized out>,
fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdeb8) at ../csu/libc-start.c:310
#5 0x00000000004003da in _start ()
(gdb)
Но хотя, вероятно, полезно знать об этой опции GDB (а я не знал об этом до 15 минут go), на самом деле это не обязательно. Вы можете увидеть код, который создает кадр стека со смещением 1120 в разборке, которую вы связали, хотя это легче увидеть в выводе -S
(или с помощью удобного сервиса в http://gcc.godbolt):
0000000000001120 :
1120: 55 push %rbp
1121: 48 89 e5 mov %rsp,%rbp
1124: 48 83 ec 20 sub $0x20,%rsp
1128: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
112f: 89 7d f8 mov %edi,-0x8(%rbp)
1132: 48 89 75 f0 mov %rsi,-0x10(%rbp)
1136: c7 45 ec 00 00 00 00 movl $0x0,-0x14(%rbp)
113d: 8b 45 ec mov -0x14(%rbp),%eax
1140: 3b 45 f8 cmp -0x8(%rbp),%eax
1143: 0f 8d 1a 00 00 00 jge 1163
1149: 31 c0 xor %eax,%eax
114b: 89 c6 mov %eax,%esi
114d: 8b 7d f8 mov -0x8(%rbp),%edi
1150: e8 cb ff ff ff callq 1120
Как вы можете видеть, при входе в main
(с указанным смещением, 1120), сначала %rbp
pu sh помещается в стек , в результате чего% esp уменьшается на 8 (для 64-битного режима). Затем указатель стека уменьшается на дополнительные 0x20 (32), оставляя место для сохранения регистров, которые будут использоваться (которые включают регистры, используемые для передачи аргументов вызываемой функции, и регистр, используемый для хранения значения i
). Наконец (после небольшой работы) выполняется инструкция callq
со смещением 1150, которая помещает адрес следующей инструкции в стек, используя еще 8 байтов.
Итак, 48-байтовый стек кадр передается при каждом рекурсивном вызове. И поскольку рекурсия никогда не завершается, она должна в конечном итоге попасть на защищенную страницу, которая предшествует стеку, и в этот момент сигнализируется о сбое.
Обратите внимание, что это не происходит с clang на любом положительном уровне оптимизации:
$ clang-9 -o recursive_main -Wall -g -O1 recursive_main.c
$ ./recursive_main
$ gdb -q ./recursive_main
Reading symbols from ./recursive_main...done.
(gdb) disass main
Dump of assembler code for function main:
0x00000000004004a0 <+0>: xor %eax,%eax
0x00000000004004a2 <+2>: retq
End of assembler dump.
Здесь компилятор воспользовался требованием стандарта (в §6.8.5 / 6, см. Ниже), что можно предположить, что al oop, который не имеет наблюдаемого эффекта, завершается; в этом случае компилятор предполагает, что он завершается немедленно, что является законным, потому что ничего не изменится до того, как l oop в конце концов завершится.
G CC, похоже, не выполняет эту оптимизацию, кстати, поэтому он будет зависать независимо от уровня оптимизации. По крайней мере, это то, что произошло в моем тесте.
Стандарт C, §6.8.5:
Оператор итерации, управляющее выражение которого не является константным выражением, который не выполняет никаких операций ввода-вывода, не обращается к изменчивым объектам и не выполняет никаких операций синхронизации или атома c в своем теле, управляющем выражении или (в в случае для оператора ) его expression-3 может быть принято реализацией для завершения.