混淆了C代码竞赛2006。请解释sykes2.c


974

这个C程序如何工作?

main(_){_^448&&main(-~_);putchar(--_%64?32|-~7[__TIME__-_/8%8][">'txiZ^(~z?"-48]>>";;;====~$::199"[_*2&8|_/64]/(_&2?1:8)%8&1:10);}

它按原样编译(在上测试gcc 4.6.3)。编译时打印时间。在我的系统上:

    !!  !!!!!!              !!  !!!!!!              !!  !!!!!! 
    !!  !!  !!              !!      !!              !!  !!  !! 
    !!  !!  !!              !!      !!              !!  !!  !! 
    !!  !!!!!!    !!        !!      !!    !!        !!  !!!!!! 
    !!      !!              !!      !!              !!  !!  !! 
    !!      !!              !!      !!              !!  !!  !! 
    !!  !!!!!!              !!      !!              !!  !!!!!!

来源:sykes2 -时钟在同一行sykes2笔者提示

一些提示:默认情况下没有编译警告。编译为-Wall,将发出以下警告:

sykes2.c:1:1: warning: return type defaults to int [-Wreturn-type]
sykes2.c: In function main’:
sykes2.c:1:14: warning: value computed is not used [-Wunused-value]
sykes2.c:1:1: warning: implicit declaration of function putchar [-Wimplicit-function-declaration]
sykes2.c:1:1: warning: suggest parentheses around arithmetic in operand of ‘|’ [-Wparentheses]
sykes2.c:1:1: warning: suggest parentheses around arithmetic in operand of ‘|’ [-Wparentheses]
sykes2.c:1:1: warning: control reaches end of non-void function [-Wreturn-type]

6
调试:添加printf("%d", _);main打印的开头:pastebin.com/HHhXAYdJ
–corny

整数,每个未类型化的变量默认为int
drahnr 2013年

18
您阅读提示了吗?ioccc.org/2006/sykes2/hint.text
nhahtdh 2013年


如果您这样运行,它将崩溃:./a.out $(seq 0 447)
SS Anne

Answers:


1819

让我们对其进行模糊处理。

缩进:

main(_) {
    _^448 && main(-~_);
    putchar(--_%64
        ? 32 | -~7[__TIME__-_/8%8][">'txiZ^(~z?"-48] >> ";;;====~$::199"[_*2&8|_/64]/(_&2?1:8)%8&1
        : 10);
}

引入变量来解开这种混乱:

main(int i) {
    if(i^448)
        main(-~i);
    if(--i % 64) {
        char a = -~7[__TIME__-i/8%8][">'txiZ^(~z?"-48];
        char b = a >> ";;;====~$::199"[i*2&8|i/64]/(i&2?1:8)%8;
        putchar(32 | (b & 1));
    } else {
        putchar(10); // newline
    }
}

注意,-~i == i+1由于二进制补码。因此,我们有

main(int i) {
    if(i != 448)
        main(i+1);
    i--;
    if(i % 64 == 0) {
        putchar('\n');
    } else {
        char a = -~7[__TIME__-i/8%8][">'txiZ^(~z?"-48];
        char b = a >> ";;;====~$::199"[i*2&8|i/64]/(i&2?1:8)%8;
        putchar(32 | (b & 1));
    }
}

现在,请注意a[b]与相同b[a],然后-~ == 1+再次应用更改:

main(int i) {
    if(i != 448)
        main(i+1);
    i--;
    if(i % 64 == 0) {
        putchar('\n');
    } else {
        char a = (">'txiZ^(~z?"-48)[(__TIME__-i/8%8)[7]] + 1;
        char b = a >> ";;;====~$::199"[(i*2&8)|i/64]/(i&2?1:8)%8;
        putchar(32 | (b & 1));
    }
}

将递归转换为循环并进行更多简化:

// please don't pass any command-line arguments
main() {
    int i;
    for(i=447; i>=0; i--) {
        if(i % 64 == 0) {
            putchar('\n');
        } else {
            char t = __TIME__[7 - i/8%8];
            char a = ">'txiZ^(~z?"[t - 48] + 1;
            int shift = ";;;====~$::199"[(i*2&8) | (i/64)];
            if((i & 2) == 0)
                shift /= 8;
            shift = shift % 8;
            char b = a >> shift;
            putchar(32 | (b & 1));
        }
    }
}

