总览

Problem-int

bitXor

  • bitXor - x^y using only ~ and &
  • Example: bitXor(4, 5) = 1
  • Legal ops: ~ &
  • Max ops: 14

要求只用“非”和“与”来表示“异或”。首先想到“异或的非=同或”:

\[\begin{aligned}x\oplus y&=\overline{x}y+x\overline{y}\newline
&=\overline{\overline{\overline{x}y}\cdot\overline{\overline{y}x}}\newline
&=\overline{(x+\overline{y})\cdot(\overline{x}+y)}\newline
&=\overline{xy+\overline{x}\overline{y}}
\end{aligned}
\]

将里面的“或”变成“和”,只需利用德摩根定律:

\[\begin{aligned}
\overline{xy+\overline{x}\overline{y}}=\overline{x\cdot y}\cdot\overline{\overline{x}\cdot\overline{y}}
\end{aligned}
\]
int bitXor(int x, int y) {
return ~(x&y)&(~x&~y);
}

tmin

  • tmin - return minimum two's complement integer
  • Legal ops: ! ~ & ^ | + << >>
  • Max ops: 4

对于32位int, Tmin = 0x80000000, 将 1 左移31位即可

int tmin(void) {
return 1<<31;
}

isTmax

  • isTmax - returns 1 if x is the maximum, two's complement number,
  • and 0 otherwise
  • Legal ops: ! ~ & ^ | +
  • Max ops: 10

首先,我们知道 Tmax = 0x7FFFFFFF, 不难想到求 Tmax+1 = Tmin = 0x80000000。观察可知,~(Tmax+1) = Tmax,只需判断 ~(x+1) 与 x 是否相等即可得出结论。但是,题目要求不能用等于号,于是利用 x^x = 0 的性质进行判断,即 return !~(x+1)^x

然而,这样是过不了的。仔细揣摩,-1 = 0xFFFFFFFF。对其进行加1后,该操作数会被截断,高位进位无效,使得结果为0x00000000,每一位刚好也和-1的每一位互补。因此,x = -1的情况是需要特判的。

在C语言中,非0即为真。利用 x+1=0 时结果为假这一特点,当 x = -1 时,! (x+1) = 1, 再用 ! (x+1) 与 原判式 !$ \sim $(x+1)^x 进行或运算,即 (~(x+1)^x)|!(x+1), 则该式在 x = -1 时一定为真,x != -1 时真假就一定取决于 ~(x+1)^x

int isTmax(int x) {
return !(~(x+1)^x|!(x+1));
}

allOddBits

  • allOddBits - return 1 if all odd-numbered bits in word set to 1
  • where bits are numbered from 0 (least significant) to 31 (most significant)
  • Examples allOddBits(0xFFFFFFFD) = 0, allOddBits(0xAAAAAAAA) = 1
  • Legal ops: ! $ \sim $ & ^ | + << >>
  • Max ops: 12

这道题本身是很简单的,很容易想到构造一个32位的奇数位全为1的数 y = 0xAAAAAAAA,将其与 x 进行与运算,如果结果仍为 y, 则 x 的奇数位均为 1.

但要注意本实验的一个要求:

Integer constants 0 through 255 (0xFF), inclusive. You are not allowed to use big constants such as 0xffffffff.

考虑先构造0xAA, 然后利用移位操作得到 0xAAAAAAAA。

int allOddBits(int x) {
int y = 0xAA + (0xAA << 8);
y = y + (y << 16);
return !((x&y)^y);
}

negate

  • negate - return -x
  • Example: negate(1) = -1.
  • Legal ops: ! ~ & ^ | + << >>
  • Max ops: 5

很基础的结论: ~x + x = -1

int negate(int x) {
return ~x+1;
}

isAsciiDigit

  • isAsciiDigit - return 1 if 0x30 <= x <= 0x39 (ASCII codes for characters '0' to '9')
  • Example:
    • isAsciiDigit(0x35) = 1
    • isAsciiDigit(0x3a) = 0
    • isAsciiDigit(0x05) = 0
  • Legal ops: ! ~ & ^ | + << >>
  • Max ops: 15

0x30 = 0...0 00

11 0000

