这段经过混淆的C代码声称可以在没有main()的情况下运行,但是它实际上是做什么的呢?


84
#include <stdio.h>
#define decode(s,t,u,m,p,e,d) m##s##u##t
#define begin decode(a,n,i,m,a,t,e)

int begin()
{
    printf("Ha HA see how it is?? ");
}

这是间接调用main吗?怎么样?


146
定义的宏expand开始说“ main”。这只是一个把戏。没什么有趣的。
rghome

10
您的工具链应具有将预处理后的代码保留在文件中的选项-实际编译的文件-您将在其中看到它的确有main()

@rghome为什么不发布答案?考虑到投票的数量,这显然很有趣。
Matsemann '16

3
@Matsemann哇!我没有注意到投票。我可以将其更改为答案,如果评论增票为答增票,那将是我迄今为止的最高分,但是已经有了详细的答复。我认为我的评论的重点是,它并不是很有趣,因此对于不想投票的人来说,它可以作为一种选择。感谢您指出这一点。
rghome

伙计们,取决于链接程序作为操作系统工具来设置入口点,而不是语言本身。您甚至可以设置我们自己的入口点,并且可以创建一个可执行文件的库!unix.stackexchange.com/a/223415/37799
1

Answers:


193

C语言将执行环境定义为两类:独立式托管。在两种执行环境中,环境都会调用一个函数来启动程序。
独立的环境中,程序的启动功能可以定义实现,而在宿主环境中应该是main。如果没有定义环境中的程序启动功能,则C中的任何程序都无法运行。

在您的情况下,main被预处理器定义隐藏。begin()将会 decode(a,n,i,m,a,t,e)扩展到main

int begin() -> int decode(a,n,i,m,a,t,e)() -> int m##a##i##n() -> int main() 

decode(s,t,u,m,p,e,d)是具有7个参数的参数化宏。此宏的替换列表是m##s##u##tm, s, ut是4,1,3和2在替换列表中使用的参数。

s, t, u, m, p, e, d
1  2  3  4  5  6  7

其余都没有用(仅仅是混淆)。传递给的参数decode是“ anim,a,t,e”,因此标识符m, s, ut分别替换为参数m, a, in

 m --> m  
 s --> a 
 u --> i 
 t --> n

11
@GrijeshChauhan所有C编译器都处理宏,自C89以来,所有C标准都需要使用该宏。
jdarthenay '16

17
这是完全错误的。在Linux上,我可以使用_start()。甚至更底层,我可以尝试将程序的开始与启动后IP设置的地址对齐。main()是C标准。C本身并不对此施加限制。
ljrk

1
@haccks标准确实定义了一个入口点。语言本身不在乎
ljrk

3
你能解释一下如何decode(a,n,i,m,a,t,e)成为m##a##i##n吗?它会替换字符吗?您可以提供decode功能文档的链接吗?谢谢。
AL

1
@AL Firstbegin被定义为被decode(a,n,i,m,a,t,e)之前定义的替换。此函数接受参数s,t,u,m,p,e,d并将其以这种形式连接m##s##u##t##表示连接)。即,它忽略p,e和d的值。当您decode以s = a,t = n,u = i,m = m进行“通话”时,它将有效地替换beginmain
ljrk '16

71

尝试使用gcc -E source.c,输出结尾为:

int main()
{
    printf("Ha HA see how it is?? ");
}

因此,main()函数实际上是由预处理器生成的。


37

该程序确实是main()由于宏扩展而调用的,但您的假设存在缺陷-根本不需要调用main()

严格来说,您可以拥有一个C程序,并且可以在没有main符号的情况下对其进行编译。在完成自己的初始化之后,它mainc library期望加入的东西。通常,您会main从libc符号跳入_start。总是可能有一个非常有效的程序,该程序只执行汇编而没有main。看看这个:

/* This must be compiled with the flag -nostdlib because otherwise the
 * linker will complain about multiple definitions of the symbol _start
 * (one here and one in glibc) and a missing reference to symbol main
 * (that the libc expects to be linked against).
 */

void
_start ()
{
    /* calling the write system call, with the arguments in this order:
     * 1. the stdout file descriptor
     * 2. the buffer we want to print (Here it's just a string literal).
     * 3. the amount of bytes we want to write.
     */
    asm ("int $0x80"::"a"(4), "b"(1), "c"("Hello world!\n"), "d"(13));
    asm ("int $0x80"::"a"(1), "b"(0)); /* calling exit syscall, with the argument to be 0 */
}

使用编译上述内容gcc -nostdlib without_main.c,并Hello World!通过内联汇编发出系统调用(中断),即可在屏幕上看到它的打印内容。

有关此特定问题的更多信息,请查看ksplice博客。

