[音乐]
本周的主要内容是介绍高级语言程序的机器级表示,也就是对应的指令序列。
我们选用的指令系统是IA-32指令系统,所以上一周我们先介绍了IA-32指令系统,
讲解了IA-32的各种常用指令的功能。
在此基础上,本周将介绍基于IA-32的机器级表示。
下面开始第一讲:过程调用的机器级表示。
本讲主要介绍C语言程序中过程调用,也就是函数调用对应的机器级表示。
包括如何传递参数,如何将控制转移到被调用过程, 寄存器使用约定,递归函数的实现等等。
这部分内容较多,也比较难理解,我们分成了五段小视频进行讲解,
试图通过例子把过程调用的机器级表示相关内容解释清楚。
这里是一个例子,main呢,是调用函数, t1,
t2是两个局部变量。
这两个局部变量作为实参传递给了add函数, 在add函数当中执行求和,完了以后返回。
在这个里面我们要解决的问题是add过程调用
它对应的指令序列应该是什么?这是我们要解决的第一个问题。
第二个问题是说t1、 t2,也就是125、 80,这两个实参
如何传递给add这个函数当中的x、 y这两个形参的, 这两个参数是怎么传递。
第三个要解决的问题是执行的这个结果 返回给调用过程的时候,这个结果是如何返回给这个调用过程的。
这里的main就是调用函数,这里的add就是被调用函数。
也就是说,在这个过程当中,实际上main里面有一串指令序列是用来传递参数的。
有一条指令呢是用来控制执行的顺序,从main 转移到add,然后在add这个里面呢有
指令要取出参数,然后一串指令来执行过程体。
还要有返回结果,使得main能够取到这个结果,最后返回main。
在这个里面,调出add执行的这条指令, 它的目的地是add这个函数的首地址。
所以这个地方我们可以猜到是用call指令实现的。
call指令,我们前面讲过,它实际上是会把这个返回地址,也就是call指令下一条指-
令的地址 会放到栈里面,这样的话在add函数最后执行
return指令的时候,它可以从栈里面取出这个返回地址 使得它能够正确的返回到main执行。
我们可以看到 参数的传递,因为参数有可能比如说是两个,
三个四个,有可能很多,这些参数一定是要放到一个存储空间当中去。
这个存储空间就是栈,stack。
这个栈显然在一个存储空间当中。
可执行文件实际上它装入的时候是映射到一个存储空间
里面去的,比如说这些代码和数据都是放在不同的存储段里面,
一块存储区域里面,而栈呢,是整个这个存储空间当中的一块
区域,这块区域通常是在高地址当中,这个是个用户栈。
整个的这个地址空间分成操作系统的内核区和用户 进程的用户区。
在这个用户区当中,最高的地址部分就是栈区。
这个栈区它的栈底是一个确定的高地址,从高地址向低地址 增长。
也就是说我们往栈里面送东西,就压栈的时候,
是往低地址长的,出栈的时候再往高地址退,因此这个栈有一个栈顶,
这个栈顶的地址是放在专门的一个寄存器里面,叫栈指针 寄存器,IA-32里面的栈指针寄存器。
所以我们可以看到栈是在这个位置的。
那么刚才我们讲过了这个过程调用涉及到这样的一些过程,
在这个过程当中,第一步是放参数,存放参数,
P把这个入口参数放到Q能访问到的地方,也就是放到栈里面,
然后呢执行一个call指令,这个call指令会把返回地址压栈,
然后转移到被调用过程去执行。
然后在被调用过程当中第一件事情要保存P里面的现场, P的现场实际上就是P里面用到的通用寄存器的内容。
然后在add这个被调用过程里面,它如果有一些 静态的局部变量的话,那么这些局部变量也要分配空间。
分配好了以后就执行过程体,执行完了以后呢,再恢复现场,释放局部空间, 取返回地址返回。
在这个里面,前面 两个过程是P里面的指令完成的。
P里面的调用Q之前的最后一条指令就是call指令,这个地方就是call add,call
指令, 执行完了call指令以后,就到了被调用过程Q里面,
在Q里面一开始是准备阶段,准备阶段先要保存P里面的现场, 然后分配自己的空间,然后生成栈帧。
这些准备工作做好了以后就进行具体的处理。
具体处理以后,实际上这个返回值就放到专门的地方了。
这样的话呢P的现场就可以恢复了,因为我不再用寄存器了。
就把原来保存的寄存器的内容再放到原来的寄存器里面,并且释放存储空间。
最后呢,执行return指令,把返回地址取出来。
因此在这个地方就是把返回地址取出来以后,根据返回地址进行跳转,
又跳转到调用过程main执行。
那么这里的现场实际上就是通用寄存器的内容。
为什么要保存现场呢?是因为调用过程
和被调用过程,它们用的都是同一套寄存器, 所以它们是共享的。
在main里面,比如说用了某个寄存器,
这个寄存器执行完add以后还要用这个寄存器的话,那么在add里面就不能用这个寄存器。
如果你要用这个寄存器,就要先保存, 然后再恢复。
这个就跟妈妈和你做菜的时候, 共用同一套盘子是一样的情况。
如果在这个被调用过程里面要用到这个调用过程
当中用到的寄存器的话,被调用过程必须先保存, 再恢复。
这个寄存器的这个约定,到底 哪些是在调用过程用,哪些是被调用过程用。
它有一个约定,这种约定使得编译器 在分配寄存器的时候,因为大家有一个约定以后,
这个编译器呢,它就能够很好地对每一个过程或者函数分配这个 寄存器。
这里面的这个P实际上是调用过程,Q是被调用过程。
在P里面会执行一个call指令,call Q, call Q 以后就会执行Q了。
那么这个里面 寄存器的P和Q如何使用寄存器 它有一套约定。
这套约定就像妈妈和你做菜的时候,我们约定这个盘子是我用的, 那个盘子是你用的,一样的道理。
这个里面规定的是调用者P 保存的寄存器是这三个寄存器。
也就是说,这三个寄存器 因为在P里面保存了,所以Q可以直接使用。
可以随便用,用完了以后不需要恢复。
如果P在Q返回以后还要用的话,是P自己来保存。
这三个寄存器呢是被调用者保存寄存器, 也就是说在Q里面如果要用到这三个寄存器的话,
Q里面一定要先保存再用,用完了以后恢复。
IA-32里面有八个寄存器,还有两个寄存器
通用寄存器是EBP和ESP,这两个通用寄存器,
它是专用的,用来放帧指针和栈指针。
ESP,我们刚才看到了是栈指针寄存器,是放栈顶的。
EBP是放栈帧的底部的,是一个帧指针寄存器。
所以我们可以看到,为了减少 准备和结束阶段的开销,就是在这个Q里面
准备阶段要保存这些寄存器, 然后在结束阶段要恢复这些寄存器。
如果是用了这三个寄存器的话,则必须要保存 和恢复。
减少开销就是不执行这些指令, 为了不保存和不恢复,Q里面应该地尽量用EAX、
EDX、 ECX 用这三个寄存器,因为这三个寄存器用的时候可以随便用,不需要保存。
所以后面很多例子,任何一个过程总是先使用这三个寄存器,在不够的
情况下再使用这三个寄存器,而使用这三个寄存器的时候必须先保存 后恢复。
我们可以看这个过程,这是在 P里面调用Q,如果Q调用的函数当中
有这么多参数,那么在P这个调用过程当中它有一个栈帧, 这个叫stack
frame,在这个栈帧当中先要把这个参数放到这个栈帧里面去,
然后执行call指令,call指令会把返回值,
就call指令下面一条指令的返回地址压栈,然后再转到 被调用过程去执行。
所以在调用过程的栈帧当中, 有参数和返回地址。
我们前面讲过,这些寄存器当中如果有EAX,
ECX,EDX,在P调用完Q以后还要用的话,
那么这些寄存器必须在调用过程的栈帧当中保存,这个是必要的时候 才需要保存。
所谓必要,就是说,P调用完Q以后还要用这些寄存器, 那么这些寄存器就必须保存在这儿。
通常是不需要保存的。
调用完Q以后,在栈里面又长出来一个栈帧。
长出来一个frame, 这个frame就是过程Q,这个被调用过程 它的栈帧。
一开始都是在准备阶段 要保存P的现场。
P的现场包括在P里面的 EBP里面的值,以及P里面用到的那三个寄存器。
这三个寄存器就是EBX, ESI和 EDI。
这三个是被调用者保存的寄存器。
如果在Q 里面用到了这三个寄存器,那么这三个寄存器必须在这儿保存。
Q当中如果有一些非静态的局部变量的话,那还要在这儿分配空间。
最后执行完了以后,又退回到P这个栈帧的 顶部,所以这时候呢Q的栈帧就退回去了, 又回到这儿。
然后执行return指令的时候就可以取出返回地址, 然后再回到P执行。
这是第一步。
刚才我们讲了第一步,保存参数,然后呢,控制转移。
然后在Q里面执行,执行以后再返回,又返回到P里面。
刚才看到的这些都是在栈里面的变化过程。
栈在哪里呢,我们刚才 已经看到了,栈是在这个地址空间当中的高端部分,就是这个高地址部分, 站底是不动的。
这个栈帧,刚才我们看到的那个frame,是从这个 地方不断的长出来的,这个栈顶是ESP处的。
[音乐] [音乐]