在实验零中,我们拿到了lab1-handout.zip压缩文件,接着,我们使用unzip ./lab1-handout.zip命令,解压缩该压缩文件。解压缩成功后,使用"ls"命令查看当前工作目录下的文件和文件夹,发现得到了"lab1-handout"文件夹,如(图1:解压缩实验一文件)所示。在这里注意,zip格式的压缩文件常用于Windows系统,在Linux系统中很少用到(Ubuntu系统是Linux系统的一种,其它Linux系统还有很多,比如CentOS7, CentOS8 Stream, OpenSUSE, Fedora, Ubuntu, Debian等,以及大三上做网络空间安全攻防实验时,会用到专门用做网络攻击的Kali系统,以及国外的网络安全网站Try Hacke Me专门推出的Parrot系统,还有Redhat推出的RHEL系统,都属于Linux系统。在这里不要贪多,专精一种系统就可以。我一直选择的是CentOS8 Stream系统,因为它受Redhat支持,而Redhat是很知名的Linux解决方案提供商)Linux系统中常用的压缩文件格式一般为什么什么.tar.gz,而解压缩这些压缩文件的方法为"tar xzvf 什么什么.tar.gz"。在这里你不需要知道'tar'命令是干什么的,你也不需要知道'xzvf'四个参数分别都是做什么用的,你现在只需要记住这条命令即可,后续会讲到'tar'命令以及'xzvf'四个参数。
**************************************************************************************************************
(图1:解压缩实验一文件)
**************************************************************************************************************
解压缩该.zip文件之后,使用'cd'命令进入到该文件夹中, 然后使用'ls'命令查看该文件夹中的内容,发现有很多很多很多文件,无从下手,如(图2:实验一文件夹中的很多文件)所示。但是注意到其中包含一份README文件。注意!当我们拿到一份开源软件文件夹时,需要做的第一件事,就是去看它的README文件,了解这份开源软件是做什么用的、它的内部组成是什么、每份文件的用途是什么、该如何编译它、该如何使用它、各个选项及参数该如何使用等,而不是直接去看自己熟悉的.c文件。使用cat README命令,即可以读取README文件的内容。同样如果有一份名为1.txt的文件,使用cat 1.txt可以读取1.txt的内容;如果有一份名为2.txt的文件,可以使用cat 2.txt读取2.txt的内容;如果有一份名为3.txt的文件,可以使用cat 3.txt读取3.txt的内容......但是考虑到使用cat命令阅读README文件是困难的,我们选择使用'less README'命令来阅读README文件。如(图3:使用less命令阅读README文件)所示。在使用less命令阅读README文件时,按下”上“键即可阅读上面一行,按下”下“键即可阅读下面一行,按下"Pg up"键即可向上翻页,按下"Pg down"键即可向下翻页,按下’Q‘键即可结束阅读。注意,即使README文件是英文,也不要害怕读它,耐下性子慢慢读,千万千万不要着急,就能读懂。
**************************************************************************************************************
(图2:实验一文件夹中的很多文件)
**************************************************************************************************************
**************************************************************************************************************
(图3:使用less命令阅读README文件)
**************************************************************************************************************
大概阅读完成README文件之后,我们知道我们只需要修改一个文件的内容,其余文件的内容都不需要修改,而这个文件的名称就是"bits.c"。虽然README文件中还有很多很多内容,但是我们不要着急,一条一条慢慢看,耐下性子慢慢看,最终一定能做出来哒。保证当前目录就是包含有bits.c文件的目录(使用ls命令查看当前文件夹下的所有文件,使用cd命令进入一个文件夹,使用pwd命令显示出当前所在的文件夹),然后使用"less bits.c"命令,查看bits.c文件的内容,如(图4:使用less命令查看bits.c文件的内容)所示。
**************************************************************************************************************
(图4:使用less命令查看bits.c文件的内容)
**************************************************************************************************************
首先我们看到了一大段注释,如(图5:bits.c文件开头的一段注释)所示。这段注释对我们唯一有用的信息是,不要在文件中写入"#include<stdio.h>"这条语句,即使会使用"printf()"函数用来帮助写程序改错,也不要引入"#include<stdio.h>"这条语句。即使程序会给出警告,说必须先声明printf(),才能够再使用printf()。(因为如果不包含stdio.h头文件,就直接使用printf函数,则编译器不会知道printf()函数的具体原型,所以编译器会发出警告)这就引入了第1个要点:不要使用"#include <stdio.h>"。
**************************************************************************************************************
(图5:bits.c文件开头的一段注释)
**************************************************************************************************************
接下来是阅读步骤一。它针对整型数和浮点数,分别制定了整型数编码规则和浮点数编码规则。总结出来即为两个要点。接着要点1:不要使用"#include <stdio.h>",我们可以得到要点2与要点3:
要点2:对于整型数,只能使用& ^ | + << >> ! ~这八个运算符,只能定义并使用int型的局部变量以及函数参数变量,只能使用0x0到0xFF的int型常量;
要点3:对于浮点数,只能使用& ^ | + << >> ! ~这八个运算符,可以定义并使用int型或unsigned int型的局部变量以及函数参数变量,可以使用任意有符号整型与无符号整型常量,可以使用循环与条件判断。
如(图6:阅读步骤一时的一个小部分)所示,即使看起来很多,但其实浓缩下来,只有要点2与要点3,所以不需要因为自己C语言基础不好而感觉到害怕,大胆继续往下走就可以。
**************************************************************************************************************
(图6:阅读步骤一时的一个小部分)
**************************************************************************************************************
因为使用命令"less bits.c"来阅读bits.c文件时,没有显示出来行号,不利于阅读,所以我们使用命令"cat -n bits.c | less"来阅读bits.c文件,"cat -n bits.c"中的选项"-n"表示显示出来行号,"cat -n bits.c | less"表示可以翻页得去阅读bits.c文件,而不是一下子展示出来。如(图7:"cat -n bits.c | less")所示。我们定位到第169行,准备编写第一个函数tmin,如(图8:第一个函数tmin)所示。
**************************************************************************************************************
(图7:"cat -n bits.c | less")
**************************************************************************************************************
**************************************************************************************************************
(图8:第一个函数tmin)
**************************************************************************************************************
在包含Ubuntu系统在内的所有Linux系统中修改一个文本文件的内容的方法有很多,我习惯使用命令行vim工具(对于运维人员来说,vim工具是最简单最高效的工具,即使在没有开启图形化界面的情况下,也可以使用vim工具来编辑文本文件),但是考虑到vim工具用法有些复杂,而且现在时间紧迫,下周就要交,所以选择使用VS code。 在大一上的寒假,可以去学习vim工具的使用。用VS code打开bits.c文件,如(图9:用VS code打开bits.c文件)所示。
**************************************************************************************************************
(图9:用VS code打开bits.c文件)
**************************************************************************************************************
通过使用VS code工具浏览bits.c文件,我们了解到,我们一共需要编写15个函数,下面我们一个接一个慢慢编写:
第一个函数int tmin(void),要求返回最小的用补码形式表示的有符号整数。而最小的用补码形式表示的有符号整数即为0x8000 0000,但是如果简单的"return 0x80000000",程序将会报错,为什么呢?因为根据要点2,对于整型数来说,只能使用0x0到0xFF的int型常量。那么我们可以怎么做呢?不妨考虑考虑0x80与0x8000 0000之间的关系,发现"0x80<<24"的结果,不就是0x80000000嘛?而且我们使用python验证我们的推断(图10:使用python验证0x80<<24=0x80000000),发现我们的推断是正确的。所以我们只修改bits.c中的第176行,注意,只修改了第176行!!!,如(图11:第1个函数——修改bits.c中的第176行)所示。接着保存并退出VS code。
**************************************************************************************************************
(图10:使用python验证0x80<<24=0x80000000)
**************************************************************************************************************
**************************************************************************************************************
(图11:第1个函数——修改bits.c中的第176行)
**************************************************************************************************************
接着按照README中的指示,使用实验一文件夹中自带的"dlc"文件检查bits.c文件中我们所写的tmin函数是否满足对于运算符等的要求。但是因为dlc文件默认是没有可执行权限的,所以我们需要使用命令"chmod u+x dlc",使得dlc对于拥有者来说,具有可执行权限。"chmod"表示修改文件或者文件夹的权限,"u+x" 表示对于user拥有者来说,增加一个"x"权限,"x"权限即为可执行权限。接着按照README中给出的样例,键入命令"./dlc bits.c",发现没有任何输出。而根据README的指示,如果没有任何输出,就证明bits.c文件中我们所写的tmin函数满足对于运算符等的要求。键入命令"./dlc -e bits.c",统计每个函数中所使用到的运算符的个数,发现我们所写的第一个函数只使用了1个运算符。如(图12:第1个函数——首先检查tmin函数是否满足对于运算符等的要求)所示。
**************************************************************************************************************
(图12:第1个函数——首先检查tmin函数是否满足对于运算符等的要求)
**************************************************************************************************************
接着按照README中的指示,使用"make btest"命令编译文件,忽略出现的警告(虽然在C语言课上,老师要求大家不要留下任何一个警告,要努力去修改好它,但是考虑到这种修改好这种警告的办法比较复杂,所以既然它不影响程序的执行,那么我们可以简单的忽略掉它),并且使用"./btest -f tmin" 命令检查名为"tmin"的函数是否正确,选项"-f"指定了被检查的文件的名称为"tmin"。根据命令"./btest -f tmin" 的结果,发现没有错误,所以认为第1个函数"tmin"做完了。如(图13:第1个函数——接着检查tmin函数能否输出正确的结果)所示。
**************************************************************************************************************
(图13:第1个函数——接着检查tmin函数能否输出正确的结果)
**************************************************************************************************************
接着第2个函数, "int absVal(int x)",要求给出有符号整型变量x的绝对值。我们来分析,如果是正数或者0的话,其16进制表示必将小于0x8000 0000,这时直接返回x就可以。比如x=0x1234 5678,那就可以只需返回0x1234 5678而如果是负数的话,比如-2,其16进制表示形式即为0xFFFF FFFE,这时我们该怎么做,才能够让0xFFFF FFFE变为0x0000 0002呢?发现0xFFFF FFFE各位取反(~),之后再加1,即可得到0x0000 0002,这时需要返回~x+1(不需要加括号的原因是,按位取反~的优先级大于+,如(图14:C语言中各运算符的优先级)所示)。那么如何将这两种情况结合在一起呢?(毕竟根据要点2,整型数的相关规则规定,不允许使用条件判断)只好通过判断最高位,也就是通过判断符号位,来确定该返回x呢,还是该返回~x+1。可以利用flag=((0x1<<31)&x)>>31来判断符号位,如果x是正数,则0x1<<31=0x8000 0000,(0x1<<31)&x=0x0000 0000,((0x1<<31)&x)>>31=0x0000 0000;如果x是负数,则0x1<<31=0x8000 0000,(0x1<<31)&x=0x8000 0000,((0x1<<31)&x)>>31=0xFFFF FFFF(根据README,已经说明右移为算术右移,即最高位是1,则右移31位后结果为0xFFFF FFFF)。最后返回flag&(~x+1)|~flag&x(这里不需要加括号,因为&的优先级大于|),如果x是正数,flag为0x0,则flag&(~x+1)的结果也为0,~flag为0xFFFF FFFF,~flag&x的结果为x,flag&(~x+1)|~flag&x返回x;如果x是负数,flag为0xFFFF FFFF,flag&(~x+1)的结果为~x+1,~flag为0x0,~flag&x为0,flag&(~x+1)|~flag&x返回~x+1(作者实在想不出更好的解决方法了)。只修改bits.c的第187行,如(图15:第2个函数——修改bits.c中的第187行)所示,保存VS code。接着按照README中的指示,使用命令"./dlc -e bits.c"判断函数absVal是否满足对于运算符等的要求,如(图16:第2个函数——首先检查absVal函数是否满足对于运算符等的要求)所示,发现编译成功,即使有两个警告(这两个警告可以被忽略掉,因为这个警告是害怕我们搞混|与&的优先级)。接着按照README中的指示,先后使用"make clean"与"make btest"命令(作者也不知道这两个命令是做什么用的,等到后面有时间了,可以再慢慢研究),再使用命令"./btest -f absVal"检查absVal函数能否产生正确的结果。如(图17:第2个函数——接着检查absVal函数能否输出正确的结果)所示,成功。
**************************************************************************************************************
(图14:C语言中各运算符的优先级)
**************************************************************************************************************
**************************************************************************************************************
(图15:第2个函数——修改bits.c中的第187行)
**************************************************************************************************************
**************************************************************************************************************
(图16:第2个函数——首先检查absVal函数是否满足对于运算符等的要求)
**************************************************************************************************************
**************************************************************************************************************
(图17:第2个函数——接着检查absVal函数能否输出正确的结果)
**************************************************************************************************************
接着第3个函数,int bitAnd(int x, int y),只使用~与|两个运算符,实现x&y。从最简单的方法开始,0&0=0, 0&1=0, 1&0=0, 1&1=1,如何只使用~与|来实现x&y呢?很容易发现,0|0=0, 0|1=1, 1|0=1, 1|1=1,把这四个新的式子调换顺序,即1|1=1, 1|0=1, 0|1=1, 0|0=0,即(~0)|(~0)=1, (~0)|(~1)=1, (~1)|(~0)=1, (~1)|(~1)=0, 进而~((~0)|(~0))=0, ~((~0)|(~1))=0, ~((~1)|(~0))=0, ~((~1)|(~1))=1。所以我们猜测,x&y=~(~x|~y)。使用Python验证想法。如(图18:第3个函数——用Python验证想法)所示,我们的想法是正确的。重复与第1、2个函数相同的做法,如(图19:第3个函数) 所示,第3个函数成功。
**************************************************************************************************************
(图18:第3个函数——用Python验证想法)
**************************************************************************************************************
**************************************************************************************************************
(图19:第3个函数)
**************************************************************************************************************