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?? ");
}

Does this indirectly call main? how?

14052 次浏览

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的参数是“ N、 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 $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 程序,这只会使编译器在您升级到 Warnings 级别时发出抱怨。

/* 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 程序,但是编译并运行它们当然是非常可能的,即使它们违反了规范本身。

在你的例子中,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(),而是使用 而是 ABC1或 wWinMain,尽管 你可以使用 main(),甚至使用微软的工具链。在 Linux 中,你可以使用 _start

这取决于链接器作为操作系统工具来设置入口点,而不是语言本身。你甚至可以 设置我们自己的入口点,您可以创建一个也是可执行的库