本文搬运自本人 CSDN 博客:https://blog.csdn.net/ajianyingxiaoqinghan/article/details/70889362
程序生成之编译、链接、加载浅析
最近笔者看论文烦得慌,便又重新拾起之前没有完全完成的交叉编译,准备在网上找资料,好好研究一下。
讲道理,笔者其实对编译链接的过程都不是很明白,所以如果想要了解交叉编译,还是先从编译链接的基本概念看起吧。
本文参考链接:
http://blog.csdn.net/shenjianxz/article/details/52130111
http://blog.csdn.net/koudaidai/article/details/8092647
http://blog.163.com/gene_lu/blog/static/6402542120138181597392/
一. 编译
- 输入对象:程序源码
- 输出目标:目标文件
- 工具:编译器
- 如PC机常用编译器为gcc,ARM常用编译器为arm-linux-gcc
编译过程又可以被分为两个阶段:<font color=#ff0000>编译</font>、<font color=#ff0000>汇编</font>。
1. 编译
编译是指编译器读取字符流的源程序,对其进行<font color=#ff0000>词法与语法的分析</font>,将高级语言指令转换为功能等效的<font color=#ff0000>汇编代码</font>。
编译主要分为两个过程:预处理过程、编译过程。
(1) 预处理过程
预处理过程将.c文件转换为.i文件,当编译器为gcc时,使用的命令是gcc -E,对应于预处理命名cpp。即进行预处理的命令如下:
<code>gcc -E hello.c -o hello.i</code>
其中的参数-E代表只进行预处理。或:
<code>cpp hello.c > hello.i</code>
预处理过程,主要是以下几部分:
- 宏定义指令
- 如 #define a b对于这种伪指令,预编译所要做的是将程序中的所有a用b替换,但作为字符串常量的 a则不被替换。
- 条件编译指令
- 如#ifdef,#ifndef,#else,#elif,#endif等。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉
- 头文件包含指令
- 如#include "FileName"或者#include 等。 该指令将头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
- 特殊符号
- 预编译程序可以识别一些特殊的符号。 例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
关于头文件的搜索规则:
- 所有头文件的搜寻从<code>-I</code>开始;
- 然后在环境变量指定路径中寻找(环境变量如<code>C_INCLUDE_PATH</code>, <code>CPLUS_INCLUDE_PATH</code>, <code>OBJC_INCLUDE_PATH</code>等);
- 最后在默认目录中寻找(如<code>/usr/include</code>, <code>/usr/local/include</code>, <code>/usr/lib/gcc-lib/i386-linux/2.95.2/include</code>等);
(2) 编译过程
编译是把预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码。
指令如下:
<code>gcc -S hello.i -o hello.s</code>
或:
<code>/usr/lib/gcc/x86_64-linux-gnu/4.8/cc1 hello.c</code>
注:
1、现在版本的GCC把预处理和编译两个步骤合成一个步骤,用cc1工具来完成;
2、gcc其实是后台程序的一些包装,根据不同参数去调用其他的实际处理程序,比如:预编译编译程序cc1、汇编器as、连接器ld
2. 汇编
汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。汇编相对于编译过程比较简单,根据汇编指令和机器指令的对照表一一翻译即可。
指令如下:
<code>gcc -c hello.c -o hello.o</code>
或:
<code>as hello.s -o hello.o</code>
汇编生成的目标文件中,存放的是与源程序等效的机器语言代码。生成的目标文件由段组成,通常至少有两个段:
- 代码段:该段中包含的是程序指令。该段一般可读可执行,但一般不可写;
- 数据段:主要存放程序中用到的各种全局变量或静态数据。一般数据段都是可读、可写、可执行的。
3. 目标文件
生成的目标文件一般为下列三种:
- 可重定位(Relocatable)目标文件:由编译器和汇编器生成,可与其他可重定位目标文件合并,创建一个可执行或共享的目标文件;
- 共享(Shared)目标文件:一种特殊的可重定位目标文件,可以在链接(静态共享库)时加入目标文件,也可以在加载或运行时(动态共享库)动态的被加载到内存并执行;
- 可执行(Executable)目标文件:由链接器生成,可直接通过加载器加载到内存中,充当进程执行的文件。
4. 静态库与动态库
库从本质上来说,都是一种可执行代码的二进制格式,可以被载入内存中执行,可分为静态库与动态库两种。静态函数库与动态函数库相同之处在于,都是由*.o目标文件生成。
<font color=#ff0000>静态函数库</font>的名字一般是<code>libxxx.a</code>。利用静态函数库编译成的文件比较大,因为整个函数库的所有数据都会被整合进目标代码中。
- 优点
- 程序员不需要显式的指定所有需要链接的目标模块,因为编译后的执行程序不需要外部的函数库支持,因为所有使用的函数都已经被编译进去了,且指定的工作本身就是一个耗时且容易出错的过程;
- 链接时,链接程序只从静态库中拷贝被程序引用的目标模块,这样就减小了可执行文件在磁盘和内存中的大小。
- 缺点
- 如果静态函数库改变了,那么程序必须重新编译;
<font color=#ff0000>动态函数库</font>的名字一般是<code>libxxx.so</code>。相对于静态函数库,动态函数库在编译的时候并没有被编译进目标代码中,只有程序执行到相关函数时,才调用该动态函数库里的相应函数,因此,动态函数库产生的可执行文件较小。
- 优点
- 动态函数库产生的可执行文件较小;
- 动态函数库的升级比较方便,因为动态函数库的改变并不影响你的程序;
- 运行中可供多个程序使用,内存中只需要有一份,节省内存。
- 缺点
- 由于函数库没有被整合进你的程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库;
二. 链接
链接的主要工作是把各个模块之间相互引用的部分处理好,使各个模块之间可以正确衔接。
链接的作用,一方面在于使得分离编译成为可能;另一方面在于动态绑定的实现,即定义、实现、使用分离(笔者理解成头文件*.h、源文件*.cpp文件、目标文件的分离)
1. 链接的时机
- 编译:源代码被编译成机器代码时,静态链接器负责链接;
- 加载:程序被加载到内存时,加载器负责链接;
- 运行:应用程序运行时,动态链接器负责链接。
2. 链接库的搜索路径
静态库的搜索由静态链接器负责,搜索路径如下:
- 先从gcc参数-L开始寻找;
- 再寻找环境变量LIBRARY_PATH指定的搜索路径;
- 最后在内定目录下搜索(如<code>/lib</code>, <code>/usr/lib</code>, <code>/usr/local/lib</code>等)
动态库的搜索由动态链接器负责,搜索路径如下:
- 先从gcc参数-L开始寻找;
- 再寻找环境变量LD_LIBRARY_PATH指定的搜索路径;
- 配置文件<code>/etc/ld.so.conf</code>中指定的动态库搜索路径;
- 最后在默认动态库搜索路径中搜索(如<code>/lib</code>, <code>/usr/lib</code>, <code>/usr/local/lib</code>等)
3. 静态/动态链接
链接可以分为静态链接与动态链接。
(1) 静态链接
静态链接是指在编译阶段直接把静态库加入到可执行文件中去。一般静态链接生成的可执行文件较大。静态链接过程的流程如下图所示:
静态链接过程
链接器将函数的代码从其所在地(目标文件或静态链接库中)拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。
为了创建可执行文件,链接器必须完成的任务是:
- 符号解析:把目标文件中符号的定义和引用联系起来;
- 重定位:把符号定义和内存地址对应起来,然后修改所有对符号的引用。
(2) 动态链接
动态链接指链接阶段仅仅加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中。
在此种方式下,函数的定义在动态链接库或共享对象的目标文件中。在编译的链接阶段,动态链接库只提供符号表和其他少量信息用于保证所有符号引用都有定义,保证编译顺利通过。动态链接器(ld-linux.so)链接程序在运行过程中,根据记录的共享对象符号定义来动态加载共享库,然后完成重定位。在该可执行文件被执行时,动态链接库的全部内容被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。
三. 加载
加载器将可执行文件从外存加载到内存中,并执行。加载过程如下:
加载器首先创建内存映像。Linux进程运行时的内存映像如下:
Linux进程运行时的内存映像
根据上面的内存映像,加载器跳转到程序入口点(即_start符号的地址),执行启动代码(startup code)。启动代码的调用顺序如下:
0x080480c0 <_start>:
call __libc_init_first
call _init
call atexit
call main
call _exit
在执行完初始化任务,即_init之后,启动代码调用atexit例程,该例程注册了一系列调用exit函数时必须的例程。随后,启动代码调用应用程序的main例程,执行用户程序代码。当用户程序返回后,启动代码调用_exit例程,将控制权交还给操作系统。
网友评论