程序的编译,装载与链接过程

程序编译四个过程:
前言:

经过扫描,语法分析,语义分析,源代码优化,代码生成和目标代码优化,编译器忙活了这么多个步骤以后,源代码终于可以被编译成了目标代码。但是这个目标代码有一个问题:index和array的地址还没有确定。
如果我们要把目标代码使用汇编器编译成能够执行的指令,那么index和array的地址应该从哪里得到呢还有and so on?事实上,定义其他模块的全局变量和函数在最终运行时的绝对地址都是要在最终链接的时候才能确定。
所以现代编译器可以将一个源代码文件编译成一个未链接的目标文件,然后由链接器最终确定这些目标文件链接起来形成可执行文件。
预处理(Prepressing)
源代码和相关的头文件被预编译器cpp预编译为一个 .i 文件(#define ,#include,#if,删除注释行)

编译(Compilation)
将预处理之后的文件进行一系列词法分析,语法分析,语义分析及优化后生产相应的汇编代码文件

汇编(Assembly)
将汇编代码转化为机器可以执行的机器代码,例如使用gcc命令从C源代码文件开始,经过预编译,编译和汇编直接输出目标文件(Object File)(还没有经过链接的过程)

链接(Linking):
目标文件和库一起链接形成最中的可执行文件

目标文件的特点
1. 各个段没有具体的起始地址,只有段大小信息
2. 各个标识符没有实际地址,只有段中的相对地址
3. 段和标识符的实际地址需要链接器具体确定
链接器的工作:
将各个文件和库文件整合为最终的可执行程序
合并各个目标文件中的段
确定各个段和段之间的标识符的最终地址(重定位)
_start函数
程序加载后,_start函数是第一个被调用执行的函数(_start函数入口地址就是代码段的起始位置)
_start函数准备好参数之后,立即调用_libc_start_main()函数
_libc_start_main()函数初始化运行环境后调用main()函数
_libc_start_main()的作用
调用_libc_csu_init()函数:完成必要的初始化操作
启动程序的第一个线程(主线程),main为线程入口
注册_libc_csu_fini()函数:程序运行终止时被调用
扩展
也可以自定义程序入口函数:
nostartfiles // 指定入口函数
nodefaultlibs // 不使用默认库文件
nostdlib // 不使用标准库函数
//program.c
#include <stdio.h> 
#include <stdlib.h>
int program()       // entry function
{
    printf(" Hello BT \n");
    exit(0);
}
1
2
3
4
5
6
7
8
gcc -e program -nostartfiles program.c将程序的入口地址定义为program。

可执行文件的文件格式在linux下为ELF,文件后缀为 .o ,在 windows下为PE文件格式。
文件格式如下:文件头,代码段,数据段和只读数据段,BSS段,其他段等等

2. 静态链接
对于链接器而言,整个链接过程就是将几个输入目标文件加工后合并成一个输出文件,如何将多个输入文件,将他们的各个段合并输出到输出文件?
相似段合并:


第一步:空间与地址分配(分析这两个步骤中链接器的工作过程,在第一步的扫描和空间分配阶段,链接器按照前面介绍的空间分配方法进行分配,这时输入文件中各个段在链接后的虚拟地址就已经确定,比如.text段的起始位置和.data的起始位置)
第二步:符号解析与重定位
重定位:我们在程序模块main.C中使用另外一个模块func.c中的函数foo()我们在每一处调用foo的时候都必须确切知道foo这个函数的地址,所以它暂时把这些调用foo的指令的目标地址搁置,等待最后链接的时候由链接器去将这些指令的目标地址修正,则填入正确的foo函数地址。当func.c模块重新编译,foo函数位置地址有可能改变,那么我们在main.c中所有使用到foo的地址的指令将要全部重新调整。

3. 可执行文件的装载
程序是一个静态的概念:预编译好的指令和数据集合的一个文件。进程则是一个动态的概念:是程序运行时的一个过程。

我们知道每个程序运行起来,它将有自己独立的虚拟地址空间(Virtual address Space)
程序运行时需要将所需要的指令和数据必须放在内存中才能够正常运行。
页映射
就是将内存和所有磁盘中的数据和指令按照页(page)为单位进行划分,然后一一形成映射关系。

从操作系统角度看可执行文件的装载
如果程序使用物理地址直接操作,那么每次页被装入时都需要进行重定位。在虚拟存储中,现代的硬件MMU提供地址转换功能。有了硬件的地址转换+页映射机制,操作系统动态加载可执行文件与静态加载有了很大的区别。

进程的建立

创建一个独立的虚拟地址空间(虚拟空间由一组页映射函数将虚拟空间的各个页映射至相应的物理空间)
读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系
将CPU的指令寄存器设置成可执行文件(映像文件)的入口地址,启动运行
页错误
4.动态链接
静态链接浪费空间:每个程序内部除了都保留和printf()函数,scanf()函数等这样的公共库函数,还有数量相当可观的其他库函数以及辅助数据结构。例如,program1与program2都用到了Lib.o这个模块,它们同时在链接输出可执行文件program1和program2有两个副本。当我们同时运行program1与program2时,Lib.o在磁盘和内存中都有两份副本。

静态链接更新,部署和发布也很困难。如program1所使用的Lib.o是一个第三方提供的,当该方更新了Lib.o时候,那么program厂商就需要拿到最新的Lib.o,然后将其与Program1.o链接后,将新的program1整个发布给用户。

共享对象文件( .so )
用gcc命令生成共享对象
        gcc -fPIC -shared -o Lib.so Lib.c
1
使用readelf来获取ELF文件的相关信息
        readelf -d Lib.so   显示动态段的信息
        readelf -r Lib.so   显示可重定位段信息
        readelf -S Lib.so   显示节头信息
        readelf -s Lib.so   显示符号段中的项
        readelf -a Lib.so   显示全部信息
1
2
3
4
5
objdump命令查看目标文件或者可执行的目标文件的构成的gcc工具
        objdump    -a   显示档案成员信息,类似ls -l 将lid*.a的信息列出
        objdump    -d   从objfile中反汇编那些特定指令机器代码
        objdump    -f   显示objfile 中每个文件的整体头部摘要信息
        objdump    -r   显示文件的重定位入口
        objdump    -s   显示指定section的完整内容
        objdump    -S   尽可能反汇编出源代码
        objdump    -t   显示文件的符号表入口
        objdump    -x   显示所可用的头信息
1
2
3
4
5
6
7
8
5. DLL简介(windows系统下的概念)
windows下的PE的动态链接与linux下的ELF动态链接相比,有很多类似的地方,但是也有不同的地方。下面我们围绕PE与windows的动态链接来展开,介绍PE的符号导入导出机制,重定位和DLL的创建于安装以及DLL的性能等。

DLL即动态链接库(dynamic-Link library)相当于Linux下的共享对象。DLL与EXE文件实际上是一个概念,都是PE格式的二进制文件

DLL的设计与共享对象有些出入,DLL更加强模块化。

导出表
当PE需要将一些函数或变量提供给其他PE文件使用,我们就把这种行为叫符号导出。
导入表
当我们某个程序使用某个来自DLL的函数或者变量,那么我们就把这种行为叫做符号导入。
6. 堆栈管理
栈:用于维护函数调用的上下文
堆:是用来容纳应用程序动态分配的内存区域。光有栈对于面向过程的程序设计还远远不够,因为栈上的数据在函数返回的时候就会被释放掉,所以无法将数据传递至函数外部。而全局变量没法动态的产生,只能编译的时候定义,有很多情况下缺乏表现力。在这种情况下,堆是唯一的选择。
 

转自:https://blog.csdn.net/qq_28485501/article/details/82972333

猜你喜欢

转载自blog.csdn.net/phenixyf/article/details/89248233