下面的问题是在一次大学节目比赛中提出的。我们被要求猜测输出和/或解释其工作。不用说,我们都没有成功。
1
| main(_){write(read(0,&_,1)&&main());} |
一些简短的谷歌搜索让我找到了这个问题,在codegolf.stackexchange.com中问道:
https://codegolf.stackexchange.com/a/1336/4085
在那里,它解释了它的作用:Reverse stdin and place on stdout,但没有解释它是如何做到的。
在这个问题上,我也找到了一些帮助:三个主要论点和其他模糊的技巧。但它仍然不能解释main(_)、&_和&&main()是如何工作的。
我的问题是,这些语法是如何工作的?它们是我应该知道的吗,比如说,它们仍然相关吗?
如果没有直接的答案,我会感激任何提示(指向资源链接等)。
- 该程序不会在C++中编译。删除C++标签。
- "Rob?"啊,谢谢。我太粗心了。
- 即使在C语言中,该程序也以多种方式调用未定义的行为。结果只有针对特定类型CPU的特定编译器才是可预测的(即使在codegolf上,这个程序也只在特定的优化级别上做一些有趣的事情)。正确回答"这个程序是做什么的?"包括"视情况而定"、"随心所欲"和"被解雇"。
- "Rob?"或者,在我的情况下,它让我在比赛中获得了出口。不过,我还是想知道它是如何工作的。我可以使用调试器或IDE中的任何工具(我正在使用代码块)来获得一些想法吗?
- 不,劳纳克斯,这是你生活中的一个出口。你真的不想和那些认为这是一个有效的编程问题的人交往。
- 如果要调试它,请确保使用调试器,它将允许您单步执行各个机器指令。单步执行源代码根本没有什么帮助。
- "Rob?"GDB或Valgrind是否允许我逐步完成机器说明?如果没有,你有什么建议?
- GDB会的。我认为有用的命令是"display/i$pc"、"x/i$pc"、"nexti"和"stepi"。
- 让我们在聊天中继续讨论
这个程序是做什么的?
1
| main(_){write(read(0,&_,1)&&main());} |
在分析之前,让我们先对它进行美化:
1 2 3
| main(_) {
write ( read(0, &_, 1) && main() );
} |
号
首先,您应该知道_是一个有效的变量名,尽管它很难看。让我们改变一下:
1 2 3
| main(argc) {
write( read(0, &argc, 1) && main() );
} |
接下来,实现函数的返回类型和参数的类型在C中是可选的(而不是C++):
1 2 3
| int main(int argc) {
write( read(0, &argc, 1) && main() );
} |
。
接下来,了解返回值是如何工作的。对于某些CPU类型,返回值总是存储在同一个寄存器中(例如,x86上的EAX)。因此,如果省略return语句,返回值可能是返回的最新函数。
1 2 3 4
| int main(int argc) {
int result = write( read(0, &argc, 1) && main() );
return result;
} |
对read的调用或多或少是显而易见的:它从(文件描述符0)中的标准读取到位于&argc的内存中,用于1字节。如果读取成功,则返回1,否则返回0。
&&是逻辑"and"运算符。当且仅当其左侧为"真"(技术上,任何非零值)时,它才会评估其右侧。&&表达式的结果是int,它总是1(表示"真")或0(表示"假")。
在这种情况下,右侧调用main,没有任何参数。用1个参数声明后,不带参数调用main是未定义的行为。不过,只要您不关心argc参数的初始值,它通常是有效的。
然后将&&的结果传递给write()。因此,我们的代码现在看起来是:
1 2 3 4 5
| int main(int argc) {
int read_result = read(0, &argc, 1) && main();
int result = write(read_result);
return result;
} |
。
嗯。快速看一下手册页就可以发现,write有三个论点,而不是一个。另一种未定义行为的情况。就像用太少的参数调用main一样,我们无法预测write第二个和第三个参数会得到什么。在典型的计算机上,他们会得到一些东西,但我们不能确定是什么。(在不典型的计算机上,可能会发生奇怪的事情。)作者依靠write接收先前存储在内存堆栈上的内容。而且,他依赖于第二和第三个论点来阅读。
1 2 3 4 5
| int main(int argc) {
int read_result = read(0, &argc, 1) && main();
int result = write(read_result, &argc, 1);
return result;
} |
。
修复对main的无效调用,添加头,扩展&&,我们有:
1 2 3 4 5 6 7 8
| #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;
} |
结论
这个程序在许多计算机上不能按预期工作。即使使用与原始作者相同的计算机,它也可能无法在不同的操作系统上工作。即使使用同一台计算机和同一个操作系统,它也不能在许多编译器上工作。即使使用相同的计算机编译器和操作系统,如果更改编译器的命令行标志,它也可能无法工作。
正如我在评论中所说,这个问题没有一个有效的答案。如果你发现一个竞赛组织者或竞赛裁判说了别的话,不要邀请他们参加你的下一个比赛。
- 哦,哇,那太全面了。说明:write()语法是int write(int fd, char *Buff, int NumBytes)。那么,对于写入标准输出,read()的返回值正在变成1?
- 0是标准输入,1是标准输出,2是标准错误。因此,从read成功返回(结合从递归调用main成功返回)会产生对stdout的写入。从读取返回失败将导致写入stdin。这是另一个未定义的行为。
- 啊,是的,我应该在问之前先用wiki。这段代码将是一个很好的IOCCC竞争者。这种未定义的行为是否可以复制?我的意思是,在同一个编译器(GCC4.4.1)上,这会始终产生相同的结果吗?
- @raunaks在同一体系结构上使用相同的编译选项的同一个编译器可能总是给出相同的结果。更改其中一个参数,所有下注都关闭。
- "接下来,要认识到函数的返回类型和参数的类型在C中是可选的",仅对于C的足够旧的值。从C99开始,它们是强制的。
- 谢谢,@danielfischer。我还没读过新标准。:)
- @我明白你的意思了。我刚刚尝试在GNUGCC中编译,我的浏览器崩溃了。在CygwinGCC上,终端停止响应。代码块部分工作,只有(ideone.com)提供了预期的输出。我想看看比赛组织者在哪里编写了他们的代码。
- 事实上,在C99和C++中,EDCOX1 OR 8的显式返回值可以在闭合括号中省略,在这种情况下,编译器必须在该点隐式返回0。假设它会返回一些其他无效的东西,就像这里的其他东西一样。
好的,_只是早期k&r c语法中声明的一个变量,默认类型为int。它用作临时存储器。
程序将尝试从标准输入中读取一个字节。如果有输入,它将以递归方式调用main,继续读取一个字节。
在输入结束时,read(2)将返回0,表达式将返回0,执行write(2)系统调用,调用链可能会展开。
我在这里说"可能",因为从这一点上来说,结果高度依赖于实现。write(2)的其他参数丢失了,但是寄存器和堆栈中会有一些内容,所以会有一些内容传递到内核中。同样的未定义行为也适用于main的各种递归激活的返回值。
在我的x86 Mac上,程序读取标准输入直到EOF,然后退出,完全不写任何东西。
- 有关于什么是_的引文吗?很想知道
- 它只是一个形参("变量")名称。相当于main(int _)…想象一下,他们称之为"ARGC",一切都会很清楚。也就是说:main(argc)将是使用默认int的早期C,原型声明将在后面添加。他们没有声明通常的argv,但结果不会发生什么剧烈的变化。
- 是的,一个普通的_是一个合法的变量名。