C/C++ 编译、连接与执行

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


1.编译
       一个个源文件,经过编译系统的处理,生成目标文件的过程叫做编译。编译是对一个个源文件分别处理的,因此每个源文件构成了一个独立的编译单元编译过程中不同的编译单元互不影响。a.cpp和b.cpp这两个源文件经过编译后,在Linux下会生成a.o和b.o两个目标文件。

       目标文件主要用来描述程序在运行过程中需要放在内存中的内容,这些内容包括两大类—代码和数据。相应地,目标文件也分成代码段和数据段。

       代码段(.text)中的内容就是源文件中定义的一个个函数编译后得到的目标代码。目标文件a.o的代码段中应当包含,main函数的目标代码,而目标文件b.o的代码段中应当包含func函数的代码。无论是普通函数的代码,还是类的成员函数的代码,都放在代码段中。

       数据段中包含对源文件中定义的各个静态生存区对象(包括基本类型变量)的描述,数据段又分为初始化的数据段(.data)和未初始化的数据段(.bss).其中,初始化的数据段中包含了那些在定义的同时设置了初值的静态生存区对象(通过执行构造函数赋初值的不在此列)。对于这些对象,其初值被放在初始化的数据段中,这些对象在运行时占多少内存空间,在目标文件中就要提供多少空间存放它们的初值。例如:由于b.cpp中定义了静态生成区的整型变量x,在b.o的初始化的数据段中,需要存储x的初值3.

       其他静态生存期对象,都放在未初始化的数据段中。由于它们没有静态的初值,目标文件中不需要保留专门空间存储它们的信息,只需记录这个段的大小。b.cpp中的变量y就属于该段。

       几个段的内容,都是在该源文件中有定义的内容,而那些只声明而未经定义的全局变量或函数并不出现在这几个段中。例如a.cpp中的y并没有出现在a.o的数据段中,而func也没有出现在a.o的代码段中.但是,目标文件的信息到此还不完整,例如,a.cpp的main函数中改写了变量y的值,但y是在b.cpp中定义的,这种不同编译中被引用但未定义的外部变量、外部函数,在符号表中也有相关的条目,但条目中只有符号名,而位置信息是未定义的。a.o和b.o两个目标文件中的各段和符号表的内容如图。

 


提示   符号表中,函数并不只以它在源程序中的名字命名,函数在符号表中的名字至少要包括源程序中的函数名和参数表类型信息。因为函数可以重载,由于符号表中没有专门的类型信息,参数表信息只能在名字中有所体现,否则在目标文件中无法对函数名相同但参数表不同的函数加以区分。a.o和b.o的符号表中,func函数的名字为_Z4funci.

       最后需要指出的是,目标文件代码的目标代码中对静态生存区对象的引用和对函数的调用所使用的地址都是未定义的,因为它们的地址在连接阶段才能确定。因此在目标文件中还需要保存一些信息,用来将目标代码中的地址和符号表中的条目建立关联,这样到连接时,通过这些信息就可以将这些指令中的地址设置为有效的地址。这些信息成为重定位信息。

       目标文件中的内容还不止以上这些,例如,为了配合调试工具,他还要包含一些调试信息。

2.连接

       在连接期间,需要将各个编译单元的目标文件和运行库当中被调用过的单元加以合并运行库实际上就是一个目标代码文件的集合,运行库的各个组成部分和a.o,b.o这样的目标代码具有相同的结构。经过合并后,不同编译单元的代码段和两类数据段就分别合并到一起了,程序在运行时代码和静态数据需要占据的内存空间就全部已知了,因此所有代码和数据都可以被分配确定的地址了。

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

连接时出错主要是运行库文件或者目标文件缺失以及编译单元中重复定义或未定义函数等问题。

提示      符号表能够被正确综合的一个前提是,对于同一个符号,只在刚好一个编译单元中有定义,而在其它编译单元中是未定义的。之所以要有这个要求,是因为合并后符号表中各符号的地址,需要根据该符号在有定义的编译单元中的相对地址来确定。如果一个符号在各个编译单元中都有定义,那么它的地址将无所适从,这时会出现符号定义冲突的连接错误。这从一个方面说明了,为什么对于任何一个对象或函数,引用性声明可以有多个,但定义性声明有且只能有一个。

       连接的对象除了用户源程序生成的目标文件外,还有系统的运行库。例如,执行输入输出功能,调用sin,fabs这类标准函数,都需要通过系统运行库。此外,系统运行库中还包括程序的引导代码。在执行main函数之前,程序需要执行一些初始化工作;在main函数返回后,需要通知操作系统程序执行完毕,这些都需要由运行库中的代码来完成。

       连接后生成的可执行文件的主体,和目标文件一样,也是各个段的信息,只是可执行文件的代码段中所有指令的地址,都是有效地址了。符号表可以出现在可执行文件中,也可以不出现,这不会影响到程序的执行,如果可执行文件中出现了符号表,也只是对调试工具有用。

3.执行

程序的执行,是以进程为单位的。程序的一次动态执行过程称为一个进程。进程与程序的关系,就像是一次具体的函数调用与函数的关系,程序只有在执行时才会生成进程,执行结束后进程就会消失。

       程序是存储在磁盘上的,在执行前,操作系统需要首先将它载入内存中,并为它分配足够大的内存空间来容纳代码段和数据段,然后把文件中存放的代码段和初始化的数据段的内容载入其中—一部分静态生存区对象的初始化就是通过这种方式完成的,这与动态生存区对象的初始化不同。例如b.cpp中的:

int x = 3;

       x的初始化在操作系统载入初始化的数据段时就已经完成了。而a.cpp中的下列代码:

       int x = 1;

       z在局部作用域中,z的初始化,需要等到执行到这条语句时,由编译器生成的代码来完成。

细节       那些需要用构造函数来初始化的静态生存区对象又有所不同,它们的初始化,需要由编译器生成专门的代码来调用构造函数,这些代码被调用的时机也由编译器控制。命名空间作用域中的此类对象的初始化代码,一般在执行main函数之前,由引导代码调用;局部作用域中的此类对象,其初始化代码一般会内嵌在函数体中,并用一些静态的标志变量来标识这样的对象是否已初始化,从而保证它们的初始化代码只执行了一次。   

       此外,操作系统还要做一些进程的初始化工作,这些工作完成后,就会跳转到程序的引导代码,开始执行程序。当程序执行结束后,引导代码会通知操作系统,操作系统会完成一些善后工作,程序的一个执行周期就这样结束了。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值