以下程序在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尝试的开始。
答案 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的符号。