每次迭代输出一个字符。每第64个字符,它将输出一个换行符。否则,它将使用一对数据表找出要输出的内容,并放置字符32(空格)或字符33(a !)。第一个表(">'txiZ^(~z?")是一组10个位图,描述每个字符的外观,第二个表(";;;====~$::199")从位图中选择要显示的适当位。

第二张桌子

让我们从检查第二张表开始int shift = ";;;====~$::199"[(i*2&8) | (i/64)];i/64是行号(6到0),i*2&8如果i是4、5、6或7 mod 8,则为8。

if((i & 2) == 0) shift /= 8; shift = shift % 8选择表值的高八进制数字(对于i%8= 0,1,4,5)或低八进制数字(对于= 2,3,6,7)i%8。移位表最终看起来像这样:

row col val
6   6-7 0
6   4-5 0
6   2-3 5
6   0-1 7
5   6-7 1
5   4-5 7
5   2-3 5
5   0-1 7
4   6-7 1
4   4-5 7
4   2-3 5
4   0-1 7
3   6-7 1
3   4-5 6
3   2-3 5
3   0-1 7
2   6-7 2
2   4-5 7
2   2-3 3
2   0-1 7
1   6-7 2
1   4-5 7
1   2-3 3
1   0-1 7
0   6-7 4
0   4-5 4
0   2-3 3
0   0-1 7

或表格形式

00005577
11775577
11775577
11665577
22773377
22773377
44443377

请注意,作者对前两个表条目使用了空终止符(偷偷摸摸!)。

这是在七段显示器之后设计的,以7s为空白。因此,第一个表中的条目必须定义被点亮的段。

第一张桌子

__TIME__是预处理器定义的特殊宏。它以形式扩展为一个字符串常量,其中包含预处理器的运行时间"HH:MM:SS"。请注意,它恰好包含8个字符。请注意,0-9的ASCII值从48到57,:ASCII值58。输出每行64个字符,因此每个字符留8个字符__TIME__

7 - i/8%8因此__TIME__是当前正在输出的索引(7-之所以需要该索引,是因为我们i向下迭代)。因此,t__TIME__输出的特征。

a最终等于二进制的以下内容,具体取决于输入t

0 00111111
1 00101000
2 01110101
3 01111001
4 01101010
5 01011011
6 01011111
7 00101001
8 01111111
9 01111011
: 01000000

每个数字都是一个位图,描述了在我们的七段显示器中亮起的段。由于字符均为7位ASCII,因此始终清除高位。因此,7在段表中始终打印为空白。第二个表如下所示,其中7s为空白:

000055  
11  55  
11  55  
116655  
22  33  
22  33  
444433  

因此,例如,401101010(位1、3、5和6设置),其输出为

----!!--
!!--!!--
!!--!!--
!!!!!!--
----!!--
----!!--
----!!--

为了显示我们对代码的理解,让我们使用此表对输出进行一些调整:

  00  
11  55
11  55
  66  
22  33
22  33
  44

这被编码为"?;;?==? '::799\x07"。出于艺术目的,我们会将64个字符添加到一些字符中(因为仅使用了低6位,所以这不会影响输出);这样就可以了"?{{?}}?gg::799G"(请注意,第8个字符尚未使用,因此我们实际上可以将其设为任何所需的字符)。将我们的新表放入原始代码中:

main(_){_^448&&main(-~_);putchar(--_%64?32|-~7[__TIME__-_/8%8][">'txiZ^(~z?"-48]>>"?{{?}}?gg::799G"[_*2&8|_/64]/(_&2?1:8)%8&1:10);}

我们得到

          !!              !!                              !!   
    !!  !!              !!  !!  !!  !!              !!  !!  !! 
    !!  !!              !!  !!  !!  !!              !!  !!  !! 
          !!      !!              !!      !!                   
    !!  !!  !!          !!  !!      !!              !!  !!  !! 
    !!  !!  !!          !!  !!      !!              !!  !!  !! 
          !!              !!                              !!   

就像我们期望的那样。它看起来不像原始的那样坚固,这解释了为什么作者选择使用他所做的表格。


2
@drahnr-从技术上讲,它既是*(取消引用)又是+:P
有意思,这是2013年

