Почему эта программа, которая определяет main как указатель на функцию, терпит неудачу?

Следующая программа отлично компилируется без ошибок или предупреждений (даже с -Wall) в g++, но сбой немедленно.

#include <cstdio>

int stuff(void)
{
    puts("hello there.");
    return 0;
}


int (*main)(void) = stuff;

Это (явно ужасно ошибочная) попытка запуска программы на С++ без явного объявления main как функции. Это было мое намерение для программы выполнить stuff, привязывая ее к символу main. Я был очень удивлен, что это скомпилировано, но почему именно он терпит неудачу, скомпилировав? Я посмотрел на сгенерированную сборку, но я не знаю достаточно, чтобы понять ее вообще.

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

Примечание: это не то, что я пытался сделать в реальном коде. На самом деле это было началом попытки написать Haskell в С++.

Ответы

Ответ 1

В коде, который выполняется до main, есть что-то вроде:

extern "C" int main(int argc, char **argv);

Проблема с вашим кодом заключается в том, что если у вас есть указатель на функцию main, это не то же самое, что функция (в отличие от Haskell, где функция и указатель funciton в значительной степени взаимозаменяемы - по крайней мере, с мои 0,1% знания Haskell).

Пока компилятор с радостью примет:

int (*func)()  = ...;

int x = func();

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

Если у вас есть:

int func() { ... }

int x = func();

вызов func означает только адрес func и вызывает это.

Итак, если ваш код действительно компилируется, код запуска до main будет вызывать адрес вашей переменной main, а не косвенно читать значение в main, а затем вызывать это. В современных системах это приведет к segfault, потому что main живет в сегменте данных, который не является исполняемым, но в более старой ОС он скорее всего сбой из-за main не содержит реального кода (но он может выполнять несколько инструкций прежде чем он упадет в этом случае - в тусклом и отдаленном прошлом, я случайно запускаю всевозможные "мусор" с довольно трудными для обнаружения причинами...)

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

Он работал много лет назад, чтобы сделать это:

char main[] = { 0xXX, 0xYY, 0xZZ ... }; 

но опять же, это не работает в современной ОС, потому что main заканчивается в разделе данных и не является исполняемым в этом разделе.

Изменить: после фактического тестирования опубликованного кода, по крайней мере, на моем 64-битном Linux, код действительно компилируется, но сбой, неудивительно, когда он пытается выполнить main.

Запуск в GDB дает следующее:

Program received signal SIGSEGV, Segmentation fault.
0x0000000000600950 in main ()
(gdb) bt
#0  0x0000000000600950 in main ()
(gdb) disass
Dump of assembler code for function main:
=> 0x0000000000600950 <+0>: and    %al,0x40(%rip)        # 0x600996
   0x0000000000600956 <+6>: add    %al,(%rax)
End of assembler dump.
(gdb) disass stuff
Dump of assembler code for function stuff():
   0x0000000000400520 <+0>: push   %rbp
   0x0000000000400521 <+1>: mov    %rsp,%rbp
   0x0000000000400524 <+4>: sub    $0x10,%rsp
   0x0000000000400528 <+8>: lea    0x400648,%rdi
   0x0000000000400530 <+16>:    callq  0x400410 <[email protected]>
   0x0000000000400535 <+21>:    mov    $0x0,%ecx
   0x000000000040053a <+26>:    mov    %eax,-0x4(%rbp)
   0x000000000040053d <+29>:    mov    %ecx,%eax
   0x000000000040053f <+31>:    add    $0x10,%rsp
   0x0000000000400543 <+35>:    pop    %rbp
   0x0000000000400544 <+36>:    retq   
End of assembler dump.
(gdb) x main
0x400520 <stuff()>: 0xe5894855
(gdb) p main
$1 = (int (*)(void)) 0x400520 <stuff()>
(gdb) 

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

Edit2:

Проверка dmesg показывает:

a.out [7035]: segfault at 600950 ip 0000000000600950 sp 00007fff4e7cb928 ошибка 15 в a.out [600000 + 1000]

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

Edit3:

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

Листинг __libc_start_main в GDB:

87  STATIC int
88  LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
89           int argc, char *__unbounded *__unbounded ubp_av,
90  #ifdef LIBC_START_MAIN_AUXVEC_ARG
91           ElfW(auxv_t) *__unbounded auxvec,
92  #endif

В этот момент печать main дает нам указатель на функцию, указывающий на 0x600950, который является переменной с именем main (такой же, как и я, описан выше)

(gdb) p main
$1 = (int (*)(int, char **, char **)) 0x600950 <main>

Обратите внимание, что это другая переменная main, чем та, которая называется main в источнике, отправленном в вопросе.

Ответ 2

Здесь нет ничего особенного в том, что это main(). То же самое произойдет, если вы сделаете это для любой функции. Рассмотрим этот пример:

file1.cpp:

#include <cstdio>

void stuff(void)
{
     puts("hello there.");
}

void (*func)(void) = stuff;

file2.cpp:

extern "C" {void func(void);}

int main(int argc, char**argv)
{
    func();
}

Это также будет компилироваться, а затем segfault. Это, по сути, делает то же самое для функции func, но поскольку кодирование является явным, теперь он выглядит более явно неправильно. main() - это простая функция типа C, не имеющая названия, и просто отображается как имя в таблице символов. Если вы делаете это чем-то другим, кроме функции, вы получаете segfault, когда он выполняет указатель.

Я думаю, интересная часть состоит в том, что компилятор позволит вам определить символ main, когда он уже неявно объявлен с другим типом.