couser2610_lab17 编译与链接的机理(下)

前文描述了, 标准库的存在形式

既然C语言提供了标准库函数供我们使用,那么以什么形式提供呢?源代码吗?当然不是了。下面我们引入静态链接库的概念。我们几乎每一次写程序都难免去使用库函数,那么每一次去编译岂不是太麻烦了。标准库函数提前编译好,需要的时候直接链接。

这里继续介绍, 静态库和动态库两种:

1. 静态库实例

1.1 静态库

那么,标准库以什么形式存在呢?一个目标文件?

我们知道,链接的最小单位就是一个个目标文件,如果我们只用到一个printf函数,就需要和整个库链接的话岂不是太浪费资源了么?

但是,如果把库函数分别定义在彼此独立的代码文件里,这样编译出来的可是一大堆目标文件,有点混乱吧?

所以,编辑器系统提供了一种机制,将所有的编译出来的目标文件打包成一个单独的文件,叫做静态库(static library)。

当链接器和静态库链接的时候,链接器会从这个打包的文件中“解压缩”出需要的部分目标文件进行链接。这样就解决了资源浪费的问题。

1.2 编译得到静态库

在swap.c里定义一个swap函数,在add.c里定义了一个add函数

add 函数

//add.c
//
int add(int a, int b){
    	return a + b;
}

swap 函数

// swap.c
//

void swap(int *num1, int *num2){

          int tmp  = *num1;
	     *num1 = *num2;
	     *num2 = tmp;
}

声明上述两个函数的头文件;

// calc.h

#ifndef CALC_H_
#define CALC_H_
#ifdef  _cplusplus

extern "C"{

#endif
void swap(int *, int *);
int  add(int, int );

#ifdef  _cplusplus

}

#endif
#endif // CALC_H_


  1. 编译 生成目标文件

$ gcc  add.c -c  -o add.o
$ gcc -c swap.c -o swap.o
$ ls
add.c  add.o  calc.h  swap.c  swap.o  test.c
  1. 使用 ar 命令将目标文件打包成一个 静态库;
ar rcs libExp.a add.o swap.orespecting-god@Lenovo-Legion:~/Documents/course_2610/lab17_compile_link/static_exp$ ls
add.c  add.o  calc.h  libExp.a  swap.c  swap.o  test.c

  1. 应用程序调用这个库函数

编写应用程序test.c

#include <stdio.h>
#include <stdlib.h>
#include "calc.h"


int main(int argc, char *argv[]){

	int a = 1, b = 2;
	swap(&a, &b);
	printf("after swap, a = %d, b = %d \n ", a,b );
	return EXIT_SUCCESS;
}
# 链接上面的libExp.a 库函数,  生成可执行文件
gcc test.c ./libExp.a  -o testExp

# 执行 可执行文件
 ./testExp 
after swap, a = 2, b = 1

使用C语言标准库的时候,编译并不需要加什么库名。

是的,不需要。因为标准库已经是标准了,所以会被默认链接。

不过因为数学函数库libm.a没有默认链接,所以我们使用了数学函数的代码在编译时需要在命令行指定 -lm 链接(-l是制定链接库,m是去掉lib之后的库名),不过现在好多gcc都默认链接libm.c库了;

正如我们所看到的,静态链接库解决了一些问题,但是它同时带来了另一些问题。

  1. 比如说每一个使用了相同的C标准函数的程序都需要和相关目标文件进行链接,浪费磁盘空间;

  2. 当一个程序有多个副本执行时,相同的库代码部分被载入内存,浪费内存;

  3. 当库代码更新之后,使用这些库的函数必须全部重新编译……

有更好的办法吗?当然有。我们接下来引入动态链接库/共享库(shared library)。

2. 动态库实例

动态链接库/共享库是一个目标模块,在运行时可以加载到任意的存储器地址,并和一个正在运行的程序链接起来。

这个过程就是动态链接(dynamic linking),是由一个叫做动态链接器(dynamic linker)的程序完成的。

Unix/Linux中共享库的后缀名通常是.so(微软那个估计大家很熟悉,就是DLL文件)。怎么建立一个动态链接库呢?

2.1 生成动态链接库

还是以上面的代码为例,我们先删除之前的静态库和目标文件。首先是建立动态链接库,我们执行
gcc swap.c add.c -shared -o libcalc.so

最好在gcc命令行加上一句-fPIC让其生成与位置无关的代码(PIC);

$ gcc add.c swap.c -shared -fPIC -o libDynamic.so
$ ls
add.c  calc.h  libDynamic.so  swap.c  test.c

2.2 链接动态库

$ gcc test.c  ./libDynamic.so  -o test

$ ./test
after swap, a = 2, b = 1

我们用ldd命令(ldd是GNU binutils工具包的组成之一)
检查test文件的依赖

$ ldd ./test
	linux-vdso.so.1 (0x00007fff15dc6000)
	./libDynamic.so (0x00007f5cd1375000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5cd0f84000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f5cd1779000)

我们看到这个文件能顺利运行需要依赖libcalc.so这个动态库,我们还能看到C语言的标准库默认也是动态链接的
(在gcc编译的命令行加上 -static 可以要求静态链接)。

好处在哪?

第一,库更新之后,只需要替换掉动态库文件即可,无需编译所有依赖库的可执行文件。

