第一章 我的程序是怎么跑起来的?

 第一章 我的程序是怎么跑起来的?

在你看到程序跑起来之前,其实编译器和系统为你做了很多工作,它默默地为你奔波着:预处理、编译、汇编、链接,装载……,于是你可以被感动的声泪俱下:原来,我不是一个人……在战斗!

1.1 编译过程全景图

 

1.2 装载是怎么回事?

 

1.3 本章小结

  

 

1.1 编译过程全景图

 

写了n多的程序,也许我们都不曾停下来想想我们的程序是怎么跑起来的,于是在找工作的时候可能就被下面这样的题目彻底的问住了:

【面试题1】

来自国内某著名电子商务集团研发院

问:能不能描述一下进程内存空间是什么样子的?

答:……呃……不能……

Ok,歇菜了吧?无从下口了吧?此时此刻也许我们才幡然醒悟:原来我们号称搞了n多年的程序,却还不知道什么叫做编译、编译之后发生了什么。书到用时方恨少呐~下面,我们就来对程序的编译过程做一个最基本的了解。

 

 

【什么是“编译器”】

首先,问一下自己:为什么有编译器这种物质存在?

实际上编译器是程序员与机器之间的翻译官,他把一种计算机语言从人类比较好认知的形式转换为计算机比较好认知的形式。Ok,看来我们应该管编译器叫翻译器或许更恰当。

在具体计算机上实现一种语言,一个关键的问题是程序执行时的基本表示是实际计算机上的机器语言还是虚拟机的机器语言。这个问题决定了语言的实现。根据这个问题的回答,可以将程序设计语言划分为两大类:编译型语言和解释型语言。

于是我们的翻译官也分成两种:解释器(interpreter)和编译器(compiler)。

解释器是能够执行用其他计算机语言编写的程序的系统软件,它是一种翻译程序。它的执行方式是一边翻译一边执行,因此其执行效率一般偏低,但是解释器的实现较为简单,而且编写源程序的高级语言可以使用更加灵活和富于表现力的语法。Python、TCL和各种Shell程序一般而言是使用解释器执行的。

下面,话锋一转,我们来看看编译器。

编译器是干什么的?实际他干了这么一件事情:高级语言---->机器语言。下面我们通过一个图来了解一下编译系统为我们立下了哪些汗马功劳。

 

 

 

图1.1 编译系统

上面那一堆文件中,我们最感兴趣的是以.o为后缀的object文件(中文世界里管它叫目标文件)以及最终产生的executable文件(即可执行文件)。目标文件就相当于是一个程序的链接使用说明书,链接器通过读这个说明书生成可执行文件,可执行文件就相当于一个程序的运行说明书,操作系统通过阅读这个说明书来加载和运行程序。实际上呢,目标文件和可执行文件的存储格式几乎是一样的【1】。所以下面我们就来详细的分析一下可执行文件这个“运行说明书”。

【有哪些可执行文件?】

在当今的PC机上,UNIX/LINUX 平台下有三种主要的可执行文件格式:a.out(assembler and link editor output 汇编器和链接编辑器的输出)、COFF(Common Object File Format 通用对象文件格式)、ELF(Executable and Linking Format 可执行和链接格式)。windows平台下有: MZ格式(MZ是MZ格式的主要作者Mark Zbikowski的名字的缩写),NE(New Executable:分段可执行文件),LE(Linear Executable:线性可执行文件,专用于VxD文件),以及PE(Portable Executable:可移动的可执行文件)。

下面,我们以ELF格式的文件为例,来说明一下可执行文件长什么模样。

【ELF文件长啥样】

ELF文件有三种类型:可重定位文件,也就是通常称的目标文件,后缀为.o;共享文件,也就是通常称的库文件,后缀为.so;可执行文件,也就是我们主要讨论的文件格式。前面我们也说了这三种文件的格式几乎是一样的,所以接下来我们总是把它们叫做ELF文件格式,。

Portable Formats Specification 1.1中用这样一张图(如图1.2)来说明ELF文件的格式,左侧称为链接视图(Linking View),右侧称为执行视图(Execution View)

 

 

图1.2 Object File Format

下面,我们就图1.2中的一些项目作出解释,解释的结果如图1.3所示。

 

 

图1.3 ELF文件各项简要说明

不过这里有一个需要注意的地方:图中所示的顺序并不代表是ELF文件各个项目存放的实际位置,另外,Section和segment之间也没有特定的顺序。只有ELF头是放在文件的初始位置的。

更多的细节我们在这里就不讲了,我们的目的只是知道,编译完了之后我们会得到一个可执行文件,这个可执行文件是什么样子的呢?就是ELF这样子的格式。有兴趣的话可以参考一下Tool Interface Standards (TIS) 组织的Portable Formats Specification。

 