0x39 = 0...0 0011 1001

  1. 先检查高26位是否全为0,作为条件1

    int flag1 = !(x >> 6);
  2. 再检查中间是否为0011,作为条件2

    int flag2 = !(0b11 ^ (x >> 4));
  3. 最后检查末尾4位是否在0000与1001之间, 先得到最后四位数

    int y = x & (0xF);
  4. y 在 0 到 9 之间,则 y - 10 < 0,由于符号限制,我们通过移位后判断符号位来作为条件3。由于不能用减法,可以通过-x = ~x +1达到目的

    int flag3 = (y + ~0xA + 1) >> 31;
int isAsciiDigit(int x) {
return (!(x >> 6)) & (!(0b11 ^ (x >> 4))) & (((x & (0xF)) + ~0xA + 1) >> 31);
}

conditional

  • conditional - same as x ? y : z
  • Example: conditional(2,4,5) = 4
  • Legal ops: ! ~ & ^ | + << >>
  • Max ops: 16

这个题很基础

\[F = \overline xz+xy
\]

先将 x 归整化

x = !x

当 \(x=0\)时, y = 0x00000001, 考虑 \(\sim y+1 =\) 0xFFFFFFFF

当 \(x\neq0\)时, y = 0, 考虑 \(~\sim(\sim y+1) =\) 0xFFFFFFFF

int conditional(int x, int y, int z) {
x = ~(!x)+1;
return (x&z)+(~x&y);
}

isLessOrEqual

  • isLessOrEqual - if x <= y then return 1, else return 0
  • Example: isLessOrEqual(4,5) = 1.
  • Legal ops: ! ~ & ^ | + << >>
  • Max ops: 24

考虑 x - y

当 y 与 x 同号时,x - y 不会溢出,因此判断 x - y 的符号即可

当 y 与 x 异号时,x - y 可能会溢出,这时只分别判断 y 和 x 的符号即可

首先利用移位操作,分别得到 x, y 的符号位。

int signx = (x >> 31) & 1;
int signy = (y >> 31) & 1;
  1. 设置情况flag1, x负 y正,满足情况,返回1;x正 y负,返回0

    正负 signx signy 返回值
    x正 y负 0 1 0
    x负 y正 1 0 1
    int flag1 = signx & (!signy)
  2. 在x, y 同号的前提下,计算 x - y,也就是 x+~y+1, 移位取符号位。

    但是要注意到:x = y 时,x+~y+1 = 0的符号位为0,与 x < y 时的符号位为 1 不同,因此,x = y 的情况需要特判。

    我在这里用了一种较为巧妙的办法,\(x\leq y\) 等价于\(x <y + 1\) , 使x = y + 1 时,符号位才为0,只需要将x+ ~y+1减 1 即可。

    即最终求 x+~y 的符号位。

    int e = signx ^ signy; //同号
    int flag2 = ((!e) & ((x + ~y) >> 31) & 1);
int isLessOrEqual(int x, int y) {
int signx = (x >> 31) & 1;
int signy = (y >> 31) & 1;
int flag1 = signx & (!signy);
int e = signx ^ signy; //同号
int flag2 = ((!e) & ((x + ~y) >> 31) & 1);
return flag1 | flag2;
}

logicalNeg

  • logicalNeg - implement the ! operator, using all of
  • the legal operators except !
  • Examples: logicalNeg(3) = 0, logicalNeg(0) = 1
  • Legal ops: ~ & ^ | + << >>
  • Max ops: 12

当 x = 0 时,-x = 0,两者符号位相同,而当 \(x\neq 0\) 时,-x 与 x 的符号位显然不同,由此就可以解决本题。

令 x 与 -x 异或,则若 x = 0, 则异或后的符号位为0,否则为1,取符号位作为结果,则得到的结果刚好与题目中要求的返回值相反。

接下来要解决的问题就是实现 0x00000000 与 0x00000001 的相互转换,只需将其取反再加 2 即可

int logicalNeg(int x) {
return ~((((~ x + 1) | x) >> 31) & 0x1) + 2;
}

进一步优化:上述做法使用了 7 个操作符,总给人一种南辕北辙的感觉。原因在于没有利用算术右移的特点

(~ x + 1) | x) >> 31;

在这一步中,如果\(x\neq 0\), 那么由于符号位为 1 ,右移后的结果便为0x11111111, 令其加 1 ,刚好为 0;显然,当 x = 0 时,令结果加 1 ,刚好为 1 。于是,不需要“与”操作来置0高位,在移位完成后直接将结果加 1 ,便可过掉本题

