SegFault вызывая главный - PullRequest
       62

SegFault вызывая главный

0 голосов
/ 28 марта 2020

Я упростил этот небольшой кусочек кода, который, как я ожидал, будет бесконечным l oop до:

#include <stddef.h>

int main(int argc, char* argv[]) {
  for(int i = 0; i < argc; i++) {
    main(argc, NULL);
  }
}

(независимо от того, передаете ли вы argv или нет, на самом деле не имеет значения. В любом случае компилятор имеет тенденцию оптимизировать его.)

Однако при обоих значениях clang 9.0.1 и g cc 9.2.0 вышеприведенный код выдает ошибку с ошибкой границы адреса.

Глядя на asm (который я выкинул здесь ), я все еще не вижу ничего, что могло бы вызвать это go haywire.

Ответы [ 2 ]

2 голосов
/ 28 марта 2020

Если вы используете 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 может быть принято реализацией для завершения.
0 голосов
/ 28 марта 2020

Программа вызывает неопределенное поведение из-за отсутствия продвижения вперед. C11 6.8.5 / 6:

Оператор итерации, управляющее выражение которого не является константным выражением, не выполняет операций ввода / вывода, не обращается к изменчивым объектам и не выполняет синхронизацию или атомы c операции в его теле, управляющие выражением или (в случае оператора for) его выражением-3, могут быть приняты реализацией для завершения.

Таким образом, компилятор может принять for l oop заканчивается. Поскольку выполнение l oop фактически никогда не завершится, поведение не определяется пропуском, и поэтому может произойти все что угодно.

...