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

IOCCC 1986/wall.c - why does TCC beat GCC in handling earlier C code?

IOCCC 早期的另一颗明珠是 Larry Wall 1986 年的条目:

http://www.ioccc.org/years.html#1986(墙)

我怀疑今天没有 C 编译器可以实际直接编译该源代码,因为它包含严重的预处理器滥用:

然而,在从混淆的原始代码中提取预处理代码后(总是使用 GCC 的 cpp -traditional),TCC 和 GCC 都设法编译它;然而,GCC 的努力白费了,因为程序在尝试开始解码其混淆的介绍文本时卡住了(对于那些想要自己钻研的人,这里不会破坏它!)

另一方面,

TCC 设法对 system()read()write() 的隐式声明发出简要警告,并迅速生成一个工作程序。

我尝试使用 GDB 逐步执行 GCC 代码,这就是我发现编译后的 GCC 代码在 for 循环的第二遍中阻塞的原因,该循环遍历文本字符串以对其进行解码:

[Inferior 1 (process 9460) exited with code 030000000005]

该进程 ID 无关紧要,因为它代表崩溃的调试构建可执行文件。 但是,退出代码保持不变。

显然,TCC 比 GCC 更适合此类 IOCCC 条目。后者仍然设法成功编译甚至 运行 一些条目,但对于像这样的棘手案例,TCC 很难被击败。它唯一的缺点是在预处理像这个例子这样的极度滥用代码时它不够用。它在某些预处理条目之间留有空格,因此无法在某些地方将它们连接到作者预期的 C 关键字中,而 GCC 的 cpp 工作 100%。

我的问题是,听起来很哲学甚至是修辞:

与 TCC 不同,在现代 GCC 中,是什么导致它无法编译,或者在编译时产生无法使用的代码,早期的 C 程序?

提前感谢所有反馈,非常感谢!

注意:我正在使用 Windows 10 版本 2004 和 WSL 2; GCC 在 Windows 和 WSL 2 环境中均失败。我也计划在 WSL 2 中编译 TCC,以便在该环境中进行比较。

PS:当这个程序最终按预期执行时,我非常喜欢它。毫无疑问,当之无愧当年的“最全才迷糊大奖”!

崩溃是由程序写入字符串文字的内容引起的。 “传统”C 编译器通常会将它们放在可写内存中,但在现代系统上,它们基本上总是在 read-only 内存中。我很惊讶它不会与 TCC 崩溃。

这是程序的一个版本,它在我的计算机上使用 GCC 编译时没有任何问题(即使有非常高级别的警告)并且似乎可以正常工作。我做了尽可能少的改动。与往常一样,对于最好的 IOCCC 条目,预处理和重新格式化几乎没有任何帮助,尽管它们确实为临时逆向工程师移除了一些陷阱。

该程序假定 system 调用 Bourne-style shell,并且 Unix-style stty 命令可用于该 shell。此外,如果执行字符集不是 ASCII,它将出现故障(可能以一种有趣的方式)。

#include <stdlib.h>
#include <unistd.h>

const char o[] = ",,B3-u;.(&*5., /(b*(16!a%1m,,,,,\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), '[=10=]');
        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, c, 1);
            break;
        }
    }
    return 0;
}

What is it in modern GCC that makes it either fail to compile, or produce unusable code when it does compile, earlier C programs, unlike TCC?

未定义的行为。这更像是一条规则。看看 this classic 1984 entry.


如今的 C 编译器按照 ISO 9899 标准中的规定编译 C,该标准的第一个修订版于 1990 年(或 1989 年)发布。该计划早于此。值得注意的是,它使用了一些非常奇怪的传统预处理器语法,这些语法在 C89、C99、C11 等中无效。

通常的想法是您不想默认使用这种语法,因为传统的预处理器不会生成与现代预处理器兼容的代码——例如,传统的预处理器也会替换字符串中的宏:

#define greeting(thing) puts("Hello thing")
main() {
    greeting(world!!!);
}

预处理为

main() {
    puts("Hello world!!!");
}

程序有效的C89,虽然风格不好;但它会预处理为

main() {
    puts("Hello thing");
}

因此最好在 任何符号 non-standard 预处理器使用中出错,否则代码可能会被巧妙地破坏,因为不会进行此类替换。


另一件事是可写字符串。反混淆代码直接尝试修改字符串文字。 C89 指定这具有未定义的行为 - 这些会导致崩溃,因为它们被映射到 GCC-compiled 程序的 read-only 页中。较旧的 GCC 版本支持 -fwriteable-strings 但它在很久以前就被弃用了,因为它无论如何都是有问题的。


我通过 GCC 9.3.0 的这些最小更改获得了程序 运行。 -traditional 不再支持编译,所以必须先预处理再编译:

gcc -traditional -E wall.c > wall_preprocessed.c

perl -pi -e '/^[^#]/ && s/(".*?")/(char[]){}/g'  wall_preprocessed.c
# thanks Larry ;)

gcc wall_preprocessed.c

即我将不在编译器行指令(以 # 开头的行)中看起来像字符串文字 "..." 的所有内容包装到 (char[]){"..."} 数组复合文字中 - 众所周知,复合文字具有范围内的存储持续时间并且 non-const 合格的是可写的。