只用了 5 个操作符

int logicalNeg(int x) {
return (((~ x + 1) | x) >> 31) + 1;
}

howManyBits

  • howManyBits - return the minimum number of bits required to represent x in
  • two's complement
  • Examples:
    • howManyBits(12) = 5
    • howManyBits(298) = 10
    • howManyBits(-5) = 4
    • howManyBits(0) = 1
    • howManyBits(-1) = 1
    • howManyBits(0x80000000) = 32
  • Legal ops: ! ~ & ^ | + << >>
  • Max ops: 90

题意理解

x 为正数,以八位为例:0011 1010,需找到最高位 1,除此以外,还需一位 0 作为符号位;

x 为负数,以八位为例:1100 1001,需找到最高位 0,除此以外,还需更高一位 1 作为符号位

做法

  1. 为了统一,不妨当 x 为负数时,将其取反,如上例:$\sim $x = 0011 0110, 那么也只需要找到最高位 1 后再加一位就好,这步操作如下

    int flag = x >> 31;
    x = ((~flag) & x) | (flag & (~x));
  2. 利用二分的思想,先考虑高16位:

    0001 1000 1100 0000 | 0000 0100 1000 0000

    将 x 右移16位 x = x >> 16 :

    0000 0000 0000 0000 | 0001 1000 1100 0000

    进行规格化处理 x = !! x:

    0000 0000 0000 0000 | 0000 0000 0000 0001

    若高 16 位有 1,处理后的 x = 0x00000001。需要的位数至少为 16,引入变量 bit_16 记录该权重。怎么做呢?将处理后的 x 左移 4 位即可

    int bit_16 = (!!(x >> 16)) << 4;
  3. 如果高 16 位有 1 ,则将 x 右移 16 位,对右移后的 x 的低 16 位中的高 8 位进行同样的操作,从而二分地在 x 的高16位中找到最大位的 1 ;如果高 16 位没有 1 ,则 x 无需右移, 在 x 的低 16 位中的高 8 位进行同样的操作。

    由此可得右移操作:

    x = x >> bit_16;
  4. 同理,分别对高 8 位,4位,2位,1位进行检查,检查后进行同样的操作。

  5. 最后将所有权重求和,便是最终结果

举例

  • 设 x = 1101 1000 1100 0000 | 0000 0100 1000 0000

  • 负数取反:y = 0010 0111 0011 1111 | 1111 1011 0111 1111

  • 高16位右移:y = 0000 0000 0000 0000 | 0010 0111 0011 1111

  • 规格化:y = 0000 0000 0000 0000 | 0000 0000 0000 0001

  • bit_16 处理:bit_16 = y << 4 = 16 //说明 x 至少需要 16 位

  • x 右移:x = x >> bit_16 = 0000 0000 0000 0000 | 0010 0111 0011 1111

  • 低 16 位中的高 8 位右移:y = 0000 0000 0000 0000 | 0000 0000 | 0010 0111

  • 规格化:y = 0000 0000 0000 0000 | 0000 0000 | 0000 0001

  • bit_8 处理:bit_8 = y << 3 = 8 //说明 x 又需要 8 位

  • x 右移:x = x >> bit_8 = 0000 0000 0000 0000 | 0000 0000 | 0010 0111

  • 低 8 位的高 4 位右移:y = 0000 0000 0000 0000 | 0000 0000 | 0000 | 0010

  • 规格化:y = 0000 0000 0000 0000 | 0000 0000 | 0000 | 0001

  • bit_4 处理:bit_4 = y << 2 = 4 //说明 x 又需要 4 位

  • x 右移:x = x >> bit_4 = 0000 0000 0000 0000 | 0000 0000 | 0000 | 0010

  • 低 4 位的高 2 位右移:y = 0000 0000 0000 0000 | 0000 0000 | 0000 | 00 | 00

  • 规格化:y = 0000 0000 0000 0000 | 0000 0000 | 0000 | 00 | 00

  • bit_2 处理:bit_2 = y << 1 = 0 //说明 x 这 4 位中的 1 在低 2 位中

  • x 右移:x = x >> bit_2 = 0000 0000 0000 0000 | 0000 0000 | 0000 | 00 | 10 //不移动

  • 低 2 位的高 1 位右移:y = 0000 0000 0000 0000 | 0000 0000 | 0000 | 00 | 0 | 1

  • 规格化:y = 0000 0000 0000 0000 | 0000 0000 | 0000 | 00 | 0 | 1

  • bit_1 处理:bit_1 = y << 0 = 1 //说明 x 又需要1位

  • x 右移:x = x >> bit_1 = 0000 0000 0000 0000 | 0000 0000 | 0000 | 00 | 0 | 1 //不移动

  • bit_0 处理:bit_0 = x = 1

  • bit_8 + bit_4 + bit_2 + bit_1 + bit_0 + 1= 31 与正确答案符合!

