为什么将main定义为函数指针的程序失败了?

时间:2014-03-26 23:52:49

标签: c++ main

以下程序在g ++中完美编译,没有错误或警告(即使使用-Wall),但会立即崩溃。

#include <cstdio>

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


int (*main)(void) = stuff;

这是一种(显然可怕的误导)尝试运行C ++程序而没有明确地将main声明为函数。我打算让程序通过将它绑定到符号stuff来执行main。我很惊讶这个编译,但为什么它失败了,编译?我已经查看了生成的程序集,但我根本不了解它。

我完全清楚main有很多main可以定义/使用{{1}},但我不明白我的程序是如何打破它们的。我没有超载main或者在我的程序中调用它...所以我通过这种方式定义{{1}}确切地说我打破了什么规则?

注意:这是我在实际代码中尝试做的事情。这实际上是restrictions尝试的开始。

2 个答案:

答案 0 :(得分:22)

main之前运行的代码中,有类似的内容:

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

代码的问题在于,如果你有一个名为main的函数指针,它与函数不同(与Haskell相反,函数和函数指针几乎可以互换 - 在至少我对Haskell的0.1%知识)。

虽然编译器很乐意接受:

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

int x = func();

作为对函数指针func的有效调用。但是,当编译器生成调用func的代码时,它实际上以不同的方式执行此操作[尽管标准没有说明应如何完成此操作,并且它在不同的处理器体系结构上有所不同,实际上它会加载指针变量中的值,然后调用此内容]。

当你有:

int func() { ... }

int x = func();

func的调用只是指func本身的地址,并调用它。

因此,假设您的代码实际上已编译,main之前的启动代码将调用变量main的地址,而不是间接读取main中的值,然后调用它。在现代系统中,这将导致段错误,因为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 <puts@plt>
   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]:段错误在600950 ip 0000000000600950 sp 00007fff4e7cb928错误15在a.out [600000 + 1000]

换句话说,分段错误会在执行main时立即发生 - 因为它不可执行。

EDIT3:

好吧,所以它比那更复杂(至少在我的C运行时库中),因为调用main的代码是一个函数,它将指向main的指针作为参数,并通过指针调用它。然而,这并没有改变这样的事实:当编译器构建代码时,它产生的间接级别低于它所需的级别,并尝试执行名为main的变量而不是变量所指向的函数。

在GDB中列出__libc_start_main

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不同的变量{{1}}。

答案 1 :(得分:6)

这里没有什么特别的东西是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类函数,没有名称修改,只是在符号表中显示为名称。如果你使它不是函数,你会在执行指针时遇到段错误。

我想有趣的是,当编译器已经使用不同的类型隐式声明时,编译器将允许您定义一个名为main的符号。