学习CSAPP-第二章

436 阅读15分钟

信息的表示和处理

现代计算机存储和处理的信息以二值信号表示。

计算机的表示法使用有限数量的位来对一个数字编码,因此,当结果太大以至不能表示时,某些运算就会溢出(overflow)。比如200×300×400×500会得到-884901888

由于表示的精度有限,浮点运算是不可结合的。比如(3.14+1e20)-1e20=0.0,而3.14+(1e20-1e20)=3.14

整数运算和浮点数运算会有不同的数学属性是因为它们处理数字表示有限性的方式不同———整数的表示虽然只能编码一个相对较小的数值范围,但是这种表示是精确的;而浮点数虽然可以编码一个较大的数值范围,但是这种表示只是近似的。

大量计算机的安全漏洞都是由于计算机算数运算的微妙细节引发的。

计算机用几种不同的二进制表示形式来编码数值。

程序员需要对计算机运算与更为人熟悉的整数和实数运算之间的关系有清晰的理解。

2.1 信息存储

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

机器级程序将内存视为一个非常大的字节数组,称为虚拟内存。内存的每个字节都由一个唯一的数字来标识,称为它的地址,所有可能的地址的集合就称为虚拟地址空间。毫无疑问,这个虚拟地址空间只是一个展现给机器级程序的概念性映像

程序对象:程序数据、指令和控制信息

C语言中一个指针的值(无论它指向一个整数、一个结构、或是某个其他程序对象)都是某个存储块的第一个字节的虚拟地址。

每个程序对象可以简单地视为一个字节块,而程序本身就是一个字节序列。

2.1.1 十六进制表示法

二进制表示法太冗长,而十进制表示法与位模式的互相转化很麻烦。

用十六进制书写,一个字节的值域为00(16)~FF(16)

在C语言中,以0x或0X开头的数字常量被认为是十六进制的值。

2.1.2 字数据大小

每台计算机都有一个字长,指明指针数据的标称大小。因为虚拟地址是以这样一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。也就是说,对于一个字长为w位的机器而言,虚拟地址的范围为:0~2的w次方-1,程序最多访问2的w次方字节(因为虚拟内存被视为字节数组)。

大多数字长(64)位机器也可以运行为32位机器编译的程序,这是一种向后兼容

例子:

 当程序prog.c在32位机器上用如下指令编译后
 linux> gcc -m32 prog.c
 该程序既可以在32位机器运行,也可以在64位机器上运行
当程序prog.c在64位机器上用如下指令编译后
linux> gcc -m64 prog.c
该程序只能在64位机器运行

我们将程序称为“32位程序”或“64位程序”时,区别在于该程序是如何编译的,而不是其运行的机器类型。

image.png

有些数据类型的确切字节数依赖于程序是如何被编译的(比如是32位系统还是64位系统编译)

图2-3还展示了指针(例如一个被声明为类型位“char*” 的变量)使用程序的全字长

程序可移植性的一个方面就是使程序对不同数据类型的确切大小不敏感。 曾经32位机器和32位程序是主流组合,而随着64位机器的日益普及,在将这些程序移植到新机器上时,许多隐藏的对字长的依赖性就会显现出来,成为错误。比如:许多程序员假设一个声明为int类型的程序对象能被用来存储一个指针。这在大多数32位机器上能正常工作,因为32位机器上的指针的字节数是4个字节,而int类型的大小也是4个字节。但是在一台64位机器上会导致问题,因为64位机器上的指针的字节数是8个字节,而int类型的大小是4个字节 。

2.1.3 寻址和字节顺序

对于跨越多字节的程序对象,我们必须建立两个规则:这个对象的地址是什么,以及在内存中如何排列这些字节。

在几乎所有机器上,多字节对象都被存储为连续的字节序列对象的地址为所使用字节中最小的地址

排列表示一个对象的字节有两个通用的规则:大端法与小端法

大端法:

