Linux内核分析 实验一 计算机工作过程分析

程峰 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

计算机是如何工作的?

学过《计算机组成原理》的人都知道,计算机有两大典型的体系结构:冯诺依曼体系结构和哈弗体系结构。现在大多数计算机采用的都是冯诺依曼体系结构,它的基本思想是存储程序。一次取一条指令,执行完后继续取下一条指令。今天我们通过汇编一段简单的C代码,来向大家展示采用冯诺依曼体系结构的计算机是如何工作的。


汇编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自增,再判断:

  • 若遇到跳转语句(JMP 等)时,先将 eip 压栈保存,即当前指令的下一条指令的地址,然后将需要跳转的目的地址赋给 eip,实现跳转;
  • 若遇到函数调用时,除了将 eip 压栈,还要保存当前堆栈的状态,将栈基指针 ebp 压栈,然后将相应函数地址赋给 eip,实现跳转;
  • 若为普通指令,则继续从 eip 指向的地址取出下一条指令执行。
最新评论
暂无评论~