寄存器
寄存器是 CPU 内部用来存放一些关键的数据的小型存储区域,用来暂时存储参与运算的数据和运算结果
寄存器有很多,axbxcxdx…与函数调用有关(最重要)的寄存器有两个:rsp(stack pointer)和 rbp(base pointer)
最前面的字母表示大小(r64e32 无 16),如果是 1byte 的还有特殊名字(不过一般用不到)故适配 32or64 我们直接用后面两个字母方便表示就行了
要了解它们运作的原理,还需要先了解栈这个数据结构
栈
栈是一种抽象的结构,它的作用是储存元素和读取元素,但是它的存储和读取是有顺序的,并且是单向的
就像一叠扑克牌,规定了只能从最上面拿牌和取牌
正是这种严格的顺序性,完美的符合了函数调用这种需要保持当前状态调用新的函数并在调用完函数后能恢复原来状态的性质
栈区
所以程序运行的过程中总是维护一个充满调用的函数地址和局部变量的巨大的栈,这个内存区域被称为栈区
在一般的程序中,栈区中的栈总是向下生长的,也就是新的元素总是被加载到更低的地址上,这和一般如字符串等常见数据放置的方式不一样,需要注意(下文会有用)
栈区的常见操作
-
push
为栈增加一个元素,相当于从扑克牌堆最上方放一张牌
-
pop
为栈减去一个元素,相当于从扑克牌最上方拿走一张牌
但是这个减去元素一定需要更改内存的数据(清零)吗?不必要,只要标记了这个栈的顶部(rsp),然后再需要 pop 的时候让 rsp 标记移动就可以了
前面提到过栈区中栈顶是低地址,所以 rsp 增加,栈 pop:rsp 减少,栈 push
函数栈帧
下面是一个示例
当 CPU 再执行 Add 函数的时候,会从代码区中 main 函数对应的机器码的区域跳转到 Add 函数对应的区域,并在那里执行;当遇到 return 时,又会跳回到 main 函数 Add 后面的指令继续执行 main 函数
这里有两个难点,一是怎么跳转保证正确性和便利性,二是怎么保持并恢复调用者函数的局部的环境
有一个方法可以一次性解决这两个问题,那就是用栈帧储存状态
当函数被调用时,系统栈会为这个函数开辟一个新的栈帧,并把它压入栈中。这个栈帧中的内存空间被它所属的函数独占,正常情况下是不会和别的函数共享的。当函数返回时,系统栈会弹出该函数所对应的栈帧。
每一个函数独占自己的栈帧空间
当前正在运行的函数的栈帧总是在栈顶
sp 和 bp 之间的内存空间为当前栈帧,sp 标识了当前栈帧的顶部,bp 标识了当前栈帧的底部
汇编实现函数栈帧
调用新函数的过程:
-
参数入栈 函数需要参数的话,就把一些数据放到寄存器里,放不下了就压到栈里(mov)(sp 到 1)
-
call 函数
-
把当前指令的地址入栈 保存返回的地址 (sp 到 2)
-
获取调用的函数的地址 讲程序跳转到该地址上
-
-
栈帧调整
-
保存当前的状态(bp 入栈) (sp 到 3)
-
切换成新的栈帧 (sp 赋值给 bp)(bp 指到 3)
-
分配新栈帧空间 (sp 减去所需空间大小)(sp 指到 5)
-
高地址 -------------------------------------------------------⇒ 低地址
可能要传的参数 -----return 的地址 ---- 前面栈帧的 bp----- 分配的新空间 -----(新 sp 所指位置)
----------------------(call 产生的) (新 bp 所指位置)
1------------------------- 2 -----------------3-------------------- 4 ---------------------5
函数返回的过程:
-
保存返回值 (如果是 int 则存入 ax)
-
弹出当前栈帧
-
给 sp 加上栈帧的大小,回收当前栈帧的空间 (sp 到 3)
-
一个 pop 操作,让栈上储存的前面栈帧的 pb 回到 bp 寄存器上 (sp 到 2)
-
一个 pop 操作,让栈上存储的 return 地址弹到 bi 寄存器上 (看环境是不是认 bi 作为跳转还是最近栈顶跳转,我这里好像不用这步)
-
-
跳转 retn 程序继续执行返回地址后的指令
常见结构:
一般不能破坏这个相对完整的结构,比如说在 add 之后才进入