【项目构建】01:代码编译链接与执行的过程

代码编译链接与执行的过程


在这里插入图片描述

  1. 预处理Preprocessing:将.cpp文件转化为.i文件,cpp -o test.i test.cppgcc -E test.c -o test.i

    预处理器把所有include的文件包括递归包含的文件内容,都展开到输出文件,并展开了所有的宏定义。

  2. 编译Compilation:将.cpp/.h文件转换成.s文件,cc test.i -o test.sgcc -S test.i -o test.x

    编译的过程将预处理的文件进行一系列的词法分析、语法分析、语义分析及优化成相应的汇编代码。这一步中一般会进行优化,比如去除没有用到的类的声明、循环语句的优化等。

  3. 汇编Assemble:将.s文件转化为.o文件,as -o test.o test.sgcc -c test.s -o test.o

    as汇编器会将汇编代码转换为机器指令,并以特定的二进制格式输出保存在目标文件中

  4. 链接Linking:将.o文件转换为可执行程序,ld test.o -o testgcc test.c

    ld链接器将程序的相关目标文件组合链接在一起,生成程序的可执行映像文件。要解决的问题是:可能调用了库函数、或者一个目标文件中调用了另外一个文件中的库函数,需要通过链接器建立对应的关系,使程序能够正常的执行。

补充:静态链接与动态链接

  • 静态链接:将源代码从静态链接库中拷贝到最终的可执行程序中,这样可能会导致最终的目标文件很大。
  • 动态链接:需要调用的库函数以动态链接库的形式存在,多个进程之间共享。而链接的时候只需要知道要调用的函数的位置即可。在程序执行时当需要调用某个动态链接库中的函数式,操作系统首先会查找所有正在运行的程序,看内存中是否已经有该库函数的拷贝了,如果有则多进程之间可以共享该拷贝,否则才会将其载入到该进行的虚拟内存中。

1.编译

源文件经过编译系统处理,生成目标文件的过程称为编译的过程。编译是对一个个源文件分别处理的,因此每个源文件构成了一个独立的编译单元,编译的过程中不同的编译单元互不影响。

编译后生成的目标文件:源文件编译后会生成 xxx.o 的目标文件,目标文件主要用于描述程序在运行过程中需要放在内存中的内容(代码和数据),相应的目标文件也被分为代码段和数据段。

  • 代码段.text:源文件中定义的一个个函数编译后得到的目标代码(普通函数和类成员函数的代码都放在代码段中)
  • 数据段:源文件中定义的各个静态生存期对象(包括基本类型变量)的描述,
    • 初始化的数据段.data:在定义时同时设置了初值的静态生存期对象(通过执行构造函数的方式赋初值的不在此列),其初始值被放在初始化的数据段中,这些对象在运行时占多少内存空间,在目标文件中就要提供多少空间存放它们的初值。
    • 未初始化的数据段.bss:其他静态生存期对象都存放在未初始化的数据段中,由于没有设置初始值,目标文件中不需要保留专门空间存储他们的信息,只需记录这个段的大小即可。
  • 注意只有声明,而未经定义的全局变量or函数并不出现在这几个段中

编译后生成的目标文件中的信息还不完整,不同编译单元间的相同变量or函数还需要建立联系(通过目标代码的符号表)

关于符号表:用于将各个标识符名称和它们在各段中的地址关联起来的数据结构(若干条目),

  • 每个静态生存期对象 or 函数都对应于符号表中的一个条目,其存储了该静态生存期对象 or 函数的名字和在该目标文件中的位置,位置是通过所在段以及相对于该段首偏移地址来表示的。
  • 对于在编译单元中被引用但未定义的外部变量/函数,在符号表中也有相关的条目(只有符号名,而位置信息是未定义)

如下案例所示:

//a.cpp
extern int y;
int func(int v);
int main() {
    int z = 1;
    y = func(z);
    return 0;
}
//b.cpp
int x = 3;
int y;
int func(int v) {
    return v + x;
}

在这里插入图片描述

2.链接

在链接期间需要将各个编译单元的目标文件运行库当中被调用过的单元(系统运行库)加以合并,

  1. 运行库实际上就是一个个目标代码文件的集合,
  2. 运行库的各个组成部分和a.o、b.o这样的目标代码具有相同的结构,
  3. 标准库函数、程序的引导代码、初始化工作、通知操作系统程序执行完毕,都需要通过系统运行库

经合并后不同编译单元的代码段和两类数据段就分别合并到一起了,程序在运行时代码和静态数据需要占据的内存空间就全部已知了(所有代码和数据都可以被分配确定的地址了),

与此同时各个目标文件的符号表也可被综合起来,符号表中的条目也会有确定的地址,重定位信息这时能够发回作用了,各段代码中未定义的地址都可以被替换为有效地址,

链接后生成的可执行文件主体和目标文件一样也是各个段的信息,只是可执行文件的代码段中所有指令的地址,都是有效地址了。

符号表可以出现在可执行文件中(也可不出现),不会影响到程序的执行,如果出现了符号表也只是对调试工具有用。

3.执行

程序的执行是以进程为单位的,程序的一次动态执行过程被称为一个进程。程序只有在执行时才会生成进程,执行结束后进程就会消失。

  • 程序是存储在磁盘上的,在执行前需要操作系统将其载入到内存中,并为其分配足够大的内存空间来容纳代码段和数据段,然后将文件中存放的代码端和初始化的数据段内容载入其中(一部分静态生存期对象的初始化就是通过这种方式完成的),
  • 与需要用构造函数初始化的静态变量不同,其初始化需要由编译器生成专门的代码来调用构造函数,调用时机由编译器控制
  • 命名空间作用域中的此类对象初始化代码,一般在执行main函数之前由引导代码调用。
  • 局部作用域中的此类对象初始化代码,一般会内嵌在函数体中,并用一些静态的标志变量来标识这样的对象是否已初始化,从而保证他们的初始化代码只被执行一次。

4.内存分布

image-20230826103902683

内存总共分为五大分区:栈区,堆区,全局静态区,常量存储区,程序代码区(自由存储区)

  1. 栈,存储局部变量、函数参数值,

    • 栈从高地址向低地址增长,是一块连续的空间。
    • 在执行函数时,函数内局部变量的存储单元都可以在栈上创建,在需要时分配不需要时自动清除的存储区,
  2. 堆,存储由new分配的内存块,

    • 动态申请内存用,堆从低地址向高地址增长
    • 由new分配的内存块,其释放由程序员控制,new需要对应进行delete操作
  3. 全局/静态存储区:全局变量和静态变量被分配到同一块内存中

    • .data数据段:存放程序中已初始化的全局变量和静态变量的一块内存区域。
    • .bss数据段:存放程序中未初始化的全局变量和静态变量的一块内存区域。
  4. 常量存储区:存放常量,并不允许修改,

  5. 代码区/自由存储区:存放程序体的二进制代码,比如我们写的函数,都是在代码区的。

    • 由malloc等分配的内存块,和堆是十分相似的,不过是用free来结束自己的生命,
    • 代码段为只读数据,代码段的头部还会包含一些只读的常数变量。

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值