CSAPP要点总结 第2章 信息的表示和处理 part1

隐藏

信息的存储

字长word size

字长可理解为32位或者64位计算机中的“32”,“64”。

以下总结内容错误,例如8086是16位结构CPU但其地址总线是20根。 应该来说是指寄存器和每次运算处理1个word,地址总线不一定是一个word

硬件中出现1个word意味着:
1. 总线每次传1个word 
2. 寄存器包括PC是1个word size
3. 程序命令加载,跳转等操作对象都是1个word size
4. 指针大小1个word size(由此也决定了虚拟地址空间大小,例如32位最大约4G byte)

补充说明:8086如何做到寄存器16位而地址总线20位? 答:因为其寻址方式是段地址:偏移地址 例如CS:IP (CS*16 + IP)指向指令或者DS:[0]指向数据等,是CS寄存器左移4位作为段地址,在加上IP偏移量。

不同数据类型的大小

以C语言为例,简单来说,就是不同系统,不同数据类型大小不一样,小心埋雷。

例如,如果以int* 作为地址指针时,因为其有可能是32位,如果是,那么在64位系统上就用不了。 因此,稍微注意一下,要使得程序对数据类型的确切大小不敏感。

寻址

对于一个多字节的数据,寻址重点一个是地址,一个是多字节数据如何排列(大端/小端)

大端和小端

例如0x76543210这个数字 对于大端,排列顺序是: …低位地址 | 76 | 54 | 32 | 10 | 高位地址…

对于小端,排列顺序是: …低位地址 | 10 | 32 | 54 | 76 | 高位地址…

可以看出,小端对于从低位到高位顺序阅读代码而言,较麻烦。但是,它更适合于机器处理数据。例如加法,如果低八位需要进位,直接地址+1个byte即可。可见其设计上(猜测包括硬件设计),以一个byte为单位处理数据。

小端机器较为常见,例如intel处理器,大端机器包括IBM和Sun Microsystems的机器,如果没记错的话也包括游戏机。这也是为什么通常游戏引擎要考虑大端小端转换的原因。

大端小端注意的场合

通常不需要考虑大端小端,但在有些场合需要:

  1. 不同类型机器之间通过网络传送二进制文件时,网络应用代码需要准守标准。
  2. 反汇编的情况下阅读汇编代码时。小端法机器通常字节按照与书写顺序相反的顺序显示。
  3. 如上所说用于游戏机移植的时候
  4. 编写特殊程序时,例如通过强制类型转化的指针(例如unsigned char*)显示数据字节码。如下:
#include <stdio.h>

typedef unsigned char* P_Byte; //unsigned char* 8位 1个byte长度

void print_byte(P_Byte start, int len) {
  int i;
  for(i = 0; i < len; ++i) 
    printf(" %.2x", start[i]);  // 打印字节码
  printf("\n");
};

// 例如打印float数据的字节码
void show_float(float x) {
  print_byte( (P_Byte) &x, sizeof(float) ); //使用sizeof 而不是固定值,可移植性好
};

详细代码参见: CodeExample/code/data/show-bytes.c

表示字符串

C语言中字符串结尾是0,在ASCII中是0x00

字符编码

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

  2. unicode ASCII用于英文没有问题,但是只有8位256个值实在太少,不适用于如中文等其他多国语言,因此用32位的unicode可以涵盖多个语言,unicode始终是32位的。JAVA使用unicode。

  3. urf-8 unicode都是32位实在太浪费了,因此utf-8就满足需要时再用32位,不需要就只用8位,且跟ASCII码统一起来。

指令编码

即使一个简单的sum函数,在windows,linux上,32位、64位上生成的二进制代码都是不同的。因此,不同系统的代码的二进制是执行文件不兼容的

bool代数

让我很郁闷的一点是,命题逻辑的符号约定跟布尔运算的符号约定不一样,所以用mathjax做成表格如下: (注:**以下内容部分参考自CSAPP,可能有误)