int howManyBits(int x) {
int flag = x >> 31;
x = ((~flag) & x) | (flag & (~x));
int bit_16 = (!!(x >> 16)) << 4;
x = x >> bit_16;
int bit_8 = !!(x>>8)<<3;
x = x >> bit_8;
int bit_4 = !!(x >> 4) << 2;
x = x >> bit_4;
int bit_2 = !!(x >> 2) << 1;
x = x >> bit_2;
int bit_1 = !!(x >> 1);
x = x >> bit_1;
int bit_0 = x;
return bit_16+bit_8+bit_4+bit_2+bit_1+bit_0+1;
}

Problem-float

floatScale2

  • floatScale2 - Return bit-level equivalent of expression 2*f for
  • floating point argument f.
  • Both the argument and result are passed as unsigned int's, but
  • they are to be interpreted as the bit-level representation of
  • single-precision floating point values.
  • When argument is NaN, return argument
  • Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
  • Max ops: 30

先分别取符号 s,尾数 frac,和阶码 exp

unsigned s = (uf >> 31) & 0x1;
unsigned exp = (uf >> 23) & 0xFF;
unsigned frac = (uf & 0x7FFFFF);
  1. 非规格化的

    此时,exp == 0,由于此时 frac 就是尾码,直接 frac << 1 即可

  2. 规格化的

    此时,exp!=0 && !=255,exp ++即可

  3. 特殊值

    根据题目要求,返回 uf

unsigned floatScale2(unsigned uf) {
unsigned s = (uf >> 31) & 0x1;
unsigned exp = (uf >> 23) & 0xFF;
unsigned frac = (uf & 0x7FFFFF);
//NaN
if(exp == 0xFF)
return uf;
//
else if(exp == 0){
frac <<= 1;
return (s << 31) | (exp << 23) | frac;
}
//else
exp++;
return (s << 31) | (exp << 23) | frac; }

floatFloat2Int

  • floatFloat2Int - Return bit-level equivalent of expression (int) f
  • for floating point argument f.
  • Argument is passed as unsigned int, but
  • it is to be interpreted as the bit-level representation of a
  • single-precision floating point value.
  • Anything out of range (including NaN and infinity) should return0x80000000u.
  • Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
  • Max ops: 30

与上题一样,先取符号 s,尾数 frac,和阶码 exp

unsigned s = (uf >> 31) & 0x1;
unsigned exp = (uf >> 23) & 0xFF;
unsigned frac = (uf & 0x7FFFFF);
  1. 非规格化的

    此时,exp == 0,而 E = 1 - Bias = 1 - 127 = -126, M < 1。显然,return 0

  2. 规格化的

    此时,exp!=0 && exp!=255。我们把 frac 作为基数进行修改,最后返回 frac 。首先由于这是规格化的,所以要加上开头的“1”。

    int E = exp - 127;
    frac = frac | (1 << 23);
    \[V = (-1)^s\times M\times2^E
    \]
    • 当 E < 0 时,显然 V < 1, return 0

    • frac 为 23 位,若 E > 23, 则进行加权时,能在 frac 的末尾添加 (E - 23) 个 0

    • 若 E < 23, 则 frac 末尾的 (23 - E)个数无法保留

    • 若 E >= 31, 显然为 infinity ,return 0x80000000u

  3. 特殊值 exp == 0xFF, return 0x80000000u

  4. 还要注意考虑负数的情况,根据 s 的值进行判断,最后利用我们前面用到的取负技巧即可。

