链接的接口-符号

链接的接口-符号


链接过程的本质就是要把多个不同的目标文件之间相互粘在一起,或者说像玩具积木一样,可以拼接形成一个整体,为了使不同目标文件之间能够相互粘合,这些目标文件之间必须有固定的规则才行,就像积木模块必须有凹凸部分才能够相互拼合,在链接中,目标文件之间相互拼合实际上是目标文件对地址的引用,即对函数和变量的地址的引用,比如目标文件B要用到了目标文件A中的函数"foo",那么我们就称目标文件A定义了函数"foo",那么我们就称目标文件A定义了函数"foo",称目标文件B引用了目标文件A中的函数"foo",这两个概念也同样适用于变量,每个含或变量都有自己独特的名字,才能避免链接过程中不同变量和函数之间的混淆。在链接中,我们将函数和变量统称为符号,函数名或变量名就是符号名。
我们可以将符号看作是链接中的粘合剂,整个链接过程正是基于符号才能够正确完成。链接过程中很关键的一部分就是符号的管理每一个目标都会有一个相应的符号表,这个表里面记录了目标文件中所用到的所有符号,每哥定义的符号有一个对应的值,叫做符号值,对于变量和函数来说,符号值就是它们的地址,除了函数和变量之外,还存在其他几种不常用到的符号,我们将符号表中所有的符号进行分类,它们有可能是下面这些类型的一种:
1.定义在本目标文件的全局符号,可以被其它目标文件引用。
2.在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫外部符号。比如SimpleSection.o里面的"printf"。
3.段名,这种符号往往由编译器产生,它的值就是该段的起始地址,比如SimpleSection.o里面的".text",".data"。
4.局部符号,这类符号只在编译单元内部可见,调试器可以使用这些符号来分析程序或崩溃时的核心转储文件,这些局部符号对于链接过程没有作用,链接器往往也忽略它们。‘
5.行号信息,即目标文件指令与源代码行的对应关系,它也是可选的。
对应
对于我们来说,最值得关注的就是全局变量,即上面分类中的第一类和第二类,因为链接过程只关心全局符号的相互"粘合",局部符号,段名,行号等都是次要的,它们对于其他目标文件来说是不可见的,在链接过程中也是无关紧要的。
特殊符号
当我们使用ID作为链接器来链接生产可执行文件,它会为我们定义很多特殊的符号,这些符号并没有在你的程序中定义,但是你可以直接声明并且引用它,我们称为特殊符号,链接器会在将程序最终链接成可执行文件的时候将其解析成正确的值,注意,只有使用ID链接生产最终可执行文件的时候这些符号才会存在,几个很具有代表性的特殊符号如下:
_executable_start,该符号为程序起始地址,注意,不是入口地址,是程序的最开始的地址。
exext或_etext或etext,该符号为代码段结束地址,即代码最末尾的地址。
edata或edata,该符号为数据段结束地址,即数据段最末尾的地址。
end或end,该符号为程序结束地址。
以上地址都为程序被装载时的虚拟地址。
符号修饰与函数签名
约在20世纪70年代以前,编译器编译源代码产生目标文件时,符号名与相应的变量和函数的名字是一样的,比如一个汇编源代码里面包含了一个函数foo,那么汇编器将它编译成目标文件以后,foo在目标文件中的相对应的符号名为foo,当后来UNIX平台和C语言发明时,已经存在了相当多的使用汇编编写的库和目标文件,这样就产生了一个问题,那就是如果一个C语言程序要使用这些库的话,C语言中不可以使用这些库中定义的函数和变量的名字作为符号名,否则将会跟现有的目标文件冲突。比如有一个用汇编编写的库中定义了一个函数叫做main,那么我们在C语言里面就不可以再定义一个main函数或变量了。同样的道理,如果一个C语言的目标文件要用到一个使用Fortran语言编写的目标文件,我们也必须防止它们的名字冲突。’
为了防止类型的符号名字冲突,UNIX下的C语言就规定,C语言源代码文件中的所有全局的变量和函数经过编译以后,相对应的符号名前加上下划线"
"。而Fortran语言的源代码经过编译后,所有的符号名前加上"
",后面也加上"
"。
这种简单而原始的方法的确能够暂时减少多种语言目标文件之间的符号冲突的概率,但还是没有从根本上解决符号冲突的问题,比如同一种语言编写的目标文件还有可能会产生符号冲突,当程序很大时,不同的模块由多个部门开发,它们之间的命名规范如果不严格,则有可能导致冲突,于是像c++这样的后来设计的语言开始考虑这个问题,增加了名称空间的方法来解决多模块的符号冲突问题。
C++符号修饰
众所周知,强大而又复杂的C++拥有类,继承,虚机制,重载,名称空间等这些特性,它们使得符号管理更为复杂。最简单的例子,两个相同名字的函数func(int)和func(double),尽管函数名相同,但是参数列表不同,这是C++里面函数重载的最简单的一种情况,那么编译器和链接器在链接过程中如何区分这两个函数?为了支持C++这些复杂的特性,人们发明了符号修饰或符号改编的机制。
首先出现的一个问题是C++允许多个不同参数类型的函数拥有一样的名字,就是所谓的函数重载,另外C++还在语言级别支持名称空间,即允许在不同的名称空间有多个同样名字的符号。

	int func(int);
	float func(float);
	class C{
		int func(int);
		class C2{
			int func(int);
		};
		
	}; 
	namespace N{
		int func(int);
		class C{
			int func(int);
		};
	}

