《Linux操作系统分析》之分析计算机如何启动以及如何工作运行

来源:互联网 发布:多媒体电子教室软件 编辑:程序博客网 时间:2024/06/11 08:28
从最初的基于冯诺依曼思想:存储程序和顺序执行的原理开始,计算机经历的快速的发展。经历了单核到多核,单任务到多任务。指令级操作也有了单指令单数据、单指令多数据。但是对于理解计算机可以化繁为简,理解单任务后,再分析多任务。
计算机的启动
首先我们从计算机的启动开始说起。先问一个问题,”启动”用英语怎么说?回答是boot。可是,boot原来的意思是靴子,”启动”与靴子有什么关系呢? 原来,这里的boot是bootstrap(鞋带)的缩写,它来自一句谚语:"pull oneself up by one's bootstraps"字面意思是”拽着鞋带把自己拉起来”,这当然是不可能的事情。最早的时候,工程师们用它来比喻,计算机启动是一个很矛盾的过程:必须先运行程序,然后计算机才能启动,但是计算机不启动就无法运行程序!早期真的是这样,必须想尽各种办法,把一小段程序装进内存,然后计算机才能正常运行。所以,工程师们把这个过程叫做”拉鞋带”,久而久之就简称为boot了。
计算机的整个启动过程分成四个阶段:
第一阶段:BIOS
第二阶段:主引导记录
第三阶段:硬盘启动
第四阶段:操作系统
至此,全部启动过程完成。(该段来自阮一峰的一篇博客:计算机是如何启动的,每阶段启动详细内容请点击)
计算机的状态如下图所示:

 
 在启动之初我们需要第一条指令,然后一直等待输入指令。那么第一条指令是什么时候输入的那。在BIOS中电脑在固定位置被写入了第一条指令。当第一条指令加载后,就可以执行后续的行为。
计算机的工作
在这里我们用下面的简单代码进行分析。
int g(int x){return x+100;}int f(int x){return g(x);}int main(){return f(101)+110;}
然后在Linux环境下,生成汇编代码。先将所有指令放在这里,以便大家查看。

 
 
第一条:ls 列出主文件夹下内容。
第二条:cd code/…………/ 进入到我的实验的文件夹下。
第三条:touch main.c创建新的文件:main.c
第四条:gedit main.c 编辑文件main.c
第五条:gcc main.c -o main 编译main.c生成main.out
第六条:objdump -d main > main.txt 反汇编生成汇编代码,保存在main.txt中
第七条:gcc -E main.c -o main.i 输出test.i文件中存放着test.c经预处理之后的代码
第八条:gcc -S main.i -o main.s 生成汇编代码,保存在main.s中

在这里第五、六条指令和第七、八条指令的效果是一样的。但是第七、八条指令生成的代码中需要手动删去不需要的代码,而经第五、六条指令生成的汇编代码是不需要这一步操作的。抽出main函数做个比较:

经过七、八条指令生成的main函数的汇编码
 

 
经过第五、六条指令生成的main函数的汇编码
因此我建议使用第五、六条指令。
080483cb <g>: 80483cb:55                   push   %ebp 80483cc:89 e5                mov    %esp,%ebp 80483ce:8b 45 08           mov    0x8(%ebp),%eax 80483d1:83 c0 64           add    $0x64,%eax 80483d4:5d                   pop    %ebp 80483d5:c3                   ret    080483d6 <f>: 80483d6:55                   push   %ebp 80483d7:89 e5                mov    %esp,%ebp 80483d9:ff 75 08             pushl  0x8(%ebp) 80483dc:e8 ea ff ff ff       call   80483cb <g> 80483e1:83 c4 04           add    $0x4,%esp 80483e4:c9                   leave   80483e5:c3                   ret080483e6 <main>: 80483e6:55                   push   %ebp 80483e7:89 e5                mov    %esp,%ebp 80483e9:6a 65                push   $0x65 80483eb:e8 e6 ff ff ff       call   80483d6 <f> 80483f0:83 c4 04            add    $0x4,%esp 80483f3:83 c0 6e           add    $0x6e,%eax 80483f6:c9                   leave   80483f7:c3                   ret
以上是main函数、g函数、f函数的汇编码。
在分析程序运行之前,我们先介绍一些相关的知识。
一、过程
一个过程调用包括将数据(以过程参数和返回值的形式)和控制从代码的一部分传递到另一个部分。另外,它还必须在进入时为过程的局部变量分配空间,并在退出时释放这些空间。数据传递、局部变量的分和释放通过操纵程序栈来实现。
二、栈帧结构
机器用栈来传递过程参数、存储返回信息、保存寄存器用语以后恢复,以及本地存储。为单个过程分配的那部分栈称为栈帧(stack frame)。栈帧的结构如下图:

 
 栈帧的最顶端以两个指针界定,寄存器%ebp为帧指针,而寄存器%esp为栈指针。(参考深入理解计算机系统第三章3.7节)。