到目前为止,我们已经大概知道了ELF文件的格式,那么,具体来看的话,各个Section表示的是什么呢?一般来讲,有这么几个段是最常见的:代码段、数据段和只读数据段、BSS段。那么,我们用这几个段把前面的那些类似Section n这样的占位标示符替换掉,于是就可以得到如图1.4所示的ELF文件基本结构图,当然,这个图也是被我们阉割过的——略去了很多复杂的结构,留下的是主要结构。

 

图1.4 ELF文件基本结构

图1.4所示的内容就是我们编译阶段最终得到的一个东西——可执行文件,接下来我们就让操作系统来装载这个可执行文件,让它真正的跑起来。这个所谓的装载会在下一小节详细讲述。

 

1.2 装载是怎么回事?

通过阅读上面的文字,我们已经可以了解到“我的程序是怎么跑起来的”超过三分之一的内容了,怎么样,离真相越来越近的感觉是不是很好?ok,下面,请保持你的状态,让我们继续拨开层层迷雾,继续向着真相前进!

在我们具体来看程序装载之前,先讲个小故事,这个故事里包含了我们将要解决的问题——“我的程序是怎么跑起来的”几乎所有要素。

话说有一天你去路边的一个小饭馆吃饭,踏步进去坐定之后你文绉绉的说道:老板,点菜!快点!快你妈饿死了!!老板笑吟吟的颠了过来,手里拿这个本子,口齿不大灵光:您……您自个儿写……写吧,馆子小,忙……忙不过来,见见见谅。话毕,给你比划了一个敬礼。你也大度,没跟他计较,打左手边抄起了一份菜单,边看边记,不一会,心里头就打定了主意,拿起了刚才老板递上来的小本,写下了要点的菜交给老板。

 

 

图1.5 菜单

门分两边,话至两处。老板拿到这个小本之后就把它交给了后厨的一个小伙计,小伙计年纪轻轻,刚从农村出来没多长时间,不过小伙子精瘦精瘦的,很精神,在后厨干什么呢?配菜!小伙子拿到菜单之后,不忙着胡乱找菜切菜,他从耳夹上抽出一根铅笔来,把菜单上的菜分了分类:

 

 

图1.6 配菜/料清单

 

写完,小伙子就开始把主料、辅料一一备齐,分门别类的用盘子装好,递给里边的掌勺师傅,师傅一看:得嘞,马上就做。话说间师傅就开始忙活起来了:涮锅、炝锅,搁作料,这一通忙活,一会的功夫,一碗热气腾腾,看起来无比没有吃相的盖饭就出锅了。递给你一看:嗬!把你乐坏了,好几天没吃了,于是看着这碗成功的饭,你咧嘴乐了……

故事讲完了,言归正传,话上正轨。这个故事里面出现了几个非常重要的角色,他们可以说是对应了我们一个程序跑起来的各个要素,让我们来看一下:

老板——操作系统。他管理着这个小饭馆的各个细节,负责协调后厨(内核)和客户(用户)的交互。

你——用户,比如你代表了一个程序员。

“北京市朝阳区小吃店”那张纸片——程序清单,你用高级语言写的。

小伙计——编译系统。他把你想要的东西,从你的语言翻译成了厨师的语言。

“配菜/料清单”——经过编译之后的目标文件(或者说是可执行文件)

掌勺师傅做饭的这个过程——进程,厨子利用各种各样的资源帮助你实现了你想要的东西。

锅、碗、瓢、盆——虚拟地址空间,每道菜都有自己装配料的容器,否则要是混在一起就乱套了。(其实这点本来是应该在进程里面说的,但是为了清楚起见,还是单独拿出来说)

最后那碗饭——输出结果。

Ok,我前面们已经把“小伙计”和“配菜/料清单”这两个东西说的比较清楚了。“老板”这件事我们今后可能会讲。这一节,我们希望能把“掌勺师傅做饭的这个过程”和“锅、碗、瓢、盆”这两件事讲明白了。显然,我们需要一个约定:我们不需要知道掌勺师傅是从哪里来的(爹妈生的,石头缝里蹦出来的或者外星人带来的,对我们来说意义不大),我们只需知道有这么个厨子,他是可以帮我们做饭的。同时,我们也不需要知道这些锅碗瓢盆是怎么来的,只需要知道这些东西有什么用就行。下面从厨子开始说起。

【1.2.1看得见摸得着的进程】

或许进程这个东西对我们来说还玄乎的狠,看不见摸不着,到底什么是进程呢?