这段代码中有6个同名函数叫func,只不过他们的返回类型和参数及所在的名称空间不同,我们引入一个术语叫做函数签名,函数签名包含了一个函数的信息,包括函数名,它的参数类型,它所在的类和名称空间及其他信息。函数签名用于识别不同的函数,就像签名用于识别不同的人一样,函数的名字只是函数签名的一部分,由于上面6个同名函数的参数类型及所处的类和名称空间不同,我们可以认为它们的函数签名不同,在编译器及链接器处理符号时,它们使用某种名称修饰的方法,使得每个函数签名对应一个修饰后名称,编译器在将C++源代码编译成目标文件时,会将函数和变量的名字进行修饰,形成符号名,也就是说,C++的源代码编译后的目标文件中所使用的符号名是相应的函数和变量的修饰后名称。C++编译器和链接器都使用符号来识别和处理函数和变量,所以对于不同函数签名的函数,即使函数名相同,编译器和链接器都认为它们是数和变量,所以对于不同函数签名的函数,即使函数名相同,编译器和链接器都认为它们是不同的函数。

函数签名 修饰后名称
int func(int) _Z4funci
float func(float) _Z4funcf
int C::func(int) _ZN1C4funcEi
int C::C2::func(int) _ZN1C2C24funcEi

GCC的基本C++名称修饰方法如下:所以的符号都以“_Z”开头,对于嵌套的名字(在名称空间或在类里面的),后面紧跟"N",然后是各个名称空间和类的名字,每个名字前是名字字符串长度,再以"E"结尾,比如N::C::func经过名称修饰后就是_ZN1N1C4funcE。对于一个函数来说,它的参数列表紧跟在"E"后面,对于int类型来说,就是字母"i".所以整个N::C::func(int)函数签名经过修饰为_ZN1N1C4funcEi。更为具体的修饰方法我们在这里不详细介绍,有兴趣的读者可以参考GCC的名称修饰标准。幸好我们平时程序开发中也很少手工分析名称修饰问题,所以无须很详细地了解这个过程。binutils里面提供了一个叫"c++filt"的工具可以用来解析被修饰过的名称,比如:
签名和名称修饰机制不光被使用到函数上,C++中的全局变量和静态变量也有同样的机制,对于全局变量来说,它跟函数一样都是一个全局可见的名称,它也遵循上面的名称修饰机制,比如一个名称空间foo中的全局变量bar,它修饰后的名字为:_ZN3foo3barE.值得注意的是,变量的类型并没有被加入到修饰后名称中,所以不论这个变量实整型还是浮点型甚至是一个全局对象,它的名称都是一样的。
名称修饰机制也被用来防止静态变量的名字冲突,比如main()函数里面有一个静态变量叫foo,而func()函数里面也有一个静态变量叫foo,为了区分这两个变量,GCC会将它们的符号名分别修饰成两个不同的名字_ZZ4mainE3foo和_ZZ4funcvE3foo,这样就区分了这两个变量。
不同的编译器厂商的名称修饰方法可能不同,所以不同的编译器对于通一个函数签名可能对应不同的修饰后名称。
由于不同的编译器采用不同的名字修饰方法,必然会导致由不同编译器编译产生的目标文件无法正常相互链接,这是导致不同编译器之间不能互操作的主要原因之一。
extern“C”
C++为了与C兼容,在符号管理上,C++有一个用来声明或定义一个C的符号的"extern"C"关键字用法:

	extern "c"{
		int func(int);
		int var; 
	} 

C++编译器会将在"extern C"的大括号内部的代码当作C语言代码处理,所以很明显,上面的代码中,C++的名称修饰机制将不会起作用。他声明了一个C的函数func。定义了一个整型全局变量var。从上文我们得知,在Visual C++平台下会将C语言的符号进行修饰,所以上述代码中的func和var的修饰后符号分别为_func和_var,但是在Linux版本的GCC编译器下却没有这种修饰,extern“C”里面的符号都为修饰后符号,即前面不用加下划线。
很多时候我们会碰到有些头文件声明了一些C语言的函数和全局变量,但是这个头文件可能会被C语言代码或C++代码包含,比如很常见的,我们的C语言库函数中的string.h中声明了memset这个函数,它的原型如下:

void *memset(void *,int ,size_t);

如果不加任何处理,当我们的C语言程序包含string.h的时候,并且用到了memset这个函数,编译器会将memset符号引用正确处理,但是在C++语言中,编译器会认为这个memset函数时一个C++函数,将memset的符号修饰成_Z6memsetPvii,这样链接器就无法与C语言库中的memset符号进行链接。所以对于C++来说,必须使用extern C来声明memset这个函数。但是C语言又不支持extern"C"语法,如果为了兼容C语言和C++语言定义两套头文件,未免过于麻烦,幸好我们有一种很好的方法可以解决上述问题,就是使用C++的宏“_cplusplus”,C++编译器会在编译C++的程序时默认定义这个宏,我们可以使用条件宏来判断当前编译单元是不是C++代码。
如果当前编译单元是C++代码,那么memset会在extern“C”里面被声明,如果是C代码,就直接声明,上面这段代码中的技巧几乎在所有的系统头文件里面都被用到。

你可能感兴趣的:(编译链接过程,c++,编译器)