题 混淆的C代码竞赛2006.请解释sykes2.c


这个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]

917
2018-03-13 18:22


起源


调试:添加 printf("%d", _); 到了开头 main 打印: pastebin.com/HHhXAYdJ - corny
整数,每个无类型变量默认为 int - drahnr
你看过提示了吗? ioccc.org/2006/sykes2/hint.text - nhahtdh
还看了 stackoverflow.com/questions/10321196/... - Xofo


答案:


让我们去混淆它。

缩进:

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 是8 iff i 是4,5,6或7 mod 8。

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

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

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

这是在七段显示后设计的 7作为空白。因此,第一个表中的条目必须定义亮起的段。

第一张桌子

__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 在段表中始终打印为空白。第二个表看起来像这样 7作为空白:

000055  
11  55  
11  55  
116655  
22  33  
22  33  
444433  

所以,例如, 4 是 01101010 (位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);}

我们得到

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

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


1768
2018-03-13 19:46



每个人都应该记住这一点 [] 只是一个 + 伪装.. - drahnr
大坝...我发现了前一位开发人员留在我公司的“反混淆”代码。 - Fred
@АртёмЦарионов:大约30分钟,但我一直回来编辑它。我经常使用C,之前为了个人兴趣做了一些IOCCC反混淆(我做的最后一个,仅仅是为了个人兴趣,是 这美丽的光线追踪器)。如果你想问它是如何工作的,我很乐意帮忙;) - nneonneo
C ..汇编语言的所有强大功能与汇编语言的可读性相结合 - wim
这是一个非常好的解释 - 我不认为我自己可以做得更好。我在第一时间写了这个程序:) - sdsykes


我们将其格式化以便于阅读:

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 _),所以它真的会去448递归(只有条件在哪里 _^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');
}

现在, 32 ASCII空格是十进制的。它要么打印一个空格,要么打印'!' (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代码操作继续通过表达式,导致打印''或'!'取决于角色的字形内的位置。


97
2018-03-13 21:11





添加到其他解决方案, -~x 等于 x+1 因为 ~x 相当于 (0xffffffff-x)。这等于 (-1-x) 2s补,所以 -~x 是 -(-1-x) = x+1


46
2018-03-14 23:54



有趣。我已经知道了一段时间~x == -x - 1,但我不知道背后的数学推理。 - ApproachingDarknessFish
Ey,Cole,( - 1-x​​)与(-x-1)相同,你不需要“修复”它! - Thomas Song
同样的原因,如果有人是-1338,那么他们不是1337。 - Andrew Mao


我尽可能地模糊了模数算术并删除了递归

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');
}

3
2018-04-21 14:48