深入理解计算机系统-学习笔记 (2.1)

这笔记整理起来还怪麻烦的
这只是第二章的一半。另一半看啥时候整理完吧

信息的表示和处理

三种最重要的数字表示:

  • 无符号编码

    基于传统的二进制表示法,表示大于或者等于0的数字

  • 补码编码

    表示有符号整数的最常见的方式,有符号整数就是可以为正或者为负的数字

  • 浮点数编码

    表示实数的科学计数法的以2为基数的版本

整数的表示只能编码一个相对较小的数值范围,但这种表示是精确的

浮点数虽然可以编码一个较大的范围,但这种表示只是近似的

2.1 信息存储

大多数计算机使用8位的块,或者字节(byte)作为最小的可寻址的内存单位,而不是访问内存中单独的位

机器级程序将内存视为一个巨大的字节数组,称为虚拟内存。内存中的每个字节都由一个唯一的数字来标识,称为它的地址(address),所有可能地址的集合就称为虚拟地址空间

十六进制表示法

一个字节由8位组成,在二进制表示法中,他的值域为0000000011111111,转化为十进制则是0255 。但二进制太长,而十进制与位模式的互相转化又很麻烦。替代方法便是用十六进制来表示位模式

16进制和2进制的互相转化很好实现。如一个十六进制数字 0x173A4C,可以通过对每个十六进制数字进行二进制展开来转化位二进制格式。转化方法如下:

image

这样就得到了二进制数字 000101110011101001001100

反过来也是一样,只需将二进制数字每四位为一组转化为十六进制数字即可。若二进制数字的位总数不是4的倍数,只需将最左边的一组的前面用0补足即可

而要将十进制转化为十六进制,则需要将十进制数字不断用16整除,所得余数r为十六进制的最低位,商q则继续用16整除。如数字314156的转化:

image

则十六进制数字为0x4CB2C

反之则需要用相应的16的幂乘以每个十六进制数字。如数字0x7AF的转化:

$$
7 * 16^2 + 10 * 16 + 15 * 16^0 = 7 * 256 + 160 + 15 = 1967
$$

字数据大小

每台计算机都有一个字长,指明指针数据的标称大小。字长决定的最重要的系统参数就是虚拟地址空间的最大大小。对于一个字长为w位的机器而言,虚拟地址的范围是0 ~ 2^w – 1,程序最多访问2^w个字节

32位系统的限制虚拟地址空间位4千兆字节(4GB),64位系统则是16EB

计算机和编译器支持多种不同方式编码的数字格式,如不同长度的整数和浮点数。C语言支持的多种数据格式如下图所示:

image

寻址和字节顺序

对象在内存中排列字节的方法大致有两种,分别为大端法和小端法。假设一个变量x的类型为int,地址位于0x100,十六进制值为0x01234567,则该变量在内存中的存储如下图所示:

image

大端法将最高有效字节排列在前面,而小端法则与之相反,将最高有效字节排列在后面

大多数Intel兼容机都只用小端模式,手机上的Android系统和IOS系统也只能运行小端模式,但实际上两者并无明显优劣

表示字符串

C语言中字符串被编码为一个以null(其值为0)字符结尾的字符数组,每个字符都由某个标准编码来表示,最常用的就是ASCII编码

所有将ASCII码作为字符码的系统在编码后的结果都会相同,而不受字节顺序和字大小规则影响

表示代码

考虑下面的C函数:

int sum(int x, int y)
{
    return x + y;
}

这段代码在不同系统编译后产生的机器代码如下:

image

可见不同机器类型拥有不同的编码规则,因此二进制代码是不兼容的,纯粹的二进制代码很难在不同机器与操作系统间移植。所以才会有将高级语言通过编译器生成不同系统的原始语言的机制

布尔代数简介

布尔运算大致有4种:

  1. ~ 对应 逻辑运算NOT(非),在命题逻辑中用﹁表示。当 p = 0 时, ~p = 1 ,反之亦然
  2. & 对应 逻辑运算AND(与),在命题逻辑中用∧表示。仅当 p = 1 且 q = 1 时,p&q = 1,否则 p&q = 0
  3. | 对应 逻辑运算OR(或),在命题逻辑中用∨表示。当 p = 1 或 q = 1 时,p|q = 1,否则 p|q = 0
  4. ^ 对应 逻辑运算 异或,在命题逻辑中用⊕表示。当 p = 1 且 q = 0 ,或者p = 0 且 q = 1 时,p^q = 1,否则 p^q = 0

