-
一、程序的编译过程,目标文件里究竟是什么 从源程序到目标文件的生成过程 最简单的编译命令是gcc helloworld.c,它包含了以下几个步骤: 预处理、编译、汇编、链接,下面分别简介。 预处理:处理#define宏定义、#if #ifdef等条件编译指令、#include预编译指令,删除注释,添加行号和文件名标识,保留所有的#pargma编译器指令,经过预编译后的文件为.i文件。预编译命令为:gcc -E hello.c -o hello.i或者cpp hello.c > hello.i 编译:把预处理完得文件进行一系列的词法分析、语法分析、语意分析及优化后产生的汇编代码文件。编译命令为gcc -S hello.i -o hello.s。现在版本的gcc把预编译和编译两个步骤合并成一个步骤,使用ccl程序来完成,命令为ccl hello.c。 也可以使用gcc -S hello.c -o hello.s直接从.c文件生成.s汇编文件。 汇编:将汇编代码转变成机器可以执行的指令,每一条汇编语句几乎都对应一条机器指令。命令为:as hello.s -o hello.o或者gcc -c hello.s -o hello.o或者我们最熟悉的gcc -c hello.c -o hello.o 链接:当我们的程序模块调用a另一个模块中b的函数(foo())或变量时,在编译的阶段编译器并不知道函数foo的地址,所以暂时把调用foo的指令的目标地址搁置,等待最后链接的时候由连接器去将这些指令的目标地址修正。把目标文件和库一起链接成可执行文件。最常见的库时运行时库。 目标文件中的格式 目标文件就是源代码编译后但未进行链接的那些中间文件,它和可执行文件的内容和结构其实很相似,所以一般和可执行文件采用同一种格式存储,那就是ELF格式。与ELF格式相对应的是Windows平台下的PE格式,它们都是COFF格式的变种。不光是可执行文件和目标文件按照ELF格式存储,动态链接库(.so)和静态链接库(.a)都按照ELF格式存储。 ELF格式的文件可分为以下4类: 1 可重定位文件(Relocatable File):这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类,代表是Linux的.o文件 2 可执行文件(Executable File):这类文件包含了可以直接执行的程序,它的代表就是ELF可执行文件,它们一般都没有扩展名,如/bin/bash文件 3 共享目标文件(Shared Object File):这种文件包含了代码和数据,可以在以下两种情况下使用。一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件。第二种是动态链接器可以将这几个共享目标文件与可执行文件结合,作为进程影响的一部分来运行。 4 核心转储文件(Core Dump File):当进程意外终止时,系统可以将该进程的地址空间的内容以终止时的一些其他信息转储带核心转储文件。 上面几种文件在file命令下会显示出相应的类型。 目标文件里有什么: ELF文件最重要的结构是ELF文件头(ELF Header):ELF文件头里定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段得数量等。 三个最重要的段:代码段(.text)、数据段(.data)和只读数据段(.rodata)、BSS段(.bss) 可以用objdump -s -x -d hello.o来分析各个段得内容,-d表示反汇编,-s表示把各段内容用16进制打印,-x表示详细数据。 顾名思义,.text段主要存放可执行的代码数据;.data段保存的是已经初始化了的全局变量和静态变量;.rodata段保存只读数据,一般是程序里的只读变量(如const修饰的变量)和字符串常量;bss保存未初始化的全局变量和静态变量。 除此之外,还有一些常见的段,如 .comment段存放编译器版本信息,比如字符串“GCC:(GNU)4.2.0” .dubug段存放调试信息 .dynamic存放动态链接信息 .hash段存放符号哈希表 .line段存放调试时的行号表 .note段存放额外的编译器信息 .strtab段存放字符串表,用于存储ELF文件中用到的各种字符串,比如符号的名字 .shstrtab段存放段表中用到的字符串,最常见的就是段名 .symtab段是符号表,通过符号表就能知道这个符号在哪个段,以及在这个段的具体位置,还有这个符号在字符串表中的位置 Section Table(段表)也是其中一个段,它保存了各个段的信息,如段名、段的长度、在文件中的偏移、读写权限及段的其他属性 .rel.text段是一个重定位表,正如前面所说的,链接器在处理目标文件时,需要对目标文件的某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置,这些重定位的信息都记录在ELF文件的重定位表里 通过ELF文件头的信息可以找到段表的位置,从而找到各个段得位置和信息。 可以用readelf -S hello.o命令详细查看ELF文件中各段的信息。 二、可执行文件的装载 程序执行时所需要的指令和数据必须都在内存中才能正常运行,最简单的办法就是将程序运行时所需要的指令和数据全部都装入内存,这样程序就能顺利执行,这就是最简单的静态装入的方法。但是程序所需要的内存数量可能大于物理内存,静态装入就不太现实。 后来研究发现,程序运行时具有局部性原理,所以我们可以将程序最常用的部分驻留在内存中,而将一些不太常用的数据存放在磁盘上,这就是动态装载的原理。 覆盖装入和页映射是两种典型的动态装入方法。覆盖装入就是如果两个模块不会同时运行,则可以使这两个模块共用一块内存,需要哪个模块的时候就装入。现在基本已经淘汰了。 页映射是虚拟存储机制的一部分,它随着虚拟存储的发明而诞生。在页映射机制中,程序装载和操作的单位都是页。最常见的Inter IA32处理器一般都使用4KB大小的页。 假设程序所有的指令和数据总和为32kB,那么程序总共分为8页,将其编号为P0-O7,并假设物理内存只有16kB,编号为F0-F3。如果程序刚开始执行时的入口地址为P0,这时装载管理器发现程序的P0不在内存中,于是将F0分配给P0,并将P0的内容装入F0;运行一段时间后,程序需要用到P5,于是装载管理器将P5装入F1,就这样,当程序用到P3和P6时,它们分别装入到了F2和F3。 很明显,如果这时程序只需要P0,P3,P5和P6这4个页,那么程序就能一直运行下去。但是如果这时候需要访问P4,那么装载管理器需要最初选择,它必须放弃目前正在使用的4个物理内存页中的一个来装载P4。至于选择哪个页,可以有多种算法选择,比如FIFO先入先出算法,LRU最近最少使用算法等。 其实,上面所说的装载管理器就是操作系统的存储管理器。 从操作系统的角度看可执行文件的装载 从操作系统的角度看,一个进程最关键的特征就是它拥有独立的虚拟地址空间,这使得它有别于其他进程。这在http://blog.csdn.net/vividonly/archive/2011/05/04/6393516.aspx一文中有详细解释。要使一个可执行程序得以执行,首先必须创建一个进程,然后装载相应的可执行文件并且执行。在有虚拟存储的情况下,上述过程最开始只要做三件事: 1 创建一个独立的虚拟地址空间。这时候并不设置虚拟地址页和物理地址页的映射关系,这些映射关系等到后面程序发生页错误的时候再进行设置。当发生页错误时,操作系统将从物理内存中分配一个物理页,然后将该“缺页”从磁盘中读取到内存中再设置缺页的虚拟页与物理页的映射关系。这都是通过CPU的MMU来实现的。 2 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。这个映射关系在进程中叫做VMA(虚拟内存区域)。比如对于.text段,在创建进程后,会在进程相应的数据结构中设置一个.text段的VMA,记录了它在虚拟空间的地址以及它在ELF文件中的偏移。当程序执行发生页错误,通过查找VMA结构来定位错误页在可执行文件的位置,把可执行文件的那部分内容装入刚分配的物理内存中,进程从刚才页错误的位置重新开始执行。 3 将CPU的指令寄存器设置为可执行文件的入口地址,启动运行。操作系统通过设置CPU的指令寄存器将控制权转交给进程。这里的入口地址就是ELF文件头中的入口地址。
读书笔记—程序员的自我修养
最新推荐文章于 2022-04-26 14:09:32 发布