CSAPP Data Lab

字数 5.3k
阅读
访客
时间轴

时间轴

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”:

xy=¬(¬x¬y)¬(xy)x \oplus y = \lnot (\lnot x \land \lnot y) \land \lnot ( x \land y)

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)与需要的正好相反,再加上一个!运算即可,即:
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相同。即:
  • 但是由于实验要求的限制条件,不能直接写0xAAAAAAAA这种,只能写0xAA,通过位运算得到0xAAAAAAAA

注意:根据C规范移位运算优先级低于加减

C语言运算符优先级表
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的负数
  • 我们知道:
  • 两式子相减得到:
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 不一致,处理起来不方便。因此把左端的不等式收紧为严格小于,把<=变为<

上式等价于:

这样左端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)) >> 310xFFFFFFFF,若0x2F - x >= 0,则结果为 0:

再看x <= 0x39,同样的方法,若0x39 - x < 0,则(0x39 + (~x + 1)) >> 310xFFFFFFFF,若0x39 - x >= 0,则结果为 0;我们取个反就可以满足要求:

需要同时满足上述两个条件,故

由于最后返回1或0,我们取最低位即可:

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同时关联起来,所以我们把希望留给加法运算,假设返回这样的式子:

当x!=0时,让a=-z=z+1;当x==0时,让a=-y=y+1就可以满足要求,但是这个要求同样难以满足,因为a既与y有关又与z有关,而a又由x是否为0来决定,因此我们考虑将a拆分:

当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,显然与运算可以满足:

但是当!x!=0时,!x1,而它与z+1按位与只会留下最低位不一定是z+1,因此我们需要让x!=0时得到0xFFFFFFFF,x0时得到0,因此我们定义a:

当x为0时,!x==1,(!x)==0xFFFFFFFE,a==0xFFFFFFFF;当x为非零时,!x==0,(!x)0xFFFFFFFF,a0,因此b的表达式应为

再关注c,c的情况刚好与b相反,那么只需要给a再加一个~即可

这里不用担心加法溢出问题

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:
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等于非零时返回非零,但是这样等于没做

我们可以利用另一点,非零数的负数和原来这个数的符号位肯定相反,一个必是1,一个必是0,而0的负数的符号位和0的符号位都是0,因此我们只需要将两者相与就能得到:非零时该表达式符号位为 1,为 0 时该表达式符号位为 0

接下来就是提取符号位了,顺便取个反,因为我们要求为0时返回1,为1时返回0,这个技巧我们在上面的题目用到过:即将其向右算数移位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
  1. 高 16 位:检查高 16 位是否有 1,如果有,则至少需要 16 位。
  2. 高 8 位:在剩下的 16 位中,检查高 8 位是否有 1。
  3. 高 4 位:在剩下的 8 位中,检查高 4 位是否有 1。
  4. 高 2 位:在剩下的 4 位中,检查高 2 位是否有 1。
  5. 高 1 位:在剩下的 2 位中,检查高 1 位是否有 1。
  6. 最低位:最后剩下的 1 位。最终,将所有部分的位数相加,并加 1(符号位)。
  • 对负数的处理:
    flag 是 x >> 31,即符号位。如果 x 是负数,flag 为 1;否则为 0。
  1. 如果 x 是负数(flag == 1),则 x 被取反:x = ~x。
  2. 如果 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的表示形式
IEEE 754的表示形式

单精度float 双精度double
单精度float 双精度double

对于IEEE 754表示的浮点数,一共有三种类别

类别1:规格化数
类别1:规格化数

非规格化数用于表示接近零的极小数值,防止浮点下溢(Underflow),保证浮点运算的连续性。

类别2:非规格化数
类别2:非规格化数

类别3:特殊值
类别3:特殊值

IEEE 754 表示范围
IEEE 754 表示范围

首先我们先提取符号位sign,尾数frac,阶码exp:

根据阶码判断这个浮点数是规格化数,非规格化数还是特殊情况中的哪一种

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位

V=(1)sign×2exp127×(1+frac)V = (-1)^{sign} \times 2^{exp - 127} \times (1 + frac)

我们可以看成 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,

V=(1)S×2E×M=(1)sign×2expbias×(1+frac)V = (-1)^S \times 2^E \times M = (-1)^{sign} \times 2^{exp - bias} \times (1 + frac)

exp 范围为 1~254,E=Exp-Bias=Exp-127,E 的范围为 -126≤E≤127;而对于 frac,最小值为 0,最大值为 1-2^{-23},因此,不考虑 S 的情况下:

Vmax=(1)S×2127×(2223)=(1)S×2127×(2223)V_{max} = (-1)^S \times 2^{127} \times (2-2^{-23}) = (-1)^S \times 2^{127} \times (2-2^{-23})

Vmin=(1)S×2126×(1+0)=(1)S×2126V_{min} = (-1)^S \times 2^{-126} \times (1+0) = (-1)^S \times 2^{-126}

  • 非规格化数:阶码全为0

V=(1)S×2E×M=(1)sign×21Bias×(0+frac)V = (-1)^S \times 2^E \times M = (-1)^{sign} \times 2^{1-Bias} \times (0 + frac)

阶码exp全为0,E=1-Bias=1-127=-126,因此:

V=(1)sign×2126×(0+frac)V = (-1)^{sign} \times 2^{- 126} \times (0 + frac)

对于frac来说,最小值为0,但能表示的最小非零是2(-23),0~2(-23)无法表示,最大值为1-2(-23),因此,当frac取0时,值为0,但要计算表示的最小非零时要令frac=2(-23),在不考虑S的情况下

Vmax=(1)S×2126×(0+1223)=(1)S×2126×(1223)V_{max} = (-1)^S \times 2^{-126} \times (0+1-2^{-23}) = (-1)^S \times 2^{-126} \times (1-2^{-23})

Vmin=(1)S×2126×(0+223)=(1)S×2149V_{min} = (-1)^S \times 2^{-126} \times (0+2^{-23}) = (-1)^S \times 2^{-149}

  • 特殊值
  1. +infinity:exp所有位全为1,frac为0,S为0
  2. -infinity:exp所有位全为1,frac为0,S为1
  3. 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;  }}
评论加载中…