假设 w=4 ,参数 a=[0110], b=[1100] ,则4种运算 a&b,a|b,a^b 和 ~b 的结果如下:

image

C语言中的位级运算,逻辑运算和位移运算

C语言支持按位布尔运算,也就是上面的&,|,^ 和 ~ 。这些运算能运用到任何“整形”数据类型上

image

C语言还提供了一组逻辑运算符,&&,|| 和 ! ,分别对应命题逻辑中的 AND,OR 和 NOT 运算

逻辑运算与位级运算不同,它将所有非零的参数视为TRUE,而参数0则视为FALSE

image

C语言还提供了一组移位运算。其中,左移会将最高的k位丢弃,并在右端补k个0

右移则分为两种,一种是逻辑位移,即舍弃右端的地有效位值,直接在左端补k个0。算术位移则是在左端补上最高有效位的值

image

2.2 整数表示

整数编码有两种方式,一种只能表示非负数,也就是无符号数。另一种能够表示负数,零和正数,也就是有符号数

两者的区别主要在于有符号数需要将最高有效位用于记录数字是否为负数,因此正数的表示范围比无符号数要少一倍,但取而代之的是表达负数的能力

下图是32位系统和64位系统中无符号数和有符号数的表示范围:

image

image

整数编码方式

整数的编码方式主要有两种,一种是无符号数的编码,一种是补码编码(有符号数最常见的计算机表示方式)

image

光看公式可能有些难以理解,但加上实例就很好理解了

image

同理,补码编码也与之类似,不同的是补码需要将最高位作为符号位来表示正负

image

其实只需要在计算过程中将最高位作为负数计算即可。最高位若为0则代表该数是正数,-0仍是0,对计算结果没有影响。最高位若为1则代表该数是负数,乘以-1即可

image

此外还有一点,就是在之前的有符号数范围中,负数的范围基本都比正数大一。这正是因为有符号数的编码表示中,以1开头的负数占据了所有范围的一半,相应的正数却要拿出一位的范围分给非负数0(即二进制编码所有位数均为0的情况)

有符号数与无符号数之间的转换

以一个16位的数字为例。若为有符号数,则该数字的表示范围在 -32768 ~ 32767 之间,无符号数的范围则是 0 ~ 65535

如此一来,若有一个负数(如 -12345)要转化为无符号数,该如何转化?

当然不是将负号去掉变成12345那么简单。我们先将该有符号数用二进制表示出来:

-12345 = [1100111111000111]
该数转化为无符号数后,最高位的符号位变为普通的数字位。原本要将该有符号数转化为10进制,需要将最高位表示为: -1 * 2^15 = -32768

转变为无符号数后,最高位则表示为:1 * 2^15 = 32768

相差 2^15 * 2 = 2^16 = 65536

也就是说,有符号数 -12345 转化为无符号数的结果为 -12345 + 65536 = 53191

可以发现,两者的差值正好是 2 的位数次方

相应的,一个超出有符号数正数范围的无符号数转化为有符号数,也会按照此规律进行转换。如无符号数 53191 转换为有符号数的结果为 -12345

大致转换过程如下图所示:

image

因此,为了避免类似这样的转换造成的溢出,部分高级语言(如java)抛弃了无符号数,只使用有符号数

不同类型整数的转换

不只是有符号数和无符号数之间能够互相转换,不同类型的整数之间也能够互相转换。也就是下图中所示的数据类型

image

先看低位整数转换为高位整数。这里以16位的 short / unsigned short 转换为32位的 int / unsigned 为例

无符号数的零扩展

若是无符号数的高位扩展,只需将无符号数左边填上相应位数的零即可。二进制表示的 [01] 和 [0001] 都是一个数字

补码数的符号扩展

若是有符号数就不能那么简单了。毕竟负数需要保持最高位为1。因此有符号数只需将左边填上相应位数的最高位即可。即正数在左边填上相应位数的0,负数在左边填上相应位数的1

下图为执行转换后的结果。其中 sx 为16位有符号数, usx 为16位无符号数, x 位32位有符号数, ux 为32位无符号数:

image

再看高位整数转换为低位整数

int x = 53191;
short sx = (short)x;	/* - 12345 */
int y = sx;				/* - 12345 */

高位整数转换为低位整数的方式是将左边超出的位数直接截断

要注意 short 是补码类型,也就是说在转换为 short 后产生了溢出,32位有符号数53191转换为了16位的有符号负数 -12345

而有符号数的高位扩展方式为符号扩展,扩展为32位的 int 类型后仍是负数 -12345