关于模糊:模糊C代码竞赛2006。请解释一下sykes2.c

Obfuscated C Code Contest 2006. Please explain sykes2.c

这个C程序是如何工作的?

1
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上测试)。它打印编译时的时间。我的制度:

1
2
3
4
5
6
7
    !!  !!!!!!              !!  !!!!!!              !!  !!!!!!
    !!  !!  !!              !!      !!              !!  !!  !!
    !!  !!  !!              !!      !!              !!  !!  !!
    !!  !!!!!!    !!        !!      !!    !!        !!  !!!!!!
    !!      !!              !!      !!              !!  !!  !!
    !!      !!              !!      !!              !!  !!  !!
    !!  !!!!!!              !!      !!              !!  !!!!!!

来源:Sykes2-一行一个时钟,Sykes2作者提示

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

1
2
3
4
5
6
7
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 functionputchar[-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]


让我们把它弄模糊。

Indenting:

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

引入变量来解决这一混乱局面:

1
2
3
4
5
6
7
8
9
10
11
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是因为有两个补码。因此,我们有

1
2
3
4
5
6
7
8
9
10
11
12
13
main(int i) {
    if(i != 448)
        main(i+1);
    i--;
    if(i % 64 == 0) {
        putchar('
'
);
    } 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+更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
main(int i) {
    if(i != 448)
        main(i+1);
    i--;
    if(i % 64 == 0) {
        putchar('
'
);
    } 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));
    }
}

将递归转换为循环,并进一步简化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// please don't pass any command-line arguments
main() {
    int i;
    for(i=447; i>=0; i--) {
        if(i % 64 == 0) {
            putchar('
'
);
        } 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(!)放入其中。第一个表(">'txiZ^(~z?"是描述每个字符外观的一组10位映射,第二个表(";;;====~$::199"选择要从位图显示的适当位。

第二张桌子

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

if((i & 2) == 0) shift /= 8; shift = shift % 8选择表值的高八进制数字(对于i%8=0,1,4,5)或低八进制数字(对于i%8=2,3,6,7)。轮班表的结果是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

或以表格形式

1
2
3
4
5
6
7
00005577
11775577
11775577
11665577
22773377
22773377
44443377

注意,作者在前两个表条目中使用了空终止符(鬼鬼祟祟!).

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

第一张桌子

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

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

根据输入ta最终等于以下二进制数:

1
2
3
4
5
6
7
8
9
10
11
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是空白的:

1
2
3
4
5
6
7
000055  
11  55  
11  55  
116655  
22  33  
22  33  
444433

例如,401101010(位1、3、5和6集),打印为

1
2
3
4
5
6
7
----!!--
!!--!!--
!!--!!--
!!!!!!--
----!!--
----!!--
----!!--

为了证明我们真正理解代码,让我们用下表稍微调整一下输出:

1
2
3
4
5
6
7
  00  
11  55
11  55
  66  
22  33
22  33
  44

编码为"?;;?==? '::799\x07"。出于艺术目的,我们将在几个字符中添加64个字符(因为只使用了低6位,这不会影响输出);这将产生"?{{?}}?gg::799G"(请注意,第8个字符是未使用的,因此我们可以实际制作我们想要的任何字符)。将新表放入原始代码:

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

我们得到

1
2
3
4
5
6
7
          !!              !!                              !!  
    !!  !!              !!  !!  !!  !!              !!  !!  !!
    !!  !!              !!  !!  !!  !!              !!  !!  !!
          !!      !!              !!      !!                  
    !!  !!  !!          !!  !!      !!              !!  !!  !!
    !!  !!  !!          !!  !!      !!              !!  !!  !!
          !!              !!                              !!

如我们所料。它不像原版那样结实,这就解释了作者为什么选择使用他做的表格。


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

1
2
3
4
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()将递归地调用自身,传递-(~_)的结果(负的位不是_的结果),因此实际上它将执行448次递归(仅在_^448 == 0的情况下)。

这样,它将打印7 64个字符宽的行(外部三元条件和448/64 == 7)。让我们把它改写得更清楚一点:

1
2
3
4
5
6
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('
'
);
}

现在,32是ASCII空间的十进制。它要么打印一个空格要么打印一个"!"(33是‘!’"最后是"EDOCX1"(7)。让我们关注中间的斑点:

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

正如另一个海报所说,__TIME__是程序的编译时间,是一个字符串,因此有一些字符串算法正在进行,同时利用数组下标的双向性:a[b]与b[a]对于字符数组是相同的。

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

这将选择__TIME__中的前8个字符之一。然后索引到[">'txiZ^(~z?"-48](0-9个字符是48-57位小数)。必须为其ASCII值选择此字符串中的字符。同样的字符ascii代码操作在表达式中继续进行,从而导致打印""或"!"取决于角色glyph中的位置。


除其他方案外,-~x等于x+1,因为~x等于(0xffffffff-x)。这相当于2s补码中的(-1-x),所以-~x-(-1-x) = x+1


我尽可能地消除模运算的模糊性,并删除了重写。

1
2
3
4
5
6
7
8
9
10
11
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('
'
);
}

再扩展一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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('
'
);
}