WARNING这是《深入理解计算机系统》(CS
)的笔记,彼时还未上CO,OS等计算机系统基础课,存在理解不到位与错误之处.
一、程序运行栈与调用指令call
函数等过程的调用关系是用一个栈维护的,这个栈用寄存器 %rsp 存储栈顶指针(有了栈顶指针,就可以通过+字节数访问栈中任意元素)。栈的实体在内存中,里面可以是各种变量,可以是指向某个指令(代码)行数的“地址”。每一个过程在返回前都在这个栈中(实体栈,即内存中)有一定的空间,成为这个过程的帧,每个栈帧存储着关于本过程的一些信息,包括要传递给将要调用的函数的部分参数、调用结束后返回时立刻要执行的下一条指令的位置(给PC,程序计数器),局部变量,等等。
call 指令是调用函数等过程的指令:
call Label 或者
call *Operand 表示从程序中读取某个地址
它的效果是,将程序计数器PC下一条要执行的指令填入为将要调用的函数的位置(即 Label 后紧接着的第一条指令),将本函数中,执行完调用后面第一条语句(即返回后要执行的第一条语句)的地址压入栈中。
每个函数对应的栈帧有确定的结构:
栈底 |
---|
保存的父函数寄存器中的值,本函数返回前要将它们复原 |
本函数的局部变量(无法存储在寄存器中的部分和特殊部分) |
———————————————————————————— |
要传给本函数所调用的函数的参数n |
要传给本函数所调用的函数的参数n-1 |
… |
要传给本函数所调用的函数的参数7 |
———————————————————————————— |
即将调用新函数时,压入的返回地址 |
上面那行就是栈顶 |
二、寄存器的分工
寄存器有确定的规定与分工。
寄存器名称 | 作用 |
---|---|
%rax | 存返回值,父函数直接从此读取返回值 |
%rsp | 运行栈顶指针 |
%rdi | 调用函数时的第一个参数 |
%rsi | 调用函数时的第二个参数 |
%rdx | 调用函数时的第三个参数 |
%rcx | 调用函数时的第四个参数 |
%r8 | 调用函数时的第五个参数 |
%r9 | 调用函数时的第六个参数 |
%r10 | 调用者保存(后面解释) |
%r11 | 调用者保存(后面解释) |
其它 | 被调用者保存 |
大致可以猜测编译器如何生成机器代码,实现过程的调用、返回等。
三、转移控制
从当前过程转移到调用的过程,即call指令。
四、参数传递
寄存器中能够存储6个参数,多的要放到该函数的栈帧中,(注意这参数属于调用者的栈帧,而不是被调用者),这要改变%rsp的值来扩大栈帧,分配空间。其中参数的顺序和第一个表中的顺序严格一致。
注意,被调用的过程只是接受了这些数据,而不知道这些数据是什么类型,所有参数不管是什么类型在栈帧中都占 8 字节。
五、局部变量
有时候局部变量在寄存器中存不下,或者有取地址运算符这个变量必须有个地址因而必须在主存中,或者如数组、结构等需要能从对应数据结构引用相应的值(涉及到地址的加减),这些值必须存在主存中。编译器的做法就是将他们压入运行栈中本函数对应的栈帧内。
此时,变量在栈中占用的字节长度可以完全和其数据类型一致。
对应第一个表中“局部变量”区域。
六、局部存储
过程的调用不停套娃,但是寄存器共用一套。一些过程自己的局部变量存在寄存器中,如果直接放在寄存器中被子函数修改,程序就乱套了。因而有一套固定的规则将这些寄存器上的变量放内存中暂时存储(仍是运行栈),然后在返回前取回,恢复原样。
调用者保存寄存器 这些寄存器上的值需要让调用者(父函数)保存到主存中,然后才调用子过程,调用完返回后,父过程自己将它们从栈中取回,恢复原样。
被调用者保存寄存器 这些寄存器上的值是服务与父过程的(并不是参数),子过程要么完全不动这些寄存器,要么先把这些寄存器上的值放入自己的栈帧中,然后动用这些寄存器。运行结束后,把这些值从内存(栈)中取回,恢复原样后,返回。
这一部分存储,对应表一中的第一行。
所有局部变量,包括声明的数组等,都和函数返回地址等信息放在同一个运行栈帧中,所以有缓冲区溢出会导致覆盖重要信息的漏洞,它们实际上在一片连续的空间里。
C标准库函数gets,strcpy,strcat,sprintf 等都不检查溢出。
值得一提的是,递归函数常常在返回过程中仍要访问自己原来的参数,而存储属于自己的参数的寄存器当然要被修改以进行下一次递归调用,自己的原参数往往被转移到一个被调用者保存(如%rbx) 的寄存器中,由子过程保存。
七、执行一个有调用有返回的过程
1)真正执行本过程前,先将 被调用者保存 寄存器中的值对应放入栈帧中保存(如果完全不动这些寄存器,这步可以没有)
2)执行本过程,包括调用、转移参数,“创建”局部变量等过程。
3)调用子过程准备:将 调用者保存 寄存器中的值放入自己的栈帧中。
4)构造参数,在寄存器中与在栈帧中。
5)call: 栈顶加入下一条指令的返回地址,进入子过程,栈也进入下一个栈帧。
6)执行子过程,完毕后,从子过程返回。
7)从自己的栈帧中读取 调用者保存 的寄存器中的值,放回原位。
8)继续执行自己的过程…可以从%rax读取子过程的返回值。
9)返回准备:将返回值放到%rax中。
10)返回准备:将1)放在自己栈帧中的 被调用者保存 寄存器中的值取出来,放回原位。
11)返回,ret.
对于所有过程,包括各种递归,都适用!