【Linux】掌握库的艺术:我的动静态库封装之旅

🌈个人主页:Yui_

🌈Linux专栏:Linux

🌈C语言笔记专栏:C语言笔记

🌈数据结构专栏:数据结构

🌈C++专栏:C++

文章目录

  • 1.什么是库
    • [1.2 认识动静态库](#1.2 认识动静态库)
      • [1.2.1 动态库](#1.2.1 动态库)
      • [1.2.2 静态库](#1.2.2 静态库)
  • 2.封装动静态库
    • [2.1 封装动态库](#2.1 封装动态库)
      • [2.1.1 组织头文件和库文件](#2.1.1 组织头文件和库文件)
      • [2.1.2 动态库的使用](#2.1.2 动态库的使用)
    • [2.2 封装静态库](#2.2 封装静态库)
      • [2.2.1 使用静态库](#2.2.1 使用静态库)
  • [3. 动静态库小知识](#3. 动静态库小知识)
    • [3.1 gcc对动静态的优先级](#3.1 gcc对动静态的优先级)
    • [3.2 动静态库的区别](#3.2 动静态库的区别)
  • 4.总结

1.什么是库

在计算机编程中, (Library)是一个预先编写的代码集合,包含了可以被其他程序调用的函数、类、变量和资源。库的主要目的是为了简化编程过程,提供常用功能的实现,促进代码重用,从而减少开发时间和提高软件的可靠性。

在实践中,我们一定会使用到别人的库,如你在C语言时期一定会调用到c标准库。除了标准库,我们还可以使用第三方库,无论使用那种库都是为了节省我们的时间,不在需要我们自己来造"轮子"。

所有的库都可以从两个方面来认识:

  1. 创建者
  2. 使用者
    提问:使用者在使用库时,是否能知道该库的源代码呢?
    回答:在不逆向的情况下,使用者是无法得知库的源代码的,这也就牵扯到了,库的第二个属性隐藏源代码。
    我们都知道,形成可执行文件的步骤有4步:
  1. 预处理:头文件展开、去注释、宏替换、条件编译等,生成.i文件。
  2. 编译:语法分析、语义分析、符号汇总等,检查无误后将代码翻译成汇编指令,生成.s文件,
  3. 汇编:将汇编指令转化成二进制指令,生成.o文件。
  4. 链接:将生成的各个.o文件进行链接,生成可执行程序。

而库就是所有.o文件用特定的方式,进行打包,形成的一个文件。
注意库文件需要配合对应的头文件进行使用,头文件不必隐藏

当我们在main.c中使用对应的功能函数:

当有许多不同的源文件去调用这些功能函数时,那要的话,功能函数会被重复的进行预处理、编译、汇编的操作,各自生成.o文件,然后我们的源文件再和这些功能函数一起生成可执行文件。

如此操作会有许多重复的操作,我们完全可以提前让功能函数变成一个个.o文件,再去和源文件进行链接,但是我们还要考虑到,可能会存在非常多的.o文件,为了减轻我们的操作,我们可以对这些.o文件进行打包,这样的文件我们称为库。

如此一来,库的本质就是若干个目标文件的集合,每一个目标文件都包含了由源码编译生成的二进制代码,在保证使用的同时,还有很好的隐藏性

1.2 认识动静态库

动静态库是编程中常用的两种库类型,用于封装和重用代码。它们在链接、加载和使用方面存在显著的差异。

1.2.1 动态库

动态库(或共享库)是在运行时加载的库,通常以 .so(Linux)或 .dll(Windows)文件格式存在。动态库的代码不被复制到可执行文件中,而是在运行时由操作系统加载。

我们先来写一段简单的代码:

c 复制代码
#include <stdio.h>

int main()
{
    printf("hello world!!!!\n");
    return 0;
}
//生成可执行文件mybin

编译成功后,我们使用ldd指令来查看其所链接的动态库
ldd语法:

shell 复制代码
ldd filename

功能:

用于打印程序或库文件所依赖的动态库(共享库)列表。ldd不是一个可执行程序,只是一个shell脚本。

shell 复制代码
ubuntu@VM-20-9-ubuntu:~/libraryTest$ ldd mybin 
	linux-vdso.so.1 (0x00007ffc9c4db000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd8df175000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fd8df3ab000)

libc.so 就是该程序所依赖的动态库,那么我们一个如何识别一个动态库的名字呢?

去掉前缀lib,再去掉后缀.so就是库名了。

也就是说,这个动态库的名字就是c,没错这就是c运行库。

1.2.2 静态库

静态库是将一组对象文件(.o 文件)打包成一个库文件(通常为 .a 后缀),在编译时将其链接到最终生成的可执行文件中。链接过程是在编译阶段完成的,库的代码被复制到可执行文件中。

正常情况下,gcc默认都是连接的动态库,如果需要进行静态连接要特别指定。

shell 复制代码
gcc -o staticBin libTest.c -static

ubuntu@VM-20-9-ubuntu:~/libraryTest$ ls -l
total 904
-rw-rw-r-- 1 ubuntu ubuntu     82 Oct 29 11:40 libTest.c
-rw-rw-r-- 1 ubuntu ubuntu     74 Oct 29 11:41 makefile
-rwxrwxr-x 1 ubuntu ubuntu  15952 Oct 29 11:41 mybin
-rwxrwxr-x 1 ubuntu ubuntu 900344 Oct 29 14:49 staticBin

可以发现静态连接的可执行程序所占的空间大小远大于动态连接的。

使用ldd观察是否有依赖的库

shell 复制代码
ubuntu@VM-20-9-ubuntu:~/libraryTest$ ldd staticBin 
	not a dynamic executable

发现静态链接生成的可执行程序不依赖其他库文件。

使用file查看:

shell 复制代码
ubuntu@VM-20-9-ubuntu:~/libraryTest$ file staticBin 
staticBin: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=d4a39b68ac04f9fd8b37bd8c17ce42a8b27ad8c2, for GNU/Linux 3.2.0, not stripped

发现是:statically linked

如果你是centos用户,可能会遇到静态库未安装的情况,只需要搜索一下安装方法即可。

2.封装动静态库

2.1 封装动态库

先写好需要封装的代码:

add.h:

c 复制代码
#pragma once
int add(int a,int b);

sub.h:

c 复制代码
#pragma once
int sub(int a,int b);

add.c:

c 复制代码
#include "add.h"
int add(int a,int b)
{
    return a+b;
}

sub.c:

c 复制代码
#include "sub.h"
int sub(int a,int b)
{
    return a-b;
}

dynLibTest.c:

c 复制代码
#include <stdio.h>
#include "add.h"
#include "sub.h"

int main()
{
    int a = 100;
    int b = 200;
    printf("%d + %d = %d\n",a,b,add(a,b));
    printf("%d - %d = %d\n",a,b,sub(a,b));
    return 0;
}

然后我们还需要知道位置无关码的概念(Position-Independent Code,PIC)
位置无关码 (Position-Independent Code,PIC)是一种编译代码的方式,使得生成的代码可以在内存的任意位置执行,而不需要修改代码中的地址。这种特性在动态链接库和共享库中非常重要,因为它们可以被多个进程共享,并在加载时被放置到不同的内存地址。

为了实现位置无关,编译器在生成代码时使用相对地址而不是绝对地址。例如,在访问全局变量时,编译器不会生成直接访问变量的绝对地址的代码,而是使用相对于当前指令位置的偏移量。这样,无论代码被加载到哪个地址,访问都可以通过相对计算来实现。

位置无关码是一种重要的编程技术,尤其在动态链接和共享库中具有广泛的应用。它提供了灵活性和内存使用效率,同时也增强了程序的安全性。

如果我要实现位置无关码可以在gcc后面加上-fPIC选项

shell 复制代码
gcc -fPIC -c add.c
gcc -fPIC -c sub.c

我们也知道不加-fPIC一样可以生成.o文件。但是它们之间还是很有区别的。
位置无关码对于gcc:

  • fPIC作用于编译阶段,告诉编译器于位置无关的代码,此时产生的代码中没有绝对地址,全部都使用相对地址,从而代码可以被加载到内存的任意位置可以正确的执行。这正是共享库被加载时,在内存的位置不是不是固定的。
  • 如果不加-fPIC选项,则加载.so文件的代码时,代码段引用的数据对象需要重定位,这个重定位会修改代码段地内容,这就会造成每一个使用这个.so文件代码段的内核里都会生成这个.so文件代码的拷贝,并且每一个拷贝都不一样,这就是使得内存的消耗变大。
  • 为此我们总是会用-fPIC来生成.so文件,但是不会用点-fPIC来生成.a静态文件。我们当然可以不用-fPIC来生成.so文件,只是这样的话.so文件必须要在加载到用户的地址空间时重定向所有表目。
    使用-shard将文件打包为动态库
shell 复制代码
gcc -shared -o libmyc.so add.o sub.o

通过readelf -S 来查看库的信息,如偏移量offset

2.1.1 组织头文件和库文件

为了方便我们后续的操作,我会写一个makefile来组织头文件和库文件。

shell 复制代码
.PHONY:all
all:libmyc.so

libmyc.so:add.o sub.o
	gcc -shared add.o sub.o -o libmyc.so
add.o:add.c
	gcc -fPIC -c add.c -o add.o
sub.o:sub.c
	gcc -fPIC -c sub.c -o sub.o
.PHONY:output
output:
	mkdir -p mylib/lib
	mkdir -p mylib/include
	cp -rf *.h mylib/include
	cp -rf *.so mylib/lib
.PHONY:clean
clean:
	rm -rf *.o mylib *.a *.so output

2.1.2 动态库的使用

当我们把当前目录的头文件和目标文件删除后

shell 复制代码
ubuntu@VM-20-9-ubuntu:~/libraryTest$ rm -rf *.o *.h

此时如果我们直接使用gcc来编译会发生什么呢?

没错,肯定会报错说,找不到头文件啦。

shell 复制代码
ubuntu@VM-20-9-ubuntu:~/libraryTest$ gcc dynLibTest.c 
dynLibTest.c:2:10: fatal error: add.h: No such file or directory
    2 | #include "add.h"
      |          ^~~~~~~
compilation terminated.

为了让编译器能够找到我们的头文件,需要我们加上-I 头文件所在路径

复制代码
gcc -I./mylib/include dynLibTest.c

现在的错误就是,找不到目标函数了,因为我们没有给编译器指明在哪里,它肯定就找不到了。

shell 复制代码
ubuntu@VM-20-9-ubuntu:~/libraryTest$ gcc -I./mylib/include dynLibTest.c
/usr/bin/ld: /tmp/ccOkC8RY.o: in function `main':
dynLibTest.c:(.text+0x25): undefined reference to `add'
/usr/bin/ld: dynLibTest.c:(.text+0x52): undefined reference to `sub'
collect2: error: ld returned 1 exit status

所有我们还需要加上`-L库文件所在路径 l库名

shell 复制代码
gcc -I./mylib/include -L./mylib/lib dynLibTest.c -lmyc
  1. '-I':指定头文件搜索路径。
  2. ---L:指定库文件搜索路径。
  3. -l:指明需要链接库文件路径下的哪一个库。

然后我们就可以得到一个可执行文件了,但是如果你去执行它就会发现,它居然不能执行!!!

shell 复制代码
ubuntu@VM-20-9-ubuntu:~/libraryTest$ ./a.out 
./a.out: error while loading shared libraries: libmyc.so: cannot open shared object file: No such file or directory

为什么会这样?

算了,先用ldd看看它依赖的动态库的吧。

居然没有连到,岂有此理,辛苦打了一连串指令竟然完全没作用。

其实不是这样啦,因为是动态库,在执行时我们仍然需要知道动态库的位置在哪。

对于动态库,编译时会搜索库的路径,运行时也会搜索库的路径。

为了解决这个问题,我们有4种解决方案:

  1. 直接将库进行安装(拷贝)到系统库当中。(最傻瓜操作)
  2. 将不在系统默认库搜索路径下的库路径,添加到LD_LIBRARY_PATH
    3.使用idconfig指令。

拷贝到系统目录

shell 复制代码
ubuntu@VM-20-9-ubuntu:~/libraryTest$ sudo cp mylib/lib/libmyc.so /lib/x86_64-linux-gnu/
ubuntu@VM-20-9-ubuntu:~/libraryTest$ ldd a.out 
	linux-vdso.so.1 (0x00007fffc4f54000)
	libmyc.so => /lib/x86_64-linux-gnu/libmyc.so (0x00007f35c8f9c000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f35c8d73000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f35c8fae000)

这样虽然简单,但是由于我们自己书写的库,大概率是不成熟的,这样会污染系统库目录。

更改环境变量

shell 复制代码
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:home/ubuntu/libraryTest/mylib/lib

ldd查看:

shell 复制代码
ubuntu@VM-20-9-ubuntu:~/libraryTest$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:home/ubuntu/libraryTest/mylib/lib
ubuntu@VM-20-9-ubuntu:~/libraryTest$ ldd a.out
	linux-vdso.so.1 (0x00007fff8fdee000)
	libmyc.so (0x00007f85aaf99000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f85aad6a000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f85aafa5000)

该方法为临时方法,在系统重新登入时就失效了。
使用Idconfig指令

/etc/ld.so.conf.d/目录下的文件用来指定动态库搜索路径。这些文件被包含在/etc/ld.so.conf文件中,ldconfig命令会在默认搜寻目录(/lib和/usr/lib)以及动态库配置文件/etc/ld.so.conf内所列的目录下,搜索可共享的动态链接库,并创建出动态装入程序(ld.so)所需的连接和缓存文件。
.conf文件用来存储各种文件的路径,我们只要把我们自己写的第三方库的路径存放进去,程序运行时就会去里面找了。

shell 复制代码
echo /home/ubuntu/libraryTest/mylib/lib > libmyc.conf

然后我们需要将,.conf文件拷贝到/etc/ld.so.conf.d/下。

shell 复制代码
sudo cp libmyc.conf /etc/ld.so.conf.d/

此时如果我们直接ldd是无法找到的,我们来需要更新一下。

shell 复制代码
sudo ldconfig

然后就可以了

那么动态库就先到这里,下面我们开始静态库的讲解。

2.2 封装静态库

静态库的操作会比动态库的更为简单。

代码的话,依然是上面的那些代码。

复制代码
add.c sub.c add.h sub.h staLibTest.c

staLibTest.c里面的代码和dynLibTest.c

首先我们需要把所有的.c源文件都编译为目标文件。(此时不在需要位置无关码)

shell 复制代码
gcc -c add.c
gcc -c sub.c

我们可以直接将主程序和这些.o文件进行编译。

shell 复制代码
ubuntu@VM-20-9-ubuntu:~/libraryTest$ gcc -o staticBin staLibTest.c add.o sub.o
ubuntu@VM-20-9-ubuntu:~/libraryTest$ ./staticBin 
100 + 200 = 300
100 - 200 = -100

正常编译,不过目标文件少的情况下还好,如果有很多的目标文件,我们就有点吃力了。为此我们可以打一个包。

使用ar指令将目标文件打包为静态库:

ar 指令是一个用于创建、修改和管理静态库文件的命令行工具,广泛应用于 Unix 和类 Unix 系统(如 Linux)。静态库通常以 .a 后缀表示,ar 允许开发者将多个目标文件(.o 文件)打包成一个单一的库文件,便于代码重用和管理。

语法:

shell 复制代码
ar [选项] [库名] [依赖文件]

主要功能:

  • r:插入文件。如果目标文件已经存在,则替换它。
  • c:创建一个新的库,如果库文件不存在。
  • s:创建索引,以加快链接过程。
  • t:列出库中包含的文件。
  • x:从库中提取指定的文件。
  • d:从库中删除指定的文件。
  • v:查看库的信息
    举例:将add.osub.o打包为静态库
shell 复制代码
ar -rc libmyc.a add.o sub.o

利用-t -v选项来查看静态库的文件以及信息。

复制代码
ubuntu@VM-20-9-ubuntu:~/libraryTest$ ar -tv libmyc.a
rw-r--r-- 0/0   1224 Jan  1 08:00 1970 add.o
rw-r--r-- 0/0   1224 Jan  1 08:00 1970 sub.o

和动态库时一样,我们现在利用makefile来完成这些操作

shell 复制代码
libmyc.a:add.o sub.o
	ar -rc libmyc.a add.o sub.o
add.o:add.c
	gcc -o add.o -c add.c
sub.o:sub.c
	gcc -o sub.o -c sub.c

.PHONY:output
output:
	mkdir -p mylibs/lib
	mkdir -p mylibs/include
	cp -rf *.h mylibs/include
	cp -rf *.a mylibs/lib
.PHONY:clean
clean:
	rm -rf *.o mylibs libmyc.a

2.2.1 使用静态库

  1. '-I':指定头文件搜索路径。
  2. ---L:指定库文件搜索路径。
  3. -l:指明需要链接库文件路径下的哪一个库。

在动态库的篇章中,我们就已经了解了指定库的路径了。

在静态库也同理。

复制代码
gcc staLibTest.c -I./mybins/include -L./mylibs/lib -lmyc

编译完后就可以直接运行了。

shell 复制代码
ubuntu@VM-20-9-ubuntu:~/libraryTest$ gcc staLibTest.c -I./mybins/include -L./mylibs/lib -lmyc -o testbin -static
ubuntu@VM-20-9-ubuntu:~/libraryTest$ ./testbin 
100 + 200 = 300
100 - 200 = -100

同样的,如果你觉得没错都显示指定库连接很麻烦,可以把目标文件拷贝到系统库当中。

3. 动静态库小知识

3.1 gcc对动静态的优先级

如果我们同时提供动态库和静态库,gcc会默认使用动态库。如果我们非要静态链接,必须使用static指定。如果我们只提供静态库,那我们的程序只能对该库进行静态链接,但是程序不一定整体是静态链接的。如果我们只提供动态库,默认只能动态连接,非要静态链接的话会报错。

3.2 动静态库的区别

特性 静态库 动态库
文件后缀 .a .so (Linux), .dll (Windows)
链接方式 编译时链接 运行时链接
可执行文件大小 较大(包含所有库代码) 较小(只包含引用)
外部依赖 需要在运行时提供
更新方式 需要重新编译所有依赖的程序 只需替换库文件
性能 加载速度快 加载速度相对较慢
共享性 不支持多个进程共享 支持多个进程共享

4.总结

动静态库各有优缺点,选择使用哪种库通常取决于具体的项目需求、资源限制和开发环境。静态库适用于对依赖性和更新不敏感的应用,而动态库则更灵活,适合需要频繁更新和共享代码的场景。在实际开发中,合理选择和使用这两种库能够提高代码的复用性和维护性。

相关推荐
xuanzdhc1 小时前
Linux 基础IO
linux·运维·服务器
愚润求学1 小时前
【Linux】网络基础
linux·运维·网络
bantinghy1 小时前
Linux进程单例模式运行
linux·服务器·单例模式
小和尚同志2 小时前
29.4k!使用 1Panel 来管理你的服务器吧
linux·运维
帽儿山的枪手2 小时前
为什么Linux需要3种NAT地址转换?一探究竟
linux·网络协议·安全
shadon1789 天前
回答 如何通过inode client的SSLVPN登录之后,访问需要通过域名才能打开的服务
linux
AWS官方合作商9 天前
AWS ACM 重磅上线:公有 SSL/TLS 证书现可导出,突破 AWS 边界! (突出新功能的重要性和突破性)
服务器·https·ssl·aws
小米里的大麦9 天前
014 Linux 2.6内核进程调度队列(了解)
linux·运维·驱动开发
程序员的世界你不懂9 天前
Appium+python自动化(三十)yaml配置数据隔离
运维·appium·自动化
算法练习生9 天前
Linux文件元信息完全指南:权限、链接与时间属性
linux·运维·服务器