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

从最初的 基于冯诺依曼思想:存储程序和顺序执行的原理 开始,计算机经历的快速的发展。经历了单核到多核,单任务到多任务。指令级操作也有了 单指令单数据、 单指令多数据。但是对于理解计算机可以化繁为简,理解单任务后,再分析多任务。
计算机的启动
首先我们从计算机的启动开始说起。先问一个问题,”启动”用英语怎么说?回答是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                   	ret
080483e6 <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函数的首地址:0X 080483e6 。即寄存器%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(0X 80483f0 )压栈 (%esp减4) ,将f的首地址(0X 080483e6 )赋值给%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(0X 80483e1 )压栈 (%esp减4) ,将g的首地址(0X 80483cb)赋值给%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的首地址(0X 80483cb)赋值给%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的首地址(0X 080483e6 )赋值给%eip。
—————————————— %ebp指向0的位置,%esp指向1的位置
到此返回到main函数,执行main函数剩余的指令。
1.5 add $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

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值