时间轴
时间轴
2025-03-07
写到howManyBits
2025-03-07
写完了,再一次感觉这个实验不错
本文介绍了CSAPP Data Lab实验的解题思路与实现过程。作者在Win10 WSL2环境下搭建实验环境,并逐一完成了bits.c中的函数实现。文章详细讲解了bitXor、tmin、isTmax、allOddBits、negate、isAsciiDigit、conditional、isLessOrEqual等题目的位运算解法,包括利用离散数学等值演算实现异或、补码特性求最小整数、通过异或判断最大值、构造掩码检测奇数位、利用算术右移提取符号位判断数值范围、以及通过条件表达式与加法运算实现三目运算符等技巧。同时,文章强调了实验中对操作符和常量的限制,并给出了避免边界歧义的处理方法。
最近担任计算机系统助教要验收这个CSAPP的实验,这个实验本科的时候就做过,但过了这么久基本忘光了,遂重新做一遍记录于此。
实验环境搭建
我是在 Win10 WSL2 环境下做的:

具体如何搭建环境参考下面这个链接,有脚本一键搭建运行环境,非常方便:
简介
在bits.c中完成函数实现,但对代码有一定的限制
123456 | # 检测代码是否符合要求的规范./dlc bits.c# 查看分数make clean make ./btest |
实验内容
bitXor
题目:
12345678910 | /* * bitXor - x^y using only ~ and & * Example: bitXor(4, 5) = 1 * Legal ops: ~ & * Max ops: 14 * Rating: 1 */int bitXor(int x, int y) { return 2;} |
要求只用~(按位取反)和&(按位与)实现异或,可以使用离散数学中的等值演算,异或即“相同为0,不同为1”:
123 | int bitXor(int x, int y) { return ( ~(~x & ~y)) & (~(x & y));} |
tmin
题目:
123456789 | /* * tmin - return minimum two's complement integer * Legal ops: ! ~ & ^ | + << >> * Max ops: 4 * Rating: 1 */int tmin(void) { return 2;} |
返回最小的二进制补码整数,关于原码,反码,补码定义如下:
| 表示方法 | 定义 |
|---|---|
| 原码 | 最高位为符号位(0 表示正数,1 表示负数),其余位表示数值的绝对值 |
| 反码 | 正数的反码与原码相同;负数的反码是符号位不变,其余位按位取反 |
| 补码 | 正数的补码与原码相同;负数的补码是其反码加 1 |
补码的编码具有不对称性,除了最小的负数,其他负数都有其对应的正数。本实验中 int 为 32 位,补码表示范围是:
- 最小负数:0x80000000(即 -2^31)
- 最大正数:0x7FFFFFFF(即 2^31 - 1)
123 | int tmin(void) { return 1 << 31;} |
isTmax
题目:
12345678910 | /* * isTmax - returns 1 if x is the maximum, two's complement number, * and 0 otherwise * Legal ops: ! ~ & ^ | + * Max ops: 10 * Rating: 1 */int isTmax(int x) { return 2;} |
- 如果 x 是二进制补码最大值则返回 1,否则返回 0。
- 最大值是0x7FFFFFFF,它的特点是最高位为0其余为1,那么0x7FFFFFFF+0x1 后变为 0x80000000,最高位为1其余为0,其特点正好相反。
- 这里需要用到异或的特性,如xtarget,x取反后与target异或,由于异或是不同为1相同为0,则只有当x与target完全相同时,xtarget才会为0x0,那么加上!操作符就能得到!(x^target)即 x 与 target 相同时为 1,不同时为 0。对于0x7FFFFFFF,它加1后为0x80000000,由于特点正好相反,取反后又变为 0x7FFFFFFF,故可以利用异或判断是否相同,即:!(~(x+1)^x)。
- 继续思考,是不是只有0x7FFFFFFF才满足这样的特性:+1后相当于按位取反?显然0xFFFFFFFF也满足这个特性,这是因为溢出时最高位被丢弃变为0x0。所以我们还需要构造一个表达式与!(~(x+1)^x) 相与记为expression1,即!(~(x+1)^x) & expression1。当x为0x7FFFFFFF时expression1为1,当x为0xFFFFFFFF时为0。
- 排除的方法必定需要利用0x7FFFFFFF有而0xFFFFFFFF没有的特点0,即这两个数的区别,而最简单的区别就是 0x7FFFFFFF+0x1 后变为 0x80000000 是个非零数,而0xFFFFFFFF+0x1后变为0。故!(x+1)在x为0x7FFFFFFF时为0,在x为0xFFFFFFFF时为1,!(x+1)与需要的正好相反,再加上一个!运算即可,即:
!( ~(x + 1) ^ x) & !!(x + 1)
123 | int isTmax(int x) { return !( ~(x + 1) ^ x) & !!(x + 1);} |
allOddBits
题目:
1234567891011 | /* * 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: ! ~ & ^ | + << >> * Max ops: 12 * Rating: 2 */int allOddBits(int x) { return 2;} |
- 如果所有奇数位的比特位是1返回1,否则返回0,即一个数只要其 1、3、5、7… 位为1(其余位不用管)返回1,否则返回0。
- 我们可以去除所有的偶数位的影响,看奇数位,比如让x与0xAAAAAAAA相与,则所有的偶数位都为0,再看其是否和 0xAAAAAAAA 相同(利用 isTmax 中阐述的通过异或判断两个数是否相同)。也可以让x与0x55555555相或,则所有的偶数位为1,再看其是否和0xFFFFFFFF相同。即:
!((x|0x55555555)^0xFFFFFFFF)
!((x&0xAAAAAAAA)^(0xAAAAAAAA))
- 但是由于实验要求的限制条件,不能直接写0xAAAAAAAA这种,只能写0xAA,通过位运算得到0xAAAAAAAA
注意:根据C规范移位运算优先级低于加减

