CSAPP-Lab1-DataLab

位运算实战:实现各种逻辑和算术操作
本文详细介绍了如何使用位运算符(如异或、与、或、非等)来实现特定的计算任务,包括求补码表示的最小负数、判断是否为INT_MAX、检测所有奇数位是否为1、求相反数、判断ASCII数字、实现条件选择、比较运算以及浮点数的位操作。每个任务都给出了逻辑分析和位运算的解决方案,并提供了相应的测试验证。

实验主页

进行实验前,请阅读Writeup

实验目录下的文件有:

daniel@u22:~/csapp/labs/datalab-handout$ tree
.
├── bits.c		# 需要实现的文件,且只需要实现这个文件
├── bits.h
├── btest		# 测试文件:每次编辑bits.c后都要make再运行它进行功能测试
├── btest.c
├── btest.h
├── decl.c
├── dlc			# data lab checker,检查是否满足限制要求
├── Driverhdrs.pm
├── Driverlib.pm
├── driver.pl
├── fshow		# 查看浮点数的二进制表示
├── fshow.c
├── ishow		# 查看整数的二进制表示
├── ishow.c
├── Makefile
├── README
└── tests.c

0 directories, 17 files

bitXor

使用 ~& 实现异或运算

列出异或运算的真值表:

aba ^ b
000
011
101
110

得出使用与或非表达的逻辑表达式为(类似于析取范式的形式):

Y = (~a & b ) | (a & ~b)

由于题目限制,我们进一步使用DeMorgen律消去“或”联结词:

Y = ~( ~ ( ~a & b) & ~ (a & ~b) )

所以本题的答案为:

int bitXor(int x, int y) {
  return ~(~(~x & y) & ~(x & ~y));
}
$ ./dlc bits.c		# 检查是否满足限制要求

$ make

$ ./btest -f bitXor		# 测试功能是否正确
Score   Rating  Errors  Function
 1      1       0       bitXor
Total points: 1/1		# 通过了测试

tmin

求补码表示的最小负数,当然是 0x8000_0000

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

isTmax

使用 ! ~ & ^ | + 这些运算符实现判断一个数是否为INT_MAX。若是返回1,否则返回0

由于 INT_MAX = 0X7fff_ffff,将其加1后得到 0x8000_0000,然后取反得到 0x7fff_ffff

所以一种native的想法是将其加一取反后再与原数据做异或,如果得到0,就说明它是INT_MAX

但测试结果表明这是错误的,因为 0xffff_ffff 经过上述运算后也能得到0,如何绕开这个bug呢?我们可以再加上一个限制条件:加一后结果不为0

也就是下面的代码所示:

int isTmax(int x) {
  int a = x + 1;
  int b = ~a;
  return (!(b ^ x) & a);	//加一取反异或==0 && 加一!=0
}

但是上面的代码还是不能通过测试,问题很明显:当我们用“位与”代替了“逻辑与”,试想输入INT_MAX时,!(b ^ a) = 0x0000_0001 的结果为1,而 a = 0x1000_0000,二者相与还是0,结果错误

如何改进呢——使用DeMorgen律,把“位与”换成“位或”,因为“位或”和“逻辑或”的语义更加相近。于是我们得到了下面的正确答案:

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

检查编译测试一条龙:

$ ./dlc -e bits.c && make && ./btest -f isTmax

allOddBits

检测是否所有奇数位都为1

思路很简单,让x与 0xaaaa_aaaa 进行“位与”运算,则当且仅当“位与”的结果还是 0xaaaa_aaaa 时,满足检测条件:

int allOddBits(int x) {
  int mask, a;
  mask = 0xaa;
  mask <<= 8, mask |= 0xaa;
  mask <<= 8, mask |= 0xaa;
  mask <<= 8, mask |= 0xaa;

  a = x & mask;
  return !(a ^ mask);
}

注意,在题目不允许使用 == 进行判等的情况下,要用异或


negate

求相反数。牢记求补规则:所有位取反,末尾加1

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

isAsciiDigit

判断x是否是一个ascii数字,即是否在0x30到0x39之间

判定过程分为2步:

首先判断x的最低字节的高4位是否=3:

  • 让x位与 0xffff_fff0,判定其结果是否为 0x0000_0030(用异或判等)

然后判断x的最低字节的低4位是否在0和9之间,这相当于在实现一个“比较器”,判断低4位是否<9。我们需要分析一下大于9的数字的位模式:

数值二进制
A1010
B1011
C1100
D1101
E1110
F1111

我们将低四位的二进制编码表示为 b 3 b 2 b 1 b 0 b_3b_2b_1b_0 b3b2b1b0,则根据位模式,判定一个数字y是否大于9的逻辑表达式为:

Y = b 3 & ( b 2 ∣ b 1 ) Y = b_3 \&(b_2 | b_1) Y=b3&(b2b1)