18
@АртёмЦарионов:大约30分钟,但是我一直在回来编辑它。我经常使用C,并且出于个人利益我做了一些IOCCC去模糊处理(我为个人利益所做的最后一个是这个漂亮的raytracer)。如果您想问一下它是如何工作的,我很乐意;)
nneonneo 2013年

5
@АртёмЦарионов:大约是IIRC的一天(也包括花费在理解光线跟踪器几何形状上的时间)。该程序也非常聪明,因为它不使用任何关键字
nneonneo

178
C ..汇编语言的所有功能以及汇编语言的可读性
2013年

6
有关更多内容,请查看Don Libes撰写的“混淆C和其他奥秘”。它通过分析混淆的C竞赛条目来讲授C技术。
克里斯N

102

让我们格式化它以便于阅读:

main(_){
  _^448&&main(-~_);
  putchar((--_%64) ? (32|-(~7[__TIME__-_/8%8])[">'txiZ^(~z?"-48]>>(";;;====~$::199")[_*2&8|_/64]/(_&2?1:8)%8&1):10);
}

因此,在不带任何参数的情况下运行_(通常为argc)为1main()会递归地调用自身,传递-(~_)(的按位NOT NOT _)的结果,所以实际上它将进行448次递归(仅在where条件下_^448 == 0)。

以此,它将打印7个64个字符的宽行(外部三元条件和448/64 == 7)。因此,让我们重写它一点点:

main(int argc) {
  if (argc^448) main(-(~argc));
  if (argc % 64) {
    putchar((32|-(~7[__TIME__-argc/8%8])[">'txiZ^(~z?"-48]>>(";;;====~$::199")[argc*2&8|argc/64]/(argc&2?1:8)%8&1));
  } else putchar('\n');
}

现在,32ASCII码空间为十进制。它会打印一个空格或一个'!' (33是'!',因此&1末尾是' ')。让我们关注中间的blob:

-(~(7[__TIME__-argc/8%8][">'txiZ^(~z?"-48]) >>
     (";;;====~$::199"[argc*2&8|argc/64]) / (argc&2?1:8) % 8

就像另一位发布者所说的那样,__TIME__是程序的编译时间,它是一个字符串,因此正在进行一些字符串运算,并且利用了数组下标是双向的:a [b]与b [a]相同用于字符数组。

7[__TIME__ - (argc/8)%8]

这将选择中的前8个字符之一__TIME__。然后将其编入索引[">'txiZ^(~z?"-48](0-9个字符为48-57个十进制)。必须选择该字符串中的字符作为其ASCII值。相同的字符ASCII代码操作将继续贯穿表达式,从而导致打印''或'!'。取决于角色字形中的位置。


49

与其他解决方案相加,-~x等于,x+1因为~x等于(0xffffffff-x)。这等于(-1-x)在二进制补码,所以-~x-(-1-x) = x+1


5
有趣。我已经知道〜x == -x-1一段时间了,但是我不知道其背后的数学原理。
ApproachingDarknessFish

3
Ey,Cole(-1-x)与(-x-1)相同,您无需“修复”它!
Thomas Song

7
同样的原因,如果有人是-1338,那么他们不是1337年
安德鲁·毛

4

我尽可能地消除了模运算的混淆,并删除了递归

int pixelX, line, digit ;
for(line=6; line >= 0; line--){
  for (digit =0; digit<8; digit++){
    for(pixelX=7;pixelX > 0; pixelX--){ 
        putchar(' '| 1 + ">'txiZ^(~z?"["12:34:56"[digit]-'0'] >> 
          (";;;====~$::199"[pixel*2 & 8  | line] / (pixelX&2 ? 1 : 8) ) % 8 & 1);               
    }
  }
  putchar('\n');
}

进一步扩展:

int pixelX, line, digit, shift;
char shiftChar;
for(line=6; line >= 0; line--){
    for (digit =0; digit<8; digit++){
        for(pixelX=7;pixelX >= 0; pixelX--){ 
            shiftChar = ";;;====~$::199"[pixelX*2 & 8 | line];
            if (pixelX & 2)
                shift = shiftChar & 7;
            else
                shift = shiftChar >> 3;     
            putchar(' '| (">'txiZ^(~z?"["12:34:56"[digit]-'0'] + 1) >> shift & 1 );
        }

    }
    putchar('\n');
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.