前置知识
链接库
在链接的过程中,除了把多个.o文件进行链接,其实还要链接库一些库,比如printf的实现……这些方法也要链接进来
一般使用一个库要有两个操作:包头文件(在代码中#include)、链接函数库(在命令行中)
-
头文件
内含函数的声明给我们提供了可以使用的方法(函数的声明)
(我们平时在开发环境看到的语法提示,其实就是通过头文件帮我们搜索的)
-
库文件(函数的实现)
提供了可以使用的方法的实现,以供链接,形成我们自己的可执行程序
使用案例:printf()
当我们要使用printf这个库函数,首先要包一个头文件:
#include <stdio.h>
stdio.h这个文件中会有printf的声明:
int printf(const char *restrict __format, …);
那函数的定义在哪?
这些C语言提供的库一般放在/lib64/libc*
目录下:这些库即使我们不手动去链接,编译器也会自动到这个目录下找到所需库文件链接进来
如果是我们自己实现的库,就需要手动链接进来(方法在后面)
可以链接的库分为两类:动态库、静态库,这两种库的链接方式也对应动静两种
动静态链接的感性认识
这里为了让大家感性认识一下动、静态链接的区别,给大家引个小故事,方便理解
《动态链接》
我有个朋友是一个网瘾少年,学校里不允许带电子设备,所以作为一个刚入学的高一学生,一进学校就提前到学长那里打听好了,出校门往东500米就有一家网吧”极速码头“。他到了宿舍,睡了三十分钟觉,看了30分钟书,扣了20分钟手,然后就想去打游戏,计划说打完游戏就回来接着看书、上课。于是按照学长说的那个地方就去了,去了之后他让网管开一台机器,老板告诉他去40号机器,钱一交就去40号机痛快的打游戏了。打完游戏他一看表,完了时间到了,赶紧跑回去继续执行剩下的计划。
这其中他给自己定的这个计划,就相当于我们自己写的一行一行代码,都属于自己的操作,虽然学校里没有地方完成,但是刚入学的时候你已经跟学长问好了,他也暂时不去,但是他知道在哪,这个过程就叫链接,然后执行他的一项一项任务,即我们写的代码,走到打游戏这一步,就跳转过去执行,这个网吧就是我们所说的库,问网管的过程就相当于查库的过程,找到你所需要的40号函数,然后执行函数对应的方法,执行完毕再返回继续向后。像这样我们自己编写自己的程序,库一直在那里,只需要在链接的时候把我需要的方法和库中的位置通过地址的方式关联起来,这种方式就叫动态链接。
那么也同时存在这样一种情况:并非只有他一个人想打游戏,学校里还可能存在几百上千名躁动的少年,都想在合适的时候去上网,所以那个网吧就是被学校里所有少年共享的一个库。相当于整个系统有成百上千条命令,大家一旦想上网,一旦想去上网都可以使用,这就叫动态链接。
《静态链接》
我的另一个朋友呢,也是一个网瘾少年,只不过他的学校是一个管制不太严的学校,可以带电子设备,又恰好隔壁的网吧生意不景气,要破产卖电脑,于是他就买了一台带进了宿舍。同样他也定了一系列的计划,看书、扣手……打游戏,但是这回他不再需要出去上网,而是下床就直接开始打游戏,他旁边的张三李四等人也都有电脑,他们每一个人上网都是上自己的网,和别人没有关系,调的都是自己的方法,这就叫静态链接。
这里把网吧的电脑带到宿舍,就相当于我们把库中的,你所想要的方法拷贝到你的可执行程序当中,这种方法就叫静态。也就是说所有的程序每次有调用这个方法,都会在链接时提前进行拷贝,就都是调用自己的方法了。
动静态库的优缺点:
-
动态库:全校所有的学生要上网都要来这家网吧,如果有一天,这家网吧突然被警察局查封了,网吧一旦不存在,那全校所有的学生都无法完成这项任务。
-
所以动态链接的优点:大家共享一个库,可以节省资源
-
缺点:一旦库缺失,会导致几乎所有程序失效
-
-
静态库:将库中的相关代码,直接拷贝到自己的可执行程序当中
- 优点:只要链接完成,形成可执行程序,不依赖任何库,程序可以独立执行
- 缺点:浪费资源
动静态库的性质
我们总结一下:
-
静态库(.a):
-
程序在编译链接的时候把库的代码链接到可执行文件中。
-
程序运行的时候将不再需要静态库。
-
-
动态库(.so):
-
程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
-
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
-
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
-
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间
-
库的制作
测试文件
如下是后文用于测试的头文件和源文件:
其中mymath
提供了一个进行累加计算的方法函数
myprint
提供了一个打印数字和当前时间戳的函数
这两个用于制作库
test
有main函数会调用到这两个函数
mymath.h
#pragma once
#include <cassert>
extern int addToVal(int from, int to);
mymath.cpp
#include "mymath.h"
int addToVal(int from, int to)
{
assert(from <= to);
int res = 0;
for (int i = from; i <= to; i++)
{
res += i;
}
return res;
}
myprint.h
#pragma once
#include <cstdio>
#include <iostream>
#include <time.h>
using namespace std;
extern void print(int msg);
myprint.cpp
#include "myprint.h"
void print(int msg)
{
printf("%d: %lld\n", msg, (long long)time(NULL));
}
test.cpp
#include "mymath.h"
#include "myprint.h"
int main()
{
int res = addToVal(1, 100);
print(res);
}
产生.o
文件:
gcc -c *.c
当前目录的每个.c文件都可以产生它的.o文件
此时再把mymath.o
和myprint.o
直接与test.o
链接到一起形成的可执行程序就可以直接运行
g++ test.o mymath.o myprint.o -o test
如上这个链接的过程就是静态链接
静态库
生成静态库
我们把多个.o文件进行打包,形成一个.a文件,这个文件就是静态库
ar -rc libmymath_s.a mymath.o myprint_s.o
<ar是gnu归档工具,rc表示(replace and create) ,也可用来更新一个已存在的库 >
查看静态库的目录列表
ar -tv libmymath.a
- t:列出静态库中的文件
- v:verbose 详细信息
动态库
生成动态库
动态库所需的.o文件必须用 -fPIC
格式生成
g++ -fPIC -c -o mymath_s.o mymath.cpp
g++ -fPIC -c -o myprint_s.o myprint.cpp
g++ -shared -o libmymath.so:mymath_d.o myprint_d.o
- shared: 表示生成共享库格式
- fPIC:产生位置无关码(position independent code)
- 库名规则:libxxx.so
库的发布
我们知道,一个库的使用需要头文件和库文件
所以,我们把头文件和**.a文件**分别放入lib-static目录中的include和lib目录
把头文件和libxxx.so文件分别放入lib-dynamic目录中的include和lib目录
为了方便修改发布,我们写个makefile
makefile
#生成动静态库
.PHONY:all
all:libmymath.so libmymath.a
#生成动态库
libmymath.so:mymath_d.o myprint_d.o
g++ -shared -o $@ $^
mymath_d.o:mymath.cpp
g++ -fPIC -c -o $@ $^
myprint_d.o:myprint.cpp
g++ -fPIC -c -o $@ $^
#生成动态库
libmymath.a:mymath_s.o myprint_s.o
ar -rc $@ $^
mymath_s.o:mymath.cpp
g++ -c -o $@ $^
myprint_s.o:myprint.cpp
g++ -c -o $@ $^
#生成对应库目录
.PHONY:lib
lib:
mkdir -p lib-static/lib
mkdir -p lib-static/include
cp *.a lib-static/lib
cp *.h lib-static/include
mkdir -p lib-dynamic/lib
mkdir -p lib-dynamic/include
cp *.so lib-dynamic/lib
cp *.h lib-dynamic/include
#删除库
.PHONY:clean
clean:
rm -rf *.o *.a *.so lib-static lib-dynamic
库的使用
上面我们创建了两个库lib-static、lib-dynamic
下面我们写个程序使用一下上面的两个库
用于测试的目录结构:
test.cpp
#include "mymath.h"
#include "myprint.h"
int main()
{
int res = addToVal(1, 100);
print(res);
}
链接静态库
如果直接编译,会报错找不到头文件
因为编译器在程序的预处理阶段会在两个位置找头文件
-
当前路径
-
系统头文件路径(
/usr/include/
)
在链接的阶段会到系统库文件的路径(/lib64/
)找库文件
方法一:安装库
所以我们可以把我们的头文件和库文件移到这两个目录
sudo cp lib-static/include/* /usr/include
sudo cp lib-static/lib/* /lib64/
虽然报错信息变了,但还是无法完成编译
此时预处理阶段可以找到头文件了不会报错,
但是在链接阶段,编译器只知道去/lib64/
找,但不知道链接哪个库,
那些系统库之所以能被找到,是g++知道那些系统库,我们移入的库g++不认识
所以我们要让g++认识我们的库
g++ test.cpp -lmymath
- -l:告诉g++我们要链接的库,后面的
mymath
是libmymath.a
掐掉前面的lib去掉后面的.a形成的
此时就链接成功,形成了可执行程序
总结:
如上我们链接一个库共三步:
- 将头文件移入系统头文件目录
- 将库文件移入系统库文件目录
- g++链接时指定库名称
前两步我们称为安装库
但是我们把自己写的头文件和库直接移入系统目录,这无疑会污染系统的头文件、库文件集
所以,我们将把刚刚移入的库和头先移除,再换种方式链接库
sudo rm /usr/include/mymath.h
sudo rm /usr/include/myprint.h
sudo rm /lib64/libmymath.a
这个删除文件的过程就叫卸载库
方法二:g++指定路径
还是只有三个步骤:
- 让g++知道头文件的路径
- 让g++知道库文件的路径
- 让g++知道库文件名
只不过此时是直接告诉g++
g++ test.cpp -o test -I./lib-static/include/ -L./lib-static/lib/ -lmymath
- -I:添加头文件路径
- -L:添加库文件路径
- -l:指定添加的库
使用file a.out
可以看到这个文件的信息:
其中说明了它是一个静态链接的可执行程序。
链接动态库
编译
与静态库的链接相同,也有两种方法
这里就只用第二种方法演示
g++ test.cpp -o test -I./lib-dynamic/include/ -L./lib-dynamic/lib/ -lmymath
编译g++编译成功了,但是程序运行的时候出错了
这是因为g++在动态链接的时候,我们告诉了g++去哪找那个库,编译成功了,你test后面去哪找这个库跟我g++无关了
但是在程序运行的时候,test进程不知道去哪找这个库
前面我们静态链接的时候运行之所以不会出问题,就是静态链接的会把库的代码拷贝到程序中
但是动态链接的程序不会有库中的代码,在运行程序的时候操作系统需要把库这个文件也加载到内存,如果要执行库的代码块,会跳转到库的代码区执行
所以,我们想办法让进程找到动态库即可,
运行
如下提供了三种方法:
-
把动态库拷贝到系统路径下:/lib64/ – 安装
sudo cp lib-dynamic/lib/libmymath.so /lib64/
或者在在这个路径下创建动态库的软连接
sudo ln -s /home/yb/code/linux-learning/test_11_14/uselib/lib-dynamic/lib/libmymath.so /lib64/libmymath.so
-
通过导入环境变量的方式 :
进程运行的时候,在环境变量中查找自己需要的动态库路径 —
LD_LIBRARY_PATH
只需要把我们库的路径追加到
LD_LIBRARY_PATH
环境变量即可export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/yb/code/linux-learning/test_11_14/uselib/lib-dynamic/lib
此时ldd test就看到可以找到动态库了
程序也可以运行了
-
系统配置文件
一旦我们退出了当前终端,重新打开一个终端
上面的配置的环境变量就不见了
在进程概念–>环境变量中我们解释过
所以,我们就要配置系统文件
在
/etc/ld.so.conf.d/
目录下有一些文件当操作系统找库的时候,除了到系统库目录中查找,还会到这个目录,一个一个读取上面的配置文件,找到动态库
这些文件的内容也非常简单,就存了库的路径
所以,我们只需要在当前目录下创建一个
.conf
文件把动态库的路径存进去就行sudo sh -c ‘echo “/home/yb/code/linux-learning/test_11_14/uselib/lib-dynamic/lib” >/etc/ld.so.conf.d/mymath.conf’
然后使用
lsconfig
指令,让这个配置文件生效sudo ldconfig /etc/ld.so.conf.d/mymath.conf
此时ldd test就可以找到了库
程序也可以运行了
关掉终端重新打开,依然可以运行
系统库的静态链接
gcc/g++编译的时候会自动链接c/c++的库,默认的链接方式是动态链接
如果指定动态链接库,可以加
-static
选项g++ test.cpp -o test_st -static
小贴士:
因为一般而言,系统都没有带静态库
如果出现如下报错可可以安装对应的库:
安装C语言静态库:
sudo yum -y install glibc-static
安装C++静态库:
sudo yum -y install libstdc+±static
我们对比一下动态链接系统库和静态链接系统库产生的两个文件
可以看出,仅仅调用了几个printf()
静态链接就大了很多。
所以没有特殊情况情况我们更推荐使用动态链接
动态链接底层原理
这部分设计到进程概念–>Linux进程地址空间的一些东西,下面的内容如果无法理解可以看看这部分
当我我们开始运行test进程,进程控制块task_struct中维护着一个当前进程的虚拟地址空间(用mm_struct管理)
在栈区和堆区中间有一块区域称为共享区
这块虚拟地址空间可以用来指向库文件的代码
当我们把一个程序运行起来,首先会把程序的代码和动态库的代码从磁盘加载到物理内存中
虚拟地址空间的正文代码通过页表映射到物理内存的程序代码,共享区函数的代码就会映射到动态库的代码
当我们在代码中调用了printf这样的库函数,调用即跳转到共享区,然后通过页表映射到物理内存执行printf的代码块,执行完之后再跳转回调用printf的下一条指令,继续执行
如果此时又有一个程序被启动,如果它也使用了相同的库,它的共享区也会通过页表映射到相同的物理内存
这便是多个进程共享同一个动态库的底层体现