某些机器按照从最高有效字节到最低有效字节的顺序存储。(也就是最高有效字节存储在最小的地址

小端法:

某些机器按照从最低有效字节到最高有效字节的顺序存储。(也就是最低有效字节存储在最小的地址

例子:

image.png

有些硬件可以按小端或大端两种模式操作,但一旦选择了特定的操作系统,那么字节顺序也就固定下来了。

有时候字节顺序会成为问题:

image.png

A70D5531EF4F71BA0C50606E7356D0A9.png

B71466F64C57C782B2529B360E6F01BE.png

例子:

image.png

数据类型void* 是一种特殊类型的指针,没有相关联的类型信息。

解释:

上面这段代码使用强制类型转换来访问和打印不同程序对象的字节表示

byte_pointer定义为一个指向类型为"unsigned char"的对象的指针。因为char类型刚好为1个字节,所以可以把byte_pointer视为一个字节指针,可引用一个字节序列

数据类型size_t表示数据结构大小的首选数据类型。

show_bytes(... ,...)打印出每个以十六进制表示的字节,其形参start表示的是字节指针,len表示的是字节数

C格式化指令"%.2x"表明整数必须用至少两个数字的十六进制格式输出。

564EC69FF7EC7B82B86BA7B183EFC2C3.png

运行:

image.png

image.png

97537834D1A3A6FCA7F8EC1A9C5280C0.png

image.png

2.1.4 表示字符串

C语言中字符串被编码位一个以null(其值为0)字符结尾的字符数组。

在使用ASCII码作为字符码的任何系统上都将得到相同的结果,与字节顺序和字大小规则无关。因而,文本数据比二进制数据具有更强的平台独立性。

    ASCII字符集适合于编码英文文档,但在表达一些特殊字符方面没有太多办法,并且完全不适合希腊文、中文等语言的文档。
    因此Unicode联合会修订了最全面且广泛接受的文字编码标准。当前的Unicode标准字库包括将近100000个字符,支持广泛的语言类型。  
    基本编码,称为Unicode的“统一字符集”,使用32位来表示字符。这好像要求文本串中每个字符都要占用4个字节。不过有一些替代编码,其中常见的字符只需要12个字节,而不太常用的字符需要多一些的字节数。
    特别地,UTF-8表示将每个字符编码为一个字节序列,这样ASCII字符还是使用和它们在ASCII中一样的单字节编码,这也就意味着所有的ASCII字节序列用ASCII码表示和用UTF-8表示是一样的。
    JAVA使用Unicode表示字符串。

2.1.5 表示代码

image.png

可以发现指令的编码是不同的。不同的机器类型使用不同的且不兼容的指令和编码方式。因此二进制代码是不兼容的。二进制代码很少能在不同机器和操作系统组合之间移植。

计算机系统的一个基本概念:从机器角度来看,程序仅仅只是字节序列。机器没有关于原始源程序的任何信息。

2.1.6 布尔代数简介

布尔运算符号:~ & | ^

可以将上述4个布尔运算扩展到位向量的运算。位向量:固定长度为w、由0和1组成的串。

a^a=0

位向量一个很有用的应用就是表示有限集合

32579889A109D7BC36570E80098BCBF1.png

2.1.7 C语言中的位级运算

image.png 确定一个位级表达式的结果最好的办法,就是将十六进制的参数扩展成二进制表示并执行二进制运算,然后再转换回十六进制。

位级运算的一个常见用法就是实现掩码运算。掩码指的是一个位模式,表示从一个字中选出的位的集合。

例子:

掩码0xFF(最低的8位为1)可用来参与掩码运算得到一个字的低位字节,如:

位级运算x&0xFF可生成一个由x的最低有效字节组成的值,而其他的字节就被置为0。比如当x=89ABCDEF时,x&0XFF得到的就是0x000000EF。

而掩码~0将生成一个全1的掩码,可以不用管机器的字长。尽管对于32位机器来说,同样的掩码可以写成0xFFFFFFFF,但是这样的代码是不可移植的。

有一个练习题:对于下面的值,写出变量x的C语言表达式。你的代码应该对任何字长w>=8都能工作。我们给出了当x=0x87654321以及w=32时表达式求值的结果,仅供参考。

A.x的最低有效字节,其他位均置0。[0x00000021]

x&0xFF

B.除了x的最低有效字节外,其他的位置都取补,最低有效字节保持不变。[0x789ABC21]

x^(~0xFF)

C.x的最低有效字节设置成全1,其他字节都保持不变。[0x876543EF]

x|0xFF

总结:几种常用的掩码:0xFF、~0xFF、~0(并且这三种写法都是在不同字长的机器上具有可移植性的)

感觉常见的子网掩码255.255.255.0就是~ 0xFF在32位字长下的十进制格式,我们可以用~0xFF掩码来判断两个ip地址的网段是否相同,也就是可以进行掩码运算:ip&(~0xFF),从而实现统一ip地址后三位的目的(统一为0)

网上查的一个具体案例:

[wenku.baidu.com/view/bb6d7a…]

2.1.8 C语言中的逻辑运算

不要把逻辑运算和位运算搞混

image.png

2.1.9 C语言中的移位运算

x<<k

x>>k分为逻辑右移算数右移。逻辑右移在左端补k个0;算术右移在左端补k个最高有效位的值算术右移对有符号整数数据的运算非常有用

C语言标准没有明确定义对于有符号数应该使用那种类型的右移——算术右移或者逻辑右移都可以。然而,几乎所有的编译器/机器组合都对有符号数使用算术右移但对于无符号数,必须使用逻辑右移

与C相比,java对于如何进行右移有明确的定义。x>>k代表算术右移,x>>>代表逻辑右移。

对于一个由w位组成的数据类型,如果要移动k>=w位会得到什么结果呢?可以用k mod w来规避这种问题

2.2 整数表示

用位来编码整数,有两种方式:一种只能表示非负数,而另一种能够表示负数、零和正数。

image.png

2.2.1 整型数据类型

image.png

image.png

图中有个注意的点:取值范围不对称——负数的范围比正数的范围大1

C语言标准定义了每种数据类型必须能够表示的最小的取值范围。比如数据类型int可以用2个字节的数字来实现,这几乎回退到了16位机器的时代;long的大小也可以用4个字节的数字来实现,这对于32位程序来说是很典型的。

固定大小的数据类型保证的数值范围在任何机器上都一致,int32_t、uint32_t、int64_t、uint64_t都是固定大小的数据类型。

image.png

C与C++支持有符号和无符号数,Java只支持有符号数。

2.2.2 无符号数的编码

D5D7C4991C3CF06E033609D8C428E167.png

2.2.3 补码编码

对于许多应用,我们还希望表示负数值。最常见的有符号数的计算机表示方式就是补码形式。在这个定义中,将字的最高有效位解释为负权

94ED6BBC5B245C01A58ECF6A6E2C1A09.png

TMin没有与之对应的正数,这导致了补码运算的某些特殊的属性,并且容易造成程序中细微的错误。

C语言标准没有要求用补码形式来表示有符号整数,但是几乎所有的机器都是这么做的。

关于整数数据类型的取值和表示,Java标准是非常明确的。它要求采用补码表示,取值范围与图2-10中64位情况一样。并且在Java中,单字节数据类型称为byte,而不是char。这些非常具体的要求都是为了保证无论在什么机器上运行,Java程序都能表现地完全一样。

有符号数的其他表示方法:

5214561CEABDF834DE2F44B246A29C26.png

2.2.4 有符号数和无符号数之间的转换

对于大多数C语言的实现,处理同样字长的有符号数和无符号数之间相互转换的一般规则是:数值可能会改变,但是位模式不变。

82B0933890505125509FCDDFB8AA40E0.png

FAEA18C3F85FA6F353DE4A4BFEE321AB.png

9E95340C54BA138F51BFBBD0D6B01257.png

52A3EBE5E930A890F7077C38422AC6D3.png

2.2.5 C语言中的有符号数和无符号数

当用printf输出数值时,分别用指示符%d、%u、%x以有符号十进制、无符号十进制和十六进制格式输出一个数字。

C语言允许无符号数和有符号数之间的转换。

分为显式的强制类型转换与隐式的强制类型转换

当执行一个运算时,如果它的一个运算数是有符号的而另一个是无符号的,那么C语言会隐式地将有符号参数强制类型转换为无符号数。

06A35E9998BB8E67A1F922C9C088D0A3.png

2.2.6 扩展一个数字的位表示

一个常见的运算是在不同字长的整数之间转换,同时又保持数值不变。当然,当目标数据类型太小以至于不能表示想要的值时,这根本就是不可能的。然而从一个较小的数据类型转换到一个较大的类型,应该总是可能的。

把short转为unsigned时,要先改变大小,再完成从有符号到无符号的转换,例子:

84E64C4B693C2DCF56BD0DCD09ED1A08.png

58DBD379DBBFF036D43187F7D5C5FEED.png

2.2.7 截断数字

FACDA0F0A5521812577BD62E903B5522.png

2.2.8 关于有符号数与无符号数的建议

有符号数到无符号数的隐式强制类型转换导致了某些非直观的行为,会导致错误或者漏洞,避免这类错误的一种方法就是绝不使用无符号数

除了C以外很少有语言支持无符号整数。

不过,当我们想要把字仅仅看做是位的集合而没有任何数字意义时,无符号数值是非常有用的。

2.3 整数运算

2.3.1 无符号加法

当执行C程序时,不会将溢出作为错误而发信号。

14B8CD827A02682239CC98A171C51F13.png

2.3.2 补码加法

两个数的w位补码之和与无符号之和有完全相同的位级表示。实际上,大多数计算机使用同样的机器指令来执行无符号或者有符号加法。

88AA2C9893DB853427F084E325B25EDA.png

2.3.3 补码的非

550CD568EEFCCA5B982058A348FC1165.png

2.3.4 无符号乘法

10B7FCBA7F8FD70125D3184FA24B99C1.png

2.3.5 补码乘法

4482C882D263E361E49553E404296C5D.png

A0EA26194F1EEC9A9132575C5830229B.png

2.3.6 乘以常数

6AF7AEBBBA4C4C83B54A2D9D3DE255D7.png

3EE54A1E9CAD3309B3AEBF47914E26C8.png

tip:当位置n为最高有效位时,则字长为w=n+1

2.3.7 除以2的幂

76C3410A2EEF2FE0C98C7924308E2596.png

除以2的幂可以通过逻辑或者算数右移来实现。这也正是为什么大多数机器上提供这两种类型的右移。不幸的是,这种方法不能推广到除以任意常数。同乘法不同,我们不能用除以2的幂的除法来标识除以任意常数K的除法。

2.3.8 关于整数运算的最后思考

计算机执行的“整数”运算实际上是一种模运算形式

表示数字的有限字长限制了可能的值的取值范围,运算结构可能会溢出

无符号数运算、补码运算都使用了相同的位级实现,都有完全一样或者非常类似的位级行为。

2.4 浮点数

to be continued...