三、汇编指令
Call 地址:返回地址入栈(等价于“Push %eip,mov 地址,%eip”;注意eip指向下一条尚未执行的指令)
ret:从栈中弹出地址,并跳到那个地址(pop %eip) 
leave:使栈做好返回准备,等价于 mov %ebp,%esp, pop %ebp

此时我们可以分析程序的运行过程了。我们的分析用图文结合的方式解释(图在下方)。最开始时程序计数器中存放着main函数的首地址:0X080483e6 。即寄存器%eip中指向此处。然后开始顺序执行。
1.1、将%ebp的值压栈(%esp减4)。——————————————%ebp指向0的位置,%esp指向0的位置
1.2、将当前%esp赋值给%ebp。 ——————————————栈无变化
1.3、将0X65压栈(%esp减4)——————————————%ebp指向0的位置,%esp指向1的位置
1.4、使用call指令调用f函数。此时栈的变化是:将%eip(0X80483f0)压栈(%esp减4),将f的首地址(0X080483e6)赋值给%eip。
——————————————%ebp指向0的位置,%esp指向2的位置
到此跳转到f函数执行。
2.1、将%ebp的值压栈(%esp减4)——————————————%ebp指向0的位置,%esp指向3的位置
2.2、将当前%esp赋值给%ebp。——————————————%ebp指向3的位置,%esp指向3的位置
2.3、pushl 0x8(%ebp),此为间接寻址,将地址为%ebp+0X8的值取出压栈(即0X65压栈)。
——————————————%ebp指向3的位置,%esp指向4的位置
2.4、使用call指令调用g函数。此时栈的变化是:将%eip(0X80483e1)压栈(%esp减4),将g的首地址(0X80483cb)赋值给%eip。
——————————————%ebp指向3的位置,%esp指向5的位置
到此跳转到g函数执行。
3.1、将%ebp的值压栈(%esp减4)——————————————%ebp指向3的位置,%esp指向6的位置
3.2、将当前%esp赋值给%ebp。——————————————%ebp指向6的位置,%esp指向6的位置
3.3、mov 0x8(%ebp),%eax,此为间接寻址,将地址为%ebp+0X8的值取出给%eax(%eax=0X65)。
3.4、add $0x64,%eax,即实现101+100这个动作。然后存储在%eax中。
3.5、此处没有使用leave指令,是因为此时的%ebp与%esp的地址相同。使用pop %ebp将调用者的ebp恢复即可(%esp加4)
——————————————%ebp指向3的位置,%esp指向5的位置
3.6、返回调用者函数。此时栈的变化是:将%eip出栈(%esp加4),将g的首地址(0X80483cb)赋值给%eip。
——————————————%ebp指向3的位置,%esp指向4的位置
到此返回到f函数,执行f函数剩余的指令。
2.5、add $0x4,%esp,将esp加4。——————————————%ebp指向3的位置,%esp指向3的位置
2.6、此处使用leave指令。先将%ebp指向的地址给%esp,然后恢复%ebp(%esp加4)
——————————————%ebp指向0的位置,%esp指向2的位置
2.7、返回调用者函数。此时栈的变化是:将%eip出栈(%esp加4),将f的首地址(0X080483e6)赋值给%eip。
——————————————%ebp指向0的位置,%esp指向1的位置
到此返回到main函数,执行main函数剩余的指令。
1.5add $0x4,%esp,将esp加4。——————————————%ebp指向0的位置,%esp指向0的位置
1.6、此处使用leave指令。先将%ebp指向的地址给%esp,然后恢复%ebp(%esp加4)
1.7、返回调用者函数。此时栈的变化是:将%eip出栈(%esp加4)

  
 上面的过程就是程序执行的过程。
总结

这里是大概的解释了一下计算机的工作过程。实际中的计算机系统的工作比这种单任务的要复杂。在多任务的情况下,进程切换的时候需要保存上下文环境以及恢复进程,以及各进程的调度等等。但是本质上没有多大的区别。最后如果大家看到有错误的地方,请指出。大家相互学习,共同进步!谢谢!

备注

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

2 0