故事是这样的,看到一个知乎专栏名为一段鬼畜的代码的分析文章,超想要感受一下,所以顺着作者的思路来梳理一遍。
第一段代码:输出当前时间
根据原作者的描述,这段代码来自国际 C 语言混乱代码大赛,即 IOCCC,找了一番却并没有找到,以下是源代码。
1 | main(_) |
长这么大我还从没分析过长这么奇怪的代码,能成功编译已经让我备感诧异了,然后不出所料,运行结果如下图:
顺着作者的逻辑,添加头文件和返回值,规范 main
函数,并展开源代码,函数形参不指定类型时默认为 int
,我们用 int x
替换 _
,得到如下结果: 1
2
3
4
5
6
int main(int x) {
x ^ 448 && main(-~x);
putchar(--x % 64 ? 32 | -~7[__TIME__ - x / 8 % 8][">'txiZ^(~z?" - 48] >> ";;;====~$::199"[x * 2 & 8 | x / 64] / (x & 2 ? 1 : 8) % 8 & 1 : 10);
return 0;
}
展开第三行,x ^ 448 && main(-~x);
包含一个判断语句,即: 1
2if (x ^ 448)
main(-~x);
第四行的 putchar
函数的参数也包含了判断逻辑,将 ? :
运算符展开成 if ... else ...
得到如下: 1
2
3
4
5
6
7
8
9
10
int main(int x) {
if (x ^ 448)
main(-~x);
if (--x % 64)
putchar(32 | -~7[__TIME__ - x / 8 % 8][">'txiZ^(~z?" - 48] >> ";;;====~$::199"[x * 2 & 8 | x / 64] / (x & 2 ? 1 : 8) % 8 & 1);
else
putchar(10);
return 0;
}
第六行 putchar
中包含的逻辑依然过于复杂,我们拆成两个变量来保存: 1
2
3char a = -~7[__TIME__ - x / 8 % 8][">'txiZ^(~z?" - 48];
char b = ";;;====~$::199"[x * 2 & 8 | x / 64] / (x & 2 ? 1 : 8) % 8;
putchar(32 | a >> b & 1);
这里有个很隐蔽的变换,取下标运算符例如 x[i]
本质是 x
指向的地址加上偏移量 i
,即 *(x+i)
,因此 i[x] == x[i]
,则上面的第一行代码就可以变换成: 1
char a = -~(__TIME__ - x / 8 % 8)[7][">'txiZ^(~z?" - 48];
继续分析,补码按位取反的规律为 ~x = -(x+1)
,则 -~x
即为 x+1
;putchar(10)
中输出的 10
对应的 ascii 为 \n
;x ^ 448
隐含了只要 x != 448
时都满足条件,代码如下: 1
2
3
4
5
6
7
8
9
10
11
12
13
int main(int x) {
if (x != 448)
main(x + 1);
if (--x % 64) {
char a = 1 + (__TIME__ - x / 8 % 8)[7][">'txiZ^(~z?" - 48];
char b = ";;;====~$::199"[x * 2 & 8 | x / 64] / (x & 2 ? 1 : 8) % 8;
putchar(32 | a >> b & 1);
}
else
putchar('\n');
return 0;
}
函数参数的输入值 x
为 1
,3-4 行代码表示 x
的取值范围为 [1, 448]
,但是由于递归,5-12 行代码实际是从 x=448
倒过来执行,即转换为循环后,x = [448, 1]
,又由于第 5 行代码先执行了 --x
,所以循环应该从 447
开始执行到 0
,每轮自减 1
: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
int x;
for (x = 447; x >= 0; x--) {
if (x % 64 != 0) {
char a = 1 + (__TIME__ - x / 8 % 8)[7][">'txiZ^(~z?" - 48];
char b = ";;;====~$::199"[x * 2 & 8 | x / 64] / (x & 2 ? 1 : 8) % 8;
putchar(32 | a >> b & 1);
}
else
putchar('\n');
}
return 0;
}
根据输出结果显示,输出长度为 64 个字符,一共输出了 7 行,因此我们可以推测,循环的 448 个字符正是输出结果,一共有三种类型的字符被输出,即 \n
、!
、。这样我们就可以推测出,循环中的判断就是当输完了一行 64 个字符后,就换一行继续。接下来我们分析剩下的三行代码。
1
2
3char a = 1 + (__TIME__ - x / 8 % 8)[7][">'txiZ^(~z?" - 48];
char b = ";;;====~$::199"[x * 2 & 8 | x / 64] / (x & 2 ? 1 : 8) % 8;
putchar(32 | a >> b & 1);
第三行 a >> b & 1
的结果为 0
或 1
,可以修改为: 1
2
3
4
5char c = a >> b;
if (c % 2 == 0)
putchar(' ');
else
putchar('!');
根据 i[x] == x[i]
的变换规则再变换,得到: 1
2
3
4
5
6
7char a = (">'txiZ^(~z?" - 48)[(__TIME__ - x / 8 % 8)[7]] + 1;
char b = (";;;====~$::199")[x * 2 & 8 | x / 64] / (x & 2 ? 1 : 8) % 8;
char c = a >> b;
if (c % 2 == 0)
putchar(' ');
else
putchar('!');
然后然后,然后就分析不下去了,这都是什么鬼???
上面提到 i[x] == x[i]
,那么可以联想到 (i+j)[x] == (x+j)[i]
,因为 (x+j)[i]
等价于 *(i+j+x)
,则将 (__TIME__ - x / 8 % 8)[7]
转换为 __TIME__[7 - x / 8 % 8]
,得到如下内容: 1
char a = ">'txiZ^(~z?"[__TIME__[7 - x / 8 % 8] - 48] + 1;
将 char b=
一行后面的 ? :
改写成 if ...
语句,至此得到如下完整的代码: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main() {
int x;
for (x = 447; x >= 0; x--) {
if (x % 64 != 0) {
char t = __TIME__[7 - x / 8 % 8];
char a = ">'txiZ^(~z?"[t - 48] + 1;
char s = x * 2 & 8 | x / 64;
char b = ";;;====~$::199"[s];
if (x & 2 == 0)
b /= 8;
b %= 8;
char c = a >> b;
if (c % 2 == 0)
putchar(' ');
else
putchar('!');
}
else
putchar('\n');
}
return 0;
}
接下来我们分析 ">'txiZ^(~z?"
和 ;;;====~$::199
。前者有 10 个字符,且不重复,推测这 10 个字符分别对应到数字 0-9。输出的每行长度为 64 个字符,__TIME__
宏使用格式 HH:MM:ss
,可以分析出 7 - x / 8 % 8
的区间 0-7 正好对应到 __TIME__
宏值的下标。测试如下代码,得出运行结果: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19int main() {
for (int i=447; i >= 0; i--) {
int j = 7 - i / 8 % 8;
if (i % 64)
printf("%d", j);
else
printf("\n");
}
return 0;
}
result:
000000001111111122222222333333334444444455555555666666667777777
000000001111111122222222333333334444444455555555666666667777777
000000001111111122222222333333334444444455555555666666667777777
000000001111111122222222333333334444444455555555666666667777777
000000001111111122222222333333334444444455555555666666667777777
000000001111111122222222333333334444444455555555666666667777777
000000001111111122222222333333334444444455555555666666667777777
到这一步可以知道,char t = __TIME__[7 - x / 8 % 8];
每一次取出对应 HH:MM:ss
位置的值。继续分析,对于代码段: 1
char a = ">'txiZ^(~z?"[t - 48] + 1;
t
是取出的时间字符,48
是 0
的 ASCII 码,0123456789:
正好对应到 ASCII 的 48-58,这样就实现了映射: 1
2
3
4char t 0 1 2 3 4 5 6 7 8 9 :
> ' t x i Z ^ ( ~ z ?
char a ? ( u y j [ _ ) 127 { @
ascii 63 40 117 121 106 91 95 41 127 123 64
分析第二个 bitmap,char b = ";;;====~$::199"[s];
,可以推测出 s
的取值范围,我们仍然测试取值,运行如下代码得到结果: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19int main() {
for (int i=447; i >= 0; i--) {
int j = i * 2 & 8 | i / 64;
if (i % 64)
printf("%x", j);
else
printf("\n");
}
return 0;
}
result:
eeee6666eeee6666eeee6666eeee6666eeee6666eeee6666eeee6666eeee666
dddd5555dddd5555dddd5555dddd5555dddd5555dddd5555dddd5555dddd555
cccc4444cccc4444cccc4444cccc4444cccc4444cccc4444cccc4444cccc444
bbbb3333bbbb3333bbbb3333bbbb3333bbbb3333bbbb3333bbbb3333bbbb333
aaaa2222aaaa2222aaaa2222aaaa2222aaaa2222aaaa2222aaaa2222aaaa222
999911119999111199991111999911119999111199991111999911119999111
888800008888000088880000888800008888000088880000888800008888000
建立映射表: 1
2
3
4 0 1 2 3 4 5 6 7 8 9 10 11 12 13
; ; ; = = = = ~ $ : : 1 9 9
59 59 59 61 61 61 61 126 36 58 58 49 57 57
3 3 3 5 5 5 5 6 4 2 2 1 1 1
这样就得出了 a
和 b
的映射值,如下表: 1
2a= [63, 40, 117, 121, 106, 91, 95, 41, 127, 123, 64]
b= [3, 5, 6, 4, 2, 1, 0]
建立每个绘制单元的矩阵: 1
2
3
4
5
6
700005577
11775577
11775577
11665577
22773377
22773377
44443377
其中 7
将被直接打印为 ' '
,剔除后我们可以得到如下的 bitmap
,0-6
分别控制一个边的显示: 1
2
3
4
5
6
7000055
11 55
11 55
116655
22 33
22 33
444433
最后分析 char c = a >> b;
,将 a
的取值写成二进制: 1
2
3
4
5
6
7
8
9
10
11
12char ascii binary
0, 63, 00111111
1, 40, 00101000
2, 117, 01110101
3, 121, 01111001
4, 106, 01101010
5, 91, 01011011
6, 95, 01011111
7, 41, 00101001
8, 127, 01111111
9, 123, 01111011
:, 64, 01000000
例如对于 20:19
,根据 a >> b % 2
可以算出对应掩码的显示状态: 1
2
3
4
5
6
7
8
9
10
11
122 = 01110101
0 = 00111111
: = 01000000
1 = 00101000
9 = 01111011
b = 0 1 2 3 4 5 6
2 = T F T F T T T
0 = T T T T T T F
: = F F F F F F T
1 = F F F T F T F
9 = T T F T T T T
将掩码的结果填充到 bitmap
中就可以得到如下的结果: 1
2
3
4
5
6
7!!!!!! !!!!!! !! !!!!!!
!! !! !! !! !! !!
!! !! !! !! !! !!
!!!! !! !! !! !! !!!!!!
!! !! !! !! !!
!! !! !! !! !!
!!!! !!!!!! !! !!!!!!
解毕。
第二段代码:字符转换为 ASCII
代码如下: 1
char O,o[];main(l){for(;~l;O||puts(o))O=(O[o]=~(l=getchar())?4<(4^l>>5)?l:46:0)?-~O&printf("%02x ",l)*5:!O;}
运行结果:
有了第一段代码的分析经验,第二段代码首先格式化,补充头文件做调整得到: 1
2
3
4
5
6
7
char O, o[];
int main(void) {
for(int l=1; ~l; O || puts(o))
O = (O[o]= ~(l=getchar()) ? 4<(4^l>>5) ? l : 46 : 0) ? -~O & printf("%02x ",l)*5 : !O;
return 0;
}
根据 ~l == -(l+1)
;-~l == l+1
;O[o] == o[O]
做变换,将 ? :
修改为 if ... else ...
的形式得到: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
char O, o[];
int main(void) {
for(int l=1; -(l+1) != 0; O || puts(o)) {
l = getchar();
if (-(l+1) != 0) {
if (4 ^ l >> 5 > 4)
o[O] = l;
else
o[O] = 46;
}
else
o[O] = 0;
if (o[O] != 0) {
O = (O+1) & printf("%02x ", l)*5;
}
else
O = !O;
}
return 0;
}
将 O || puts(o)
修改为 if
语句,将 4 ^ l >> 5 > 4
修改为 4 ^ l > 36
,调整逻辑的顺序得到: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
char O, o[];
int main(void) {
for(int l=1; l != -1; ) {
l = getchar();
if (l == -1)
O = !O;
else {
if (4 ^ l > 36)
o[O] = l;
else
o[O] = 46;
O = (O+1) & printf("%02x ", l)*5;
}
if (!O)
puts(o);
}
return 0;
}
进一步地,输入的 l
为正数,且当 l > 2 时就能满足条件 4 ^ l > 36
,因此将该条件修改为: 1
2if (l > 2)
o[O] = l;
又因为可输入的字符一定满足 l>2
的条件,所以代码可以进一步修改为: 1
2
3
4
5
6
7
8
9
10
11
12
char O, o[];
int main(void) {
for(int l=1; l != -1; ) {
l = getchar();
o[O] = l;
O = (O+1) & printf("%02x ", l)*5;
if (!O)
puts(o);
}
return 0;
}
函数 printf
返回输出的字符数,这里返回 3
,建立 O
的取值映射表: 1
2O = (O+1) & 15;
O = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
这样通过 o[O]
的操作,数组实际操作的容量为 16
,每处理完 16 个字符,就通过 puts(o)
将缓冲区全部输出。
解毕。
第三段代码:鬼知道这代码是干啥的
1 |
|
代码初步格式化: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef unsigned short U;
U(main)[32768], n, r[8];
__attribute__((
U(x)() {
for (;; *r += 2, *r += !n ? _exit(write(2, "Illeg" "al ins" "truction ;-" "(\n",
24)), 0 : n >> 8 == 001 ? (signed char)n * 2 : 548 == n >> 6 && usleep(10) +
n % 64 == 4 ? 0 * write(r[7 ], C(*C(*r)), *C(*r + 2)) + 4 : n >> 9 == 63 &&
--r[7 - n / 64 % 8] ? n % + 64 * - 2 : 0, n >> 6 == 47 ? *R(0) : n >> 12 ==
1 ? *R(0) = *R(+6) : n >> 12 == +14 ? * R(0) -= *R(2 * 3) : 0) n = *C(*r);
}
这一步还是不知道这代码是干啥的,但似乎不应该将 __attribute__
与 typedef
拆开,我们暂时不管这段,先借助中间变量把函数循环中的一大段语句拆开来,并转换为 while
循环: 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
typedef unsigned short U;U(main)[32768], n, r[8];__attribute__((
)
U(x)() {
while (1) {
n = *C(*r);
*r += 2;
if (!n)
_exit(write(2, "Illeg" "al ins" "truction ;-" "(\n",24));
else {
int tmp;
if (n >> 8 == 001)
tmp = (signed char)n * 2;
else {
if (548 == n >> 6 && usleep(10) + n % 64 == 4) {
write(r[7 ], C(*C(*r)), *C(*r + 2));
tmp = 4;
}
else {
if (n >> 9 == 63 && --r[7 - n / 64 % 8])
tmp = n % + 64 * - 2;
else {
tmp = 0;
if (n >> 6 == 47)
*R(0)
else {
if (n >> 12 == 1)
*R(0) = *R(+6)
else {
if (n >> 12 == +14)
*R(0) -= *R(2 * 3)
}
}
}
}
}
*r += tmp;
}
}
}
__attribute__
中设定的是 constructor
属性,该属性会使函数 U x()
在 main()
函数之前被执行,这样我们就可以确定 __attribute__
是与后面的函数 U x()
绑定了。去掉 __attribute__
,将 typedef unsigned short U;
展开,并将宏定义独立于 __attribute__
,替换变量名 main
为 buff
,并修改为标准的 main()
函数,得到如下代码: 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
unsigned short buff[32768], n, r[8];
int main() {
while (1) {
n = *C(*r);
*r += 2;
if (!n)
_exit(write(2, "Illeg" "al ins" "truction ;-" "(\n",24));
else {
int tmp;
if (n >> 8 == 001)
tmp = (signed char)n * 2;
else {
if (548 == n >> 6 && usleep(10) + n % 64 == 4) {
write(r[7 ], C(*C(*r)), *C(*r + 2));
tmp = 4;
}
else {
if (n >> 9 == 63 && --r[7 - n / 64 % 8])
tmp = n % + 64 * - 2;
else {
tmp = 0;
if (n >> 6 == 47)
*R(0)
else {
if (n >> 12 == 1)
*R(0) = *R(+6)
else {
if (n >> 12 == +14)
*R(0) -= *R(2 * 3)
}
}
}
}
}
*r += tmp;
}
}
return 0;
}
将宏展开成函数,略微调整判断的逻辑,得到下面的代码: 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
unsigned short buff[32768], n, r[8];
unsigned short* A(unsigned short v, unsigned short i) {
if (!i)
return &v;
if (i < 2)
return C(v);
if (i == 4)
return C(v -= 2);
v += 2;
if (i == 6)
return C(v + *C(v-2));
return C(v-2);
}
unsigned short* R(unsigned short x) {
unsigned short x1 = r[7 - (n >> x & 7)];
unsigned short x2 = (n >> x >> 3) % 8;
return A(x1,x2);
}
int main() {
while (1) {
n = *C(*r);
*r += 2;
if (!n)
_exit(write(2, "Illegal instruction ;-\n",24);
else if (n >> 8 == 1)
*r += (signed char)n * 2;
else if (548 == n >> 6 && usleep(10) + n % 64 == 4) {
write(r[7], C(*C(*r)), *C(*r + 2));
*r += 4;
}
else if (n >> 9 == 63 && --r[7 - n / 64 % 8])
*r += n % -128;
else if (n >> 6 == 47)
*R(0);
else if (n >> 12 == 1)
*R(0) = *R(6);
else if (n >> 12 == 14)
*R(0) -= *R(2 * 3);
}
return 0;
}
好了到此为止,鬼知道这代码在做什么事情.....
(完)