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的机器,如果没记错的话也包括游戏机。这也是为什么通常游戏引擎要考虑大端小端转换的原因。
大端小端注意的场合
通常不需要考虑大端小端,但在有些场合需要:
- 不同类型机器之间通过网络传送二进制文件时,网络应用代码需要准守标准。
- 反汇编的情况下阅读汇编代码时。小端法机器通常字节按照与书写顺序相反的顺序显示。
- 如上所说用于游戏机移植的时候
- 编写特殊程序时,例如通过强制类型转化的指针(例如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
字符编码
-
ASCII 在使用ASCII码作为字符码的任何机器上都得到相同结果,与字节顺序和字的大小规则无关,因此文本数据相比二进制数据具有更好的平台独立性。
-
unicode ASCII用于英文没有问题,但是只有8位256个值实在太少,不适用于如中文等其他多国语言,因此用32位的unicode可以涵盖多个语言,unicode始终是32位的。JAVA使用unicode。
-
urf-8 unicode都是32位实在太浪费了,因此utf-8就满足需要时再用32位,不需要就只用8位,且跟ASCII码统一起来。
指令编码
即使一个简单的sum函数,在windows,linux上,32位、64位上生成的二进制代码都是不同的。因此,不同系统的代码的二进制是执行文件不兼容的。
bool代数
让我很郁闷的一点是,命题逻辑的符号约定跟布尔运算的符号约定不一样,所以用mathjax做成表格如下: (注:**以下内容部分参考自CSAPP,可能有误)
猜测两套符号的原因:命题逻辑更偏数学,在计算机中进行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。
- 总结就是,C/C++中整数乘除法先不考虑符号,乘法对应左移;除法对应右移
- 对于负奇数的除法和算术右移位有所不同,
-x >> 1
对应floor(-x / 2)
即向下取整,取更小值; 而x / 2
则直接不考虑符号位并抹去末尾。 - 总结就是:对于负整型的除法和移位要特别小心。
何时逻辑右移、何时算术右移
C语言标准并没有明确定义应该用哪种右移,通常: 1.unsigned类型是逻辑右移,因为最高位没有符号意义了。 2.有符号整型,几乎所有编译器都使用算术右移(虽然没强制规定,可用逻辑右移,理论上存在移植性问题)
- JAVA对于右移有明确定义,
x >> k
(算术右移),x >>> k
(逻辑右移)
移位数比数据长度还大的情况
如果出现这种情况,为了保证其意义,通常是求mod,例如int a = 0xFFFFEEEE >> 40
实际是右移8位(40%32 = 8)。当然保险起见,最好不要出现这种情况。