目前电子计算机的结构还是以冯·诺依曼提出的以程序存储和程序控制为基础的,其核心是存储程序。
对于我们程序员写出的程序我们将它存储到内存里面,内存有很多区域,如代码区,数据区,堆栈区等等。我们的代码会存储到代码区中,cpu通过总线与存储器进行连接,cpu中有很多寄存器,在x86中,有一个指令寄存器%eip用来指向下一次执行指令的地址,通过cs和%eip中的内容可唯一指向程序指令的地址。对于我们编写的程序(高级语言),一般都是通过编译,链接两步执行的。高级语言写出来的程序首先汇编成汇编指令,汇编指令在经过编译器编译成目标代码(二进制代码),此时编译的代码还不能完全执行,因为这可能只是一个程序片段其中缺少了许多函数所必须的库,这时通过链接将所需要的程序和库链接起来放在内存中,cpu通过%eip指向的内容依次取指令,这样反复执行一个计算机程序就运行完毕了。
例如如下一个简单的c程序:
在gcc下汇编成汇编代码如下:
首先解释一下各个寄存器的含义:在x86中有很多寄存器,%eax累加寄存器,%ebx基地址寄存器,%ecx计数寄存器,%edx数据寄存器,%ebp堆栈基址针寄存器,%esp堆栈顶指针寄存器。
我们从main函数开始:
pushl %ebp;
首先将%ebp的内容压栈,栈顶指针向下移动4个字节,相当于
subl $4,%esp;
movl %ebp,(%esp);
在x86内存中栈是向下增长的。 其中$表示立即数,%表示寄存器,()表示寄存器中的内容。
movl %esp,%ebp;
将%esp中的内容赋值给%ebp。相当于堆栈基址针向下移动4个字节。
subl $4,%esp;
%esp中的内容减去4,也就是堆栈顶址针向下移动4个字节。
movl $9,(%esp);
将立即数9赋值给%esp所表示的内容。
call f;
调用f 函数;进入到函数f当中。
一个call命令的执行过程相当于先将当前cpu中%eip的内容压栈,然后将函数f的首地址赋值给%eip,这样就完成了函数的调用。
f:
pushl %ebp;
movl %esp,%ebp;
subl $4,%esp;
movl 8(%ebp),%eax; 将%ebp中的内容加上8然后在赋值给%eax
movl %eax,(%esp); 将%eax值赋值给%esp
call g; 调用g函数;进入到函数g当中。
g:
pushl %ebp;
movl %esp,%ebp;
movl 8(%ebp),%eax;
addl $4,%eax; 将%eax中内容加上立即数4
popl %ebp ; 将%ebp中内容出栈
一条popl指令相当于如下两条指令:
movl (%esp),%ebp;
addl $4,%esp;
ret;返回原地址,效果相当于popl %eip;
函数f和main函数类似:
leave指令相当于如下指令:movl%ebp,%esp;
popl%ebp;
在每一个函数结束前都会有leave和ret指令,当函数内没有参数传递时,leave指令可以省略。
最后总结一下程序运行的过程:
-
调用其它函数时,将指令指针入栈保存,以便函数执行结束能返回来继续下一条指令的执行;
-
被调用函数执行时,要将当前栈基地址压栈,以便调用结束后能恢复到调用函数栈空间;
-
函数参数入栈,参数入栈顺序是从右到左进栈;
-
函数退出时,将 %esp 赋值为 %ebp,释放当前函数所使用的栈空间;
-
然后将栈顶元素出栈保存到 %ebp,把%ebp恢复到调用函数(前一个函数)的栈基地址;
-
%eip退回到上一个函数即将要执行的那条语句的地址上。