2

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

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

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

  • 最新TDM-GCC 9.2.0 设置为 ANSI 模式失败
  • 最后TCC 0.9.27失败

然而,在从混淆的原始代码(总是使用 GCC's 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 程序时产生不可用的代码?

提前感谢所有反馈,我很感激!

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

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

4

2 回答 2

2

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

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

该程序假定system调用 Bourne 风格的 shell,并且该 shell 可以使用 Unix 风格的stty命令。此外,如果执行字符集不是 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, c, 1);
            break;
        }
    }
    return 0;
}
于 2020-10-04T21:52:20.407 回答
2

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

未定义的行为。这更像是一个规则。看看这个经典的 1984 条目


现在的 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");
}

因此,最好在任何使用非标准预处理器的迹象时出错,否则代码可能会被巧妙地破坏,因为不会进行此类替换。


另一件事可写字符串。反混淆代码直接尝试修改字符串文字。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 限定的可写。

于 2020-10-04T22:05:18.210 回答