CSAPP: 信息的表示和处理

689 阅读4分钟
原文链接: tianshuang.me
算数运算

对计算机的算数运算有深刻的理解是写出可靠程序的关键。比如,程序员和编译器不能用表达式(x-y<0)来替代(x<y),因为前者可能会产生溢出。甚至于也不能用表达式(-y<-x)来替代,因为在二进制补码表示中负数和正数的范围是不对称的。算术溢出是造成程序错误和安全漏洞的一个常见根源。

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

寻址和字节顺序

某些机器选择在存储器中按照最低有效字节到最高有效字节的顺序存储对象,而另一些机器则按照从最高有效字节到最低有效字节的顺序存储。前一种规则————最低有效字节在最前面的方式,称为小端法(little endian)。大多数Intel兼容机都采用这种规则。后一种规则————最高有效字节在最前面的方式,称为大端法(big endian)。大多数IBM和Sun Microsystems的机器都采用这种规则。注意我们说的是“大多数”。这些规则并没有按照企业界限来划分。比如,IBM和Sun制造的个人计算机使用的是Intel兼容的处理器,因此用的就是小端法。许多比较新的微处理器使用双端法(bi-endian),也就是说可以把它们配置成座位大端或者小端的机器运行。

移位运算

一般而言,机器支持两种形式的右移:逻辑右移和算术右移。逻辑右移在左端补k个0,算术右移是在左端补k个最高有效位的值。C语言标准并没有明确定义应该是用哪种类型的右移。对于无符号数据(也就是以限定词unsigned声明的整型对象),右移必须是逻辑的。而对于有符号数据(默认的声明的整型对象),算术的或者逻辑的右移都可以。不幸的是,这就意味着假设一种或者另一种右移形式的代码都潜在着可移植性问题。然而,实际上,几乎所有的编译器/机器组合都对有符号数据使用算术右移,且许多程序员也都假设机器会使用这种右移。另一方面,Java对于如何进行右移有明确的定义。表达式x>>k会将x算术右移k个位置,而x>>>k会对x做逻辑右移。

移动k位,这里k很大
int      lval = 0xFEDCBA98  << 32;
int      aval = 0xFEDCBA98  >> 36;
unsigned uval = 0xFEDCBA98u >> 40;

C语言标准很小心地规避了说明在这种情况下该如何做。在许多机器上,当移动一个w位的值时,移位指令只考虑位移量的低log2w位,因此实际上位移量就是通过计算k mod w得到的。例如,在一台采用这个规则的32位机器上,上面三个移位运算分别是移动0、4和8位,得到结果:

lval    0xFEDCBA98
aval    0xFFEDCBA9
uval    0x00FEDCBA

不过这种行为对于C程序来说是没有保证的,所以移位数量应该保持小于字长。另一方面,Java特别要求位移数量应该按照我们前面所讲的求模的方法来计算。

与移位运算有关的操作符优先级问题

常常有人会写这样的表达式1<<2+3<<4,其本意是(1<<2)+(3<<4)。但是在C语言中,前面的表达式等价于1<<(2+3)<<4,这是由于加法(和减法)的优先级比移位运算要高。然后,按照从左至右综合性规则,括号应该是这样打的(1<<(2+3))<<4,因此得到的结果是512,而不是期望的52。在C表达式中搞错优先级是一种常见的程序错误,而且常常很难检查出来。所以当你拿不准的时候,请加上括号!