CSAPP 第二章信息表示和处理 课后作业
2.60
假设我们将一个 w 位的字中的字节从 0 (最低位) 到 w/8-1 (最高位) 编号。写出下面 C 函数的代码,它会返回一个无符号值,
其中参数 x 的字节 i 被替换成字节 b:
unsigned replace_byte(unsigned x, int i, unsigned char b);
以下示例,说明了这个函数该如何工作:
replace_byte(0x12345678, 2, 0xAB) --> 0x12AB5678
replace_byte(0x12345678, 0, 0xAB) --> 0x123456AB
#include <stdio.h>
unsigned replace_byte(unsigned x, int i, unsigned char b) {
// Shift Amount i * 8
int shift_amount = i << 3;
return (unsigned)b << shift_amount | ~(0xff << shift_amount) & x;
}
int main() {
unsigned x = 0x12345678;
int i = 2;
unsigned char b = 0xAB;
// Case 1:
printf("replace_byte(0x%X, %d, 0x%X) --> 0x%X\n", x, i, b, replace_byte(x, i, b));
// Case 2:
i = 0;
printf("replace_byte(0x%X, %d, 0x%X) --> 0x%X\n", x, i, b, replace_byte(x, i, b));
return 0;
}
位级整数编码规则:
位级整数编码规则:
- 假设
- 整数用补码的形式表示。
- 有符号数的右移是算术右移。
- 数据类型 int 是 w 位长的。对于某些题目,会给定 w 的值,但是在其他情况下,只要 w 是 8 的整数倍,你的代码就应该能正常工作。你可以用表达式
sizeof(int)<<3
来计算 w。- 禁止使用
- 条件语句( if 或者 ?: )、循环、分支语句、函数调用和宏调用。
- 除法、模运算和乘法。
- 相对比较运算(<、>、<= 和 >=)。
- 允许的运算
- 所有位级和逻辑运算
- 左移和右移,但是位移量只能在 0 和 w-1 之间。
- 加法和减法。
- 相等 (==)和不相等(!=)的测试。(有些题目中,也不允许这样的运算)。
- 整型常数 INT_MAX 和 INT_MIN。
- 对 int 和 unsigned 进行强制类型转换,不论是显式还是隐式的。
即使有这些条件的限制,你仍然可以选择带有描述性的变量名,并且使用注释来描述你的解决方案的逻辑,尽量提高代码的可读性。例如,下面这段代码从整数参数 x 中抽取出最高有效字节:
/* Get most significant byte from x */ int get_msb(int x) { /* Shift by w-8 */ int shift_val = (sizeof(int) - 1) << 3; /* Arithmetic shift */ int xright = x >> shift_val; /* Zero all but LSB */ return xright & 0xFF; }
2.61
写一个 C 表达式,在下列条件下产生1,而其它情况得到 0,假设 x 是 int 类型,代码应该遵循位级整数编码规则,另外还有一个限制,你不能使用相等(==)和不相等(!=)测试。
-
A. x的任何位都等于1
int int_all_ones(int x) { return !(x ^ ~0); }
-
B. x的任何位都等于0
int int_all_zeros(int x) { return !(x ^ 0); }
-
C. x的最低有效字节中的位都等于1
int lsb_all_ones(int x) { return !((x & 0xFF) ^ 0xFF); }
-
D. x的最高有效字节中的位都等于0
int msb_all_zeros(int x) { return !((x >> ((sizeof(int) - 1) << 3) & 0xFF) ^ 0); }
分析:对于判断 x 等于某一个确定的值 val,可以通过生成 val,然后使用异或运算符以及逻辑非运算符进行判断,若 x == val
,则!(x ^ val) == 1
,否则表达式的值为 0 。
2.62
编写一个函数 int_shifts_are_arithmetic(),在对 int 类型的数使用算数右移的机器上运行时,这个函数生成1,而其他情况生成0.你的代码应该可以运行在任何字长的机器上。在几种机器上测试你的代码。
int int_shifts_are_arithmetic() {
/* Use TMin to test whether shift right are arithmetic */
int shift_val = (sizeof(int) << 3) - 1;
/* If 1 << shift_val is not equal to 1 then shifts are arithmetic */
return ((1 << shift_val) >> shift_val) != 1;
}
分析:由于题目限制代码可以运行在任何机器上,因此可以使用 sizeof
运算符,思路是先将 1 左移 w-1 位,然后右移 w-1位,得到的结果 res 与 1 进行不相等测试,如果 res != 1,表达式的值就为 1,表明这台机器使用算数右移。
2.63
将下面的 C 函数代码补充完整。函数 srl 用算术右移(由值 xsra 给出)来完成逻辑右移,后面的其它操作不包括右移或者除法。函数sra用逻辑右移(由值 xsrl 给出)来完成算术右移,后面的其他操作不包括右移或者除法。可以通过计算 8 * sizeof(int) 来计算数据类型 int 中的位数 w。位移量 k 的取值范围是:0 ~ w-1。
unsigned srl(unsigned x, int k) {
/* Perform shift arithmetically */
unsigned xsra = (int)x >> k;
int w = 8 * sizeof(int);
unsigned mask = ((1 << (w - k)) - 1);
/* Get the lower bits */
unsigned xsrl = xsra & mask;
return xsrl;
}
int sra(int x, int k) {
/* Perform shift logically */
int xsrl = (unsigned)x >> k;
int w = 8 * sizeof(int);
/* Get x sign bit, and generate mask */
int flag = !!(x & (1 << (w - 1)));
unsigned mask = ~((flag << (w - k)) - 1);
int xsra = xsrl | mask;
return xsra;
}
分析:使用算术右移实现逻辑右移,关键在于消除算数右移带来的附加作用,即高位比特会填充为符号位,因此只要消除这一影响即可,考虑使用掩码运算,保留低 w-k 位,使用按位与 & 运算,消除高 k 位的影响即可;使用逻辑右移实现算数右移,关键在于如果 x 为负数,则高 w-k 位需要填充 1,因此先判断 x 的符号位是否为 1,如果 x < 0,flag为 1,否则 flag 为 0,然后通过 flag 生成掩码,使用按位或 | 运算,补充算数右移填充符号位的附加作用即可。
2.64
写出代码实现如下函数,函数应该遵循位级编码规则,不过你可以假设数据类型 int 有 w=32 位:
/* Return 1 when odd bit of x equals 1; 0 otherwise
Assume w=32 */
int any_odd_one(unsigned x) {
/* Mask 0xAAAAAAAA */
unsigned mask = (0xAA << 24) | (0xAA << 16) | (0xAA << 8) | (0xAA);
return !((mask & x) ^ mask);
}
分析:首先生成掩码0xAAAAAAAA,然后按位与上 x,消除偶数位的影响,然后使用异或和逻辑非运算符实现相等的判断。
2.65-Check
写出代码实现函数,函数应该遵循位级整数编码规则,不过你可以假设数据类型 int 有 w=32 位,你的代码最多只能包含12个算术运算、位运算和逻辑运算:
/* Return 1 when x contains an odd number of 1s; 0 otherwise.
Assume w=32 */
int odd_ones(unsigned x) {
unsigned left = x >> 16;
x = x ^ left;
left = x >> 8;
x = x ^ left;
left = x >> 4;
x = x ^ left;
left = x >> 2;
x = x ^ left;
left = x >> 1;
x= x ^ left;
return x;
}
分析:这里参考了 gengshuokuo 博主的解法,具体的链接🔗在这:odd_ones solution。
这里我简单地说一下解法,可以异或来解这题,首先将32位的unsigned x分成左右16bit两部分,然后两个部分直接进行异或操作,这时候,由于异或的特性:1 ^ 1= 1
, 0 ^ 0 = 0
,而 0^1=1
、1^0=1
,也就是说将这两部分进行异或之后,如果对应位置出现两个1,那么将会变成0,即消除了偶数个1的影响,如果两个都为0,实际上就是0个1,也是消除偶数个1的影响,而如果两个互异,那么结果为1,也就是奇数个1,保留了奇数的标记,接着只需要判断这个异或后的16 bit结果是否有奇数个1即可,判断的方法还是一样的,先将左右分成8bit,依次类推。
2.66
写出下列代码实现如下函数,函数应该遵循位级整数编码规则,不过你可以假设数据类型 int 有 w=32 位。你的代码最多只能包含15个算术运算、位运算和逻辑运算。提示:先将 x 转换为形如 [0…011…1] 的位向量:
/*
* Generate mask indicating leftmost 1 in x. Assume w=32.
* For example, 0xFF00 -> 0x8000, and 0x6600 -> 0x4000
* If x = 0, then return 0.
*/
int leftmost_one(unsigned x) {
if (x == 0) return 0;
/* todo: Convert x in form of [0..011..1] */
x = x | (x >> 1);
x = x | (x >> 2);
x = x | (x >> 4);
x = x | (x >> 8);
x = x | (x >> 16);
return (x & ((~x >> 1) | 0x80000000))
}
2.67
给你一个任务,编写一个过程 int_size_is_32(),当一个 int 是 32 位的机器上运行时,该程序产生 1,而其他情况产生 0,不允许使用 sizeof 运算符。下面是开始时的尝试:
/* The following code does not run properly on some machines */
int bad_int_size_is32() {
/* Set most significant bit (msb) of 32-bit machine */
int set_msb = 1 << 31;
/* Shift past msb of 32-bit word */
int beyond_msb = 1 << 32;
/* Set_msb is nonzero when word size >= 32
beyond_msb is zero when word size <= 32 */
return set_msb && !beyond_msb;
}
当在SUN SPARC 这样的 32 位机器上编译并运行时,这个过程返回的却是0。下面的编译器信息给我们一个问题的提示:
warning:left shift count >= width of type
-
我们的代码在哪个方面没有遵守 C 语言标准?
位移运算的位移量 k 大于或等于类型的位长 w。
-
修改代码,使得它在 int 至少为 32 位的任何机器上都能正确地运行。
int int_size_is_32() { int set_msb = 1 << 31; int beyond_msb = (1 << 31) << 1; return set_msb && !beyond_msb; }
-
修改代码,使得它在至少为 16 位的任何机器上都能正确地运行。
int int_size_is_32() { int set_msb = ((1 << 15) << 15) << 1; int beyond_msb = ((1 << 15) << 15) << 2; return set_msb && !beyond_msb; }
分析:编译器输出的这条警告信息十分有用,对于位长为 w 的 int 类型变量,如果位移量为 w,那么实际运行过程中,它位移的结果是不会改变的,实际的位移量为:k mod w
,因此对于 32 位机器上对 int 类型进行左移 32位,相当于左移 0位,beyond_msb
始终等于 1,因此结果出错。修改的代码起始很简单,就是确保位移量不超过这个类型的位长即可。
2.68
写出如下原型的函数的代码,函数应该遵循位级整数编码规则。要注意 n=w 的情况:
/*
* Mask with least signficant n bits set to 1
* Examples: n = 6 --> 0x3F, n = 17 --> 0x1FFFF
* Assume 1 <= n <= w
*/
int lower_one_mask(int n) {
return (1 << (n - 1)) - 1 | 1 << (n - 1);
}
分析:当 n < w 时,结果为 (1 << n)-1
,当 n = w时,结果为 ~0
,由于我们位移量 k 不能等于 w,所以先计算位移量为 n-1
的值,然后或上 1 << (n-1)
即为所求,这种计算方式避免了位移量 k = w。
2.69
写出具有如下原型的函数的代码,函数应该遵循位级整数编码规则。要注意 n=0 的情况:
/*
* Do ratating left shift. Assume 0 <= n < w
* Examples when x = 0x12345678 and w = 32:
* n=4 -> 0x23456781, n=20 -> 0x67812345
*/
unsigned rotate_left(unsigned x, int n) {
int w = sizeof(int) << 3;
unsigned nbits = (x >> (w-n-1)) >> 1;
return (x << n) | nbits;
}
分析:基本的做法是先提取高 n 比特,然后将 x 左移 n 位,或上 nbits 即可,但这里当 n=0时,提取的 0 比特,若操作为 x >> (w-n)
,直接提取nbits,这样做是不可行的,因为当 n=0 时,该表达式结果仍为 x,但是只要修改成 (x >> (w-n-1) ) >> 1
,此时,即便 n=0,x的结果也为 0,最终结果也是正确的。
2.70
写出具有如下原型的函数的代码
/*
* Return 1 when x can be represented as an n-bit, 2's complement
* number; 0 otherwise
* Assume 1 <= n <= w
*/
int fits_bits(int x, int n) {
int w = sizeof(int) << 3;
/* Get sign bit */
int sx = x & (1 << (w - 1));
int sbits = sx >> (w - n);
int mask = ~((1 << (n - 1) - 1);
int hbits = x & mask;
return hbits == sbits;
}
分析:这里涉及截断问题,对于给定的 n,如果 x 能被 n 位二进制数表示,只需要满足 x 的高 (w - n + 1)位全为0或全为1,该区域的位与符号位相同。
2.71
你刚刚开始在一家公司工作,它们要实现一组过程来操作一个数据结构,要将4个有符号字节封装成一个32位unsigned。一个字中的字节从0(最低有效字节)编号到3(最高有效字节)。分配给你的任务时:为一个使用补码运算和算术右移的机器编写一个具有如下原型的函数:
/* Declaration of data type where 4 bytes are packed
into an unsigned */
typedef unsigned packed_t;
/* Extract byte from word. Return as signed integer */
int xbyte(packed_t word, int bytenum);
也就是说,函数会抽取出指定的字节,再把它符号扩展为一个32位 int。
你的前任(因为水平不够高而被解雇了)编写了下面的代码:
/* Failed attempt at xbyte */
int xbyte(packed_t word, int bytenum)
{
return (word >> (bytenum << 3) ) & 0xFF;
}
-
这段代码错在哪里?
这段代码没有实现原本函数声明的语义,它要求返回一个有符号的整数,因为
packed_t
类型实际上是一个由4个有符号字节包装而成的32位unsigned类型,这里的代码忽视了当xbyte为负数的情况,当xbyte为负数时,高24位的int应该补1才正确。 -
给出一个函数的正确实现,只能使用左右移位和一个减法。
/* Correct version */ int xbyte(packed_t word, int bytenum) { int shift_val = bytenum << 3; /* unsigned x byte */ int uxbyte = (word >> shift_val); return (uxbyte << 24) >> 24; }
首先获取无符号的uxbyte值,然后通过 int 的算术右移特性将uxbyte左移24位再右移24位即可将高位填充为符号位。
2.72
给你一个任务,写一个函数,将整数 val 复制到缓冲区 buf 中,但是只有当缓冲区中有足够可用的空间时,才进行复制:
你写的代码如下:
/* Copy integer into buffer if space is available */
/* WARNING: The following code is buggy */
void copy_int(int val, void *buf, int maxbytes) {
if (maxbytes-sizeof(val) >= 0)
memcpy(buf, (void *) &val, sizeof(val));
}
这段代码使用了库函数 memcpy。虽然在这里用这个函数有些可以,因为我们想复制一个 int,但是它说明了一种复制较大数据结构的常见方法。
你仔细地测试了这段代码后发现,哪怕 maxbytes 很小的时候,它也能把值复制到缓冲区中。
-
解释为什么代码中的条件测试总是成功。提示:sizeof 运算符返回类型为 size_t 的值。
因为条件表达式
maxbytes-sizeof(val) >= 0
为无符号类型的表达式,因此,当maxbytes
很小的时候,我们预想的表达式结果为0,但是由于这是一个无符号的表达式类型,因此左端会是一个非常大的无符号数,因此,表达式无论如何,都会成立,memcpy始终会被调用。 -
你该如何重写这个条件测试,使之工作正确。
void copy_int(int val, void *buf, int maxbytes) { if (maxbytes >= sizeof(val)) memcpy(buf, (void *)buf, sizeof(val)); }
修改很简单,只需要减法不执行即可,直接使用 >= 测试即可。
2.73
写出具有如下原型的函数的代码:
/* Addition that saturates to TMin or TMax */
int saturating_add(int x, int y);
同正常的补码加法溢出的方式不同,当正溢出时,返回 TMax,负溢出时,返回TMin。饱和运算常常用在执行数字信号处理的程序中。
你的函数应该遵循位级整数编码规则。
分析:根据饱和补码加法的逻辑,我们不难编写出含有条件结构的如下代码:
int saturating_add(int x, int y) {
int w = sizeof(int) << 3;
int xsig = (x >> (w-1)) & 1;
int ysig = (y >> (w-1)) & 1;
int res = x + y;
int rsig = (res >> (w-1)) & 1;
if (xsig == ysig && rsig != xsig) {
if (xsig == 0)
res = INT_MAX;
else
res = INT_MIN;
}
return res;
}
它的语义是和饱和加法一致的,我们可以进一步修改,去掉条件结构,这里介绍一种使用位级运算符的方式,去掉条件结构的方法:
仿照多路复用器Multiplexer的方式,将条件测试表达式转换为掩码mask,然后进行选择即可,比如如果要实现三目运算符,即
int condition(int x, int y, int z) {
return x ? y : z;
}
它等价于以下代码:
int condition(int x, int y, int z) {
int mask = (!x << 31) >> 31;
return (mask & y) | (~mask & z);
}
类似地,对于嵌套的条件结构,我们可以使用多个掩码来实现相同的功能,因此,去掉条件结构,结果如下:
int saturating_add(int x, int y) {
int w = sizeof(int) << 3;
/* Get x sign and y sign bit*/
int xsig = (x >> (w-1)) & 1;
int ysig = (y >> (w-1)) & 1;
/* Compute x + y */
int res = x + y;
/* Get res sign bit */
int rsig = (res >> (w-1)) & 1;
/* Compute mask1 and mask2 */
int mask1 = ((xsig == ysig && rsig != xsig) << (w-1)) >> (w-1);
int mask2 = ((xsig == 0) << (w-1)) >> (w-1);
/* Compute Saturating value */
int saturating_val = (INT_MAX & mask2) | (INT_MIN & ~mask2);
return (mask1 & saturating_val) | (~mask1 & res);
}
2.74
写出具有如下原型的函数代码:
/* Determine whether arguments can be subtracted without overflow */
int tsub_ok(int x, int y);
如果计算 x-y 不溢出,这个函数就返回 1。
int tsub_ok(int x, int y) {
int w = sizeof(int) << 3;
int xsig = (x >> (w-1)) & 1;
int ysig = (y >> (w-1)) & 1;
/* Compute x - y */
int subval = x - y