我们写的程序只是一个普通文件,是一个机器代码指令和数据的集合,这些指令和数据存储在磁盘上的一个可执行映象(Executable Image)中,所以,程序是一个静态的东西,那么怎么跑起来呢?这就需要装载,有一个东西它常驻内存,它可以帮助我们把可执行映象装载到内存中来,这个东西就叫做装载器(Loader),当然,装载的过程也是非常复杂的,但已超出了我们的讨论范围,总之我们不妨假设它把可执行文件从外存挪到了内存,但是组织结构没变。程序装入内存后就可以运行了:在指令指针寄存器的控制下,逐条地将指令取至送到CPU运行。这些指令控制的对象无非就是各种存储器(内存、外存和各种CPU寄存器等),这些存储器中保存有待运行的指令和待处理的数据。所以说,我们之所以看到程序是在“跑”的,实际就是这堆数据和指令在不断的变化。于是,我们可以这么理解:程序的执行过程实际上就是一个集合,这个集合里的各种指令和数据为了做成某件事情在不断的变化。我们把这个集合称作“进程”,它代表程序的执行过程,是一个动态的实体,它随着程序中指令的执行而不断地变化。在某个特定时刻的进程的内容被称为进程映象(process image)。

在此时此刻,我们给某个进程拍张照片,他是什么样子的呢?我们来看看下面这张“照片”,如图1.7

 

图1.7 task_struct结构简图

这个task_struct是什么东西呢?就是我们通常在操作系统书上看到的叫做进程控制块(PBC)的东西,这个东西就像是进程的身份证,它详细的描述了一个进程方方面面,主要包括下面一些:

·进程状态(State)

·进程调度信息(Scheduling Information)

·各种标识符(Identifiers)

·进程通信有关信息(IPC:Inter_Process Communication)

·时间和定时器信息(Times and Timers)

·进程链接信息(Links)

·文件系统信息(File System)

·虚拟内存信息(Virtual Memory)

·页面管理信息(page)

 

·对称多处理器(SMP)信息

·和处理器相关的环境(上下文)信息(Processor Specific Context)

·其它

Ok,了解到这里就差不多了,下面我们只需看其中的一项,它叫做虚拟内存信息(Virtual Memory),我们通常管它叫虚拟地址空间,也就是我们前面所说的锅碗瓢盆。

 

【1.2.2进程的虚拟地址空间】

如果还不知道Virtual Memory是怎么回事的,那么可以Google一下,我们这里直奔主题。

接着前面,我们把一个可执行映像映射到内存之后,比方说进程A,那么A的Virtual Memory就如图1.8所示

 

 

图1.8 进程虚拟内存信息

Linux将Virtual Memory组织为一个集合,这个集合中的元素称之为area。这个area就是就是我们所熟知的什么代码段(.text)、数据段(.data)、栈(stack)、堆(heap)等等这些。那么站在操作系统的角度我们是怎么知道这些area是在哪里的呢?请回头看图1.6,它里面有一行叫做struct mm_struct *mm;,可以看出mm 指向一个叫做mm_struct的结构体,而这个结构体就是描述进程虚拟内存当前状态的数据结构,在mm_struct这个结构体里呢,有一个数据成员是我们比较感兴趣的,它叫做mmap,这个mmap指向一个链表,这个链表的每个节点类型叫做vm_area_struct,正是vm_area_struct这个结构体描述了当前虚拟地址空间的一个区域。在vm_area_struct中有下面几个字段是比较关键的:

·unsigned long vm_start:记录此 VMA 的开始位置(start address)。

·unsigned long vm_end:记录此 VMA 的结束为止(end address)。

·struct vm_area_struct *vm_next:指向链表中下一个结构体的指针。

说了这么多,可能还不是很清晰,那么我们通过扩展一下图1.8,从而得到图1.9,通过图1.9我们就可以很清楚的明白上面说的这些话是什么意思了

 

图1.9 Linux虚拟内存组织方式

到目前为止,我们已经知道装载器把我们的可执行文件映像装载到内存里面来是个什么样子了,接下来,取指令取数据运行程序就是操作系统的事情了,在此不作细表(实际是我也没弄太明白,囧…)。

 

1.3 本章小结

好啦,说到这里,我们第一章就说完了,在这一章里面,我们说了一件事情:我的程序是怎么跑起来的。要理解这个事情必须知道编译系统做了哪些事情、操作系统做了哪些事情。简单来说呢,编译系统就是把我们写的程序搞成操作系统认识的格式,放在磁盘上备用。操作系统拿到可执行文件之后,创建一个进程,把这些指令和数据装载到进程的虚拟地址空间里面去,然后开始执行。

所以,要更深入的了解的话,可就有的搞了,比如研究研究编译器原理,研究研究操作系统原理,总之,这里只是抛砖引玉,做一个简单的梗概,让我们心里有一个大体的概念和框架。

 

 

 

 

开始解决我们开头提出的那个问题吧,进程的虚拟内存空间是怎么样的呢?呵呵,看看图1.8就可以啦!


 

【1】我们可以在TIS 给出的 Portable Formats Specification 1.1 版本中,看到这么一段话:“Object files participate in program linking (building a program) and program execution (running a program).For convenience and efficiency, the object file format provides parallel views of a file’s contents,reflecting the differing needs of these activities.”

展开阅读全文
©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值