$$ \begin{array}{c|cc} \text{逻辑运算} & \text{命题逻辑} & \text{布尔运算} \\ \hline \text{NOT} & \neg & \sim \\ \text{AND} & \land & \And \\ \text{OR} & \lor & | \\ \text{XOR} & \oplus & ^{\land} \\ \end{array} $$

猜测两套符号的原因:命题逻辑更偏数学,在计算机中进行bool计算时由于键盘上没有相应的符号表示,所以又定制了一套符号。

bool运算可以用于有限集合(通过位置和值[1/0]来表示有限集合,这样空间占用小),或者用于例如C语言的位级计算(用于任何“ints”类型,掩码运算,优化计算速度)。

掩码计算时需要考虑可移植性问题, 例如同样是全为1的掩码,~0不论在32位还是64位字长下都可以,但是0xFFFFFFFF只能用于32位。

C语言中的逻辑运算

C语言中,逻辑表达式的结果只能是0x00(表示假)或者0x01(表示真)。

C语言中的逻辑运算有一个特点,如果第一个参数求值就能确定结果,就不会对第二个参数求值,例如a&&5/a不会被零除;p&&p++不会导致间接引用空指针。

C语言中的移位运算

左移没什么问题,右移分两种,逻辑右移算术右移,如下

操作
x [0110 0011] [1001 0101]
x « 4 [0011 0000] [0101 0000]
x » 4 (逻辑右移) [0000 0110] [0000 1001]
x » 4 (算术右移) [0000 0110] [1111 1001]

可见,逻辑上右移应该都是填充0,但是有时候最高位为1时表示负数,因此算术右移填充0。

算术右移的意义

以16位数为例:

-47 [1111 1111 1101 0001] -48 [1111 1111 1101 0000] -24 [1111 1111 1110 1000]

-47 » 1 = -48 » 1 = -24 因为数学上$ -24 = -48 * 2 = -47*2$,用如下例子进一步分析, 测试环境: win7 visual stdio c++以及cygiwn gcc

//测试用例 c
#include <stdio.h>

int main(){
  int a = 47;
  int b = -a;

  printf("47 / 2 = %d \n",a/2);
  printf("-47 / 2 = %d \n",b/2);
  printf("47 >> 1 = %d \n",a >> 1);
  printf("-47 >> 1 = %d \n",b >> 1);
}
/*测试结果:
cygwin gcc 以及 visual studio c/c++
47 / 2 = 23 
-47 / 2 = -23 
47 >> 1 = 23 
-47 >> 1 = -24
*/

特别注意: 1.整数除法已经跟数学意义不一样了,在C/C++语言中-48 / 2实际上是首先不考虑符号的情况下,48/2=24再加上符号,结果为 -24,而不是算术意义上的-48 /2 = -96

  1. 总结就是,C/C++中整数乘除法先不考虑符号,乘法对应左移;除法对应右移
  2. 对于负奇数的除法和算术右移位有所不同-x >> 1 对应 floor(-x / 2) 即向下取整,取更小值; 而 x / 2则直接不考虑符号位并抹去末尾。
  3. 总结就是:对于负整型的除法和移位要特别小心

何时逻辑右移、何时算术右移

C语言标准并没有明确定义应该用哪种右移,通常: 1.unsigned类型是逻辑右移,因为最高位没有符号意义了。 2.有符号整型,几乎所有编译器都使用算术右移(虽然没强制规定,可用逻辑右移,理论上存在移植性问题)

  1. JAVA对于右移有明确定义,x >> k (算术右移), x >>> k (逻辑右移)

移位数比数据长度还大的情况

如果出现这种情况,为了保证其意义,通常是求mod,例如int a = 0xFFFFEEEE >> 40实际是右移8位(40%32 = 8)。当然保险起见,最好不要出现这种情况。

-----EOF-----

Categories: csapp Tags: computer system