另一个有趣的问题是,您还可以拥有一个无需main符号与C函数相对应即可进行编译的程序。例如,您可以将以下代码作为非常有效的C程序,只有在您达到警告级别时,编译器才会发出抱怨。

/* These values are extracted from the decimal representation of the instructions
 * of a hello world program written in asm, that gdb provides.
 */
const int main[] = {
    -443987883, 440, 113408, -1922629632,
    4149, 899584, 84869120, 15544,
    266023168, 1818576901, 1461743468, 1684828783,
    -1017312735
};

数组中的值是对应于在屏幕上打印Hello World所需指令的字节。有关此特定程序如何工作的更详细说明,请参阅此博客文章,这也是我首先阅读的地方。

我要对这些程序做出最后的通知。我不知道它们是否根据C语言规范注册为有效的C程序,但是即使它们违反了规范本身,也很有可能编译并运行它们。


1
是已_start定义标准的一部分的名称,还是仅特定于实现的名称?当然,您的“作为阵列的主体”是特定于体系结构的。同样重要的是,由于安全方面的限制,“作为阵列的主体”技巧在运行时失败不会是不合理的(尽管如果不使用const限定符,这种可能性更大,而且仍然有很多系统允许这样做)。
2016年

1
@mah:_start不在ELF标准,尽管AMD64 psABI包含参照_start3.4进程初始化。正式地,ELF只知道e_entryELF标头中的地址,_start这只是实现选择的名称。
ninjalj 2016年

1
@mah同样重要的是,由于安全限制,“作为数组的主体”技巧在运行时失败不会是不合理的(尽管如果您不使用const限定符,这种可能性更大,而且仍然有很多系统允许它)。 仅当最终可执行文件在某种程度上可以区分为不安全时,二进制可执行文件才是二进制可执行文件,无论它如何到达那里。而且const没关系-该二进制可执行文件中的符号名称为main。不多不少。 const是一个C构造,在执行时没有任何意义。
Andrew Henle

1
@Stewart:它肯定在ARMv6l上失败(分段错误)。但是它应该可以在任何x86-64架构上运行。
leftaboutabout

@AndrewHenle二进制可执行文件无论它如何到达都是二进制可执行文件-并非完全正确。二进制可执行文件不是可执行指令的单个Blob,而是精心映射的分区Blob,其中有些是指令,有些是只读数据,有些是要初始化为读写数据的数据。(某些)安全硬件MMU可以阻止未标记页面的执行,这是防止(例如)防止堆栈溢出导致在堆栈上执行代码的好功能,但可悲的是有时这是合法的或经常未启用。
2016年

30

有人试图扮演魔术师。他认为他可以欺骗我们。但众所周知,c程序的执行始于main()

int begin()将被替换为decode(a,n,i,m,a,t,e)由预处理器级的一个通。然后再次decode(a,n,i,m,a,t,e)将其替换为m ## a ## i ## n。如通过宏调用的位置关联, s将具有character的值a。同样,u将被替换为“ i”,t将被替换为“ n”。而且,这m##s##u##t将成为main

关于##符号,在宏扩展中,它是预处理运算符,它执行令牌粘贴。扩展宏时,每个'##'运算符两侧的两个标记合并为一个标记,然后替换宏扩展中的'##'和两个原始标记。

如果您不相信我,可以使用-Eflag编译代码。预处理后它将停止编译过程,您可以看到令牌粘贴的结果。

gcc -E FILENAME.c

11

decode(a,b,c,d,[...])将前四个参数混洗,并按顺序将它们连接以获得新的标识符dacb。(将忽略其余三个参数。)例如,decode(a,n,i,m,[...])给出标识符main。请注意,这就是begin宏的定义。

因此,该begin宏仅定义为main


2

在您的示例中,main()函数实际上存在,因为它begin是一个宏,编译器将decode其替换为宏,该宏又被表达式m ## s ## u ## t替换。使用宏扩展##,您可以main从...得到这个词decode。这是跟踪:

begin --> decode(a,n,i,m,a,t,e) --> m##parameter1##parameter3##parameter2 ---> main

只是一个技巧main(),但是main()在C编程语言中,不必为程序的入口函数使用名称。它取决于您的操作系统和链接器作为其工具之一。

在Windows中,你并不总是使用main(),但是相当WinMain或者wWinMain,虽然可以使用main(),甚至与微软的工具链。在Linux中,可以使用_start

链接程序作为操作系统工具来设置入口点,而不是语言本身。您甚至可以设置我们自己的入口点,并且可以创建一个可执行文件的库


@vaxquis你是对的,但这是我写的部分答案,用以称赞/纠正将main()功能绑定到C编程语言的第一个答案,这是不正确的。
Ho1 2016年

@vaxquis我认为解释“ main()函数在C程序中不是必需的”将是部分答案。我添加了一个段落以使答案完整。- HO1 16分钟前
110 1
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.