程序是怎么执行的

Docker是一个建立在操作系统+编译器基础之上的系统,所以了解操作系统,编译器以及程序运行机制对我们理解Docker来说非常重要。

  一直想写篇文章来说明在程序运行过程中操作系统都干了些什么事。下面我试着说明:

  首先,任何程序都是有格式的,所谓无规矩不成方圆,任何美的,精巧的事物都是精密组织的,程序也一样。我之前用的最多的是c#与java,有趣的是,当时很多人嘲笑java与c#们一直在用脚本写程序,大概在他们眼里c与c++才是真正的程序。但是,现实就是现实,其实我们都是在一个叫做虚拟机的程序下写托管代码,它掌握着程序的编译,链接,加载,映射与最终执行与终止。它就是操作系统,准确的讲是操作系统+编译器。他们是真正的元虚拟机。

  然后我来解释下如何运行一个程序:

  程序是精巧与复杂的,熟悉它以后你也会觉得它是脆弱的,因为只要有一个bit发生错误,整个系统就会崩溃。这个系统就是执行文件格式,在linux下叫elf(executable linkable format)而windows下叫pe(portable execute)。我想写操作系统第一步就是制定这个规则,不然一切都没有规律。所以我想linus牛,但是ken tomason有过之而无不及,毕竟你是在人家基础之上发展而来的,计算机世界就是如此没办法,谁让你在人家下面呢?

  我以linux系统为例,简单讲讲程序由编译 链接装载与执行。elf文件格式分为很多段—section,总体分为只读可执行的代码段与可读可写的数据段。.txt就是典型的代码段,.data .rodata .symbl .rel .got .plt都是数据段。那么,编译器负责将程序员写的程序,编译成elf文件,代码,注视,代码行对应机器码信息,就是调试信息啦会进去.txt .code .comment .debug段,常量与静态变量进入.data .rodata .bss。接下来,编译器将引用的头文件中的代码(特指静态编译)与引用的glibc中的库函数打包(链接)到整个可执行文件中,然后在elf文件中设置文件头信息,如段表位置,程序入口位置等信息。当然,这里不得不提的是符号表,与重定位表,他们是整个程序最终能跑起来的关键。gcc是靠符号,或者说程序是靠符号来链接的,不管是函数还是变量,都是符号而已,所以从侧面讲,写程序跟写文章没啥区别。程序就像个图书馆,每个函数与变量都是书,链接程序好比在图书馆看书,当你看到一个点时,就会叫你去某某位置拿另一本书,翻到特定位置开始继续读,如果没找到就会爆出链接错误。而重定位表就是一次性讲所有对需要跳转的位置进行更改,以确保程序中不存在没有拿到手的书。

  好,现在程序已经链接好了,接下来就是操作系统进行装载与执行了。当然这是静态的链接,动态链接会稍微复杂,会写很多,这里不讨论。操作系统会打开elf文件的装载视图,它能根据装载视图的段表—segment这跟section在中文都是段,没办法!这个视图是将数据与代码分开的,相似section链接在一起,所以数量也比section少很多,目的是在装载时节约内存。因为,段映射到内存是要地址对齐的,如按照地址4096(一般簇大小为4k)整除来对齐,这样做是有好处的,能减少内存碎片,加快磁盘读写速度,磁盘最小扇区512byte,所以整数倍读取能少一次寻址,当然效率更高。这在游戏引擎,数据库设计领域比较多见,毕竟io是最大瓶颈,所以再这程序时也要考虑对象占用内存大小是否是操作系统最小簇的整数倍来判断一个程序是否是高人所做。

  回来,操作系统会最先读取可执行的文件头,因为里面有运行程序的信息,如段表位置,程序入口,程序类型等。对于操作系统最重要的是段表与程序入口。其中段表就是elf中有多少段,每个段在文件中的偏移,入口则是常说得main函数的虚拟地址。这里就出现一个问题,程序非得以main函数开始吗?其实看出来了,不用!只是gcc认定符号main为c语言的入口,其他程序照抄罢了,当然你可以加入编译条件更改入口即可。gcc是stallman写的,他是个黑客,全世界只要运行c的地方,他都能黑,呵呵。

  好了,操作系统在读取可执行程序头时做了三件事:1.创建虚拟内存空间来容纳一个进程,2.根据文件头内容建立程序虚拟内存地址与elf文件的映射关系表,vma(virtual memory area)结构,3.初始化程序的栈空间与堆空间。下面解释下这三个过程。

  1,虚拟内存。虚拟内存是编译器与操作系统的一个约定。任何程序在编译无链接时得地址都是虚拟地址。为什么要用虚拟地址这个问题说来话长。话说在很久以前,大家都很穷,都没内存,但是要运行的程序很多,系统不可能为每个程序分配单独的内存,同时领导还要求同时所有程序都要运行,咋办呢?办法总比问题多,咱可以分时嘛,你上完cpu我再上,但是大家各自在用cpu时,其他只能看着,直到一个人说"下一个",这个人不管在干嘛都得放弃,让其他人用cpu。这样对所有人都公平,而且每个人在用cpu是能感觉到cpu只被它独有,用户体验还挺好。所以一次解决可所有问题。而,这个组织人,就是那个喊“下一个”的家伙就是操作系统。那,说这么多,跟虚拟地址有啥关系呢?其实仔细想想如果大家都是用物理地址,而彼此在运行时都独占系统资源,那前一个程序修改了我的数据咋办,得了,都由操作系统说了算吧,它做内存映射的维护,大家都用统一的地址空间,但是运行时映射到不同的物理内存互不干扰来。所以你可以看到所有linux程序都从相同的虚拟地址开始执行。

  2.建立内存到文件得映射。我们知道,程序都不是一次性加载到内存的,而是一段段的,这是由著名的copy on write规则约束而来的。而这一段也是规定好大小的一般是操作系统簇的大小,也叫一页。当程序运行过程中发现某个数据在内存中没有则会报一个页读取错误,并触发操作系统的缺页中断。这时就要靠操作系统通过读取elf文件头建立的从文件系统到虚拟内存的映射来获取了。它等于是程序运行时到程序得一个索引结构,存储了运行时程序虚拟内存地址到文件地址的对应表。

  3.好了,第三步最简单,就是操作系统载人main函数后面跟的那个char argc与char*argv了。他们是程序启动参数。还要载入程序运行的环境变量,栈空间,堆空间,也就是静态数据与全局变量部分。然后把程序执行寄存器指向程序开始的地方。开始执行!看似简单,但是很复杂的过程开始了!

  好了,这就是简单的程序如何被操作系统执行的简单描述,当然这只是静态链接程序的加载,动态链接稍微复杂点。

