问题描述
Larry Wall 1986年的参赛作品是IOCCC初期的另一颗明珠:
http://www.ioccc.org/years.html#1986(墙)
我怀疑由于包含严重的预处理器滥用,因此今天没有C编译器可以直接编译该源代码
- 最新的
TDM-GCC 9.2.0
设置为ANSI模式失败 - 最后
TCC 0.9.27
失败
但是,在将经过预处理的代码从经过混淆的原始文件中提取出来后(总是带有GCC的cpp -Traditional
),TCC和GCC都设法对其进行编译;但是,GCC的工作浪费了精力,因为在尝试开始解码其模糊的介绍性文本时,程序感到窒息(对于那些想深入研究自己的人来说,不会破坏这里的内容!)
另一方面,TCC设法对system()
,read()
和write()
的隐式声明发出警告,并迅速生成一个工作程序。
我试图通过GDB逐步执行GCC代码,这就是为什么我发现for
循环的第二遍经过编译后的GCC代码会阻塞,该循环遍历文本字符串以对其进行解码:
[Inferior 1 (process 9460) exited with code 030000000005]
该进程ID无关紧要,因为它表示崩溃的调试构建可执行文件。 但是,退出代码保持不变。
很明显,TCC比GCC更适合于这样的IOCCC条目。后者仍然可以成功地编译甚至运行某些条目,但是对于像这样的困难情况,TCC很难被击败。唯一的缺点是,在预处理此示例之类的极度滥用代码时,它不够用。它在某些经过预处理的条目之间留有空格,因此无法将它们与地方的作者预期的C关键字连接起来,而GCC的cpp
可以100%起作用。
我的问题是,听起来像是哲学上的甚至是修辞上的:
在现代GCC中,它是什么导致它无法编译,或者在编译时(与TCC不同)会生成无法使用的代码?
在此先感谢所有反馈!
注意:我正在将Windows 10版本2004与WSL 2配合使用;在Windows和WSL 2环境中,GCC均失败。我正计划在WSL 2中编译TCC,以便在该环境中进行比较。
PS:当程序最终按计划执行时,我非常喜欢它。毫无疑问,它值得当年的“最全面,最混乱的大奖”!
解决方法
崩溃是由程序写入字符串文字的内容引起的。 “传统” C编译器通常会将它们放在可写内存中,但是在现代系统上,它们基本上总是在只读内存中。我很惊讶它不会随着TCC崩溃。
这是程序的一个版本,可以在我的计算机上对GCC进行投诉而无需抱怨(即使警告级别很高),并且可以正常运行。我所做的更改越少越好。像往常一样,拥有最好的IOCCC条目,预处理和重新格式化几乎没有什么帮助,尽管它们确实消除了临时反向工程师的一些陷阱。
该程序假定system
调用了Bourne风格的shell,并且Unix风格的stty
命令可用于该shell。另外,如果执行字符集不是ASCII,则会出现故障(可能是一种有趣的方式)。
#include <stdlib.h>
#include <unistd.h>
const char o[] = ",B3-u;.(&*5.,/(b*(1\036!a%\031m,\r\n";
static char *ccc (char *cc)
{
char *cccc = cc;
int c;
for (; (c = (*cc)); *cc++ = c)
{
switch (0xb + (c >> 5))
{
case '\v':
break;
case '\f':
switch (c)
{
case (8098) & ('|' + 3):
c = (8098) >> ('\n' - 3);
break;
case (6055) & ('|' + 3):
c = (6055) >> ('\n' - 3);
break;
case (14779) & ('|' + 3):
c = (14779) >> ('\n' - 3);
break;
case (10682) & ('|' + 3):
c = (10682) >> ('\n' - 3);
break;
case (15276) & ('|' + 3):
c = (15276) >> ('\n' - 3);
break;
case (11196) & ('|' + 3):
c = (11196) >> ('\n' - 3);
break;
case (15150) & ('|' + 3):
c = (15150) >> ('\n' - 3);
break;
case (11070) & ('|' + 3):
c = (11070) >> ('\n' - 3);
break;
case (15663) & ('|' + 3):
c = (15663) >> ('\n' - 3);
break;
case (11583) & ('|' + 3):
c = (11583) >> ('\n' - 3);
break;
}
break;
default:
c += o[c & (38 - 007)];
switch (c -= '-' - 1)
{
case 0214:
case 0216:
c += 025;
/*fallthru*/
case 0207:
c -= 4;
/*fallthru*/
case 0233:
c += ' ' - 1;
}
}
c &= 'z' + 5;
}
return cccc;
}
int
main (void)
{
char c[] = "O";
char cccc[] = "dijs QH.soav Vdtnsaoh DmfpaksoQz;kkt oa,-dijs";
char ccccc[] = ";kkt -oa,dijszdijs QQ";
system (ccc (cccc));
for (;;)
{
read (0,c,1);
*c &= '~' + 1;
write (1,ccc (c),'\0');
switch (*c)
{
case 4:
system (ccc (ccccc));
return 0;
case 13:
write (1,o + ' ',3);
break;
case 127:
write (1,"\b \b",3);
break;
default:
write (1,1);
break;
}
}
return 0;
}
,
在现代的GCC中,是什么导致它无法编译,或者在编译时会生成早期的C程序,而产生不可用的代码呢?
未定义的行为。多数情况下是这样。只需看看this classic 1984 entry。
如今,C编译器按照ISO 9899标准(其第一版于1990年(或1989年)发布)中的C进行编译。该程序早于此。值得注意的是,它使用了一些真正奇怪的传统预处理器语法,在C89,C99,C11等中无效。
通常的想法是,您不希望默认情况下使用此语法,因为传统预处理器不会生成与现代预处理器兼容的代码-例如,传统预处理器也会在字符串中替换宏 :
#define greeting(thing) puts("Hello thing")
main() {
greeting(world!!!);
}
预处理为
main() {
puts("Hello world!!!");
}
程序 是有效的C89,尽管样式不好;但这会预处理为
main() {
puts("Hello thing");
}
因此,最好在非标准预处理器用法的任何符号处出错,否则可能会巧妙地破坏代码,因为不会进行此类替换。
另一件事是可写的字符串。反混淆代码直接尝试修改字符串文字。 C89指定它具有未定义的行为-导致崩溃,因为它们已映射到GCC编译程序的只读页面中。较旧的GCC版本支持-fwriteable-strings
,但很久以前就不推荐使用它,因为它还是有问题的。
我通过GCC 9.3.0的这些最小更改使程序运行。 -traditional
不再受编译支持,因此您必须先进行预处理,然后再进行编译:
gcc -traditional -E wall.c > wall_preprocessed.c
perl -pi -e '/^[^#]/ && s/(".*?")/(char[]){$1}/g' wall_preprocessed.c
# thanks Larry ;)
gcc wall_preprocessed.c
即我将所有看起来不在字符串编译器指令(以"..."
开头的行)内的字符串文字#
包裹到(char[]){"..."}
数组复合文字中-众所周知,复合文字具有限定的存储期限,并且非const限定的期限是可写的。