C语言程序的内存布局(三)可执行程序的连接

13.3  可执行程序的连接

13.3.1  可执行程序的组成

上一节分析了C语言应用程序中各段的情况,实际的C语言可执行程序,将由各个文件经过连接生成。目标文件是由每一个C语言源程序(*.c)经过编译器生成,目标文件(.o)的主要组成部分即代码段、只读数据段和读写数据段三个段。未初始化数据段、堆和栈不会占用目标文件的空间。

可执行程序是由各个目标文件经过连接而成。其主体部分依然是代码段、只读数据段和读写数据段,这三个段由各个目标文件(.o)经过"组合"而成。C语言目标文件到可执行程序的连接如图13-2所示。

连接器将根据连接顺序将各个文件中的代码段取出,组成可执行程序的代码段,只读数据段和读写数据段也是如此。在连接过程中,如果出现符号重名、符号未定义等问题,将会产生连接错误。如果连接成功,将会生成一个统一的文件,这就是可执行程序。

由连接器生成的可执行程序包含了各个目标文件的各个段的内容,并且会附加可执行程序的头信息。在可执行程序中,各个目标文件的代码段、只读数据段、读写数据段经过了重新的排列组合。因此,在最终的可执行程序中,已经没有了各个目标文件的概念。

值得注意的是,在连接的过程中,连接器可以得到未初始化数据段的大小,它也是各个目标文件的各个未初始化数据段数据段之和,但是这个段是不会影响可执行程序大小的。从C语言使用的角度,读写数据段和未初始化数据段都是可读写的。实质上,在目标文件(*.o)中未初始化数据段和读写数据段的区别也在于此:读写数据段占用目标文件的容量,而未初始化数据段只是一个标识,不需要占用实际的空间。

 
(点击查看大图)图13-2  C语言目标文件到可执行程序的连接

例如,在某一个C语言的源程序文件中,具有以下的内容:

static char bss_data[2048];
static char rw_data[1024] = {""};

以上定义了两个静态数组,由于bss_data没有初始化,是一个未初始化数据段的数组,编译器只需要标识它的大小即可,而rw_data已经有了一定的初始化数据(即使这个初始化数据没有实际的内容),它建立在已初始化数据段之上,编译器需要在读写数据段内为其开辟空间并赋初值。因此,在生成目标文件的时候,由于rw_data[1024]的存在,目标文件的大小将增加1024字节,而bss_data [2048]虽然定义了2048字节的数组,目标文件的大小并不会因此而增加。

连接器在处理的过程中,会将各个目标文件读写数据段组合成可执行程序的读写数据段,类似rw_data等内容都会被组合,因此可执行程序中读写数据段的大小会等于各个目标文件读写数据段之和。对于bss_data等未初始化数据段上的变量,连接器也将各个目标文件中的信息相加得到可执行程序的未初始化段的大小,但是这个段同样不会占用可执行文件的空间。

在C语言中,读写数据段和未初始化数据段都包含了以下几种情况:整个程序的全局变量、单个文件内使用的全局变量(函数外部用static修饰的)、局部静态变量(函数内部用static修饰的)。对于这几种变量,连接器都会按照相同的方式进行组合。

知识点:在连接过程中,C语言的各个目标文件的代码段、只读数据段和读写数据段将组合成可执行程序的这三个段,未初始化数据段只有在运行时才会产生。

原文地址:http://book.51cto.com/art/200902/111813.htm

13.3.2  各个目标文件的关系

程序中通常会有大量的函数调用,这些被调用的函数只要有声明(而不需要定义实现),编译器就可以成功处理。在生成可执行文件的过程中,连接器将各个可执行程序的代码段组合到一起,而有函数调用的地方还需要找到真正的函数定义才可以完成连接。因此,函数的定义和调用者可以在一个代码段内,也可以在不同的代码段内。连接器会根据需要根据实际的情况修改编译器生成的机器代码,完成正确的跳转。

