公告:九九网站目录为广大站长提供免费收录网站服务,会员可在线完成投稿无需添加友情链接。只收录内容正规合法的网站;快审网站软文10元

点击这里在线咨询客服
新站提交
  • 网站:8452
  • 待审:21
  • 小程序:9
  • 文章:3601
  • 会员:2183

 

程序编译的整体流程

以下面这个简单的函数为例

includeintmain(){printf("hello, world\n");return0;}

为了在系统上运行这个程序,每条C语句都必须被其他程序转化为一系列的低级机器语言指令,然后这些指令按照一种为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来。目标程序也称为可执行目标文件

在Unix系统上,从源文件到目标文件的转化是由编译器驱动程序完成的。

linux> gcc -o hello hello.c

GCC编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行目标文件hello。这个翻译过程分为四个阶段:预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)、链接(Linking)。执行这四个阶段的程序(预处理器、编译器、汇编器、和链接器)一起构成了编译系统。

预处理阶段:

  1. 预处理器(cpp)将所有的define删除,并且展开所有的宏定义。
  2. 处理所有的条件预编译指令,比如if、ifdef、elif、else、endif等。
  3. 处理include预编译指令,将被包含的文件直接插入到预编译指令的位置。
  4. 删除所有的注释。
  5. 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
  6. 保留所有的pragma编译器指令,因为编译器需要使用它们。
  7. 使用gcc -E hello.c -o hello.i命令来进行预处理, 预处理得到的另一个程序通常是以.i作为文件扩展名。

编译阶段:

编译器(ccl)将预处理完的文本文件hello.i进行一系列的词法分析、语法分析、语义分析和优化,翻译成文本文件hello.s,它包含一个汇编语言程序。如下所示

该程序包含函数main的定义,2-7行的每条语句都以一种文本格式描述了一条低级机器语言指令。

汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。

编译过程可分为6步:扫描(词法分析)、语法分析、语义分析、源代码优化、代码生成、目标代码优化

  1. 词法分析:扫描器(Scanner)将源代的字符序列分割成一系列的记号(Token)。lex工具可实现词法扫描。
  2. 语法分析:语法分析器将记号(Token)产生语法树(Syntax Tree)。yacc工具可实现语法分析(yacc: Yet Another Compiler Compiler)。
  3. 语义分析:静态语义(在编译器可以确定的语义)、动态语义(只能在运行期才能确定的语义)。
  4. 源代码优化:源代码优化器(Source Code Optimizer),将整个语法书转化为中间代码(Intermediate Code)(中间代码是与目标机器和运行环境无关的)。中间代码使得编译器被分为前端和后端。编译器前端负责产生机器无关的中间代码;编译器后端将中间代码转化为目标机器代码。
  5. 目标代码生成:代码生成器(Code Generator).
  6. 目标代码优化:目标代码优化器(Target Code Optimizer)。

汇编阶段:

汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中,hello.o是一个二进制文件。

链接阶段:

hello程序调用了printf函数,它存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。连接器(ld)就负责处理这种合并。结果就得到了hello文件,它是一个可执行目标文件(或者称为可执行文件),可以被加载到内存中,由系统执行。(链接程序运行需要的一大堆目标文件,以及所依赖的其它库文件,最后生成可执行文件)。

链接

简述:

链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(run time),也就是由应用程序来执行。链接是由叫链接器(linker)的程序自动执行的。

意义:

链接使得分离编译(separate compilation)成为可能,我们不用将一个大型的应用程序组织成一个巨大的源文件,而是可以把它分解成为更小、更好管理的模块,可以独立的修改和编译这些模块。当我们改变这些模块中的一个时,只需要简单的重新编译它,并重新链接应用,而不必重新编译其他文件。

虚拟内存(Virtual Memory)

虚拟内存又叫虚拟存储器(Virtual Memory),是建立在主存--辅存物理结构基础上,有附加的硬件装置及操作系统存储管理软件组成的一种存储体系。是一个抽象概念,它为每个进程提供了一个假象,即每个进程都在独占地使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间

它提供了三个重要的能力:

  1. 将主存看成一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据(这里存在交换空间以及页面调度等概念),通过这种方式,高效地利用主存;
  2. 为每个进程提供了统一的地址空间(以虚拟地址编址),从而简化了存储器管理;
  3. 操作系统会为每个进程提供独立的地址空间,从而保护了每个进程的地址空间不被其他进程破坏。

