CSAPP 是 CMU 享誉全球的课程,尤其以其 lab 难度之高著称。这是课程的第一个 lab:datalab,为了让我们熟练掌握计算机中的数据存储而设计。
目录
前言:什么是 cheating
之前在 B 站上 CSAPP 第一节课,教授就说了在 CMU,作弊的定义是什么,说实话有点 shocked:
CMU CSAPP 的规定是,完成 lab 的时候不可以上网查资料,只要上网搜索了,不管有没有找结果,搜索的这个过程就算作 cheating(这只是课程实验!)。
一个学期有 25 人因为 cheating 受到惩罚,而且后果十分严重。看看我们的 HNU,至今没听说过有谁因为作弊而受到处分……
按照这个要求,lab 做得真的非常痛苦。有些题目要想很久还想不出来。而且说实话还是有一两个题目不会做,去网上看了别人的 solution。但是这样做完的 datalab,相比直接上网一通搜索抄来的代码,确实更加有成就感,也对自己的「二进制思维」能力有更大的锻炼。这也正是这几个 lab 的意义所在吧。
所以,如果你在做 datalab 而上网搜索看到了这里,在继续看下去之前,不妨再继续独立思考一下吧。
本实验的一些坑
dlc
这个语法检查器按照 C89/C90(ANSI C)的标准,这个标准规定所有局部变量都要在代码块的开头定义。所以如果在函数中途定义一个局部变量,虽然用make
可以正常编译(现在的编译器很少还用 ANSI C 标准),但是用dlc
检查则会提示parse error
的错误。- 实验环境必须是旧版本的 Linux 环境(如 Ubuntu 12.04 LTS)。使用较新的发行版可能会导致无法通过 fitsBits 这一关。(目前暂不清楚是编译器还是内核等的问题)
用 docker 创建运行环境
在 x86 的主机上使用 docker 创建 Ubuntu 12.04 LTS 的容器,指定将容器内 /home
目录映射到容器外的目录,然后进入容器,安装相关软件:
# 将容器内 /home 映射到容器外 /root/Lab/HNU-CSAPP/ex2/docker-env/home,这两个目录也可以自己指定
docker run -itd --name datalab -v /root/Lab/HNU-CSAPP/ex2/docker-env/home:/home ubuntu:12.04
# 进入容器内的 shell
docker exec -it datalab /bin/bash
# 12.04 年代久远,需要将 sources 里 archive.ubuntu.com 改为 old-reloease.ubuntu.com 才能 apt-get update
sed -i -e 's/archive.ubuntu.com\|security.ubuntu.com/old-releases.ubuntu.com/g' /etc/apt/sources.list
# 获取软件包列表
apt-get update
# 安装本实验必要的软件包
apt-get install gcc make gcc-multilib
回到容器外,将文件移进指定的目录(/root/Lab/HNU-CSAPP/ex2/docker-env/home
),编辑好后再进入容器测试。
可以用 tmux 半屏开 vim 写代码,半屏进容器终端测试。
注意如果在容器外测试过了,进入容器要 make clean
再重新 make
,因为不同环境编译生成的二进制可执行文件可能不兼容。
bitAnd
用或和非实现与,用直接取反的方法即可:x and y = not ((not x) or (not y))。
getByte
很显然答案是 (x >> n*8) & 0xFF
。但是不允许使用乘法,如何获得 n*8
呢?
答案是直接用加法。8 个 n 相加会超过 ops 数量限制,我们可以先加出 2n、4n。
logicalShift
要求实现 logical shift,因为默认的 shift 是 arithmetic shift。本质的区别在于负数的 shr,前者左边出来的是 0 而右者出来的是 1。
很容易想到可以 and 上一个 keeper,这个 keeper 的值是 0000111...11
,右边的 1 就是要保留的那些位。这样可以去掉左边产生的 1。
如何得到这个 keeper?显然 keeper = 0x7FFFFFFF
>> (n-1)。
有两个问题。首先是如何产生 0x7FFFFFFF
这个数字呢?因为游戏规则规定立即数赋值不能超过 2 Byte。答案是 ~(1 << 31)
。
接下来是如何处理 n = 0 的情况。好在这关允许我们用 !
运算符,可以实现类似选择赋值的语句。
具体来说,我们需要这样一个 flag 变量,当 n 为 0 时它是 0x00000000
,当 n 非 0 时它是 0xFFFFFFFF
。
试试用 !
运算符,直接对 n 取逻辑非 notn,这样 n 为 0 时我们得到 1,n 非 0 时我们得到 0。
很显然,flag = notn - 1。
现在我们分别计算出 n 为 0 和不为 0 的两种情况 result1 和 result2,则最后只需要返回 ((!flag) & result1) | (flag & result2)
。是不是有点像数电里的「多路复用」呢。
这种对 n = 0 特判的方法,在后面的关卡中会多次遇到。
最后,这关不能用 -
减法,如何实现 -1 呢?直接用加上 0xFFFFFFFF
就行啦。
bitCount
(这题有点难想 😨)
要求统计二进制中 1 的个数(就是 popcount)。其实对于每一位是 0 是 1 我们都可以通过 and 得知,最难的就是如何累加。
累加的部分可以考虑分治,要累加 32 位的结果,我们假设得到了两个 16 位的结果,只考虑这两个如何相加;要计算 16 位的结果只考虑两个 8 位的结果如何相加,以此类推。
那么两个 16 位的结果如何相加呢?我们假设两个结果分别存储在 32 位 int 的高 16 位和低 16 位,只要把两个部分取出来,前者右移 16 位相加即可。下面的也类似。
除此之外,题目要求使用的立即数大小不超过 0xFF,需要用一些 tricky 的手段获得我们要的立即数,以免超出符号数量限制。可以参阅代码。
bang
要求计算逻辑反,也就是我们要返回这样一个值,当 x 为全 0 时其为 1,x 任何一位为 1 时其为 0。
理所当然地可以想到应该把 x 每一位 or 起来(或者把 ~x 每一位 and 起来),结果放在最低位,得到 1 或 0。
然而如果真的取出 32 位每一位进行 or,ops 会超过限制。可以用分治的方法,先将 [0,15] 和 [16,31] 这两段按位处理放入低的一段,再处理 [0,7] 和 [8,15],以此类推,可以在 ops 数量限制内完成。
tmin
返回最小的 int,就是 1 << 31。
fitsBits
第一种方法是一开始想的奇怪方法,至少需要用到 19 个 ops,超过题目限制,其实不能通过。
要我们返回 0 或 1 表示 x 是否能被 n 位补码表示,实际上就是返回是否 − 2 n − 1 ≤ x ≤ 2 n − 1 − 1 -2^{n-1} \le x \le 2^{n-1}-1 −2n−1≤x≤2n−1−1。
也就是 − 2 n − 1 ≤ x < 0 -2^{n-1} \le x \lt 0 −2n−1≤x<0 或 0 ≤ x ≤ 2 n − 1 − 1 0 \le x \le 2^{n-1}-1 0≤x≤2n−1−1。
再变换一下,就是: 0 ≤ x + 2 n − 1 < 2 n − 1 0 \le x+2^{n-1} < 2^{n-1} 0≤x+2n−1<2n−1 或 − 2 n − 1 ≤ x − 2 n − 1 ≤ − 1 -2^{n-1} \le x-2^{n-1} \le -1 −2n−1≤x−2n−1≤−1。
也就是如果 x < 0 x < 0 x<0 则要满足 x + 2 n − 1 ≥ 0 x+2^{n-1} \ge 0 x