一、库的概念和本质

在开始学习语言的时候,我们就已经接触过C库函数了,我们使用printf函数的时候,是需要包含stdio.h头文件的。

然后我们编译完成后,在Linux下,使用ldd命令,是可以查看到我们这个程序所依赖的库的。

可以看到,其就依赖上面的这些库,这些库,要不就是动态库,要不就是静态库。其都是存在我们的操作系统中的。

然后,我们要使用其的时候,要将其进行链接。

对于静态库,那么就要静态链接,如果是动态库,那么就呀动态链接。

在Linux下,静态库的文件后缀为.a。

动态库的文件后缀为.so。

然后我们在使用gcc进行编译的时候,其默认是动态编译的,我们可以在编译语句那边添加一个

-static,那么其就会为静态编译了。

不过静态编译的可执行程序,其是会比动态编译的程序要大的。

前面我们学习gcc工具的时候,已经知道,一个可执行文件是如何从我们的.c文件一步一步变成我们的可执行程序的,然后我们知道,最后是将所有的.o文件链接后,那么就是可执行程序了。

那么也就是说在链接之前,我们的.o文件之间是没有任何联系的,各自是独立的。

那么我们自己写的代码和库文件,那么也是在链接的时候才会有联系。

那么是否就是说我们的库文件就是.o文件呢?

其实我们的库是.o文件的集合。

那么我们也可以自己制作库。

二、静态库的制作和使用

打包静态库完整流程

静态库后缀:.a,本质是多个 .o 目标文件用 ar 归档打包而成,链接时会把库代码完整复制进可执行文件。

1、文件示例

新建项目目录,结构如下:

plaintext

libdemo/
├── add.c    # 库源码
├── sub.c    # 库源码
├── calc.h   # 头文件(对外接口)
└── main.c   # 测试主程序

源码内容

calc.h

#ifndef CALC_H
#define CALC_H
int add(int a, int b);
int sub(int a, int b);
#endif

add.c

#include "calc.h"
int add(int a, int b)
{
    return a + b;
}

sub.c

#include "calc.h"
int sub(int a, int b)
{
    return a - b;
}

main.c(测试调用)

#include <stdio.h>
#include "calc.h"
int main()
{
    printf("10+3=%d\n", add(10,3));
    printf("10-3=%d\n", sub(10,3));
    return 0;
}

2、完整编译打包步骤

步骤 1:编译源码生成目标文件 .o

只编译不链接 -c,生成二进制目标文件

gcc -c add.c sub.c

执行后生成:add.osub.o

步骤 2:使用 ar 工具打包静态库 libxxx.a

静态库命名规范:lib库名.a 语法:ar rcs 库名 目标文件1 目标文件2 ...

在Linux中,其会自动识别lib和.a。

  • r:插入 / 替换目标文件
  • c:创建库文件(不存在则新建)
  • s:生成索引,加快链接速度
ar rcs libcalc.a add.o sub.o

当前目录生成静态库:libcalc.a

步骤 3:查看静态库内部包含的目标文件

# 列出库内.o文件
ar tv libcalc.a
# 或 nm 查看库内符号(函数名)
nm libcalc.a

步骤 4:链接静态库编译可执行程序

三种链接写法任选其一:

方式 1:直接写库文件路径

gcc main.c libcalc.a -o test_demo

方式 2:-L 指定库目录 + -l 指定库名(去掉 lib 和.a)

# -L. 代表当前目录找库;-lcalc 对应 libcalc.a
gcc main.c -L. -lcalc -o test_demo

方式 3:库放到系统库目录(/usr/lib)全局调用

sudo cp libcalc.a /usr/lib/
sudo cp calc.h /usr/include/
# 之后不用-L,直接-lcalc
gcc main.c -lcalc -o test_demo

步骤 5:运行程序

./test_demo
10+3=13
10-3=7

3、分目录工程实战(规范项目结构)

实际开发不会所有文件放一起,分层存放:

project/
├── src/        # 库源码 add.c sub.c
├── include/    # 头文件 calc.h
├── lib/        # 输出静态库 .a
├── obj/        # 存放.o中间文件
└── main.c

编译命令:

# 1. 指定头文件路径 -I
gcc -c -I./include src/add.c src/sub.c -o obj/add.o
gcc -c -I./include src/sub.c -o obj/sub.o

# 2. 打包静态库到lib目录
ar rcs lib/libcalc.a obj/add.o obj/sub.o

# 3. 链接编译
gcc main.c -I./include -L./lib -lcalc -o app

4、Makefile 自动化一键打包(推荐)

不用手动敲命令,新建 Makefile

makefile

CC = gcc
CFLAGS = -Wall -g -I./include  # 头文件路径、警告、调试信息
SRC = src/add.c src/sub.c
OBJ = obj/add.o obj/sub.o
LIB = lib/libcalc.a
TARGET = app

# 生成.o
obj/%.o: src/%.c
	mkdir -p obj lib
	$(CC) $(CFLAGS) -c $< -o $@

# 打包静态库
$(LIB): $(OBJ)
	ar rcs $@ $(OBJ)

# 链接主程序
$(TARGET): main.c $(LIB)
	$(CC) main.c $(CFLAGS) -L./lib -lcalc -o $(TARGET)

# 清理编译产物
clean:
	rm -rf obj lib $(TARGET)

如上就是我们自己弄一个静态库还有使用的全部过程,那么如果我们自己写的库要给别人使用,那么我们要给使用的人啥内容?