函数跳转的连接过程如图13-3所示。

 
图13-3  函数跳转的连接

与函数跳转类似的是全局变量的访问,在C语言编译的过程中,程序可以访问用extern声明的外部全局变量,在连接的过程中,连接器需要找到实际变量在数据段中的位置,完成正确的变量访问。

程序中全局变量的连接如图13-4所示。

 
(点击查看大图)图13-4  全局变量的连接

对于可执行文件的生成,其主要的工作是组合各个目标文件中的三个段。还将包含一些其他的过程。首先,所有的可执行程序都需要指定一个入口,在C语言中入口即main函数,在一个C语言的应用程序各个源文件中,只能包含一个main函数。其次,不同系统所使用的可执行程序可能包含不同的头信息,头信息是在主要段之外附加的信息,可以供操作系统加载可执行程序的时候使用。

知识点:在连接过程之前,各个源文件生成目标文件相互没有关系。在连接之后,各目标文件函数和变量可以相互调用和访问,从而被联系在一起。

原文地址:http://book.51cto.com/art/200902/111814.htm

13.3.3  连接错误示例

连接过程中常见的错误是符号未找到(undefined reference)和符号重定义(redefinition)。由于在编译器在处理各个符号的时候,已经没有了各个C语言源文件的概念,只有目标文件。因此对于这种错误,连接器在报错的时候,只会给出错误的符号的名称,而不会像编译器报错一样给出错误程序的行号。

符号未定义的错误经常发生在符号已经声明,但是并没有具体的定义的情况下。在C语言中,符号可以是一个函数,也可以是一个全局变量。在程序的编译过程中,只要符号被声明,编译就可以通过,但是在连接的过程中符号必须具有具体的实现才可以成功连接。

例如:某一个源程序的文件的某一个地方调用了一个函数,如果这个函数具有声明,这时编译就可以通过。在连接的过程中,连接器将在各个代码段中寻找函数,如果函数没有在程序的任何一个位置中定义,那么就不会有函数符号,这时连接器将发生符号未定义的连接错误。请阅读如下程序:

extern void function(void);
int main(void) 
{
/* ...... */
function ();
/* ...... */
return 0;
}

在以上的例子中函数function可以和其调用者main在同一个源文件中,也可以在其他的源文件被调用中,但是它必须被定义。

符号重定义错误与符号未定义错误类似,如果连接器在连接的时候,发现一个符号在不同的地方有多于一个定义,这时就会产生符号重定义错误。对于同一个符号,如果在多个源文件中出现多次,将会产生符号重定义错误。

知识点:在连接过程中,符号未定义和符号重定义是两种最基本的错误。

下面以一个包含3个文件的程序为例,说明在连接过程中的错误。这个程序的三个文件为hello.h、hello.c和main.c。

main.c文件如下所示:

#include "hello.h"
int main(void) 
{
hello();
string[0] = 'a';
return 0;
}
hello.h文件如下所示:
#ifndef HELLO_H
#define HELLO_H
void hello(void);
extern char string [];
#endif
hello.c文件如下所示:
#include <stdio.h>
#include "hello.h"

char string [1024] = "";
void hello(void)
{
printf("=== hello ===\n");
}

以上是一个可以正常运行的程序,编译器将对main.c和hello.c两个源文件分别进行编译。在main函数中,调用了函数hello,并访问了数据string,由于包含了hello.h头文件,hello和string具有声明,因此main.c可以被成功编译。hello.c中定义了一个读写数据段上的变量string和函数hello。在连接的过程中,目标文件main.o对string和hello符号进行访问,hello.o提供了这两个符号,因此可以连接成功。

1.由于无数据定义导致符号未定义错误

将hello.c中对data的定义去掉,这时编译器依然可以成功编译main.c和hello.c。但是,由于string[0] ='a'编译后产生的代码将对string访问,在连接的过程中找不到string这个数据,因此会产生符号未定义连接错误(找不到数据)。