受限于允许使用的操作符,我们可以通过如下两步进行判断:

  • 让x位与 0xffff_fffa,若得到的结果是 0x0000_003a,则说明 b 3 = = 1 & & b 1 = = 1 b_3 == 1 \&\& b_1 == 1 b3==1&&b1==1,这是x>0x39的一个充分条件
  • 让x位与 0xffff_fffc,若得到的结果是 0x0000_003c,则说明 b 3 = = 1 & & b 2 = = 1 b_3 == 1 \&\& b_2 == 1 b3==1&&b2==1,这又是x>0x39的一个充分条件

上面两个充分条件“或”在一起,构成了x>0x39的充要条件

int isAsciiDigit(int x) {
  int x1, x2, x3, mask;
  x1 = x2 = x3 = x;
  mask = 1 << 31, mask >>= 27;  //mask = 0xffff_fff0

  x1 &= mask;
  x1 ^= 0x30;

  mask |= 0x0a;   //mask = 0xffff_fffa
  x2 &= mask;
  x2 ^= 0x3a;     //if x==0xa or x == 0xb, then x2 == 0

  mask ^= 0x06;   //mask = 0xffff_fffc
  x3 &= mask;
  x3 ^= 0x3c;     //if 0xc <= x <= 0xf, then x3 == 0

  return !(x1 | !x2 | !x3);
}

conditional

要求使用位运算实现 x ? y : z

很自然的想法就是:

  • 当x=0,需要返回z,那么就让z位与 0xffff_ffff;否则,让z位与 0x0000_0000,消灭掉它
  • 当x=1,需要返回y,那么就让y位与 0xffff_ffff;否则,让y位与 0x0000_0000,消灭掉它

把上面的两个表达式“位或”在一起,就能实现选择的目的了:

int conditional(int x, int y, int z) {
  return (z & (((!x) << 31) >> 31)) | (y & (((!(!x))<<31) >> 31));
}

isLessOrEqual

使用位运算实现判断 <= 的逻辑

类似于x86的 jle 条件转移指令,我们可以采用先做差,再判断差值的符号位进行判断。同时要注意将 x == y 的情况单独考虑,并考虑溢出

首先,当 x == y 时,很容易得到判断条件为:

c1 = !(x ^ y)
/*
	if (x == y) c1 = 1;
	else c1 = 0
*/

如果x != y,我们通过做差的方式进行判断 difference_sign = (y - x) & 0x8000_0000

  • 如果 x 与 y 的符号位相同,则减法过程不会产生溢出,判断结果Y与 !difference_sign 保持一致
  • 如果 x 与 y 的符号位不同,则不论减法过程溢出与否,我们只需要简单地考虑x的符号位即可:如果x是正数,那么y一定是负数,结果为true,and vice versa。所以判断结果Y与x的符号位保持一致即可

列出真值表如下:

is_same_signdifference_signx _signy_signY
10xx1
11xx0
0x101
0x010

于是我们得到Y的逻辑表达式:

Y = i s _ s a m e _ s i g n & & ! d i f f e r e n c e _ s i g n ∣ ∣ ! i s _ s a m e _ s i g n & & x _ s i g n Y = is\_same\_sign \&\& !difference\_sign || !is\_same\_sign \&\& x\_sign Y=is_same_sign&&!difference_sign∣∣!is_same_sign&&x_sign

将上面的逻辑用位运算表达即可:

int isLessOrEqual(int x, int y) {
  int nx, is_same_sign, difference_sign, x_sign, mask, c1, c2, c3;
  nx = (~x) + 1;
  mask = (1 << 31);   // mask = 0x8000_0000

  difference_sign = ((y + nx) & mask);  // 0x0000_0000 or 0x8000_0000
  x_sign = (x & mask);  // 0x0000_0000 or 0x8000_0000

  is_same_sign = !((x ^ y) & mask);   // 1 or 0

  c1 = !(x ^ y);    // 1 or 0
  c2 = (is_same_sign) & (!difference_sign); //x and y has the same sign, c2 will be 1 or 0
  c3 = (((!is_same_sign) << 31) & x_sign);    // x and y has different sign, c3 will b 0x8000_0000 or 0x0000_0000

  return !!(c1 | c2 | c3);  //cast to boolean value: 1 or 0
}

logicalNeg

题目要求:使用位运算实现 ! 的逻辑

首先考虑我们如何在不借助 ! 运算符的情况下将任意的非0值映射到1,而将0值还映射到0,也就是实现一个 cast int to boolean 的功能:

我的想法是:若数字x非0,如果能够则将其任意位置的1“扩展”到所有位,再与1做“位与”运算,就能得到1;而0直接与1做“位与”运算就能得到0

那么如何实现这个“扩展”操作呢?——我们能想到,“符号位”是一个不错的“任意位置”,因为在算术右移的过程中,最高位填充1,将一个负数右移31次,就能得到“全1”。至于正数,取反即可

现在我们得到了 cast int to boolean 的功能,这与题目要求的逻辑是反的,所以为了把1变0,把0变1,我们需要将结果与1做异或。于是得到下面的代码:

int logicalNeg(int x) {
  int nx;
  nx = (~x) + 1;
  return (((x >> 31) & 1) | ((nx >> 31) & 1)) ^ 1;
}

howManyBits

求使用二进制补码表示一个数字所需要的最小比特数

这道题我没做出来,参考了这里的视频讲解,思路非常精妙。这道题是我觉得最难的一道题:

int howManyBits(int x) {
  int mask, bit_16, bit_8, bit_4, bit_2, bit_1, bit_0;
  mask = x >> 31;
  x = (~mask & x) | (mask & ~x);  //正数不变,负数取反

  bit_16 = (!!(x >> 16)) << 4;
  x >>= bit_16;

  bit_8 = (!!(x >> 8)) << 3;
  x >>= bit_8;

  bit_4 = (!!(x >> 4)) << 2;
  x >>= bit_4;

  bit_2 = (!!(x >> 2)) << 1;
  x >>= bit_2;

  bit_1 = (!!(x >> 1));
  x >>= bit_1;

  bit_0 = x;

  return (!x) | (bit_16 + bit_8 + bit_4 + bit_2 + bit_1 + bit_0 + 1);
}

floatScale2

使用位运算求浮点数 f 的2倍,当参数位NaN时,直接返回参数

首先将阶码部分取出:

exp = uf & 0x7f800000;
  • 当阶码为255,说明这是NaN或无穷大,直接返回参数即可
  • 否则,当阶码不为0,说明这是一个规格化数,我们只需将阶码+1,然后将其放回原位置
  • 否则,阶码为0,说明这是一个非规格化数,则只需将尾数左移1位就能实现乘以2,但是要注意处理符号位
unsigned floatScale2(unsigned uf) {
  int exp;
  exp = uf & 0x7f800000;
  if (exp == 0x7f800000) {    // Not a Number
    return uf;
  } else if (exp != 0){     // Normalized value
    exp = exp >> 23;
    exp += 1;
    exp = exp << 23;
    uf = (uf & 0x807fffff) | exp;
    return uf;
  } else {    //DeNormalized value
    int sign = uf & 0x80000000;
    return (uf << 1) | sign;
  }
}

floatFloat2Int

使用位运算实现“将一个浮点数转化为整数”,对于超过int表示范围的数字,返回 0x80000000u

首先将阶码部分和符号位取出:

exp = uf & 0x7f800000;
sign = uf & 0x80000000;
  • 若阶码为255,说明这是NaN或无穷大,直接返回 0x80000000u
  • 否则,若阶码为0,说明这是一个非规格化的值,根据其定义,这一定是小于1的数(在0附近),取整必为0,直接返回0即可
  • 否则,阶码在1~254之间,这是一个规格化的值:
    • 首先取出尾数frac,并将阶码转化到真正的指数值
    • 若指数值 < 0,说明这个值在(-1, 1),返回0即可
    • 否则,指数值 > 0,这个值比1大:
      • 如果指数值 < = 23,小数点右移的次数不超过23,需要舍弃掉frac剩下的部分,则我们需要将frac右移 23 - exp
      • 否则,如果指数值 <= 31,小数点右移的次数超过了23,需要在frac右边补0,则我们需要将frac左移 exp - 23
      • 否则,结果超出了int的表示范围(高位溢出),返回 0x80000000u
    • 返回之前注意符号位
int floatFloat2Int(unsigned uf) {
  int exp, frac, sign, ret;
  exp = uf & 0x7f800000;
  sign = uf & 0x80000000;

  if (exp == 0x7f800000) {  //NaN and infinity
    return 0x80000000u;
  } else if (exp == 0) {  //0
    return 0;
  } else {
    exp >>= 23;
    exp -= 127;
    frac = (uf & 0x007fffff) | 0x00800000;
    if (exp >= 0) {
      if (exp <= 23) {
        ret = frac >> (23 - exp);
      } else if (exp <=31){
        ret = frac << (exp - 23);
      } else {
        return 0x80000000u;
      }
      return sign ? (~ret) + 1 : ret;
    } else {
      return 0;
    }
  }
}

floatPower2

通过位运算实现 2.0^x 的功能,其中x是一个整数。如果结果上溢,返回 +INF;如果结果下溢,返回0

首先考虑 x >= 0,这时很简单,因为尾数一定是0:

  • 如果x不超过127,则说明结果不会上溢,直接将x + 127变为阶码后,左移23位即可
  • 否则,结果上溢,返回 0x7f800000

接下来考虑 x < 0,这时需要考虑规格化和非规格化的情况:

  • 如果 x >= -126,这时为规格化的情况,还是将x + 127变为阶码后,左移23位即可
  • 否则,这时来到了非规格化的情况,我们需要通过移动尾数中的1来实现pow功能:如果 x >= -149,说明此时还能用尾数表达结果(尾数中的1还没有被右移出去),则我们需要返回尾数右移之后的值
  • 否则,产生了下溢(阶码全0,尾数中的1被右移出去了),返回0
unsigned floatPower2(int x) {
  int frac;
  if (x >= 0) {
    if (x <= 127) {
      return (x + 127) << 23;
    } else {
      return 0x7f800000;  //+INF
    }
  } else {
    if (x >= -126) {
      return (x + 127) << 23;
    } else if (x >= -149){
      frac = 0x00400000;
      return frac >> (-126 - x);
    } else {
      return 0;
    }
  }
}

我们的代码拿到了全部的分数:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值