CSAPP 第二章 信息存储
昨晚加班到2点钟,今天稀里糊涂的,还是做点题目来冷静一下。目前进度到40页,把第二章的第一部分,信息存储看完了。
第一大部分是进制之间的转换,目前主要是十六进制和二进制以及十进制之间的转换。
2.1 完成数字转换
- 0x39A7F8,转换成二进制,就是把每位16进制展开,得到0011 1001 1010 0111 1111 1000
- 1100 1001 0111 1011转换为16进制,就是把每四位转换成16进制,为0xC97B
- 0xD5E4C,转换成二进制是 1101 0101 1110 0100 1100
- 二进制 1001 1011 1001 1110 1101 01,先重新分组,按照4位补齐 0010 0110 1110 0111 1011 0101,在转换成16进制,结果是26E7B5
有一种将2的整数次幂快捷转换成16进制的方式:由于2的n次方直接可以写成1加上n个0,而四个二进制的0对应一位十六进制的0。只要计算出n里边有几个4(n/4)和n的余数(n%4),几个4就是几个16进制的0,如果正好整除,说明是16的倍数,最开头补一个1。而余数的可能值为1,2,3。余数是1的时候就是十进制的2,是2的时候是十进制的4,是3的时候就是十进制的8,直接转换成十六进制写在最前边即可。
举一个例子:2的15次方,15/4 = 3, 15%4 = 3,则 2的15次方就是余数3对应的8加上3个十六进制0,结果是0x8000。
2.2 完成数字转换
n |
2的n次方(十进制) |
2的n次方(十六进制) |
9 |
512 |
0x200 |
2的19次方 = 4*4+3 |
512*512*2 = 524288 |
0x80000 |
2的14次方 = 4*3+2 |
16384 |
0x4000 |
第一位是1,说明能整除。后边4个0,说明一共4*4 = 16 |
2的16次方 = 65536 |
0x10000 |
17 = 4*4+1 |
=16384*8 = 131072 |
0x20000 |
2的5次方,5= 4*1+1 |
32 |
0x20 |
第一位8表示余数3,然后是1位表示4,所以是2的7次方 |
128 |
0x80 |
2.3 转换字节到16进制
十进制 |
二进制 |
十六进制 |
0 |
0000 0000 |
0x00 |
167 |
1010 0111 |
0xA7 |
62 |
0011 1110 |
0x3E |
188 |
1011 1100 |
0xBC |
55 |
0011 0111 |
0x37 |
136 |
1000 1000 |
0x88 |
243 |
1111 0011 |
0xF3 |
82 |
0101 0010 |
0x52 |
172 |
1010 1100 |
0xAC |
231 |
1110 0111 |
0xE7 |
2.4 十六进制加法
- 0x503C+0x8 ,直接相加 C=12+8 = 20,进1,余4,得到 0x5044
- 0x503C-0x40 ,3-4不够减,借16,借完之后前一位变成F,得到0x4FFC
- 0x503C+64 ,64是0x40,得到0x507C
- 0x50EA-0x503C ,可以列竖式来计算,得到0xAE
C语言的长度和大小端表示整数
这里要注意的是指针的长度和位数也就是字长相等,所以32位下long=4,char * 也是4;64位下long = 8,char * 也是8。
其他C语言的数值的位数在32和64位X86上默认没有区别。
使用64位还是32位编译,要使用gcc -m32/-m64 main.c来指定编译。C99还有固定的int32_t和int64_t,固定长度的类型。
大端法和小端法。简单的说,就是计算机如果按照我们阅读的顺序,在内存的从小到大的地址中存放分组后的字节,就是大端法。反着来就是小端法。
INTEL X86使用小端法。除了硬件之外,大小端还和操作系统有关系。
通用的打印内存地址的程序:
typedef unsigned char *byte_pointer;
void show_bytes(byte_pointer start, size_t len) {
size_t i = 0;
for (; i < len; i++) {
printf("%.2x ", start[i]);
}
printf("\n");
}
void show_int(int x) {
show_bytes((byte_pointer) &x, sizeof(int));
}
void show_float(float x) {
show_bytes((byte_pointer) &x, sizeof(float));
}
void show_double(double x) {
show_bytes((byte_pointer) &x, sizeof(double));
}
由于要打印字节,一般都要使用 char *,这里要注意使用无符号整数的指针,否则会出现错误。另外长度和循环也要统一使用无符号整数互相比较,这样就不会出错。
运行的时候,指针指向的是最低字节,这个程序会打印指定的数据在内存中的实际存放方式。
在main函数中实验:
int main()
{
int x = 12345;
float y = (float) x;
show_int(x);
show_float(y);
}
结果是:
39 30 00 00
00 e4 40 46
先看上边的整数,由于12345很明显是0x3039,所以显然是大端。而强制转型并不是什么都没做,可以发现机器的表示发生了变化。
这说明浮点数和int有着某种对应换算关系,之后再回来看。现在可以知道,强制转型会让底层表示方法有变化,从而这些数值的操作(比如加减乘除)的方式也会有变化。
2.5 大小端表示法
很显然0x87654321的大端存放顺序就是 87 65 43 21,小端存放顺序是 21 43 65 87,所以:
函数 |
小端 |
大端 |
show_bytes(valp,1) |
21 |
87 |
show_bytes(valp,2) |
21 43 |
87 65 |
show_bytes(valp,3) |
21 43 65 |
87 65 43 |
2.6 整数与浮点数的表示
0x00359141写成二进制是:0000 0000 0011 0101 1001 0001 0100 0001。
0x4A564504写成二进制是:0100 1010 0101 0110 0100 0101 0000 0100。
移动一下找匹配的部分:
00000000001101011001000101000001
01001010010101100100010100000100
有21位相同。看上去除了最高的一位1,整数的部分都包含在float中。
表示字符
ACSII字符可以被解释成带符号的char类型数值,也就是整数。正好十进制的0-9,对应十六进制的0x30-0x39,方便转换。而大小写字母的转换,在于第五位的不同。
2.7的结果很显然就是打印出来61-66
布尔逻辑 - 位运算
~取反,&与,|或,^异或。
其他的都好懂,异或只要记住,两个数字相同就是0,不同就是1。取反和与1异或相同。与0异或结果不变,与&运算相同。而设置成1是使用|,设置成0则使用&0。
除了可以对标量进行逻辑运算,对向量也可以,遵循的方法是对向量的每一个相同的位进行逻辑运算。
2.8 位向量布尔运算
运算 |
结果 |
a |
01101001 |
b |
01010101 |
~a |
10010110 |
~b |
10101010 |
a&b |
01000001 |
a|b |
01111101 |
a^b |
00111100 |
一个位和自己的异或必定是0,而0和另外一个数字异或,结果还是另外一个数字。用这个方法可以有很多简化的算法。
位向量的用法很多,可以表示集合,或者取掩码。需要灵活掌握。
2.9 用三位位向量表示8种颜色的组合
- 000 黑色的补是111 白色
- 001 蓝色的补是110 黄色
- 010 绿色的补是101 红紫色
- 011 蓝绿色的补是100 即红色
另外四种颜色的补色,就是反向关系。
蓝色 | 绿色 = 001 | 010 = 011 蓝绿色
黄色 & 蓝绿色 = 110 & 011 = 010 绿色
红色 ^ 红紫色 = 100 ^ 101 = 001 蓝色
2.10 异或交换变量值
void in_place_swap(int *x,int *y){
*y = *x ^ *y; /* STEP 1 */
*x = *x ^ *y; /* STEP 2 */
*y = *x ^ *y; /* STEP 3 */
}
各步的值如下:
步骤 |
*x |
*y |
初始 |
a |
b |
第一步 |
a |
a^b |
第二步 |
b |
a^b |
第三步 |
b |
a |
2.11 会失败的交换元素位置:
void print_array(int a[], int cnt){
int i = 0;
printf("[");
for (; i < cnt; i++) {
printf("%d", a[i]);
if(i!=cnt-1){
printf(", ");
}
}
printf("]\n");
}
void reverse_array(int a[], int cnt){
int first, last;
print_array(a, cnt);
for(first=0,last= cnt-1;
first<=last;
first++,last--){
in_place_swap(&a[first], &a[last]);
}
print_array(a, cnt);
}
这样一个程序,如果长度为奇数,会失败:
int main()
{
int a[] = {1, 2, 3, 4, 5, 6};
int b[] = {1, 2, 3, 4, 5, 6, 7};
reverse_array(a, 6);
reverse_array(b, 7);
}
结果是:
[1, 2, 3, 4, 5, 6]
[6, 5, 4, 3, 2, 1]
[1, 2, 3, 4, 5, 6, 7]
[7, 6, 5, 0, 3, 2, 1]
这是因为长度在奇数的时候,&a[first]
与&a[last]
两个地址会指向同一个位置,即索引为(cnt+1)/2的元素,此时如果采用这种取异或的方法,直接会导致变量变成0。
修改这个方法也很简单,如果指向同一个元素的时候,不做任何工作就可以,所以直接将first=last的条件排除在for循环之外即可:
void reverse_array(int a[], int cnt){
int first, last;
print_array(a, cnt);
for(first=0,last= cnt-1;
first<last;
first++,last--){
in_place_swap(&a[first], &a[last]);
}
print_array(a, cnt);
}
位向量常做的是掩码运算。掩码的特点是,如果用1去做与运算,得到的是掩码的值。如果用0去做与运算,是将其置零。
想要取一个数值的某段掩码,就可以将指定的位数设置成1然后取出。而~0可以直接得到全1的掩码用于取出所有的值。
2.12 掩码取字节
x = 0x87654321,字长任何大于等于8的时候都能工作。
由于对于任何字长都要大于0,因此不能考虑符号。
//Question A
x & 0xFF /* 0xFF 的最低8位是1,高位不管多少,全都是0 */
//Question B 取补就是异或,但是最低8位不能变。很显然取补是和1异或,而和0异或就是不变。因此有:
x ^ (~0xFF)
//Question C 最低有效字节全部设置成1,其他字节不变。设置成1就是将位于1进行或运算,结果一定是1。
x | 0xFF
总之一句话,如果要不考虑字长的限制,则必须使用~来获得高位为1的掩码,而不能直接写死定长的掩码。
2.13 模拟bis和bic指令
bis指令是将掩码为1的地方强行设置为1,而其他地方不变。很显然设置为1就是用or,而且一位和0做or运算,结果不变。所以bis就是一个按位或运算。
所以实现按位或就是bis函数本身,即int result = bis(x, y)
bic是在m为1的地方,将z设置为0。实际上就是取反然后做与运算来设置为0。即 x&(~y)。
这个时候翻出计算机系统要素第10页的布尔函数表,发现异或是 x.(~y) + (~x).y
由于bic就是x&(~y),根据这个公式,只要bis(bic(x,y),bic(y,x))就可以得到异或了。
布尔逻辑 - 逻辑运算
C语言里有 ! && ||,原理和位运算的符号不同,而且|| 带有短路效果,在第一个表达式为TRUE的时候,不会再去判断后边的表达式。
依靠短路特性就可以写出很多简洁的代码比如:a&&5/a
, p&&*p++
。
2.14 位运算与逻辑运算的结果
x 是 0x66,也就是 01100110,y 是 0x39,也就是 00111001。
表达式 |
值 |
表达式 |
值 |
x & y |
20 |
x && y |
01 |
x | y |
7F |
x || y |
01 |
~x | ~y |
DF |
!x || !y |
00 |
x & !y |
00 |
x && ~y |
01 |
2.15 x==y
编写一个表达式,仅使用位级和逻辑运算,当x和y相等的时候返回1,不相等的时候返回0。
首先是如何判断两数字相等,如果两个数字的位全部一样,就算相等。看到相等就想到异或。如果两个数字各个位全部相等,则异或得到0。再对异或的结果使用逻辑取反,就得到1。
因此表达式就是!(x ^ y)
移位
移位分为两种,逻辑移位和算术移位。其中左移都是一样的,在右边补0。
而右移的时候,算术逻辑根据最高位补对应的符号位,而逻辑移位只在最高位补0。
x |
x<<3 |
x>>2(逻辑) |
x>>2(算术) |
十六进制 |
二进制 |
二进制 |
十六进制 |
二进制 |
十六进制 |
二进制 |
十六进制 |
0xC3 |
11000011 |
00011000 |
18 |
00110000 |
30 |
11110000 |
F0 |
0x75 |
01110101 |
10101000 |
A8 |
00011101 |
1D |
00011101 |
1D |
0x87 |
10000111 |
00111000 |
38 |
00100001 |
21 |
11100001 |
E1 |
0x66 |
01100110 |
00110000 |
30 |
00011001 |
19 |
00011001 |
19 |