现代计算机存储和处理的信息都是用二进制表示的,即0和1。
用多个二进制比特位的不同组合和不同解释能够表示数量有限的元素,比如用32位比特的组合则有2^32种可能,因此它能表示从0开始到4294967295 (2^32个)。
需要程序员关注的是三种二进制解释:1. 无符号二进制的组合解释;2. 有符号二进制的组合解释;3. 浮点数二进制的组合解释。
这些不同解释组合能够进行的操作也不一样,比如有符号和浮点数虽然逻辑上都能加减乘除,但CPU的具体硬件运算实现细节实现肯定不同。
由于是用多个二进制比特位的来进行组合的,因此其能够表示的数量是有限的。超出表示范围之后是什么行为?这种超出范围的描述称为:溢出。在C++中,无符号类型溢出表现是初始值对所有可能的值取模后的余数;有符号行为则不确定。
对于整数来说,其表示是精确的,假设1用0000 0000 0000 0000 0000 0000 0000 0001表示,那么0000 0000 0000 0000 0000 0000 0000 0001就是1,不会是2。
对于浮点数来说,其表示是近似值,因为某段范围内小数的个数是无穷的,无法完全表示,比如1和2之间的小数个数就不能完全表示出来。
操作系统为每个进程抽象了虚拟内存的概念,使得每个程序认为它获得到的内存是连续的。事实上,内存空间是不连续的。此外,操作系统还对内存空间进行划分,有些专门存储代码指令,有些专门存储数据信息,根据这些不同划分,操作系统可以附加一些措施,从而阻止缓冲区溢出攻击。例如,我们可以在C/C++中通过自行分配一段堆内存,然后在该堆内存上构建二进制指令,再嵌入汇编,使用jmp指令跳转到堆内存上执行构建的二进制指令。Linux和Windows通过使用一种叫做dep的技术来检查当前内存是用于存储数据的,不应该当做指令执行,从而阻止恶意代码。
在C/C++等编程语言中,虽然存取不同类型的变量都是通过访问变量名来实现的,但其底层实现并不相同,如下:
#include <iostream> int main() { int i; i = 5; short s; s = 6; return 0; }
通过反汇编,查看二进制机器码,可以发现不同类型的变量的读写用的是不同指令,对于int类型的局部变量的写入,只用了一条指令,而short类型的局部变量的写入,用了两条指令,先是写入到eax寄存器,然后再从eax的低位寄存器ax中写入到变量中。
从这里也可以联想到C/C++中的指针,为什么会有不同类型的指针,这是因为语言层面的类型指针在编译之后所生成二进制机器码不同,当然读写的字节数也不一样。