第二,程序有多个副本执行时,内存中只需要一份库代码,节省空间。

大家想想,C语言标准库好多程序都在用,但内存只有一份代码,这样节省的空间很可观吧,而且假如库代码发现bug,只需要更新libc.so即可,所有程序即可使用新的代码。

3. 链接的运作机理

3.1 地址和空间分配

首先是地址和空间分配,我们之前提到的目标文件其实全称叫做可重定位目标文件(这只是一种翻译,叫法很多…)。

目标文件的格式已经无限度接近可执行文件了,Unix/Linux下的目标文件的格式叫做ELF(Executable and Linkable Format,可执行连接格式)。详细的讨论可执行文件的格式超出了本文范围,我们只需要知道可执行文件中代码,数据,符号等内容分别存储在不同的段中就可以了,这也和保护模式下的内存分段是有一定关系的

3.2 符号决议

什么是符号(symbol)?简单说我们在代码中定义的函数和变量可以统称为符号。

符号名(symbol name)就是函数名和变量名了。

目标文件的拼合其实也就是对目标文件之间相互的符号引用的一个修正。我们知道一个C语言代码文件只要所有的符号被声明过就可以通过编译了,可是对某符号的引用怎么知道位置呢?比如我们调用了printf函数,编译时留下了要填入的函数地址,那么printf函数的实际地址在那呢?这个空位什么时候修正呢?当然是链接的时候,重定位那一步就是做这个的。但是在修改地址之前需要做符号决议,那什么是符号决议呢?正如前文所说,编译期间留下了很多需要重新定位的符号,所以目标文件中会有一块区域专门保存符号表。那链接器如何知道具体位置呢?其实链接器不知道,所以链接器会搜索全部的待链接的目标文件,寻找这个符号的位置,然后修正每一个符号的地址。

3.2 符号查找问题与重定位

这时候我们可以隆重介绍一个几乎所有人在编译程序的时候会遇见的问题——符号查找问题。这个通常有两种错误形式,即找不到某符号或者符号重定义

首先是找不到符号,比如,当我们声明了一个swap函数却没有定义它的时候,我们调用这个函数的代码可以通过编译,但是在链接期间却会遇到错误。形如“test.c:(.text+0x29): undefined reference to ‘swap’”这样,特别的,MSVC编译器报错是找不到符号_swap。咦?那个下划线哪里来的?

这得从C语言刚诞生说起。当C语言刚面世的时候,已经存在不少用汇编语言写好的库了,因为链接器的符号唯一规则,假如该库中存在main函数,我们就不能在C代码中出现main函数了,因为会遭遇符号重定义错误,倘若放弃这些库又是一大损失。所以当时的编译器会对代码中的符号进行修饰(name decoration),C语言的代码会在符号前加下划线,fortran语言在符号前后都加下划线,这样各个目标文件就不会同名了,就解决了符号冲突的问题。随着时间的流逝,操作系统和编译器都被重写了好多遍了,当前的这个问题已经可以无视了。所以新版的gcc一般不会再加下划线做符号修饰了(也可以在编译的命令行加上-fleading-underscore/-fno-fleading-underscore开打开/关闭这个是否加下划线)。而MSVC依旧保留了这个传统,所以我们可以看到_swap这样的修饰。

符号冲突是很常见的事情,特别是在大型项目的开发中,所以我们需要一个约定良好的命名规则。

C++也引入了命名空间来帮助我们解决这些问题,因为C++中存在函数重载这些东西,所以C++的符号修饰更加复杂难懂(Linux下有c++filt命令帮助我们翻译一个被C++编译器修饰过的符号)。

编程中需要注意的一个大问题了。
**即存在同名符号时链接器如何处理。**不是刚刚说了会报告重名错误吗?怎么又要研究这个?很可惜,不仅仅这么简单。

在编译时,编译器会向汇编器输出每个全局符号,分为强(strong)符号和弱符号(weak),汇编器把这个信息隐含的编码在可重定位目标文件的符号表里。

其中函数和已初始化过的全局变量是强符号,未初始化的全局变量是弱符号。根据强弱符号的定义,GNU链接器采用的规则如下:

  • 不允许多个强符号

  • 如果有一个强符号和一个或多个弱符号,则选择强符号

  • 如果有多个弱符号,则随机选择一个

好了,就三条,第一条会报符号重名错误的,而后两条默认情况下甚至连警告都不会有。关键就在这里,默认甚至连警告都没有。

// link1.c

#include <stdio.h>
int n;
int main(int argc, char *argv[])
{
printf("It is %dn", n);
return 0;
}

程序2;

// link2.c
int n = 5;

以上个文件编译运行会输出什么呢?

gcc link1.c link2.c -o link

$ ./link 
It is 5

初始化过的n是强符号,被优先选择了。但是,在很复杂的项目代码,这样的错误很难发现,特别是多线程的……

不过当我们怀疑代码中的bug可能是因为此原因引起的时候,我们可以在gcc命令行加上-fno-common这个参数,

这样链接器在遇到多重定义的符号时,都会给出一条警告信息,而无关强弱符号。
如图所示:

$ gcc link1.c link2.c -o link -fno-common
/tmp/cc7ttX0v.o:(.data+0x0): multiple definition of `n'
/tmp/ccaKyT2C.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值