程峰 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
学过《计算机组成原理》的人都知道,计算机有两大典型的体系结构:冯诺依曼体系结构和哈弗体系结构。现在大多数计算机采用的都是冯诺依曼体系结构,它的基本思想是存储程序。一次取一条指令,执行完后继续取下一条指令。今天我们通过汇编一段简单的C代码,来向大家展示采用冯诺依曼体系结构的计算机是如何工作的。
我们使用的C代码如下:
int g(int x)
{
return x + 5;
}
int f(int x)
{
return g(x);
}
int main(void)
{
return f(4) + 2;
}
copy
为了方便(其实就是偷懒,嘿嘿),我使用的是实验楼上已经搭建好的实验环境(64位 Linux 虚拟机),采用如下命令,将C代码编译成汇编代码:
gcc -S -o main.s main.c -m32
copy
其中:
-S:表示生成汇编代码
-m32:表示编译成32位汇编程序
编译完成后,生成汇编代码文件 main.s,打开是这个样子的:
咦,怎么这么多以点开头的语句呢?其实这些以点开头的语句都是程序链接时的辅助信息,不会被程序实际执行,所以在这里我们可以把所有以点开头的语句都删了。
现在是不是感觉好多了?另外还要注意,Linux 采用的是 AT&T 汇编格式,它与 Intel 汇编格式略有不同,具体可以看这篇博客http://www.cnblogs.com/awpatp/archive/2009/11/11/1600763.html。
下面我们步入正题,详细解释这段汇编代码的执行过程,以及对应堆栈变化。
首先我们从main函数开始。前两条语句:
pushl %ebp
movl %esp, %ebp
copy
这两条语句也出现在所有函数的开头,表示先将栈基指针压栈保存,然后将栈顶指针赋给栈基指针。这样即保存了进入函数前的堆栈状态,同时对于当前函数来说可视为堆栈为空,分隔了各个函数在堆栈中的数据。函数调用堆栈是由逻辑上多个堆栈叠加起来的。
紧接着
subl %4, %esp
movl %4, (%esp)
copy
将栈顶指针减4(因为栈是由高地址向低地址延伸,所以入栈时栈顶指针是减,又由于是32位汇编程序,每个栈元素为4个字节,所以是减4),将局部变量4放入栈顶指针指向的位置。这两步操作就相当于把4压入栈中。
然后
call f
copy
调用函数 f,其实这条指令等价于
pushl %eip
movl $f, %eip
copy
先保存当前指令指针eip的值(其值为当前函数下一条要执行指令的地址),这样当函数调用返回时,能找到需要继续执行的指令。然后将函数 f 的起始地址赋给eip,从而跳转到函数 f。因为eip是不允许直接修改的(下文不再重复叙述此点,请读者注意),所以需要使用call指令间接修改,以达到相同的目的。
跳转到函数f后,前两条语句和 main 函数相同,都是保存堆栈状态。通过变址寻址,将 main 函数中保存的4赋给 eax,再将 eax 入栈,默认使用 eax 存储函数的返回值。再调用函数 g,所做的工作和call f
类似,不再叙述。
函数 g 中,也是通过变址寻址,将在函数 f 中压入栈中的4赋给 eax,然后将 eax 加5,作为函数返回值。因为此时栈顶保存的是函数 f 的栈基指针,所以执行popl %ebp
后,栈基指针指向了函数 f的栈基地址,堆栈又回到了调用函数 g 前的状态。指令ret
相当于指令popl %eip
,即将eip指向call g
指令的下一条指令。至此,函数 g 整个调用过程结束。
函数 g 返回后,函数 f 中的下一条指令是leave
,这条指令相当于下面两条指令:
movl %ebp, %esp
popl %ebp
copy
它将函数堆栈恢复到当前函数被调用前的状态。然后执行ret
指令,函数返回。
函数 f 返回后,函数 main 的下一条指令将 eax(此时 eax 保存的是在函数 g 中执行addl $5, %eax
的值,即9)中的值加2,即最后结果11,然后再leave
恢复堆栈状态,ret
返回。
至此,整段代码执行过程结束。
整个程序的运行过程其实就是每次从 eip 指向的地址取出一条指令执行,然后eip自增,再判断:
学习时间 26分钟
操作时间 15分钟
按键次数 789次
实验次数 2次
报告字数 2484字
是否完成 完成