时间: 2024-07-29 04:31:32

程序是怎么执行的的相关文章

C#程序调用CMD执行命令

在windows环境下,命令行程序为cmd.exe,是一个32位的命令行程序,微软Windows系统基于Windows上的命令解释程序,类似于微软的DOS操作系统.输入一些命令,cmd.exe可以执行,比如输入shutdown -s就会在30秒后关机.总之,它非常有用.打开方法:开始-所有程序-附件 或 开始-寻找-输入:cmd/cmd.exe 回车.它也可以执行BAT文件. 下面介绍使用C#程序调用cmd执行命令: 代码: 1 using System; 2 using System.Coll

第11周阅读程序写出执行结果1(5)

/* *Copyright (c) 2016,烟台大学计算机学院 *All rights reserved. *文件名称 : *作 者 : 刘云 *完成日期 : 2016年5月8号 *版 本 号 : v6.0 * *问题描述 : 阅读程序写出执行结果1(5) *输入描述 : 无 *程序输出 : */ /*********************************(a)****************************************************/ #include

正试图在 os 加载程序锁内执行托管代码。不要尝试在 DllMain 或映像初始化函数内运行托管代码

当我在窗体初始化的时候,调用了一个外部的dll,它就不知什么原因的 抛出一个“正试图在 os 加载程序锁内执行托管代码.不要尝试在 DllMain 或映像初始化函数内运行托管代码”的异常,程序就卡掉了,在网上查了查,相关说明如下: .NET2.0中增加了42种非常强大的调试助手,MDA.Loaderlock 是其中之一.Loaderlock检测在一个拥有操作系统loader lock的线程上运行托管代码的情况.这样做有可能会引起死锁,并且有可能在操作系统加载器初始化DLL前被使用. 大致理解:就

