这个混淆的 C 代码声称 运行 没有 main(),但它到底做了什么?

This obfuscated C code claims to run without a main(), but what does it really do?

#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?怎么样?

C语言定义的执行环境分为两类:freestandinghosted。在这两个执行环境中,一个函数被环境调用用于程序启动。
freestanding 环境中程序启动函数可以实现定义,而在 hosted 环境中它应该是 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、第1st、第3rd和替换列表中使用的 2nd 参数。

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

其余的没有用(只是为了混淆)。传递给 decode 的参数是 "a,n,i,m,a,t,e" 因此,标识符 m, s, ut 分别替换为参数 m, a, in

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

尝试使用 gcc -E source.c,输出结束于:

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

所以一个main()函数实际上是由预处理器生成的。

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

因此,begin宏简单定义为main

有人想扮魔术师。 他认为他可以欺骗我们。但是众所周知,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 将具有字符 a 的值。同样,u 将替换为 'i',而 t 将替换为 'n'。而且,这就是 m##s##u##t 将变成 main

的方式

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

如果你不相信我,你可以用 -E 标志编译你的代码。预处理后会停止编译过程,你可以看到标记粘贴的结果。

gcc -E FILENAME.c

由于宏扩展,有问题的程序 确实 调用 main(),但您的假设是有缺陷的 - 它 不会 ] 根本就得打电话给main()

严格来说,您可以拥有一个 C 程序并且能够在没有 main 符号的情况下编译它。 mainc library 在完成自己的初始化后期望跳入的内容。通常你从称为 _start 的 libc 符号跳转到 main。总是有可能有一个非常有效的程序,它只执行汇编,而没有 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 [=10=]x80"::"a"(4), "b"(1), "c"("Hello world!\n"), "d"(13));
    asm ("int [=10=]x80"::"a"(1), "b"(0)); /* calling exit syscall, with the argument to be 0 */
}

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

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

另一个有趣的问题是,您还可以拥有一个程序,该程序无需 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 所需的指令。有关此特定程序如何工作的更详细说明,请查看此 blog post,这也是我首先阅读它的地方。

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

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

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

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

在Windows中,你并不总是使用main(),而是rather WinMain or wWinMain, although you can use main(), even with Microsoft's toolchain。在 Linux 中,可以使用 _start.

由链接器作为操作系统工具来设置入口点,而不是语言本身。你甚至可以 set our own entry point, and you can make a library that is also executable!