文章目录
- 指令篇
- 1. 从高级语言到机器指令
- 1.1 CPU的作用
- 1.2 代码如何变为机器码
- 1.3 指令的分类
- 2. 指令跳转
- 2.1 CPU 是如何执行指令
- 2.2 条件和循环的本质
- 3. 函数调用
- 3.1 栈的作用
- 3.2 Stack Overflow
指令篇
1. 从高级语言到机器指令
计算机或者说CPU本身并没有能力去理解这些高级语言,其只能识别由0、1组成的机器语言。
1.1 CPU的作用
CPU 是计算机的大脑。全称是 Central Processing Unit,即中央处理器。
从硬件角度来看,CPU 就是一个超大规模集成电路,通过电路实现了加法、乘法乃至各种各样的处理逻辑。
从软件工程师的角度来讲,CPU 就是一个执行各种计算机指令的逻辑机器。这些计算机指令也就是机器语言。
不同的CPU 能够听懂的语言不太一样,这些“语言”也就是计算机指令集(Instruction Set)。
一个计算机程序,不可能只有一条指令,而是由成千上万条指令组成的。但是 CPU 里不能一直放着所有指令,所以计算机程序平时是存储在存储器中的。这种程序指令存储在存储器里面的计算机,我们就叫作存储程序型计算机(Stored-program Computer)。
1.2 代码如何变为机器码
程序首先被翻译成一个汇编语言,这个过程叫编译(Compile)。
然后针对汇编代码,再用汇编器翻译成机器码,这一条条机器码,就是一条条的计算机指令。
1.3 指令的分类
常见指令可以分为五大类:
- 算术类指令。我们的加减乘除,在CPU 层面都会变成一条条算术类指令。
- 数据传输类指令。给变量赋值、在内存里读写数据,用的都是数据传输类指令。
- 逻辑类指令。逻辑上的与或非,都是这一类指令。
- 条件分支类指令。日常我们写的if / else,都是条件分支类指令。
- 无条件跳转指令。在调用函数的时候,其实就是发起了一个无条件跳转指令。
2. 指令跳转
2.1 CPU 是如何执行指令
逻辑上,我们可以认为,CPU 其实就是由寄存器组成的。二寄存器就是 CPU 内部,由多个触发器或者锁存器组成的简单电路。
常见的三种寄存器:
- PC寄存器(Program Counter Register)。也叫指令地址寄存器。起作用就是用来存放下一条需要执行的计算机指令的内存地址。
- 指令寄存器(Instruction Register)。用来存放当前正在执行的指令。
- 条件码寄存器(Status Register)。用里面的一个个标记位(Flag)来存放 CPU 进行算术或者逻辑计算的结果。
除了这些特殊的寄存器,CPU 里面还有更多用来存储数据和内存地址的寄存器。这样的寄存器通常一类里面不止一个。我们通常根据存放的数据内容来给它们取名字,比如整数寄存器、浮点数寄存器、向量寄存器和地址寄存器等等。有些寄存器既可以存放数据,又能存放地址,我们就叫它通用寄存器。
实际上,一个程序执行的时候,CPU 会根据 PC 寄存器里的地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,开始顺序读取下一条指令。可以看到,一个程序的一条条指令,在内存里面是连续保存的,也会一条条顺序加载。
而跳转指令会修改 PC 寄存器里面的地址值。这样,下一条要执行的指令就不是从内存里面顺序加载的了。
2.2 条件和循环的本质
除了简单地通过 PC 寄存器自增的方式顺序执行外,条件码寄存器会记录下当前执行指令的条件判断状态,然后通过跳转指令读取对应的条件码,修改 PC 寄存器内的下一条指令的地址,最终实现 if…else 以及 for/while 这样的程序控制流程。
if…else…、for/wihile本质上都是和goto相同的跳转到特定指令位置的方式来实现的。
想要在硬件层面实现这个 goto 语句,除了本身需要用来保存下一条指令地址,以及当前正要执行指令的 PC 寄存器、指令寄存器外,只需要再增加一个条件码寄存器,来保留条件判断的状态。这样简简单单的三个寄存器,就可以实现条件判断和循环重复执行代码的功能。
3. 函数调用
3.1 栈的作用
栈是一种后进先出(LIFO)的数据结构,是内存的一段空间。栈就像一个乒乓球桶,每次程序调用函数之前,我们都把调用返回后的地址写在一个乒乓球上,然后塞进这个球桶。这个操作其实就是我们常说的压栈。如果函数执行完了,我们就从球桶里取出最上面的那个乒乓球,很显然,这就是出栈。
整个函数所占用的内存空间,就是函数的栈帧(Stack Frame)。rbp 是 register base pointer 栈基址寄存器(栈帧指针),指向当前栈帧的栈底地址。rsp 是 register stack pointer 栈顶寄存器(栈指针),指向栈顶元素。
通过加入了栈,相当于在指令跳转的过程中,加入了一个“记忆”的功能,能在跳转去运行新的指令之后,再回到跳出去的位置,能够实现更加丰富和灵活的指令执行流程。
3.2 Stack Overflow
通过引入栈,可以看到,无论有多少层的函数调用,或者在函数 A 里调用函数 B,再在函数 B 里调用 A,这样的递归调用,我们都只需要通过维持 rbp 和 rsp,这两个维护栈顶所在地址的寄存器,就能管理好不同函数之间的跳转。不过,栈的大小也是有限的。如果函数调用层数太多,我们往栈里压入它存不下的内容,程序在执行的过程中就会遇到栈溢出的错误,这就是“stack overflow”。