IOCCC 1986 / wall.c-为什么TCC在处理早期C代码方面胜过GCC?

问题描述

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限定的期限是可写的。