IOCCC-科学计算器代码解析
本文是对IOCCC赛事中来自Qiming Hou老师的科学计算器的简单代码解析。该代码来自赛事IOCCC 20th,Hou老师获得奖项Best self documenting。
事先声明
由于本人能力欠缺,本代码仍在继续解析中,因此如果分析出现了偏差,请及时联系本人更正。
代码概览
源代码如下。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#include <stdio.h>
#include <math.h>
#define clear 1;if(c>=11){c=0;sscanf(_,"%lf%c",&r,&c);while(*++_-c);}\
else if(argc>=4&&!main(4-(*_++=='('),argv))_++;g:c+=
#define puts(d,e) return 0;}{double a;int b;char c=(argc<4?d)&15;\
b=(*_%__LINE__+7)%9*(3*e>>c&1);c+=
#define I(d) (r);if(argc<4&&*#d==*_){a=r;r=usage?r*a:r+a;goto g;}c=c
#define return if(argc==2)printf("%f\n",r);return argc>=4+
#define usage main(4-__LINE__/26,argv)
#define calculator *_*(int)
#define l (r);r=--b?r:
#define _ argv[1]
#define x
double r;
int main(int argc,char** argv){
if(argc<2){
puts(
usage: calculator 11/26+222/31
+~~~~~~~~~~~~~~~~~~~~~~~~calculator-\
! 7.584,367 )
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
! clear ! 0 ||l -x l tan I (/) |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
! 1 | 2 | 3 ||l 1/x l cos I (*) |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
! 4 | 5 | 6 ||l exp l sqrt I (+) |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
! 7 | 8 | 9 ||l sin l log I (-) |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~(0
);
}
return 0;
}
可以看到,这里在main
函数中绘制了一个十分形象的计算器,而且它是可以编译通过的!这么神奇的现象是由前面的宏定义实现的。抛开这些不谈,我们先来看一下运行结果。
赛事官方提供了如下的makefile
。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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117#!/usr/bin/env make
#
# 2011 makefile
#
# Copyright (C) 2011, Landon Curt Noll, Simon Cooper, and Leonid A. Broukhis.
#
# Creative Commons Attribution-ShareAlike 3.0 Unported License.
# See: http://creativecommons.org/licenses/by-sa/3.0/
################
# tool locations
################
#
SHELL= /bin/bash
CP= cp
CPP= cpp
GUNZIP= gunzip
LD= ld
MAKE= make
RM= rm
SED= sed
TAR= tar
TRUE= true
# optimization
#
# Most compiles will safely use -O2. Some can use only -O1 or -O.
# A few compilers have broken optimizers or this entry make break
# under those buggy optimizers and thus you may not want anything.
#
OPT= -O2
# bitness
#
# Some entries require 32-bitness,
# other entries require 64-bitess.
# By default we assume nothing.
#
ARCH=
# default flags for ANSI C compilation
#
CFLAGS= -Wall -W -ansi -pedantic ${ARCH} ${OPT} -lm
# ANSI compiler
#
# Set CC to the name of your ANSI compiler.
#
CC= cc
##############################
# Special flags for this entry
##############################
#
ENTRY= hou
DATA=
#################
# build the entry
#################
#
all: ${ENTRY} ${DATA}
@${TRUE}
${ENTRY}: ${ENTRY}.c
${CC} ${CFLAGS} ${ENTRY}.c -o ${ENTRY}
# alternative executable
#
alt:
@${TRUE}
###############
# utility rules
###############
#
everything: all alt
clean:
${RM} -f ${ENTRY}.o
clobber: clean
${RM} -f ${ENTRY}
nuke: clobber
@echo gnab gib!
install:
@echo "Surely you're joking Mr. Feynman!"
# backwards compatibility
#
build: all
##################
# 133t hacker rulz
##################
#
love:
@echo 'not war?'
haste:
$(MAKE) waste
waste:
@echo 'waste'
easter_egg:
@echo you expected to mis-understand this $${RANDOM} magic
@echo chongo '<was here>' "/\\oo/\\"
@echo Readers shall not be disallowed from being unable to partly misunderstand this final echo.
@${TRUE}
简单阅读可以知道,直接运行make
得到的结果和gcc hou.c -o hou
类似,仅仅是编译选项有所不同。
对于得到的二进制文件hou
,有如下运行结果。
1 |
|
这里体现出了题目科学计算器的功能了,调用这个看起来很像计算器的程序可以进行科学计算。它是如何实现的呢?
宏展开
第一步是要抛开宏的伪装,看清楚代码的真实逻辑。
使用如下指令可以获得宏展开后的代码结果。1
$ gcc -E hou.c > hou-expanded.c
对新的文件进行部分整理,得到如下结果。
1 |
|
直接编译这个文件依然可以得到想要的结果。
然后注意如下事实:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
的结果是0。这需要在类型转换下才能成立,否则会编译报错。- 针对
argc
的大小进行分析,去除不需要的代码。
我们可以将代码整理成为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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81#include <stdio.h>
#include <math.h>
double r;
int depth = 0;
int main(int argc,char** argv){
++depth;
if(argc<2){ --depth; return 0; }
if (argc <= 3)
{
double a=0;int b;
for(int jj = 0; jj < depth; ++jj) putchar(' ');
printf("Enter with argc=%d, argv[1]=%s\n", argc, argv[1]);
char c=1;
b=0;
for(int jj = 0; jj < depth; ++jj) putchar(' ');
printf("Now a=%f, b=%d, c=%d, r=%f\n" ,a, b, c, r);
main(4, argv);
g1:c+= 1;
if(*"/"==*argv[1]){
a=r;
main(4, argv);
r=r*a;
goto g1;
}
if(*"*"==*argv[1]){
a=r;
main(4, argv);
r=r*a;
goto g1;
}
if(*"+"==*argv[1]){
a=r;
main(3, argv);
r=r+a;
goto g1;
}
if(*"-"==*argv[1]){
a=r;
main(3, argv);
r=r+a;
goto g1;
}
if(argc==2)printf("%f\n",r);
printf("End with a=%f, b=%d, c=%d, r=%f, ret=%d\n" ,a, b, c, r, 0);
--depth;
return 0;
}
if (argc == 4)
{
double a=0;int b;
for(int jj = 0; jj < depth; ++jj) putchar(' ');
printf("Enter with argc=%d, argv[1]=%s\n", argc, argv[1]);
char c=(*argv[1]*(int) 11/26+7)&15; // number:>=11, else < 11
b=(*argv[1]%21 +7)%9*(3*367>>c&1); // number:0, else, hash
for(int jj = 0; jj < depth; ++jj) putchar(' ');
printf("Now a=%f, b=%d, c=%d, r=%f\n" ,a, b, c, r);
if(c>=11){ // number
c=0;
sscanf(argv[1],"%lf%c",&r,&c); // read number and operator, x+
while(*++argv[1]-c);
} else if(!main(4-(*argv[1]++=='('),argv)) // is operator or (
argv[1]++;
r=--b?r: - (r);
r=--b?r: tan (r);
r=--b?r: 1/ (r);
r=--b?r: cos (r);
r=--b?r: exp (r);
r=--b?r: sqrt (r);
r=--b?r: sin (r);
r=--b?r: log (r);
if(argc==2)printf("%f\n",r);
printf("End with a=%f, b=%d, c=%d, r=%f, ret=%d\n" ,a, b, c, r, 1);
--depth;
return 1;
}
}
基本介绍
这里添加了相当数量的调试代码。总之这时候代码的主要逻辑就清晰明了了。可以看出,这里的表达式解析算法基本上符合表达式的BNF形式。
作者将表达式分为两类,分别代表了exp_additive
加法表达式和exp_multiplicative
乘法表达式。然后利用了如下关系。1
2exp_additive -> exp_multiplicative ( "+"|"-" ) exp_multiplicative
exp_multiplicative -> exp_cast ( "*"|"/"|"%" ) exp_cast
其中exp_cast
为类型转换表达式。本计算器不支持这一功能,可以认为是单纯的数值。
在这一基础上,作者还将exp_multiplicative
添加了一元运算符的支持。这体现在main(4, argv)
函数中对于r
的一系列函数操作。使得计算器和编译器不同,可以跳过一元函数调用直接解析表达式。
详细分析
argc
是什么
argc
是用来判断当前处理表达式的类型的标志量。
argc |
含义 |
---|---|
2 | 当前处理exp_additive ,处理完成后结果保存在r ,并输出 |
3 | 当前处理exp_additive ,处理完成后结果保存在r |
4 | 当前处理exp_multiplicative ,处理完成后结果保存在r |
表达式解析的基本结构
参阅相关材料,并且对照本文代码,得到如下的解析结构。
1 |
|
本文代码基本符合。
标志量a,b,c的含义
标志量a
主要作用是暂存全局运算结果r
,在计算得到新的r
之后需要和先前的值合并,得到正确的计算值。比如1
2
3
4
5
6
7if(*"/"==*argv[1]){
a=r;
main(4, argv); // 更新r的值,为新表达式的值。
r=r*a; // 因为使用了*/,所以需要进行乘法运算。
// 没有使用除法的原因是,在main(4,argv)中将/解析成了一个一元运算符。
goto g1;
}b
,c
的作用是使用两个hash表判断当前的字符是数字还是字母或者运算符。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/*
char ASCII c
0 48 11
1 49 11
2 50 12
3 51 12
4 52 13
5 53 13
6 54 14
7 55 14
8 56 15
9 57 15
( 40 7
) 41 8
* 42 8
+ 43 9
- 45 10
/ 47 10
*/
/*
if c >= 11, that is number, b = 0;
else
char ASCII b
( 40 8
) 41 0
* 42 7
+ 43 8
- 45 1
a 97 2
/ 47 3
o 111 4
p 112 5
q 113 6
i 105 7
g 103 8
*/
在b
的解析式中使用了一个magic number(3*367>>c&1)
,作用是进一步限定了代码的范围,让不合适的字母的非零b
值归零。具体作用举例如下。1
2
3
4sin-> s i n
s -> 8*0 -> 0
i -> 7
n -> 3*0 -> 0
所以sin
的关键字符是i
,而s
的作用被设为0
,防止和cos
解析混淆。
main
函数的返回值
简单而言,main(3,argv)
的返回值为0
,main(4,argv)
的返回值为1
。
这是为了在某些时候简化代码。
第一部分的解析
main(3,argv)
对应exp_additive
的解析。
我们暂时去掉调试代码。
1 |
|
第二部分的解析
main(4,argv)
对应exp_multiplicative
的解析。
1 |
|
代码混淆
代码混淆的重点在于头部的语句。
1 |
|
argv[1]
是经常使用的值。#define _ argv[1]
进行一步混淆。main(3,argv)
还是main(4,argv)
,使用行号4-__LINE__/26
判断。- 一元运算符的运算需要大量格式相似的语句,使用
#define l (r);=__b?r:
可以和表格内容结合直接生成。表格中有多余的x
,使用#define x
直接清除。 - 二元运算符的循环解析,就是那个
goto
和后面的语句,使用一个#define I(d)
简化。 - 返回值需要针对
argc
的不同而不同,直接使用一个#define return
语句完成了三种情况的展开,很精妙。 #define puts(d,e)
将函数的头部定义体现了出来。注意这里的d
是从usage
到7.584
的全部部分,e
是简单的367
。- 代码中的magic number都很好的充当了hash函数的构造,而且magic number本身也是符合数学规律的,很精妙。
clear
的作用是判断hash函数的结果,转向进一步的处理。- 代码中充斥着的
+
~~~~
!
在展开形式中充当了各表达式的构成部分。同时还可以把计算器的数字部分转化为不影响表达式的部分。比如! 1 | 2 | 3 || ...
calculator
也是表达式的一部分,提供了强制类型转换。
总结
至此本份代码的解析基本完成。展开后的代码也可以得到正确的结果,分析部分也基本符合表达式的BNF定义。如果想要进一步得到代码构造的内在逻辑,就不得不更加深入的学习相关领域的知识。今天的分析就到此为止了。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!