理解main的一个不常见的参数

时间:2012-04-25 18:01:18

标签: c argument-passing deobfuscation

以下问题在大学编程竞赛中提出。我们被要求猜测输出和/或解释它的工作原理。不用说,我们都没有成功。

main(_){write(read(0,&_,1)&&main());}

一些简短的谷歌搜索让我想到了这个确切的问题,在codegolf.stackexchange.com中提到:

https://codegolf.stackexchange.com/a/1336/4085

在那里,它解释了它做什么Reverse stdin and place on stdout,但不是如何

我也在这个问题上找到了一些帮助:Three arguments to main, and other obfuscating tricks 但它仍然无法解释main(_)&_&&main()的工作原理。

我的问题是, 这些语法是如何工作的?它们是我应该知道的,因为它们仍然相关吗?

如果不是直接的答案,我会很感激任何指针(资源链接等)。

2 个答案:

答案 0 :(得分:26)

这个程序做了什么?

main(_){write(read(0,&_,1)&&main());}

在我们分析它之前,让我们美化它:

main(_) {
    write ( read(0, &_, 1) && main() );
}

首先,你应该知道_是一个有效的变量名,虽然是一个丑陋的变量名。让我们改变它:

main(argc) {
    write( read(0, &argc, 1) && main() );
}

接下来,要认识到函数的返回类型和参数的类型在C中是可选的(但不是在C ++中):

int main(int argc) {
    write( read(0, &argc, 1) && main() );
}

接下来,了解返回值的工作原理。对于某些CPU类型,返回值始终存储在相同的寄存器中(例如,x86上的EAX)。因此,如果省略return语句,则返回值可能将是最近返回的函数。

int main(int argc) {
    int result = write( read(0, &argc, 1) && main() );
    return result;
}

read的调用或多或少是明显的:它从标准的(文件描述符0)读取到位于&argc的内存中,用于1字节。如果读取成功则返回1,否则返回0.

&&是逻辑“和”运算符。当且仅当它的左侧是“真”时(技术上,任何非零值),它评估其右侧。 &&表达式的结果是int,它始终为1(对于“true”)或0(对于false)。

在这种情况下,右侧会调用main而不带参数。用1参数声明后调用main没有参数是未定义的行为。然而,只要您不关心argc参数的初始值,它通常会起作用。

然后将&&的结果传递给write()。所以,我们的代码现在看起来像:

int main(int argc) {
    int read_result = read(0, &argc, 1) && main();
    int result = write(read_result);
    return result;
}

嗯。快速查看手册页显示write有三个参数,而不是一个。另一种未定义行为的情况。就像调用main参数太少一样,我们无法预测write将获得的第二和第三个参数。在典型的计算机上,他们会得到的东西,但我们无法确定是什么。 (在非典型计算机上,可能会发生奇怪的事情。)作者依靠write接收先前存储在内存堆栈中的内容。并且,他依靠 作为第二和第三个参数来阅读。

int main(int argc) {
    int read_result = read(0, &argc, 1) && main();
    int result = write(read_result, &argc, 1);
    return result;
}

修正对main的无效调用,添加标题,并展开我们拥有的&&

#include <unistd.h>
int main(int argc, int argv) {
    int result;
    result = read(0, &argc, 1);
    if(result) result = main(argc, argv);
    result = write(result, &argc, 1);
    return result;
}

<小时/> 的结论

此程序在许多计算机上无法正常工作。即使您使用与原作者相同的计算机,它也可能无法在其他操作系统上运行。即使您使用相同的计算机和相同的操作系统,它也无法在许多编译器上运行。即使您使用相同的计算机编译器和操作系统,如果更改编译器的命令行标志,它也可能不起作用。

正如我在评论中所说,问题没有一个有效的答案。如果您发现比赛组织者或比赛裁判另有说明,请不要邀请他们参加下一场比赛。

答案 1 :(得分:9)

好的,_只是在早期K&amp; R C语法中声明的变量,默认类型为int。它起到临时存储的作用。

程序将尝试从标准输入读取一个字节。如果有输入,它将以递归方式调用main继续读取一个字节。

在输入结束时,read(2)将返回0,表达式将返回0,write(2)系统调用将执行,并且调用链可能会展开。

我在这里说“可能”,因为从这一点来看,结果是高度依赖于实现的。缺少write(2)的其他参数,但某些将位于寄存器和堆栈中,因此某些将被传递到内核中。相同的未定义行为适用于main的各种递归激活的返回值。

在我的x86_64 Mac上,程序读取标准输入直到EOF,然后退出,完全没有写任何内容。