国际C语⾔混乱代码⼤赛作品分析!
今天QQ好友,咕嘟咕嘟给我发来了⼀段C代码
#include <stdio.h>
main(t,_,a)char *a;{return!0<t?t<3?
main(-79,-13,a+main(-87,1-_,main(-86,0,a+1)+a)):1,t<_?main(t+1,_,a):3,main(-94,-27+t,a)&&t==2?_<13?
main(2,_+1,"%s %d %d\n"):9:16:t<0?t<-72?
main(_,t,"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l
q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# ){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw'
iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c ;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# }'+}##(!!/")
:t<-50?_==*a?putchar(31[a]):main(-65,_,a+1):main((*a=='/')+t,_,a+1)
:0<t?main(2,2,"%s"):*a=='/'||main(0,main(-61,*a,
"!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry"),a+1);}
咋眼⼀看,我都晕了。哪来那么多main函数。后来才知道,有两个是被宏定义了的,早期的编译器⽀持这样的动词。当然这是咕嘟咕嘟告诉我的。
以下是原⽂转载⾄
这两天闲来⽆事,重新读《C专家编程》⼀书,再次看到⾥⾯提到的国际C语⾔混乱代码⼤赛,突然对那些代码来了兴致,决定分析⼀番,⽽书中那个BASIC解释器程序,我认为从原理上说太复杂了(起码要理解如何解释BASIC语⾔吧?),更不⽤说从混乱的代码分析了!我记得以前在⽹上看过另⼀个⼤赛作品,程序打印⼀⾸英⽂歌曲的歌词,想想原理应该⽐这个简单,所以GOOGLE了⼀下,到它了:
#include <stdio.h>
main(t,_,a)char *a;{return!0<t?t<3?
main(-79,-13,a+main(-87,1-_,main(-86,0,a+1)+a)):1,t<_?main(t+1,_,a):3,main(-94,-27+t,a)&&t==2?_<13?
main(2,_+1,"%s %d %d\n"):9:16:t<0?t<-72?
main(_,t,"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l
q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# ){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw'
iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c ;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# }'+}##(!!/")
:t<-50?_==*a?putchar(31[a]):main(-65,_,a+1):main((*a=='/')+t,_,a+1)
:0<t?main(2,2,"%s"):*a=='/'||main(0,main(-61,*a,
"!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry"),a+1);}
怎么样?乍⼀看蛮吓⼈的吧?我们今天就分析它啦!可能有⼈要说,分析这种程序根本没有意义,那我冒昧借⽤Linus的⼀句名⾔“Just for fun”!只当是没事了⾃我娱乐⽽已吧!:-)
如果你有⼀个⽀持语法⾼亮的编辑器,⽴刻可以看到,程序中有⼀⼤段字符串,我们知道,被双引号括起来的字符串⾥的内容是不会解释成代码语句的(转义字符就算了吧),那我们第⼀步就是把这些字符串提取出来,现在代码看起来是这样的:
#include <stdio.h>
main(t,_,a)
char *a;
{
char * STRA="%s %d %d\n";
char * STRB="@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l
q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# ){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw'
iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c ;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# }'+}##(!!/";
char * STRC="%s";
char * STRD="!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry";
return
!0<t?t<3?main(-79,-13,a+main(-87,1-_,main(-86,0,a+1)+a))
:1,t<_?main(t+1,_,a):3,main(-94,-27+t,a)&&t==2?_<13?
main(2,_+1,STRA):9:16:t<0?t<-72?main(_,t,STRB)
:t<-50?_==*a?putchar(31[a]):main(-65,_,a+1):main((*a=='/')+t,_,a+1)
:0<t?main(2,2,STRC):*a=='/'||main(0,main(-61,*a,STRD),a+1);
}
怎么样?代码是不是⼀下⼦清晰了很多?好,我们继续观察:在抽取了字符串后,我们发现程序的实际语句只有⼀个return(如果觉得不可思议你可以搜索⼀下";" C语⾔⼀个分号对应⼀条语句嘛,可以发现,除了字符串中的内容,的确只有⼀个分号)。然后我们⼜发现,语句⾥有很多"?"和":",这是什么?对了,是三⽬运算符,⽽C语⾔中三⽬运算符的优先级基本上是最低的(除了赋值和逗号运算符之外,再次搜索代码部分,发现根本没有赋值语句,⽽逗号运算符只有两个),我们把'?'':'','当作分隔符,任意两个分隔符直接的内容都⽤⼤写字母代替,那
么程序可以变成这样:
#include <stdio.h>
#define A !0<t
#define B t<3
#define C main(-79,-13,a+main(-87,1-_,main(-86,0,a+1)+a))
#define D1 1
#define D2 t<_
#define E main(t+1,_,a)
#define F1 3
#define F2 main(-94,-27+t,a)&&t==2
#define G _<13
#define H main(2,_+1,STRA)
#define I 9
#define J 16
#define K t<0
#define L t<-72
#define M main(_,t,STRB)
#define N t<-50
#define O _==*a
#define P putchar(31[a])
#define Q main(-65,_,a+1)
#define R main((*a=='/')+t,_,a+1)
#define S 0<t
#define T main(2,2,STRC)
#define U *a=='/'||main(0,main(-61,*a,STRD),a+1)
main(t,_,a)
char *a;
{
char * STRA="%s %d %d\n";
char * STRB="@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l
q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# ){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw'
iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c ;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# }'+}##(!!/";
char * STRC="%s";
char * STRD="!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry";
return
A ?
B ?
C : D1 , D2 ? E : F1 , F2 ? G ? H : I : J : K ? L ? M : N ? O ? P : Q : R : S ? T : U ;
}
替换的到底正确与否呢?编译,通过,运⾏,和原来相同!说明替换成功!
下⾯的重点就是分析A ? B ? C : D1 , D2 ? E : F1 , F2 ? G ? H : I : J : K ? L ? M : N ? O ? P : Q : R : S ? T : U ;这个语句。
我们需要复习⼀下运算符的优先级和结合性的知识:
不同运算符之间按优先级识别,相同优先级的运算符直接按结合性识别,⽽"?:"运算符的结合性是从右向左的,那么我们可以模拟编译器读⼊此语句的⽅式得到:
A
A ?
A ? B
A ? (
B ?
A ? (
B ? C
A ? (
B ?
C :
A ? (
B ?
C : D1)
A ? (
B ?
C : D1) ,
A ? (
B ?
C : D1) , D2
A ? (
B ?
C : D1) , (D2 :
A ? (
B ?
C : D1) , (D2 : E
A ? (
B ?
C : D1) , (D2 : E :
A ? (
B ?
C : D1) , (D2 : E : F1)
A ? (
B ?
C : D1) , (D2 : E : F1) ,
A ? (
B ?
C : D1) , (D2 : E : F1) , F2
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ?
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? G
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ?
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H :
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I)
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) :
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J)
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) :
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : K
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ?
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? L
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ?
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M :
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M : N)
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M : (N ?
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M : (N ? O
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M : (N ? (O ?
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M : (N ? (O ? P
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M : (N ? (O ? P :
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M : (N ? (O ? P : Q)
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M : (N ? (O ? P : Q) :
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M : (N ? (O ? P : Q) : R))
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M : (N ? (O ? P : Q) : R)) :
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M : (N ? (O ? P : Q) : R)) : S)
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M : (N ? (O ? P : Q) : R)) : (S ?
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M : (N ? (O ? P : Q) : R)) : (S ? T
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M : (N ? (O ? P : Q) : R)) : (S ? T :
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M : (N ? (O ? P : Q) : R)) : (S ? T : U))
A ? (
B ?
C : D1) , (D2 : E : F1) , (F2 ? (G ? H : I) : J) : (K ? (L ? M : (N ? (O ? P : Q) : R)) : (S ? T : U));
为了便于观察,我们简写出最终识别结果:
return A ? (B?C:D1),(D2?E:F1),(F2?(G?H:I):J)) : (K?(L?M:(N?(O?P:Q):R)):(S?T:U));
现在整个程序除了宏定义,字符串声明,函数头之外就只剩下这⼀句有效代码了!
再次验证我们分析的结果:编译,运⾏...与原程序完全相同。
下⾯我们开始分析具体的语句:
A                  // !0<t 为假,故然后执⾏ (K(LM(N(OPQ)R))(STU))
K                  // t<0 为假,故然后执⾏ (STU)
S                  // 0<t  为真,故执⾏ T
T                  // main(2,2,"%s")递归调⽤⾃⾝,表达式的值为函数返回值,现在⽆法确定,
//进⼊main(2,2,"%s") 将此处标记为⼀号位置
A                  // 1<2 为真,故执⾏ (B?C:D1),(D2?E:F1),(F2?(G?H:I):J)) 这是⼀个逗号表达式,执⾏次序是从左向右依次求值,
// 最终取值却是最后⼀个逗号表达式的值,即 (F2?(G?H:I):J))的值,虽然前⾯的表达式的值并不被引⽤,但仍然要执⾏
//故先进⼊ (B?C:D1) 将此处标记为⼆号位置
B                  //  t<3 为真,故执⾏
C                  //  main(-79,-13,a+main(-87,1-_,main(-86,0,a+1)+a)),⾸先,这⾥再次递归调⽤⾃⾝,表达式的值为函数返回值,现在⽆法确定,将此处标记为三号位置
/
/其次,在递归调⽤的同时,传递参数分别⼜调⽤两次⾃⾝,⽽C语⾔的函数参数是从右向左⼊堆栈,所以先调⽤ main(-86,0,a+1) //下⾯进⼊main(-86,0,a+1) 将此处标记为四号位置
A                  //  1<-86为假
K                  //  -86<0为真
L                  // -86<-72为真
M                  // 调⽤ main(_,t,STRB),即main(0,-86,STRB)
//进⼊ main(0,-86,STRB)  将此处标记为五号位置
A                  // 1<0为假
K                  // 0<0为假
S                  // 0<0为假
U                  // *a此时为'@',故为假,那么继续判断 main(0,main(-61,*a,STRD),a+1),此过程⼜将进⼊ main(-61,*a,STRD)
//进⼊main(-61,*a,STRD),即使 main(-61,'n',STRD) 注意参数的运算顺序,先调⽤了a+1,然后才调⽤此函数,经过此函数,a已经指向STRD了
A                  // 1<-61为假
K                  //  -61<0为真
L                  // -61<-72为假,那么继续 (N(OPQ)R)
N                  // -61<-50 为真,那么继续 (OPQ)
O                  // 变量'_'此时的值为'n',⽽*a为'!',故为假
Q                  //  调⽤main(-65,_,a+1)
//进⼊main(-65,_,a+1),即main(-65,'@','e') ,
A                  // 1<-65为假
K                  // -65<0为真
L                  // -65<-72为假
N                  // -65<-50为真
O                  // 为假
Q                  //调⽤ main(-65,_,a+1)
//进⼊main(-65,_,a+1),即main(-65,'@','k'),这⾥可以看到,每调⽤⼀次Q,a指针向后移动⼀次,直到a指向STRD中的@
//中间过程不⽤再重复了,我们直接考虑当a指向STRD中的@时的情况:
c语言中逗号运算符怎么运算A                  //
K                  //
L                  //
N                  //
O                  // 此时终于为真,那么调⽤P
P                  // putchar(31[a]);这是什么意思呢?其实数组在编译的过程是转换为指针运算的,31[a]也就相当于a[31] ,⽽a此时所指的
//位置是STRD中的'@',相当于a[0],那么,a[31]就是 'O',到这⾥终于该从层层的递归中返回了,那么,返回值是什么呢?
//返回值就是这⾥最后的表达式P的值,也就是putchar()函数的返回值,它返回什么呢?返回输出的字符的ASCII码,也就是79 //⼀层层返回,每次返回值都是79,那么这个79最终到达哪⾥了呢?对,返回到五号位置的U语句了
//那我们继续 main(0,-86,STRB) 的U语句:
U                  //  main(0,main(-61,*a,STRD),a+1)即可变成  main(0,79,'n'),很不幸,我们需要继续判断这个函数的返回值,这⾥标记为五号位置
//进⼊ main(0,79,'n')
A                  // 1<0为假
K                  // 0<0为假
S                  // 0<0 为假
U                  // ⾮常不幸,⼜是U ! 这个时候参数a是多少呢?对了,a 的值其实在上⾯已经计算并且⼊栈了,所以还是'n'
// 还是不等于'/' ,那么,还要重复上⾯的过程!其实我们已经可以到点规律了:
// 这次进⼊main(-61,*a,STRD)函数⼜会输出什么呢?可以看出,传递进去的参数a⼜重新指向STRD的⾸地址了,
// 这次是⼀直循环到等a指向 'n'的时候,再次后移31位并输出,我们可以算出,这次应该输出 'n',并返回'n'的ASCII码110
其实,分析到这⾥我们就可以⼤概了解整个代码的运⾏过程了,可以看到,程序中输出语句只有⼀个putchar(),然⽽,⽤./a.out | wc统计得到,该程序
⼀共输出字符数是2358个!!!那么可以想到,要输出完这些字符,函数⾄少递归了2358次!(其实不⽌这个数)考虑到分析该程序的⽬的“just for fun”,
我们就到此为⾄了吧!
经过分析,我们不由得不佩服这段代码的作者,虽说这样的代码风格是绝对不提倡的,但能设计出这样的代码,不能不说是种“强悍”!