int floatFloat2Int(unsigned uf) {
unsigned s = (uf >> 31) & 0x1;
unsigned exp = (uf >> 23) & 0xFF;
unsigned frac = (uf & 0x7FFFFF);
int E = exp - 127;
frac = frac | (1 << 23);
if(E < 0) return 0;
else if(E >= 31) return 0x1 << 31;
else{
if(E<23) {
frac>>=(23 - E);
}else{
frac <<= (E - 23);
}
}
if (s)
return ~frac + 1;
return frac;
}

floatPower2

  • floatPower2 - Return bit-level equivalent of the expression 2.0^x
  • (2.0 raised to the power x) for any 32-bit integer x.
  • The unsigned value that is returned should have the identical bit
  • representation as the single-precision floating-point number 2.0^x.
  • If the result is too small to be represented as a denorm, return 0
  • If too large, return +INF.
  • Legal ops: Any integer/unsigned operations incl. ||, &&. Also if, while
  • Max ops: 30

要做这道题,首先要导出浮点数非规格化和规格化分别表示的浮点数的范围。

  1. 非规格化的

    此时,E = 1 - Bias = 1 - 127 = -126, 而\(M_{min} = 0.00\dots1 = 2^{-23}\), 所以非规格化浮点最小为 \(2^{-23}\times 2^{-126} = 2^{-149}\), \(M_{max}=2^{-1}+2^{-2}+\dots +2^{-23} = 1-2^{-23}\), 所以非规格化浮点最大为 \(2^{-126}\times (1-2^{-23})\)

  2. 规格化的

    \(M_{min} = 1\), \(E_{min} = 1-127=-126\) 所以规格化的最小为\(2^{-126}\)

    \(M_{max}=1.11\dots11\), \(E_{MINT}=1111\quad1110=2^{127}\) 所以规格化最大为不到\(2^{128}\)

可得下表:

格式 最小值 最大值
规格化 \(2^{-126}\) \(2^{127}\times (2-2^{-23})\)
非规格化 \(2^{-149}\) \(2^{-126}\times (1-2^{-23})\)

所以:

  • x > 127 时,返回 Nan
  • x <= -149时,返回 0
  • -126<= x <=127 时,为规格化的。直接让尾码为全0,控制阶码即可,由 x = expr - bias => exp = x + 127
  • -149 < x < -126 时,为非规格化的,阶码值为 E = 1 - bias = -126。这时候只能通过控制尾码来计算。由
\[M\times2^{-126}=2^x
\]

知尾码的值是二次幂的形式,所以,尾码的值一定是通过一个“1”左移得到的。尾码各位以2的次幂形式的权值如下

设1左移n位,则 x+126 = -(23 - n),得 n = x + 149

unsigned floatPower2(int x) {
if(x < -149)
return 0;
else if(x < -126)
return 1 << (x + 149);
else if(x <= 127)
return (x + 127) << 23;
else
return (0xFF) << 23;
}

完结纪念:

总结

  • DataLab 让我了解到我有多么菜。看似简单的数据操作,想要在有限的条件下写得天衣无缝需要敏锐的观察力以及强大的逻辑思维能力。
  • 本实验耗时5天,约7小时。

