大家好,我是小风哥,今天聊聊程序是怎么一步步运行起来的。
第一步我们需要知道到底什么是可执行程序,所谓可执行程序就是一个保存一系列机器指令的文件:
图片
chrome.exe就是上千万上亿条指令组成的一个普通文件,和你写的txt文件没有任何本质的区别,只不过txt文件中的内容是给人看的,而可执行程序中的内容是给CPU执行的。
图片
现在有了可执行程序,接着我们来运行它,运行一个程序很简单,双击图标或者在命令行中运行命令:
图片
这一步发生了什么?
我们已经知道可执行程序其实就是一个文件,文件是保存在磁盘上的。
当我们双击或者在命令行中运行命令后,第一件事就要找到可执行文件保存在了磁盘的哪个位置:
图片
谁来完成这件事?答案就是操作系统。
实际上操作系统也是一个程序,操作系统是管理我们写的程序的程序。
操作系统在文件系统的帮助下找到可执行程序,接下来操作系统开始解析可执行程序,实际上可执行程序中并不只包含机器指令,这里还有很多其它信息,在Linux下可执行程序一般遵循ELF文件格式:
图片
根据可执行程序的格式操作系统就能找到机器指令或程序运行依赖的全局变量等信息保存在了文件的哪个位置。
既然操作系统已经识别出了可执行程序,接下来就是重要的一步:加载,load。
所谓加载就是把磁盘上可执行程序中的指令和程序依赖的全局变量等数据copy到内存中:
图片
既然是copy到内存,那么显然操作系统需要为接下来要运行的程序分配内存。
操作系统在内存中找到一段大小合适的空闲内存分配给接下来要运行的程序:
图片
然后在该内存中划分出几个区域,这几个区域就是我们熟悉的代码区、数据区、堆区和栈区:
图片
其中代码区和数据区中的内容来自可执行程序的代码段和数据段:
图片
而堆区和栈区则是程序在运行过程中使用的,这两个区域中的内容不依赖可执行程序本身。
值得注意的是,所谓的堆区和栈区只是一个抽象的概念,真正的物理内存中并没有一块所谓的堆区或者栈区。
任何一段内存都可以被用作堆区或者栈区,这就像停车场有vip区或者普通区,所谓vip区只不过一种约定,普通区和vip区的停车位没有任何本质的不同,作为停车场管理员只要你高兴实际上可以把任何一块普通区划分为vip区。
当然,程序的内存区域中除了看到的这些区域可能还有其它区域,这取决于程序是否依赖动态库。
如果该程序依赖动态库,那么在程序运行时还需要把依赖的动态库也加载进来,加载到哪里呢?
不要忘了堆区和栈区的增长方向是相反的,因此这中间的空闲区域正好可以利用起来存放动态库:
图片
这一步完成之后程序就算加载完毕接下来可以运行了,但程序是怎么运行的呢?CPU怎么能知道该从哪里开始运行这个程序呢?
答案还得在可执行程序中寻找。
编译器在编译生成可执行程序时会记录下这个程序第一条指令的所在位置,以elf可执行程序为例,使用readelf工具你可以查看elf可执行程序的内容:
图片
注意看Entry point address这一项,这就是该程序的第一条机器指令所在地址。
当操作系统决定把CPU分配给刚创建的程序时会用这个值去初始化CPU的指令寄存器,这样CPU就知道该从哪里开始运行该程序了。
图片
就这样程序开始运行。
这就是你双击一个图标背后的故事。