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

19

下面这个问题是在大学编程比赛中提出的。我们被要求猜测输出结果和/或解释其工作原理。不用说,我们都没有成功过。

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

经过简短的谷歌搜索,我找到了在 codegolf.stackexchange.com 上提出的这个确切问题:

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

那里解释了它的作用:将 stdin 反转并放置在 stdout 上,但没有说明 how

我在这个问题中也找到了一些帮助:Three arguments to main, and other obfuscating tricks,但它仍然没有解释 main(_)&_&&main() 是如何工作的。

我的问题是,这些语法是如何工作的?它们还有用吗?如果可以的话,请提供任何指针(资源链接等),即使不是直接的答案。


那个程序在C++中无法编译。移除C++标签。 - Robᵩ
10
即使是在 C 语言中,该程序以多种方式调用未定义行为。其结果只对针对特定类型的 CPU 的特定编译器可预测(即使在 codegolf 上,该程序在特定优化级别下才会执行有趣的操作)。对于“这个程序做什么?”的正确答案包括“取决于情况”、“随它去”和“让你被解雇”。 - Robᵩ
不,RaunakS,这个问题已经被淘汰了。你真的不想和那些认为这是一个有效的编程问题的人联系在一起。 - Robᵩ
如果你想调试它,请确保使用一个调试器,它可以让你逐步执行单个机器指令。逐步执行源代码不会有太大帮助。 - Robᵩ
gdb将会。我认为有用的命令是"display /i $pc","x/i $pc","nexti"和"stepi"。 - Robᵩ
显示剩余4条评论
2个回答

27
这个程序是做什么的?
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)中读取1个字节,存储到&argc指向的内存中。如果读取成功,则返回1,否则返回0&&是逻辑“与”运算符。当且仅当左侧为“true”(技术上任何非零值)时,它才会计算右侧表达式。 &&表达式的结果是一个int类型,始终为1(表示“true”)或0(表示false)。
在这种情况下,右侧调用不带参数的main函数。在声明带有1个参数的main函数后不带参数调用main函数是未定义行为。尽管如此,只要您不关心argc参数的初始值,它通常可以正常工作。 &&的结果然后传递给write()函数。因此,我们的代码现在看起来像这样:
int main(int argc) {
    int read_result = read(0, &argc, 1) && main();
    int result = write(read_result);
    return result;
}

嗯。快速查看man页面显示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
哦,哇!那真是非常、非常详细啊。澄清一下:write() 的语法是 int write(int fd, char *Buff, int NumBytes)。所以,read() 的返回值变成了 1,来写入标准输出? - RaunakS
1
0 是标准输入,1 是标准输出,2 是标准错误。因此,从 read 函数成功返回(并且递归调用 main 函数也成功返回)会导致向 stdout 写入数据。而从 read 函数失败返回则会导致向 stdin 写入数据。这是另一种未定义的行为。 - Robᵩ
啊,是的,在提问之前我应该查一下维基百科。这段代码可以成为一个非常好的IOCCC竞赛参赛作品。而且这种未定义行为是否可以复制?我的意思是,在同一编译器(gcc 4.4.1)上,这是否总是会产生相同的结果? - RaunakS
谢谢,@DanielFischer。我还没有读过新的标准呢。 :) - Robᵩ
1
实际上,在C99和C++中,可以在闭括号处省略main的显式返回值,此时编译器必须在该点隐式返回0。假设它会返回其他无效或未定义的值是错误的 - 就像这里的许多其他情况一样! - underscore_d
显示剩余5条评论

9

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

程序将尝试从标准输入中读取一个字节。如果有输入,则会调用主函数进行递归,继续读取一个字节。

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

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

在我的x86_64 Mac上,程序会读取标准输入直到EOF,然后退出,根本不输出任何内容。


有没有关于什么是“_”的引用?很想了解它。 - Pavan Manjunath
这只是一个形式参数(“变量”)名称。它相当于 main(int _) ... 想象一下他们称它为“argc”,一切都会清楚的。也就是说:main(argc) 将是早期C语言默认为 int,后来添加了原型声明。他们没有声明通常的 argv,但不会发生什么严重的后果。 - DigitalRoss
是的,一个下划线 _ 是一个合法的变量名。 - John Bode

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接