CSAPP-Lab01 Data Lab 记录的更多相关文章

  1. 【CSAPP】Data Lab实验笔记

    前天讲到要刚CSAPP,这一刚就是两天半.CSAPP果然够爽,自带完整的说明文档,评判程序,辅助程序.样例直接百万组走起,管饱! datalab讲的是整数和浮点数怎么用二进制表示的,考验的是用基本只用 ...

  2. CS:APP配套实验 Data Lab

    刚刚完成注册博客,想写一篇随笔,方便以后自己回顾.如果恰好也能帮助到你,是我的荣幸. 这次随笔是记载我的计算机系统(CS:APP,Computer Systems:A Programer's Pers ...

  3. CSAPP:datalab实验记录

    CSAPP:datalab实验记录 bitXor /* * bitXor - x^y using only ~ and & * Example: bitXor(4, 5) = 1 * Lega ...

  4. CSAPP buffer lab记录——IA32版本

    CSAPP buffer lab为深入理解计算机系统(原书第二版)的配套的缓冲区溢出实验,该实验要求利用缓冲区溢出的原理解决5个难度递增的问题,分别为smoke(level 0).fizz(level ...

  5. CSAPP Bomb Lab记录

    记录关于CSAPP 二进制炸弹实验过程 (CSAPP配套教学网站Bomb Lab自学版本,实验地址:http://csapp.cs.cmu.edu/2e/labs.html) (个人体验:对x86汇编 ...

  6. 《深入理解计算机系统》实验一 —Data Lab

    本文是CSAPP第二章的配套实验,通过使用有限的运算符来实现正数,负数,浮点数的位级表示.通过完成这13个函数,可以使我们更好的理解计算机中数据的编码方式. 准备工作   首先去官网Lab Assig ...

  7. CSAPP-Lab03 Attack Lab 记录

    纸上得来终觉浅,绝知此事要躬行 实验概览 Attack!成为一名黑客不正是我小时候的梦想吗?这个实验一定会很有趣. CMU 对本实验的官方说明文档:http://csapp.cs.cmu.edu/3e ...

  8. 【CSAPP】Bomb Lab实验笔记

    bomblab这节搞的是二进制拆弹,可以通俗理解为利用反汇编知识找出程序的六个解锁密码. 早就听闻BOMBLAB的大名,再加上我一直觉得反汇编是个很艰难的工作,开工前我做好了打BOSS心理准备.实际上 ...

  9. 【CSAPP】Shell Lab 实验笔记

    shlab这节是要求写个支持任务(job)功能的简易shell,主要考察了linux信号机制的相关内容.难度上如果熟读了<CSAPP>的"异常控制流"一章,应该是可以不 ...

随机推荐

  1. QA(测试) 工作准则建议

    身为一个专业的 QA 当然需要有自己的测试原则,这些测试原则不仅可以帮助我们提高产品质量,对外还能体现出我们的专业性,从而让合作方后续还有意愿和我们合作. 1 测试前 1.1 需求评审 必须参与,有问 ...

  2. salesforce零基础学习(一百一十一)custom metadata type数据获取方式更新

    本篇参考: https://developer.salesforce.com/docs/atlas.en-us.234.0.apexref.meta/apexref/apex_methods_syst ...

  3. Java中的增强型for循环

    下面是关于增强型for循环对一维数组与二维数组遍历的具体实现: public class NewForLoop { public static void main(String[] args) { i ...

  4. mac不能用ip访问项目的, 还有80端口不能开启的问题

    开启80端口 参考这篇文章 解决mac无法使用80端口问题,亲测可用 1. 防火墙设置问题 2. 网卡问题 3. 服务器地址配置问题

  5. iOS 启动画面 App图标尺寸 项目上架必须Icon设置

    1.1AppIcon图标尺寸如下: 说明:AppIcon (6张) AppStore Icon (1张)(png格式) AppStore Icon --- 1024x1024(必须)   1.2启动画 ...

  6. 动态路由与RIP协议

    动态路由与RIP协议 目录 动态路由与RIP协议 一.动态路由(Dynamic Route) 1.动态路由概述 2.动态路由特点 3.动态路由协议 (1)动态路由协议概述 (2)度量值 (3)收敛 4 ...

  7. 【linux运维】Linux服务器玩转vsftpd安装与配置

    Linux服务器搭建ftp详细教程. 前言 本文,主要以vsftp软件为主进行讲解,版本为3.0.2.以介绍设置虚拟用户.权限验证为主,linux权限知识默认大家都有所了解.vsftpd设置有两种模式 ...

  8. Redis 源码简洁剖析 11 - 主 IO 线程及 Redis 6.0 多 IO 线程

    Redis 到底是不是单线程的程序? 多 IO 线程的初始化 IO 线程运行函数 IOThreadMain 如何推迟客户端「读」操作? 如何推迟客户端「写」操作? 如何把待「读」客户端分配给 IO 线 ...

  9. redis(一)-----初识redis

    Redis是一种基于键值对(key-value)的NoSQL数据库 因为Redis会将所有数据都存放在内存 中,所以它的读写性能非常惊人.不仅如此,Redis还可以将内存的数据利 用快照和日志的形式保 ...

  10. C++ 实现 Parsec

    前一段时间看到了梨梨喵聚聚写的Parser Combinator 在 C++ 里的 DSL, 感觉好厉害, 正好毕设里要写一部分前端, 昨天又把这篇文章看了一遍, 想着我也要用这么酷炫的东西来参与一下 ...