5.1 程序执行的概述
CPU 的执行过程包含以下几个步骤:取出指令、指令译码、指令执行。
(1)取出指令:在CPU 能够执行某条指令之前,它必须将这条指令从存储器中取出来,此时即将执行的指令地址就存放在程序计数器PC中,于是CPU 根据PC 地址找到主存中对应的单元,取出相应的指令。
(2)指令译码:当CPU 把一条指令取出来后,放入IR(指令寄存器)中,然后对IR 中的指令操作码进行译码。不同的指令其功能不同,也就是说不同的指令的操作不同。对于复杂指令集,操作码的长度也是不一样的,比如有的指令的高5位是操作码,有的指令的高3位是操作码。
(3)指令执行:
①取源操作数:控制器根据指令的地址码字段提供的寻址方式确定源操作数地址的计算方式,从而得到源操作数的地址(可能是存储器、寄存器,或指令本身),进而取出对应的源操作数。
②执行指令:按照指令操作码字段执行对应的操作,如加法、减法等。
③存储目的操作数:把执行的结果存储在目的操作数对应的地址中,在计算目的操作数的地址时,同样要根据该条指令的地址码字段提供的寻址方式确定目的操作数地址的计算方法,从而得到目的操作数的地址(可能会是存储器、寄存器中)存储对应的目的操作数的地址中。
④修改下一条指令地址:如果是顺序执行,下条指令地址的计算就比较简单,只需要将PC加上当前指令的长度即可;如果是跳转指令时,则需要根据条件标志、操作码和寻址方式等确定下条指令的地址。
在第4章4.1.3节中我们举了一个C语言的例子来说明程序转换的过程,本节继续使用这个C语言例子,具体说明上述程序和指令的执行过程。
我们采用第4章使用的C编译器gcc(本书采用Windows下的gcc编译器Min GW)将程序转换为可执行文件hello.exe:
然后我们使用objdump的反汇编命令:
通过这个命令可以得到main函数对应的一段输出结果:
这个结果与4.1.3节中通过对hello.o反汇编的结果进行比较:
我们发现,两者的结果差不多,只是在可执行文件hello.exe反汇编的结果中,左边的地址不再是从0开始,其中main函数对应的指令序列从存储单元401460 H 开始存放。这是因为hello.o是可重定位目标文件,因而目标代码从相对地址0开始,冒号前面的值表示每条指令相对于起始地址0的偏移量。可执行文件hello.exe的代码是在操作系统规定的虚拟地址空间产生的。另外,可重定位文件是由单个模块生成的,而可执行文件是由多个模块组合而成的。从可重定位目标文件到可执行文件需要经过链接器进行链接,链接的具体过程本书不做具体介绍。
在上述反汇编的结果中,总共有三列结果,最左边的一列从401460 H 到401480 H 为机器指令在内存中存放的地址,中间的一列为十六进制表示的机器指令,最右边的一列为每条机器指令对应的汇编指令。
并且,从上述指令可以看出,该函数对应的指令是存放在地址401460 H 开始的存储空间,每条指令的长度是不同的,如第一条指令对应的机器代码为55H,即长度为1个字节,第二条指令对应的机器代码为89 H、e5 H,即长度为2 个字节,而第三条指令为83 H、e4 H、f0 H,即长度为3个字节。
另外,每条指令的功能也不一样,如第一条指令的功能是压栈,第二条指令的功能是传送,第三条指令的功能是做加法。
然后,我们再来观察每条指令的操作码和地址码部分。如第一条指令“push ebp”,对应的机器码是55H=01010101B,其中高5位01010是该条指令的操作码,即执行压栈操作,后三位101是地址码,指明源操作数的地址为寄存器EBP。
最后,我们从第一条指令说明指令执行的过程。
取指令:对于main函数的执行,最开始时,PC 中存放的是首地址401460H,说明下一条即将执行的指令的地址就是401460H,于是CPU 根据PC 的值,从主存401460H 位置处取出对应的指令,假设每次总是读取最长指令字节数,假定最长字节数是4个字节,于是,从401460H 开始选取4个字节到IR 中,将55H、89H、E5H、83H 都送到IR 中。
指令译码:刚刚已经说过了,不同的指令操作是不一样的,比如第一条指令是执行压栈操作,第二条指令是执行传送操作,第三条指令是执行减法操作,这都是由指令的操作码译码得到的。
指令的执行:比如在执行第二条指令“mov%esp,%ebp”时,首先计算源操作数的地址,这里的源操作数就是寄存器esp,目的操作数就是寄存器ebp,指令的功能就是将esp寄存器中的内容送到目的操作数的地址ebp中。
然后重新计算下一条指令的地址,由于是顺序执行,只需要将PC的值加上当前指令的长度即可,由于“mov%esp,%ebp”是2个字节,即PC+2,就能得到下一条指令的地址,即401461+2=401463,这便是下一条指令“and xfffffff0,%esp”的地址。然后重复刚才的取指令、指令译码和指令执行的操作,直到将函数的所有指令执行完成。