在android程序中怎么执行ifconfig命令来修改android 的ip地址,

1.引入: import android.provider.Settings; import android.content.ContentResolver; ps:在Setting.System中有以下标志 WIFI_USE_STATIC_IP WIFI_STATIC_IP WIFI_STATIC_NETMASK WIFI_STATIC_GATEWAY WIFI_STATIC_DNS1 and WIFI_STATIC_DNS2 2.在AndroidManifest中加入<uses-permis

试解释下列名词:程序的顺序执行,程序的并发执行。

一个计算由若干个操作组成,若这些操作必须按照某种先后次序来执行,以保证操作的结果是正确的,则这类计算过程称为程序的顺序执行过程. 所谓的程序的并发执行是指若干个程序同时在系统中运行,这些程序的执行在时间上是重叠的,一个程序的执行尚未结束,另一个程序的执行已经开始. 补充: 顺序程序的特点: 顺序性.封闭性.可再现性. 并发程序的特点: 失去程序的封闭性.程序与计算不再一一对应.程序并发执行时的相互制约关系.

正试图在 os 加载程序锁内执行托管代码

正试图在 os 加载程序锁内执行托管代码.不要尝试在 DllMain 或映像初始化函数内运行托管代码... 当我在窗体初始化的时候,调用了一个外部的dill时,它就不知什么原因的 抛出一个"正试图在 os 加载程序锁内执行托管代码.不要尝试在 DllMain 或映像初始化函数内运行托管代码"的异常,程序就卡掉了,在网上查了查,相关说明如下: .NET2.0中增加了42种非常强大的调试助手,MDA.Loaderlock 是其中之一.Loaderlock检测在一个拥有操作系统loader

检测到 LoaderLock:DLL&quot;XXXX&quot;正试图在OS加载程序锁内执行

解决方法: ctrl+D+E或alt+ctl+e或使用菜单调试——>异常——>异常窗口——>Managed Debugging Assistants——>去掉LoaderLock选项. 参考资料: 不辣的peter中国版的博客:Loaderlock was detected 无痕客的博客:ESRI的地图控件和DEV控件存在冲突,造成调试中断无法通过 悟道人生的博客:检测到 LoaderLock Message Microsoft.DirectX.dll”正试图在 OS 加载程序锁内

Delphi 7 在程序中直接执行SQL脚本文件

Delphi 7 在程序中直接执行SQL脚本文件 在处理MSDE一些操作中.需要执行一些SQL脚本.有的是从SQLServer 2000中生成的SQL为后缀的脚本.在MSDE中没有企业管理器,操作都是在程序中完成的.所以用以下函数来执行SQL脚本. //执行一个SQL角本文件,文件只能是ANSI编码的.//如果文件是UNICODE编码的话,则会乱码.var  s:string;  sqltext : string;  sqlfile : TextFile;begin  if OpenDialog

正尝试在 OS 加载程序锁内执行托管代码。不要尝试在 DllMain 或映像初始化函数内运行托管代码,这样做会导致应用程序挂起。

当我在窗体初始化的时候,调用了一个外部的dill时,它就不知什么原因的 抛出一个“正试图在 os 加载程序锁内执行托管代码.不要尝试在 DllMain 或映像初始化函数内运行托管代码”的异常,程序就卡掉了,在网上查了查,相关说明如下: .NET2.0中增加了42种非常强大的调试助手,MDA.Loaderlock 是其中之一.Loaderlock检测在一个拥有操作系统loader lock的线程上运行托管代码的情况.这样做有可能会引起死锁,并且有可能在操作系统加载器初始化DLL前被使用. 大致理解

JAVA基础--程序是顺序执行的

class Testa { public static void main(String[] args) { String aa="aaa"; String bb="bbb"+aa; aa="cccc"; System.out.println(bb); } } 输出的是 "bbbaaa   class Testa { public static void main(String[] args) { String aa="aaa