首先,我们得给其我们的库文件,然后呢,我们还得让其知道咋使用吧。也就是一个使用手册,那么就是我们的.h文件。

为啥上面我们使用自己的静态库的时候需要将其路径告诉编译器,而我们之前使用的时候是不需要的,这和我们前面讲PATH环境变量的时候一样,再进行链接的时候,其会去一个固定路径下进行查找。

还有就是为啥在链接的时候,需要我们库文件名字要为l开头呢?但是我们使用C语言中提供的库的时候又不需要,这又是为啥?

这是因为我们的gcc,其本来就是专门对C语言进行编译链接的,那么其就认识C语言库的名字,但是我们自己自定义的,其是不认识的,所以我们需要加这个,来告诉编译器。

我们还要注意的是,多个文件中,只能有一个main函数,这个就是我们程序运行时候的入口。

三、动态库的制作和使用

首先动态库是在程序运行的时候才会进行链接的,多个程序进行共享的。

⼀个与动态库链接的可执⾏⽂件仅仅包含它⽤到的函数⼊⼝地址的⼀个表,⽽不是外部函数所在⽬ 标⽂件的整个机器码

在可执⾏⽂件开始运⾏以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中, 这个过程称为动态链接(dynamic linking)

动态库可以在多个程序间共享,所以动态链接使得可执⾏⽂件更⼩,节省了磁盘空间。操作系统采 ⽤虚拟内存机制允许物理内存中的⼀份动态库被要⽤到该库的所有进程共⽤,节省了内存和磁盘空 间。

1、动态库的生成

首先,我们的文件还是和静态库生成一样。

然后我们要先将我们的.c文件进行编译,生成.o文件,但是我们未来要生成的是动态库的话,那么我们就要在编译的时候,多加一个与位置无关码,fPIC。对于这个与位置无关码,我们后续也会进行讲解,此处我们记住我们要生成的是动态库的话,就要在编译为.o文件的时候添加。

如:gcc -c -fPIC add.c sub.c

这样就可以生成add.o和sub.o了。

那么有了.o文件,然后我们就要将其打包成动态库了,我们对动态库的打包,是直接使用gcc的,不是使用ar。为啥呢?

这是因为,我们大部分情况下,都是使用动态库,所以我们的gcc将其添加了。

指令:gcc -shared add.o sub.o -o libcalc.so

然后我们要注意的是,对于库的命名,也是lib开始,然后后面的就是我们库的名字,然后后缀为.so结尾。

链接:
方式 1:直接指定 so 文件
gcc main.c ./libcalc.so -o app
方式 2:-L 指定库目录 + -l 库名
-L. 当前目录找库;-lcalc 对应 libcalc.so
gcc main.c -L. -lcalc -o app

那么这样就可以将我们的库和我们的.c文件进行连接了。

2、动态库的使用

我们提到,我们的动态库是在程序运行的时候,才会和我们的程序进行链接的。那么就是说,我们的库文件此时在我们的程序文件中,可能就是个声明,所以在运行的时候,我们还要去找那个库。

所以在运行的时候,我们要指定我们使用到的库的位置等。

但是我们使用C语言标准库的时候,是不需要的,是可以直接进行运行的。那么因为其会去指定的环境变量中找。那么我们要想达到那个效果我们有如下几个方法:

1、临时环境变量 LD_LIBRARY_PATH

在我们的环境变量中,有一个LD_LIBRARY_PATH,那么我们可以将我们库文件的路径也添加到这个路径中,这个环境变量中保存的是我们动态库的搜索路径。

export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH

但是这个方法并不是一直有效的,我们对于环境变量的修改只是内存级的,机器后面重启,我们添加进去的路径又没了。

永久配置系统动态库路径

ldconfig⽅案:配置/ etc/ld.so.conf.d/ ,ldconfig更新
创建库配置文件
sudo echo "/home/xxx/你的库目录" > /etc/ld.so.conf.d/mycalc.conf
更新动态链接器缓存
sudo ldconfig
验证是否识别到库
ldconfig -p | grep calc
之后直接运行 ./app 无需配置环境变量

这个方法,可以永久有效。

将库文件路径copy到系统标准库目录

sudo cp libcalc.so /usr/lib/
sudo cp calc.h /usr/include/
# 无需-L运行编译,运行也不用配置路径

只有动态库,才需要在运行时搜索!

当同时存在同名的动静态库,系统默认使用的是动态库。如果想要使用静态库,要加上-static选项。-static选项是强制要求我们所有的库,必须要有对应的静态库版本,并且全部静态链接到我们的可执行程序。当我们只提供静态库时,即便默认采用动态链接,对于该库来讲,也只能静态链接;当我们只提供动态库时,只能使用动态链接。


3、动静态库的选择

在我们的系统中,会存在几种情况:

1、动态库和静态库都在

当动态库和静态库都在,那么我们使用gcc没有指定链接方式,那么其默认是去链接动态库。

那么当我们指定链接静态库,那么就是静态链接。

2、只有静态库

只有静态库的话,那么我们在链接的时候没有不进行任何的静态链接选项,或者动态链接选项,那么其是会自动进行静态链接。

那么如果我们进行动态链接呢?

这个时候,是可以编译成功的,但是呢,其本质还是链接的我们的静态库。

3、只有动态库

一样,不进行链接选项,那么也是动态链接。

但是,如果我们进行静态链接的话,其是不会链接成功的。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