最近遇到一些让我很困惑的底层问题,在寻找答案的过程中发现对linker知识的不足,这里只是我读了这篇文章的笔记,原文对linker讲得更详细,尤其是c++部分.
简单来说,以C为例,从源代码到可执行程序之间经历了这么几步:
源码->预处理器->编译器->链接器->可执行文件(或者lib)
编译器把预处理以后的代码编译为一个一个的.o(unix平台),linker再把这些.o链接成为可执行程序.
这里涉及到一个具体的语言问题,声明和定义. 声明是告诉编译器,以某个名字的定义会存在于某处,我这里只是声明并使用他,并不定义它,以后你就懂了。 定义其实也算是一种声明,只是这种声明的定义就在这里实现了,不用到别处去找了。
比如你可以写一个.c文件:
/* Initialized global variable */
int z_global = 11;
/* Second global named y_global_init, but they are both static */
static int y_global_init = 2;
/* Declaration of another global variable */
extern int x_global_init;
int fn_a(int x, int y)
{
return(x+y);
}
void fc();
int main(int argc, char *argv[])
{
const char *message = "Hello, world";
fc();
return fn_a(11,12);
}
这里fc函数只是一个声明,你可以在main中使用他,在编译阶段,这个是可以通过的,因为你告诉了编译器,这个函数会在某处被定义,但编译器本身并不关心他具体被实现在哪里了(但貌似在某些划分下,现在的链接器也在属于编译器内部,这里不做这种概念上得纠结了,自己明白就行),在链接时期,链接器去链接fc的时候,会发现找不到他的定义,这时候就会报错了,用命令行的时候应该会看到来自ld这个命令的错误,这个就是unix下使用的linker。
使用gcc -c可以只编译源代码,并不进行链接,这时候编译器把一个人类可读的.c文件变成了计算机能理解的.o文件。利用nm命令可以看到.o文件中对源码进行编译以后存储的信息.
00000000000000b8 s EH_frame0
0000000000000064 s L_.str
U _fc
0000000000000000 T _fn_a
00000000000000d0 S _fn_a.eh
0000000000000020 T _main
00000000000000f8 S _main.eh
0000000000000060 D _z_global
我的输出和链接中的文章作者的输出不一样…没他那个好看,但大体是差不多的,后面的类型如链接中类似,我的理解前面的是大小,因为fc函数并没有具体的实现,所以并没有一个具体的大小.这正是编译器的处理逻辑,如果一个遇到一个只有声明,但并没有实现的语句,编译器会给这个声明相关的东西留下一个空白区,这个空白区会等着链接器在找到他的具体实现的时候再去把这个空白填满。
linker的工作
链接器的工作就是一个填空的过程,在编译出来的所有.o文件中留下了很多关于声明的空白,链接器需要去把这些空白填满,不然就报出一个错误。不同的语言对于某些链接遇到的错误处理不同,比如有的语言可以重复声明,有得不行。如果一切正常,链接器就能生成一个可执行的二进制文件。当操作系统需要执行这个程序的时候,会把这个二进制文件加载到内存中进行执行(这里可以完全是别的一块的东西了,bss,代码段,数据段,stack,heap….)。
为了能够复用一些常用的程序功能,那么还需要引入一些别的概念:static library和shared library。
静态库 当你需要使用别人开发好的一些功能时,并不需要别人的源代码,只需要他生成的一个.a(unix)静态库和头文件声明就可以了,.a其实就是由若干.o文件生成的一个文件,他没有一个函数入口,所以不能直接被操作系统执行。使用静态库时,链接器的工作是类似的,链接器首先会填满源码文件中的.o的空白,如果存在未定义的声明,就会到静态库的.o中去寻找,最终再合成一个可执行文件或者.a,这里有意义的是,静态库中的.o并不会全部都被链接进来,只有当有未定义的声明存在于某个.o中时,才会将该.o链接进来,这也挺合理的,节约资源。
动态库 动态库是另一种复用功能的方式,我更喜欢这种方式,但在某些开放平台上存在一定的安全问题,使用动态库时, if the linker finds that the definition for a particular symbol is in a shared library, then it doesn’t include the definition of that symbol in the final executable. Instead, the linker records the name of symbol and which library it is supposed to come from in the executable file instead.也就是说,其实linker只是做了某种标记表明这个链接在运行时完成。
这里的运行时应该有两种理解,一种是在你的程序被启动时,动态库被加载进你的程序空间,动态链接就已经完成。另外一种更动态的方式是,可以使用dlopen的系统调用来在某个时间来加载动态库和动态链接。
c++的链接
c++的链接原理和c语言是类似的,但是由于c++的语言特性,加入了函数重载,模板等更复杂的编译和链接方式,使源码中的函数名在.o中会随着编译器不同和平台不同有很大的区别,其实这一部分是我读链接中得文章得到的最有用的信息,具体的细节我也没有去了解,但知道一些大概就已经足以继续我的工作。