2.由于无函数实现导致符号未定义错误

将hello.c中对hello函数的定义去掉,这时编译器还是可以成功编译main.c,而且有这个符号,因此也会产生目标文件hello.o。但是,在连接器处理函数调用的时候,需要跳转到hello符号,由于实际上并没生符号而报告未定义连接错误(找不到函数)。

知识点:在程序中使用只有声明而未定义的函数或数据,可以成功编译,但是连接时将发生符号未定义错误。

3.由于数据仅能在文件内部使用,导致符号未定义错误

将hello.c的数组string []更改为静态变量:

static char string [1024] = "";

此时编译依然是可以通过的,这时候由于string已经是一个静态数据,因此它不会出现在hello.c的目标文件的符号中。也就是说,增加static修饰后,目标文件和以前将略有不同。

连接器在处理的时候,虽然有string这个数据,但是它只有数据区而没有符号,因此依然会出现未定义符号错误。

4.函数具有声明可编译通过,但有连接错误

取消hello.c中对函数的声明,在main.c增加对该函数的声明:

void hello(void);
在hello.c中,将hello函数的定义改为静态的:
static void hello(void)
{
printf("=== hello ===\n");
}

这时,编译还是能通过,由于hello成为静态函数,只能在文件内部使用,不会产生外部的符号。因此,虽然在main中对该函数进行了声明,连接也会产生未定义符号错误。

函数在各个源文件中的声明不会对连接产生影响,因此,在本例中,只要hello函数不是静态的,连接就可以通过。

知识点:使用static的函数和变量不能给其他文件调用,否则会发生连接的符号未定义错误。

5.定义同名变量导致符号重定义错误

在hello.h文件中,取消对string的外部声明。在main函数中增加一个定义在读写数据段的字符数组string[],

char string[1024] = "main string";

编译通过后,在连接时将会产生符号重定义的连接错误。main.c和hello.c中各自有一个读写数据段的字符数组string[],虽然它们看似没有直接的关系,但连接器无法处理这种情况,依然会产生连接错误。

在这种情况下,即使没有对string的引用(即没有string[0] = 'a'),连接器依然无法处理两个同名的读写数据段全局变量,这时还是会报告符号重定义的连接错误。

原文地址:http://book.51cto.com/art/200902/111815.htm

6.实现同名函数导致符号重定义错误

在main.c文件中,增加对hello函数的定义。

void hello(void)
{
printf("+++ main hello +++\n");
}

编译通过后,连接时会产生符号重定义的错误。实际上,由于在hello.c和main.c的目标文件的代码段中分别定义hello函数。连接器认为出现重定义。与上例的程序类似,即使没有对函数hello的调用,编译器也不允许在代码段中出现2个hello函数的符号,所以还是会产生符号重定义连接错误。

知识点:在多个文件中定义全局的同名函数和变量,连接时将发生符号重定义错误。

7.静态函数与其他文件中的函数重名,可以正常使用

将main.c文件更改成如下:

#include "hello.h"
int main(void) 
{
hello();
string[0] = 'a';
return 0;
}
static void hello(void)                        /* 静态的函数,内部使用 */
{
printf("+++ main hello +++\n");
}
程序主要的变化是增加了静态的hello函数,在这种情况下,是可以成功地进行编译连接和运行的,运行结果如下所示:
+++ main hello +++

从运行结果中可以看到,main中调用的hello函数是main.c文件中定义的static 的hello函数。

值得注意的是,在这种情况下,编译器在进行编译的时候,main函数写在静态的函数hello前面,因此可以通过编译。这是由于main.c文件中包含了hello.h文件,其中具有对hello()函数的声明。但是,当编译器编译到main.c之中的hello函数的时候,由于static头文件中声明函数原型不同,可能出现一个编译报警(warning)。

8.静态变量与其他文件中的变量重名,可以正常使用

在main.c中,增加静态(static)的读写数据段的字符数组string[]的定义。

