前言
最近新的一批师弟进实验室了,有几个师弟要做嵌入式开发,需要涉及到第三方库的编译,我已经不止一次听到过师弟问我有关链接的问题了:
为什么编译的时候提示函数未定义?明明已经include了头文件,vsc也没有画波浪线呀?
为什么提示函数重定义?(一般这种,都是发现include xxx.h不管用,就直接include xxx.c,改一个include没问题,但是把其它文件里的include都改成这样,就会造成这个问题)
全局变量应该在哪里定义?extern加在哪里?
是的,这些问题中,没有一个提到了“链接”二字,但是它们的确就是不了解链接原理带来的问题。
这些问题产生之后,我们第一反应就是去百度,去看CSDN博客,看看这个报错是什么意思。但是并不是所有的博文都对症,版本不同、库不同,可能会有不一样的解决方式。对于一些自己编写的程序,在脱离了Visual Studio这种IDE之后,想要把函数写在多个源文件内,就必须对链接的原理有所了解。
由于编译一章十分重要,其它计算机原理不了解只是少吹点牛逼,链接不了解,那别人写的库(甚至自己写的多个源文件)都没法包含到自己的代码中来:(
第七章 链接
链接是将各种代码和数据片段(注意,不只是代码!)收集并组合成一个单一文件的过程。这个文件可以被加载到内存并执行。链接可以发生在编译时(如静态链接库,编译时选择静态链接),也可以发生于加载时(如程序依赖某个动态链接库,也就是编译时的动态链接),甚至发生于运行时(采用加载动态链接库的函数dlopen(),查找指定名称函数并调用,怎么听起来这么像反射机制??)
在现代系统中,链接是由链接器自动执行的。通过理解链接的原理,可以我们有下面的这些帮助:
理解符号引用、符号定义等概念,面对链接器的相关报错,可以知道原因出在哪里,进而思考如何解决;
避免一些链接器发现不了的程序错误,如符号重定义时,链接器的warning,可以帮助我们发现代码中的符号引用错误;
理解语言的作用域规则如何实现,static全局变量和static函数与非static的全局变量和函数的区别何在;
理解与其它操作系统概念的融合,如虚拟内存,程序运行,内存分页;
有助于共享库的使用。
7.1 编译器驱动程序
当我们调用gcc将多个c源文件编译并生成一个输出文件时
gcc main.c sum.c -o main
通常有这些工作:
启动编译器驱动程序,用于驱动下面的执行流程;
1.预处理器(cpp):处理带“#”号的预处理代码,比如include等,生成一个main.i文件;
2.编译器(cc1):将每个源文件都翻译成汇编语言文件(main.s),这个文件是可读的;
3.汇编器(as):将main.s文件翻译成一个可重定位目标文件(main.o);
4.对sum.c重复1~3,生成sum.o文件
5.链接器(ld):将main.o sum.o文件组合起来,创建一个可执行目标文件main。
在执行文件时,shell会调用操作系统的加载器(loader)函数,将可执行文件中的代码和数据都复制到内存,然后将控制转移到这个程序的开头。
看完了编译的过程,我想,最突出的一个问题就是,可重定位目标文件和可执行目标文件,有什么区别?虽然后面会解释这个问题,但是现在可以先用简短的两句话概括一下它们的区别:
一个.c文件对应一个.o文件,而且从上面的过程可以看出,各个.o文件都是由某个.c独立生成的。所以当有多个源文件,而且源文件之间有相互引用时,每个.o文件都只包含自己的代码和数据,以及对外部符号(位于其它.c文件中的全局变量和全局函数)的引用,链接时会将其进行解析并将符号引用转化为真正的符号地址。
每个.o文件内,各个数据段、代码段的偏移量,都是根据当前.o文件计算得出的,在链接成一个文件的过程中,显然要重新安排这些数据和代码的位置
所以重定位的含义是,.o文件内的代码和数据的引用表(等信息)都需要重新进行定位并更新。
7.2 目标文件
目标文件(常常也会成为二进制代码文件等