计算机语言发展

时间:2022-12-03 03:36:03

计算机真正能够理解的是低级语言,因为 CPU 只负责计算,本身不具备智能。你输入一条指令(instruction),它就运行一次,然后停下来,等待下一条指令。这些指令都是二进制的,称为操作码(opcode),比如加法指令就是00000011。最初,编写程序就是手写二进制指令,然后通过各种开关输入计算机,比如:要做加法了,就按一下加法开关。后来,发明了纸带打孔机,通过在纸带上打孔,将二进制指令自动输入计算机。

为了解决二进制指令的可读性问题,工程师将那些指令写成了八进制。二进制转八进制是轻而易举的,但是八进制的可读性也不行。很自然地,最后还是用文字表达,加法指令写成 ADD。内存地址也不再直接引用,而是用标签表示。这样的话,就多出一个步骤,要把这些文字指令翻译成二进制,这个步骤就称为汇编(assembling),完成这个步骤的程序就叫做 汇编(assembler)。它处理的文本,自然就叫做汇编代码(aseembly code)。标准化以后,称为汇编语言(assembly language),缩写为 asm。对于人类来说,二进制程序是不可读的,根本看不出来机器干了什么。为了解决可读性的问题,以及偶尔的编辑需求,就诞生了汇编语言。汇编语言就是低级语言,直接描述/控制 CPU 的运行。如果你想了解 CPU到底干了些什么,以及代码的运行步骤,就一定要学习汇编语言。

寄存器

CPU 本身只负责运算,不负责储存数据。数据一般都储存在内存之中,CPU 要用的时候就去内存读写数据。但是,CPU 的运算速度远高于内存的读写速度,为了避免被拖慢,CPU 都自带一级缓存和二级缓存。但是,CPU 缓存还是不够快,另外数据在缓存里面的地址是不固定的,CPU 每次读写都要寻址也会拖慢速度。因此,除了缓存之外,CPU 还自带了寄存器(register),用来储存最常用的数据。也就是说,那些最频繁读写的数据(比如循环变量),都会放在寄存器里面,CPU 优先读写寄存器,再由寄存器跟内存交换数据。寄存器不依靠地址区分数据,而依靠名称。每一个寄存器都有自己的名称,我们告诉 CPU 去具体的哪一个寄存器拿数据,这样的速度是最快的。有人比喻寄存器是 CPU 的零级缓存。寄存器只能存放很少量的数据,大多数时候,CPU 要指挥寄存器直接跟内存交换数据。

早期的 x86 CPU 只有8个寄存器,而且每个都有不同的用途。现在的寄存器已经有100多个了,都变成通用寄存器,不特别指定用途了,但寄存器的名字都被保存了下来。
eax,ebx,ecx,edx,edi,esi,ebp,esp,其中 esp 有特定用途,用于保存当前栈(Stack)的地址。

我们常常看到 32位 CPU、64位 CPU 这样的名称,其实指的就是寄存器的大小。32 位 CPU 的寄存器大小就是4个字节,64位就是8个字节。所以,64位操作系统是需要64位 CPU 支持。


内存模型:堆(Heap)

程序运行时,操作系统会给它分配一段内存,用来储存程序和运行产生的数据。这段内存有起始地址和结束地址,比如:从 0x1000 到 0x8000,起始地址是较小的那个地址,结束地址是较大的那个地址。
程序在运行中,对于动态的内存占用请求(比如:新建对象,或 malloc 命令),系统就会从预先分配好的那段内存之中,划出一部分给用户,具体规则是从起始地址开始划分(实际上,起始地址会有一段静态数据,这里忽略)。举例来说,用户要求得到10个字节内存,那么从起始地址 0x1000 开始给他分配,一直分配到地址 0x100A,如果再要求得到22个字节,那么就分配到 0x1020。这种因为用户主动请求而划分出来的内存区域,叫做堆(Heap)。它由起始地址开始,从低位(地址)向高位(地址)增长。Heap 的一个重要特点就是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。

 计算机语言发展

 

内存模型:栈(Stack)
简单说,Stack 是由于函数运行而临时占用的内存区域。

计算机语言发展


请看下面的例子。

int main() {

    int a = 2;
    int b = 3;
    return add_a_and_b(a, b);

}

 

计算机语言发展

上面代码中,系统开始执行main函数时,会为它在内存里面建立一个帧(frame),所有 main 的内部变量(a 和 b )都保存在这个帧里面。main 函数执行结束后,该帧就会被回收,释放所有的内部变量,不再占用空间。
main 函数内部调用了 add_a_and_b 函数。执行到这一行的时候,系统也会为add_a_and_b新建一个帧,用来储存它的内部变量。也就是说,此时同时存在两个帧:main和add_a_and_b。一般来说,调用栈有多少层,就有多少帧。等到add_a_and_b运行结束,它的帧就会被回收,系统会回到函数main刚才中断执行的地方,继续往下执行。通过这种机制,就实现了函数的层层调用,并且每一层都能使用自己的本地变量。

所有的帧都存放在 Stack,由于帧是一层层叠加的,所以 Stack 叫做栈。生成新的帧,叫做"入栈",英文是 push;栈的回收叫做"出栈",英文是 pop。Stack 的特点就是,后进先出,因为最内层的函数调用,最先运行结束,每一次函数执行结束,就自动释放一个帧,直到所有函数执行结束,整个 Stack 就都释放了。
Stack 是由内存区域的结束地址开始,从高位(地址)向低位(地址)分配。比如,内存区域的结束地址是0x8000,第一帧假定是16字节,那么下一次分配的地址就会从0x7FF0开始;第二帧假定需要64字节,那么地址就会移动到0x7FB0。


堆(Heap)和 栈(Stack)的区别
它们都是存放数据的一块内存空间。它们主要区别是:stack是有结构的,每个区块按照一定次序存放,可以明确知道每个区块的大小;heap是没有结构的,数据可以任意存放。因此,stack的寻址速度要快于heap。

一般来说,每个线程分配一个stack,每个进程分配一个heap,也就是说,stack 是线程独占的,heap 是线程共用的。此外,stack 创建的时候,大小是确定的,数据超过这个大小,就发生 stack overflow 错误,而heap的大小是不确定的,需要的话可以不断增加。根据上面这些区别,数据存放的规则是:只要是局部的、占用空间确定的数据,一般都存放在 stack 里面,否则就放在 heap 里面。

请看下面这段代码

//整个 Method1 方法会在 stack 里面创建一个帧
public void Method1(){

    //i 的值是整数,占用空间大小确定,而且是局部变量,所以放在 stack 中。
    int i=4; 

    //obj 是局部变量,类型为指针,占用空间大小确定,指向一个对象的实例,所以放在 stack 中。 
    //这里同时产生的对象实例( classA Object),且无法确定其所占用的内存空间大小,因此对象实例存放在堆中 。
    classA obj = new classA();

}

 

 来源: http://www.ruanyifeng.com/blog/2018/01/assembly-language-primer.html