12345678 | int allOddBits(int x) { // return !((x|0x55555555)^0xFFFFFFFF); // return !((x&0xAAAAAAAA)^(0xAAAAAAAA)); int a = 0xAA<<8;//0x00AA int b = a | 0xAA;//0xAAAA int c = b<<16 | b;//0xAAAAAAAA return !((x&c)^c);} |
negate
题目:
12345678910 | /* * negate - return -x * Example: negate(1) = -1. * Legal ops: ! ~ & ^ | + << >> * Max ops: 5 * Rating: 2 */int negate(int x) { return 2;} |
- 返回x的负数
- 我们知道:
x+(-x)=0
x+(~x)=-1
- 两式子相减得到:
-x=~x+1
123 | int negate(int x) { return ~x+1;} |
isAsciiDigit
题目:
123456789101112 | /* * 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 * Rating: 3 */int isAsciiDigit(int x) { return 2;} |
- 检测x是否属于ascii的数字,简单来说是判断x是否在0x30和0x39之间。
- 在汇编中判断大小是通过减法置标志位来完成的,因此我们可以通过让边界值减去 x 再取符号位来判断。判断
0x30 <= x <= 0x39时,若直接计算0x30 - x,当 x 恰好等于 0x30 时结果为 0,符号位为 0,与 x > 0x30 时的符号位 1 不一致,处理起来不方便。因此把左端的不等式收紧为严格小于,把<=变为<:
0x30<= x <= 0x39
上式等价于:
0x2F< x <= 0x39
这样左端0x2F - x < 0时符号位恒为 1(无边界歧义)。右端x <= 0x39不需要等价转换,因为边界 x=0x39 时0x39 - x = 0符号位为 0,恰好满足要求,若转换反而会把边界排除。让0x2F - x符号位为 1、0x39 - x符号位为 0,两者必须同时满足。先看0x2F < x,相减后如何提取符号位?可以通过算术右移 31 位,让符号位占满整个 32 位,即若0x2F - x < 0,则(0x2F + (~x + 1)) >> 31为0xFFFFFFFF,若0x2F - x >= 0,则结果为 0:
(0x2F+(~x+1))>>31
再看x <= 0x39,同样的方法,若0x39 - x < 0,则(0x39 + (~x + 1)) >> 31为0xFFFFFFFF,若0x39 - x >= 0,则结果为 0;我们取个反就可以满足要求:
~((0x39+(~x+1))>>31)
需要同时满足上述两个条件,故
(0x2F + (~x + 1)) >> 31 & ~((0x39 + (~x + 1)) >> 31)
由于最后返回1或0,我们取最低位即可:
(0x2F + (~x + 1)) >> 31 & ~((0x39 + (~x + 1)) >> 31) & 0x1
12345 | int isAsciiDigit(int x) { int low = (0x2F + (~x + 1)) >> 31; // 0x2F - x < 0 => x > 0x2F (即 x >= 0x30) int high = ~((0x39 + (~x + 1)) >> 31); // 0x39 - x >= 0 => x <= 0x39 return low & high & 0x1;} |
conditional
题目:
12345678910 | /* * conditional - same as x ? y : z * Example: conditional(2,4,5) = 4 * Legal ops: ! ~ & ^ | + << >> * Max ops: 16 * Rating: 3 */int conditional(int x, int y, int z) { return 2;} |
- conditional的表达式和 x ? y : z 相同
- 要实现这种根据x的值判断返回y还是返回z,仅仅使用位运算非常难,因为虽然我们很容易判断x是否为0,但我们很难将x与yz同时关联起来,所以我们把希望留给加法运算,假设返回这样的式子:
a+y+z
当x!=0时,让a=-z=z+1;当x==0时,让a=-y=y+1就可以满足要求,但是这个要求同样难以满足,因为a既与y有关又与z有关,而a又由x是否为0来决定,因此我们考虑将a拆分:
b+c+y+z
当x为非0时,让b=0,c=-z=z+1;当x为0时,让b=-y=y+1,c=0。这样可以让b只与y有关,c只与z有关。首先我们要判断 x 是否为 0,这个不难,使用 ! 运算即可:当 x 为非零时 !x 为 0,当 x 为 0 时 !x 为 1。我们首先关注 b,!x==0 时,b 要为 0;!x!=0 时,b 要为 ~z+1,显然与运算可以满足:
b = !x&(~y+1)
但是当!x!=0时,!x1,而它与z+1按位与只会留下最低位不一定是z+1,因此我们需要让x!=0时得到0xFFFFFFFF,x0时得到0,因此我们定义a:
a = ~(!x)+1
当x为0时,!x==1,(!x)==0xFFFFFFFE,a==0xFFFFFFFF;当x为非零时,!x==0,(!x)0xFFFFFFFF,a0,因此b的表达式应为
b = a&(~y+1)
再关注c,c的情况刚好与b相反,那么只需要给a再加一个~即可
c = ~a&(~z+1)
这里不用担心加法溢出问题
123456 | int conditional(int x, int y, int z) { int a = ~(!x)+1; int b = a&(~y+1); int c = ~a&(~z+1); return b+y+c+z;} |
isLessOrEqual
题目:
12345678910 | /* * isLessOrEqual - if x <= y then return 1, else return 0 * Example: isLessOrEqual(4,5) = 1. * Legal ops: ! ~ & ^ | + << >> * Max ops: 24 * Rating: 3 */int isLessOrEqual(int x, int y) { return 2;} |
- 如果x<=y那么返回 1,否则返回0
- 比大小利用减法,让y-x如果y-x>=0即符号位为0则返回1,如果y-x<0即符号位为1则返回0,y-x=y+~x+1,然后右移31位后取符号位,我们需要一个取反操作以满足符号位为0返回1,符号位为1则返回0:
~((y+(~x+1))>>31) & 0x1
123 | int isLessOrEqual(int x, int y) { return ~((y+(~x+1))>>31) &0x1;} |
logicalNeg
题目:
1234567891011 | /* * logicalNeg - implement the ! operator, using all of * the legal operators except ! * Examples: logicalNeg(3) = 0, logicalNeg(0) = 1 * Legal ops: ~ & ^ | + << >> * Max ops: 12 * Rating: 4 */int logicalNeg(int x) { return 2;} |
- 实现!运算符,即对于!x,当x为0时返回1,当x为非0时返回0
- 0 和非零最直观的区别就在于 0 的每一位都是 0,而非零至少有一位为1,但因为我们无法利用循环移位操作来判断,故只能另辟蹊径
- 0 和非零数的另一个不同特点在于 0 的负数还是 0,即~0+1=0,而非零数的负数不是0,我们可以利用这点,看x的负数是否与它自己相同,前面我们用到过一个技巧通过异或来判断两数是否相同:这个表达式在x等于0时返回0,x等于非零时返回非零,但是这样等于没做
(~x+1)^x
我们可以利用另一点,非零数的负数和原来这个数的符号位肯定相反,一个必是1,一个必是0,而0的负数的符号位和0的符号位都是0,因此我们只需要将两者相与就能得到:非零时该表达式符号位为 1,为 0 时该表达式符号位为 0
(~x+1)|x
接下来就是提取符号位了,顺便取个反,因为我们要求为0时返回1,为1时返回0,这个技巧我们在上面的题目用到过:即将其向右算数移位31位并取反,然后与0x1相与取最低位:
~(((~x+1)|x)>>31) & 0x1
123 | int logicalNeg(int x) { return ~(((~x+1)|x)>>31) & 0x1;} |
howManyBits
题目:
123456789101112131415 | /* 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 * Rating: 4 */int howManyBits(int x) { return 0;} |
- 返回以二进制补码的形式表示x需要几个比特数,其实就是看这个数的最高位1在第几位,然后加上1位符号位,这个问题有点难度,主要是有点难想到,下面是别人的实现方法。
- 这里用到二分法:先考虑正数和0
- 高 16 位:检查高 16 位是否有 1,如果有,则至少需要 16 位。
- 高 8 位:在剩下的 16 位中,检查高 8 位是否有 1。
- 高 4 位:在剩下的 8 位中,检查高 4 位是否有 1。
- 高 2 位:在剩下的 4 位中,检查高 2 位是否有 1。
- 高 1 位:在剩下的 2 位中,检查高 1 位是否有 1。
- 最低位:最后剩下的 1 位。最终,将所有部分的位数相加,并加 1(符号位)。
- 对负数的处理:
flag 是 x >> 31,即符号位。如果 x 是负数,flag 为 1;否则为 0。
- 如果 x 是负数(flag == 1),则 x 被取反:x = ~x。
- 如果 x 是非负数(flag == 0),则 x 保持不变。负数的补码表示中,符号位和数值部分是混合在一起的。如果直接计算负数的有效位数,符号位会导致结果错误。例如:
-1 的补码是 11111111 11111111 11111111 11111111,如果直接计算位数,会得到 32 位,但实际上我们只关心其有效位数。通过取反操作,负数的补码表示会被转换为正数,其二进制表示中的有效位数与原来的负数相同。例如:
- -1 的补码是 11111111 11111111 11111111 11111111,取反后为 00000000 00000000 00000000 00000000,有效位数为 1。
- -2 的补码是 11111111 11111111 11111111 11111110,取反后为 00000000 00000000 00000000 00000001,有效位数为 2。
1234567891011121314151617 | int howManyBits(int x) { int b16, b8, b4, b2, b1, b0; int flag = x >> 31; x = (flag & ~x) | (~flag & x); // x符号位为0不变 ,x符号位为1按位取反 b16 = !!(x >> 16) << 4; x >>= b16; b8 = !!(x >> 8) << 3; x >>= b8; b4 = !!(x >> 4) << 2; x >>= b4; b2 = !!(x >> 2) << 1; x >>= b2; b1 = !!(x >> 1); x >>= b1; b0 = x; return b0 + b1 + b2 + b4 + b8 + b16 + 1;} |
floatScale2
题目:
1234567891011121314 | /* * 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 * Rating: 4 */unsigned floatScale2(unsigned uf) { return 2;} |
将一个单精度浮点数(float)乘以 2,并返回结果的二进制表示。函数的输入和输出都是 unsigned int 类型,但它们实际上表示的是单精度浮点数的二进制位模式,当输入是NaN时返回这个输入即可,另外,终于可以用if while了😭😭😭
先回顾下IEEE 754浮点数的知识:


对于IEEE 754表示的浮点数,一共有三种类别
非规格化数用于表示接近零的极小数值,防止浮点下溢(Underflow),保证浮点运算的连续性。


首先我们先提取符号位sign,尾数frac,阶码exp:
sign=uf>>31&0x1
frac=uf&0x7FFFFF
exp = (uf&0x7f800000)>>23
根据阶码判断这个浮点数是规格化数,非规格化数还是特殊情况中的哪一种
1234567891011121314 | unsigned floatScale2(unsigned uf) { unsigned exp = (uf&0x7f800000)>>23; unsigned sign=uf>>31&0x1; unsigned frac=uf&0x7FFFFF; if(exp == 0){//非规格化数,阶码为0,直接让frac乘以2即可 frac <<= 1; return (sign << 31) | (exp << 23) | frac; }else if(exp==0xFF){//特殊情况,非数 return uf; }else{ exp++;//乘2,相当于 E+1 return (sign << 31) | (exp << 23) | frac; }} |
floatFloat2Int
题目:
123456789101112131415 | /* * 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 return * 0x80000000u. * Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while * Max ops: 30 * Rating: 4 */int floatFloat2Int(unsigned uf) { return 2;} |
- 用int表示输入的IEEE 754浮点数,如果超过表示范围则返回0x80000000u。
- 如果是非规格化数,直接返回0,如果是特殊情况直接返回0x80000000u
- 如果是规格化数,我们需要先判断是否超出 int 表示范围,int 一共 32 位,一位表示符号位,最大表示 2^31-1,最小表示 -2^31,因此阶码 E 不能超过 30(当 E 为 31 时左移 E 位会覆盖符号位);当阶码 E 小于 0 时,说明是个小于 1 的数,直接返回 0;又:
- sign 1位
- exp 8位
- frac 23位
我们可以看成 1frac(第 24 位为 1,第 1~23 位构成 frac),而此时相当于由 1.frac(整数部分为 1,小数部分为 frac)左移了 23 位,此时阶码为 E,即要乘以 2^E,也就是左移 E 位,所以,当 E 小于 23 时,应该右移(23-E)位以截断 frac 后面的 (23-E) 位;当 E 大于 23 时,由于此时相当于已经左移了 23 位,所以只需再左移 (E-23) 位。
12345678910111213141516171819202122232425262728 | int floatFloat2Int(unsigned uf) { unsigned exp = (uf & 0x7f800000) >> 23; unsigned sign = uf >> 31 & 0x1; unsigned frac = uf & 0x7FFFFF; if (exp == 0xFF) { // 特殊情况 return 0x80000000u; } else if (exp == 0) { // 非规格化数 return 0x0; } else { // 规格化数 int E = exp - 127; // 阶码,注意不要设置为unsigned if (E >= 31) { // 超范围 return 0x80000000u; }else if(E<0){//如果E<0说明是个比1小的数,返回0 return 0; } frac = frac | (1 << 23); // 填上frac的1 if (E < 23) { frac = frac >> (23 - E); } else { frac = frac << (E - 23); } if (sign == 0) {//根据符号位返回正负 return frac; } else { return -frac; } }} |
floatPower2
题目:
12345678910111213141516 | /* * 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 * Rating: 4 */unsigned floatPower2(int x) { return 2;} |
- 计算2.0^x,并返回IEEE 754下的unsigned表示,如果结果太小,小到非规格化数都不能表示则返回0,如果结果太大返回+INF,很明显这题主要考察IEEE 754的表示范围。
对于float单精度
- 符号位sign:1位
- 阶码exp: 8位
- 尾数frac:23位
表示范围
- 规格化数:阶码exp既不全为0,也不全为1,
exp 范围为 1~254,E=Exp-Bias=Exp-127,E 的范围为 -126≤E≤127;而对于 frac,最小值为 0,最大值为 1-2^{-23},因此,不考虑 S 的情况下:
- 非规格化数:阶码全为0
阶码exp全为0,E=1-Bias=1-127=-126,因此:
对于frac来说,最小值为0,但能表示的最小非零是2(-23),0~2(-23)无法表示,最大值为1-2(-23),因此,当frac取0时,值为0,但要计算表示的最小非零时要令frac=2(-23),在不考虑S的情况下
- 特殊值
- +infinity:exp所有位全为1,frac为0,S为0
- -infinity:exp所有位全为1,frac为0,S为1
- NaN:exp所有位全为1,frac不为0
因此,经上述分析:
- 当 x>127 时返回 +INF
- 当-126<=x<=127时为规格化数
- 当 -149 <= x < -126时为非规格化数
- 当 x < -149时,太小了无法表示返回0
1234567891011 | unsigned floatPower2(int x) { if (x > 127) { // 返回+infinity,S=0,exp=0xFF,frac=0; return 0xFF << 23; } else if (-126 <= x && x <= 127) { // 规格化数,exp=E+Bias=E+127 return (x + 127) << 23; } else if (-149 <= x && x < -126) { // 非规格化数,exp=0,E=1-Bias=-126,2^(-126)*frac return 1<<(23-(-x-126));//E已经有了2^(-126),当frac为0x1时表示2^(-23),假设输入x为-127,则frac要为2^(-1),即1向左移动22位 } else {//x< -149,太小了返回0 return 0; }} |