从最初的
基于冯诺依曼思想:存储程序和顺序执行的原理
开始,计算机经历的快速的发展。经历了单核到多核,单任务到多任务。指令级操作也有了
单指令单数据、
单指令多数据。但是对于理解计算机可以化繁为简,理解单任务后,再分析多任务。
栈帧的最顶端以两个指针界定,寄存器%ebp为帧指针,而寄存器%esp为栈指针。(参考深入理解计算机系统第三章3.7节)。
上面的过程就是程序执行的过程。
计算机的启动
在启动之初我们需要第一条指令,然后一直等待输入指令。那么第一条指令是什么时候输入的那。在BIOS中电脑在固定位置被写入了第一条指令。当第一条指令加载后,就可以执行后续的行为。
首先我们从计算机的启动开始说起。先问一个问题,”启动”用英语怎么说?回答是boot。可是,boot原来的意思是靴子,”启动”与靴子有什么关系呢? 原来,这里的boot是bootstrap(鞋带)的缩写,它来自一句谚语:"pull oneself up by one's bootstraps"字面意思是”拽着鞋带把自己拉起来”,这当然是不可能的事情。最早的时候,工程师们用它来比喻,计算机启动是一个很矛盾的过程:必须先运行程序,然后计算机才能启动,但是计算机不启动就无法运行程序!早期真的是这样,必须想尽各种办法,把一小段程序装进内存,然后计算机才能正常运行。所以,工程师们把这个过程叫做”拉鞋带”,久而久之就简称为boot了。
计算机的整个启动过程分成四个阶段:
第一阶段: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)。栈帧的结构如下图:
三、汇编指令
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