char string[1024] = "main string";

在这种情况下,编译连接可以成功。当连接工作完成后,可执行程序的读写数据段将出现两个string[1024]数组,均占用的空间。一个是hello.c中定义的全局的数组,一个是main.c定义的文件内部使用的数组。在数据访问的过程中,语句string[0] = 'a'访问的将是main.c中定义的数组string[]。

知识点:如果全局变量和函数已定义,而在某个文件中另外定义静态的同名变量和函数,可以在文件内部使用同名的静态变量和函数。在使用的过程中,将优先使用文件内的变量和函数。

9.在头文件中定义已经初始化数据,可能产生问题

在程序中,将string[]的定义放入hello.h的头文件中:

#ifndef HELLO_H
#define HELLO_H
void hello(void);
char string[1024] = "";
#endif

同时,取消在hello.c中对string[]数组的定义。此时,由于hello.c和main.c同时包含了hello.h头文件,因此string在内存中有两份,连接的时候将产生符号重定义错误。

如果将头文件中string的定义改为静态的,这时不会产生连接错误,但是会在hello.c和main.c的目标文件中各产生一个string[1024]。最终可执行程序的读写数据段中也会有两个string。

如果在头文件中使用如下方式定义:

const char string_const[1024] = {"constant data"};

由于具有const属性,string_const是一个在只读数据段上的常量。这样有多个文件包含该头文件的时候,在连接过程中也会出现符号重定义的错误。连接器在这个问题上,对读写数据区和只读数据区的处理方式是类似的。

知识点:具有初始值的变量将被连接到读写数据区。在头文件中不应该定义有初始值的全局变量。同样,也不应该定义只读数据段的常量。否则,在头文件被多个文件包含的时候,将发生连接错误。

10.在头文件中定义未初始化数据段,可以正常使用

在程序中,hello.h的头文件中定义string[],但是没有初值:

#ifndef HELLO_H
#define HELLO_H
void hello(void);
char string[1024];
#endif

同时,取消在hello.c中对string[]数组的定义。在这种情况下,编译连接都是可以通过的,程序也可以正常运行。

知识点:无初始化的变量将被连接到未初始化数据段,在头文件中可以定义。当头文件被多个文件包含时,该未初始化段在运行时也将只有一份。

事实上,由于没有初值,string[]将不再是读写数据段上的变量,而是未初始化数据段上的变量。未初始化段上的变量并不会占用目标文件或者可执行文件中的空间,它们只是一些标识。由于不需要分配空间,编译器允许这种做法。未初始化数据段的变量在运行的时候才会产生,而且只会有一个。

同理,可以将string修改为static的未初始化变量:

#ifndef HELLO_H
#define HELLO_H
void hello(void);
static char string[1024];
#endif

在这种情况下,在编译的时候将会在两个目标文件中各自记录一个未初始化数据段,在运行时程序将在内存上开辟两个独立的1024字节的数据区。

比较以上的两个示例(9和10),总结出以下的结论:

首先,不应该在头文件中使用全局的读写数据变量,这样当两个文件同时引用这个头文件的时候,将会产生符号重定义连接错误。

其次,在头文件中也不应该使用静态的变量,无论它有没有初值(即在读写数据段或者未初始化数据段),这样虽然不会引起连接错误,但是在各个源文件中各自产生变量,不但占用更多的空间,而且在逻辑上是不对的,也违背头文件的使用原则。

最后,在头文件中使用全局的没有初始化的变量是可以的,它在程序运行的过程中,在内存中只会有一份,可以被包含该头文件的程序访问。

从C语言程序设计的角度,不应该在头文件中定义变量或者函数。对于函数,在头文件中只是声明,需要在源文件中定义;对于变量,无论何种性质(只读数据段、可读写数据段、未初始化数据段),最好的方式是在C语言的源文件中定义,在头文件中使用extern声明。

原文地址:http://book.51cto.com/art/200902/111816.htm

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值