虚拟存储器与虚拟地址空间是两个不同的概念:虚拟存储器是假想的存储器,而虚拟存储空间是假想的内存。它们之间的关系应该与主存储器与内存空间之间的关系类似。

下图所示的是Linux进程的虚拟地址空间:

静态链接:

像LinuxLD程序这样的静态连接器(static linker)以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。输入的可重定位目标文件由各种不同的代码和数据节(selection)组成,每一节都是一个连续的字节序列。

下图是一个运行在Linux的x86-64系统,使用标准的ELF-64可重定位目标文件格式:

ELF头(ELF header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移、以及节头部表中条目大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。

夹在ELF头和节头部表之间的都是节。一个典型的ELF可重定位目标文件包含下面几个节:

.text:已编译程序的机器代码。

.rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表。

.data:已初始化的的全局和静态C变量。局部C变量在运行时被保存在栈中,即不出现在.data节中,又不出现在.bss节中。

.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0。

.symtab:一个符号表,它存放着在程序中定义和引用的函数和全局变量的信息。

.rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。

.rel.data:被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。

.debug:一个调试符号表,其条目是程序定义的局部变量和类型定义,程序中定义和引用全局变量,以及原始的C源文件。

.line:原始C源程序中的行号和.text节中的机器指令之间的映射。

.strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以null结尾的字符串序列。

静态链接过程:

  1. 空间和地址分配,扫描所有的输入目标文件,获得他们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这样,链接器将能够获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。映射关系就是指可执行文件与进程虚拟地址空间之间的映射。那么,这里程序还没有执行,更不会出现进程,哪里来的进程地址空间呢?此时虚拟存储器便发挥了很大的作用:虽然此时没有进程,但是每个进程的虚拟地址空间的格式都是一致的。所以,为可执行文件的每个段甚至每个符号符号分配地址也就不会有什么错了。注意:在链接之前,目标文件中的所有段的虚拟地址都是0,因为虚拟空间还没有被分配,默认都为0。等到链接之后,可执行文件中的各个段已经都被分配到了相应的虚拟地址。
  2. 符号解析(symbol resolution)和重定位(relocation)符号和符号表:每个可重定位目标模块m都有一个符号表,它包含m定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号(由模块m定义并能被其他模块引用的全局符号,全局链接器符号对应非静态的C函数和全局变量;由其他模块定义并被模块m引用的全局符号,这些符号被称为外部符号,对应于在其他模块定义的非静态C函数和全局变量;只被模块m定义和引用的局部符号,它们对应于带static属性的C函数和全局变量,这些符号在m模块任何位置都可见,但是不能被其它模块引用。.symtab中的符号表不包含对应于本地非静态程序变量的任何符号,这些符号在运行时在栈中被管理。Cstatic属性的本地过程变量是不在栈中管理的。符号解析:目标文件定义和引用符号,每个符号对应于一个函数、一个全局变量或一个静态变量(即C语言中任何以static属性声明的变量)。符号解析的目的是将每个符号引用正好和一个符号定义关联起来。如果找不到,则会出现编译时错误。链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。当编译器遇到一个不是在当前模块中定义的符号(变量和函数名)时,会假设该符号是在其他某个模块中定义的,生成一个链接器符号表条目,并把它交给链接器处理。如果链接器在它的任何输入模块中都找不到这个被引用符号的定义,就输出一条错误信息并终止。那么对于多重定义的全局符号,链接器是如何解析的呢?Linux编译系统采用的方法:在编译时,编译器向汇编器输出每个全局符号,或者是强(strong)或者是弱(weak),而汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。根据强弱符号的定义,Linux链接器使用下面的规则来处理多重定义的符号名:不允许有多个同名的强符号。如果有一个强符号和多个弱符号同名,那么选择强符号。如果有多个弱符号同名,那么从这些弱符号中任意选择一个。重定位:编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使它指向这个内存位置。一旦链接器完成了符号解析这一步,就把代码中的每个符号引用和正好一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道它的输入目标模块中的代码节的确切大小。现在就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。重定位有两步组成:重定位节和符号定义:链接器将所有的相同类型的节合并成为同一类型的新的聚合节。例如,来自所有输入模块的.data节被全部合并成为一个节,这个节称为输出的可执行目标文件的.data节。然后链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每一个节,以及赋给输入模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。重定位节中的符号引用:链接器修改代码节和数据节中对每个符号的引用,使的它们指向正确的运行地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目的数据结构。链接完成后,链接器就把多个目标文件合并成为了一个可执行的目标文件。结构如下所示:

加载可执行目标文件当要运行可执行的目标文件prog,我们可以在Linux shell的命令行中输入它的名字:linux> ./prog因为prog不是一个内置的shell命令,所以shell会认为prog是一个可执行目标文件,通过调用某个驻留在存储器中称为加载器(loader)的操作系统代码来运行它。任何Linux程序都可以通过调用execve函数来调用加载器。加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序。这个将程序复制到内存并运行的过程叫做加载。

动态链接共享库:

为什么要有动态链接和共享库呢?

  1. 静态库和所有的软件一样,需要定期的维护和更新。如果程序员想要使用一个库的最新版本,他们必须以某种方式了解到该库的更新情况,然后显示地将他们的程序与更新的库重新链接。
  2. 考虑内存和磁盘空间。静态链接极大地浪费内存空间。因为在静态链接的情况下,假设有两个程序共享一个模块,那么在静态链接后输出的两个可执行文件中各有一个共享模块的副本。如果同时运行这两个可执行文件,那么这个共享模块将在磁盘和内存中都有两个副本,对磁盘和内存造成极大地浪费。

共享库(shared library)是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接(dynamic linking),是由一个叫动态链接器(dynamic linker)的程序来执行的。

共享库共享的方式

  1. 在任何给定的文件系统中,对于一个库只有一个.so文件。所有引用该库的可执行目标文件共享这个.so文件中的代码和数据,而不是像静态库的内容那样被复制和嵌入到引用它们的可执行文件中。
  2. 一个.text节的一个副本可以被不同的正在运行的进程共享。

下图概括了程序的动态链接过程

为了构造图中的示例的共享库libvector.so,我们调用编译器驱动程序,给编译器和链接器如下指令:

linux> gcc -shared -fpic -o libvector.so addvec.c multvec.c

-shared选项指示链接器创建一个共享的目标文件。一旦创建了这个库,随后就要将它链接到上图所示的示例程序中:

linux> gcc -o prog21 main2.c ./libvector.so

这样就创建了一个可执行目标文件prog21,而此文件的形式使得它在运行时可以和libvector.so链接。基本思路是当创建可执行文件时,静态执行一些链接,然后在程序加载时,动态完成链接。此时,没有任何libvector.so的代码和数据节真的被复制到可执行文件prog21中。反之,链接器复制了一些重定位和符号表的信息,它们使得运行时可以解析对libvector.so中代码和数据的引用。

当加载器加载和运行可执行文件prog21时,加载部分链接的可执行文件prog21。接着,它注意到prog21包含一个.interp节,这个节包含动态链接器的路径名,动态链接器本身就是一个共享目标(比如Linux系统中的ld-linux.so)。加载器不会像它通常所作地那样将控制传递给应用,而是加载和运行这个动态链接器。然后,动态链接器通过执行下面的重定位完成链接任务:

  1. 重定位libc.so的文本和数据到某个内存段。
  2. 重定位libvector.so的文本和数据到另一个内存段。
  3. 重定位prog21中所有对libc.so和libvector.so定义和符号的引用。

最后动态链接器将控制传递给应用程序。共享库的位置就固定了,并且在程序执行的过程中都不会变。

从应用程序中加载和链接共享库

其思路是将每个生成动态的内容的函数打包在共享该库中。当一个来自Web浏览器的请求到达时,服务器动态地加载和链接适当地函数,然后直接调用它,而不是使用fork和execve在子进程的上下文中运行函数。函数会一直缓存在服务器的地址空间中,所以只要一个简单的函数调用的开销就可以处理随后的请求了。

本文参考自《深入理解计算机系统》

分享到:

  admin

注册时间:

网站:0 个   小程序:3 个  文章:0 篇

  • 452

    网站

  • 9

    小程序

  • 3601

    文章

  • 83

    会员

赶快注册账号,推广您的网站吧!
热门网站
最新入驻小程序

跳一跳2022-08-22

跳一跳是微信开发的一款小游戏,有

数独大挑战2018-06-03

数独一种数学游戏,玩家需要根据9

答题星2018-06-03

您可以通过答题星轻松地创建试卷

全阶人生考试2018-06-03

各种考试题,题库,初中,高中,大学四六

运动步数有氧达人2018-06-03

记录运动步数,积累氧气值。还可偷

每日养生app2018-06-03

每日养生,天天健康