动、静态库相关内容的详细介绍

动、静态库相关内容的详细介绍

背景

来自网络,自己整理,可跳过。

很多程序员写代码时,编译器和语言通过自举进行自我更新,变得越来越适配当前的应用环境。

在欧美有一群人数庞大(1000人左右)的工程师(科学家),他们每人都需要做一件事:根据情况向显示屏打印消息。或者说早期这些人都有各自的研究领域,他们可能需要使用编程语言编写各种算法(包括数学公式例如三角函数的泰勒公式、打印消息到显示屏上等等)。

因为进行研究之前需要先实现一部分或全部函数,后面要进行算法设计时直接使用已经写好的软件就可以。但所有人都可能自己弄一套属于自己的别人已经做出来的函数,而且很有可能大部分人写的函数都有 BUG ,极少数的能用但可能效率差、风格差,能用且效率可观的代码只是少数。更重要的是,工程师除了要懂自己的领域,还要懂编程,要求很高。

于是就分出一批人做一些基础工作:将常用的功能写一份代码即可,让每个工程师都能将资源和精力集中在自己擅长的领域。

这种模式并不是特例,在抗日战争时期以及建国后就分出一部分人成立生产建设兵团,专门负责后勤物资的生产和运输,甚至对全国的经济进行宏观调控,这样可以在保证军队战斗力的同时实现物资的补给。

更早的时候还有汉代的屯田制度,为了抵御北方少数民族的入侵,就将军队驻扎在北方进行屯田。

例如,C 程序中,并没有定义 printf 的函数实现,且在预编译中包含的 stdio.h 中也只有该函数的声明,而没有定义函数的实现。

这是因为系统把这些函数实现都被做到名为 libc.so.6 的动态库文件中去了,在没有特别指定时,gcc 会到系统默认的搜索路径/usr/lib下进行查找,也就是链接到 libc.so.6 库函数中去,这样就能实现函数 printf 了,而这也就是链接的作用。

动静态库和可执行程序的关系

动、静态库简单理解就是写给别人用的 C/C++ 等编程语言写的实现某些特定功能的函数库 或(结构体类库。这样的库一般分为静态库和动态库两种。

动态库 ,即动态链接 ,它是让用户的程序和第三方库通过地址产生关联 ,而静态库是把库文件的内容拷贝到可执行程序当中

动静态库的文件属性

  • 静态库 是指编译链接时,把库文件的代码全部加入到可执行程序文件中,因此生成的可执行程序文件比较大,但在运行时不再需要静态库库文件。

    • 在 Linux ,静态库的后缀名为 ".a" ,且使用 "lib" 作为前缀名,例如 libname.a 表示一个名为 name 的库。
    • 在 windows ,后缀名为或 ".lib" ,无前缀名的强制需求。

    c语言的函数和模块化编程-CSDN博客 中展示了在 Windows 下如何使用 Visual Studio 2019 制作静态库。

  • 动态库 与之相反,在编译链接时并没有把动态库的代码加入到可执行文件中 ,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。

    • 动态库在 Linux 的后缀名为 ".so",且和静态库一样需要使用前缀 lib 表示,在 .so 后可能还会跟 .x.y.z 表示版本号,例如 libc.so.6 就是动态库。
    • 在 windows ,后缀名为 ".dll" ,无前缀名的强制需求。

所以在 Linux 中,库的命名规则:以 lib 开头,第2个 so 表示动态库,最后 1 个或 3 个数字是版本号。把这些组成去掉后才是库真正的名字,即 libc.so.6 ,真正的名字是 c,表示 C 标准库。同理 libc.a 也能将 lib 和 .a 去掉,它也表示 C 标准库,但是是静态库。

其他操作系统有自己的库的命名规则,完全照搬 Linux 的命名规则会使编译器无法识别这些库。

一些软件(例如Typora)需要某个动态库支持才能使用某些功能,但杀毒软件总是将这个 .dll 识别为病毒并将它清理掉,因此不得不采取一些不常规的手段,比如将这个文件列入白名单,或干掉杀毒软件。

不建议禁止进程调用某个动态库。例如 libc.so.6,绝大部分进程都要调用这个库,若将这个库设置成其他普通用户不可调用(比如删了或者修改权限),则 Linux 的指令和用户生成的进程多半不可用;若是删了则后果更严重,此时操作系统将彻底失控,除了重装系统别无办法,因为文件拷贝也要这个库支持。即使要观察清理这些库做某些实验,也只推荐在完全隔离的虚拟机或废弃的测试机中进行。

在 Linux 中,C 语言的头文件大都在 /usr/include,而这些动态链接则大都保存在 /usr/lib64/usr/lib

几乎所有的操作系统,若用户没有刻意修改内置的库的话,绝大多数的库文件都是动态库,静态库比动态库少。

file指令查看可执行程序

gcc 在编译时默认使用动态库。gcc 在完成了链接之后,就可以生成可执行程序文件,这个默认生成的文件是二进制程序,是动态链接了动态库生成的,这点可以通过 file 命令验证。

bash 复制代码
[Bjarne@VM-8-8-centos cppTest]$ ls
a.c  makefile  t.c  t.h
[Bjarne@VM-8-8-centos cppTest]$ gcc *.c -o a.exe
[Bjarne@VM-8-8-centos cppTest]$ file a.exe
a.exe: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=ebb006d6659450ddb07be19c61ac37f8a8c69919, not stripped
[Bjarne@VM-8-8-centos cppTest]$ 

对这些信息的解读:

bash 复制代码
a.exe: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=ebb006d6659450ddb07be19c61ac37f8a8c69919, not stripped

ELF 64-bit

  • ELF:可执行与可链接格式,是 Unix/Linux 系统标准的可执行文件格式。
  • 64-bit:这是一个 64 位程序,针对 x86-64 架构编译。

LSB executable

  • LSB:低位优先,表示数据在内存中按小端字节序存储。
  • executable:确认这是一个可执行文件(而非库文件或目标文件)。

x86-64

  • 目标CPU架构:AMD64/Intel 64 处理器架构。

version 1 (SYSV)

  • version 1 :ELF格式版本为1。
  • SYSV:遵循 System V ABI(应用二进制接口),这是 Linux 的标准 ABI 。

dynamically linked (uses shared libs)

  • 动态链接程序在运行时依赖外部的共享库 (如 glibc )。gcc 和 g++ 在默认情况下生成的可执行程序都是动态链接。 程序文件本身不包含这些库的代码,需要在运行时从系统中加载。

for GNU/Linux 2.6.32

  • 目标平台:GNU/Linux 操作系统。内核版本兼容性:最低需要 Linux 2.6.32 内核。

BuildID[sha1]=ebb006d6659450ddb07be19c61ac37f8a8c69919

  • 构建ID:用于唯一标识此特定构建的SHA1哈希值。用途:调试符号匹配、包管理识别。

not stripped

  • 未剥离:可执行文件中包含调试符号信息。这意味着文件体积较大,但便于调试(可以使用 gdb 等工具)。发布版本通常会使用 strip 命令移除这些符号。

动、静态库是 C/C++ 的说法,其他编程语言也有相应的类似物。例如 Java 的 .jar 文件为静态库类似物,动态库类似物则为 JVM 类加载机制。但 Python、JavaScript、Node.js 等的所有模块都是动态的,需要在运行过程中去链接,因此不存在静态无类似物。

ldd指令查看可执行程序

ldd 可执行程序 可查看当前的可执行程序调用的库的位置 。但只能查看动态库静态库因为在编译阶段就已成为可执行程序的一部分所以无法查看

bash 复制代码
[Bjarne@VM-8-8-centos cppTest]$ ls
a.c  a.exe  a.i  a.o  a.s
[Bjarne@VM-8-8-centos cppTest]$ ldd a.exe
	linux-vdso.so.1 =>  (0x00007ffce5ffd000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f48e864c000) # 第三方库
	/lib64/ld-linux-x86-64.so.2 (0x00007f48e8a1a000)
[Bjarne@VM-8-8-centos cppTest]$ 

gcc 在使用的时候默认它就是专门处理 C 语言的,所以 C 标准库不用指明要链接哪个库,在什么路径下,gcc 自身就会找到,并且会去链接,所以在使用指令时可以不用显式指明部分选项。

既然有第三方库,就应该有第二方库、第一方库。但很可惜并不存在第一、第二方库的概念,有人给第一、二方库赋予自己的理解:

  • 第一方库是程序员自己写的库。
  • 第二方库是 C 编译器提供的遵循标准制作的 C 标准库。
  • 第三方库是其他程序员写的库。

也有额外的理解:

  • 第一方库是 C 编译器提供的遵循标准制作的 C 标准库。
  • 第二方库是随操作系统或开发套件(SDK)提供,是平台生态的一部分。例如 Linux 的 glibc 、Windows API 等。
  • 第三方库是包括自己在内的其他程序员写的库。

很明显后者的理解更符合实际情况,因为个人也可以按照标准自己设计 C 标准库,这就使得第一方库、第二方库的界限变得模糊。

libc.so.6 => /lib64/libc.so.6 (0x00007f48e864c000) 就是 printf 背后的库。所以程序员写的 C 语言、C++ 程序可以理解为:

自己的代码 + + + 头文件 + + + = = = 可执行程序

开发环境的安装 ,就是将这些库配置到本地的计算机中。

bash 复制代码
[Bjarne@VM-8-8-centos ~]$ ls /lib64/libc.so.6 -l
lrwxrwxrwx 1 root root 12 Jul  8  2024 /lib64/libc.so.6 -> libc-2.17.so # 第1位是l,表示这个是链接文件
[Bjarne@VM-8-8-centos ~]$ ls /lib64/*.a
/lib64/libbsd.a 
# 省略其他 lib文件名.a 格式的静态库
[Bjarne@VM-8-8-centos ~]$ 

C 语言的编译过程中只有链接和第 3 方库有关。

举个例子就能理解程序和动、静态库的关系:假设一高中生,上网的时候被学长推荐去网吧,用一点钱租借学长用过的机器几个小时。类比的话高中就是内存 ,自己就是软件 ,学长是编译器中的模块或连接器 ,自己找学长打听网吧信息的过程叫链接 ,网吧就是动态库 ,网吧中的机器就是要调用的函数 ,在网吧计划要玩的游戏叫要执行的代码 。有一天网吧被帽子叔叔给端了,这个高中生就把电脑带到宿舍去玩,这时这个被带进学校的电脑就是静态库

Linux下动静态库的生成和使用

实操环节,记录了怎么做的具体过程。

生成静态库

静态库在程序编译链接时,会把静态库自身的代码链接到可执行文件中,程序运行的时候将不再需要静态库。

根据 gcc系编译器、调试器的使用和c/c++编译原理-CSDN博客 ,代码经过汇编后会生成内容为二进制的准可执行程序(.o),这个准可执行程序在进行最后的链接生成可执行程序后,也不再需要准可执行程序。所以静态库可以由各种 .o 文件打包而成

根据 c语言的函数和模块化编程-CSDN博客制作静态库时不能在代码中带主函数 main() 。这很好理解,因为制作库就是给别人用的 ,而不是单独作为一个程序入口 ,带 main 函数的话,别人导入这个库在自己的程序中,就有 2 个 main 函数,会造成链接错误。

这里将演示如何在 Linux 环境下生成静态库,使用较为简单的四则运算器进行模拟实现。

加法模块

文件 add.h:

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

文件 add.c:

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

减法模块

文件 sub.h:

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

文件 sub.c:

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

这里可以继续增加乘法模块、除法模块,以及根据自己的兴趣添加基本初等函数的泰勒公式形式等。因为目的是模拟创建静态库,所以只做加法模块和减法模块

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ cat *.h
#pragma once 
int add(int a, int b);

#pragma once 
int sub(int a, int b);

[Bjarne@VM-8-8-centos cTest]$ cat *.c
#include "add.h"
int add(int a, int b) {
	return a + b;
}
#include "sub.h"
int sub(int a, int b) {
	return a - b;
}
[Bjarne@VM-8-8-centos cTest]$ 

使用 gcc -c file.c 可将文件生成同名的 .o 文件。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ gcc -c add.c
[Bjarne@VM-8-8-centos cTest]$ gcc -c sub.c
[Bjarne@VM-8-8-centos cTest]$ ls | grep .o
add.o
sub.o
[Bjarne@VM-8-8-centos cTest]$ 

之后使用 ar 指令工具对所有的 .o 进行打包,就形成了静态库。

ar 是 GNU 归档工具,选项:

  • -rc :表示(replace and create)。
  • -t :列出静态库中的文件。
  • v :verbose 详细信息。

注意命名格式,在 Linux 中需要遵守 lib 开头和 .a 结尾的格式,所以这里使用静态库名 libmymath.a 。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ ar -rc libmymath.a *.o # 打包所有.o,可指定具体的.o文件进行打包
[Bjarne@VM-8-8-centos cTest]$ ls -l libmymath.a
-rw-rw-r-- 1 Bjarne Bjarne 2688 Dec 20 23:25 libmymath.a
[Bjarne@VM-8-8-centos cTest]$ ar -tv libmymath.a # 查看静态库组成
rw-rw-r-- 1001/1001   1240 Dec 20 23:19 2025 add.o
rw-rw-r-- 1001/1001   1240 Dec 20 23:19 2025 sub.o
[Bjarne@VM-8-8-centos cTest]$ 

如此便完成了静态库的生成。

使用静态库

直接将本地头文件在带 main 函数的 .c 文件中展开即可。

这里测试程序 a.c(带 main 函数):

c 复制代码
#include<stdio.h>
#include"add.h"
#include"sub.h"
int main(){
    printf("3+4=%d\n3-4=%d\n",add(3,4),sub(3,4));
    return 0;
}

不使用静态库示例

若是自己使用时的情况,不使用静态库 的话,就需要将所有 .c 文件加入编译形成统一的可执行程序。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ ls # 这里进行了格式调整 
a.c  makefile # 带main函数的a.c和配套的makefile文件
add.c  add.h  add.o # 加法库
sub.c  sub.h  sub.o # 减法库
libmymath.a # 使用ar打包所有.o生成的静态库文件
[Bjarne@VM-8-8-centos cTest]$ gcc a.c -o a.exe # 需要将所有.c或静态库加入编译,否则会报错
/tmp/cciWN3Kt.o: In function `main':
a.c:(.text+0x14): undefined reference to `sub' # 这里描述说未定义
a.c:(.text+0x25): undefined reference to `add'
collect2: error: ld returned 1 exit status
[Bjarne@VM-8-8-centos cTest]$ gcc a.c add.c sub.c -o a.exe # 所有.c加入编译
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
3+4=7
3-4=-1

这些在 gcc系编译器、调试器的使用和c/c++编译原理-CSDN博客 中都有介绍。

使用静态库示例

若是别人使用,需要在使用 gcc 编译时添加 -L-l 选项指定 libmymath.a 的位置。因为 libmymath.a 属于第三方库 ,gcc 即编译器默认不会识别

  • -L:指定库路径。
  • -l:指定库名。在 Linux, 指定静态库名时需要去掉前缀 lib 和后缀 .a。

因为静态库 libmymath.a 和 2 个头文件 .h ,它们和 a.c 都在同一目录下,所以添加选项

-L.-lmymath 指定库名。测试目标文件生成后,静态库删掉,程序照样可以运行。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ gcc a.c -o a2.exe -L. -lmymath
[Bjarne@VM-8-8-centos cTest]$ ./a2.exe # 这里头文件、静态库和a.c在同一路径
3+4=7
3-4=-1
[Bjarne@VM-8-8-centos cTest]$ ls
a2.exe  a.c  add.c  add.h  add.o  libmymath.a  makefile  sub.c  sub.h  sub.o
[Bjarne@VM-8-8-centos cTest]$ ldd a2.exe # ldd指令无法查看静态库的使用
	linux-vdso.so.1 =>  (0x00007ffeed5f1000)
	libc.so.6 => /lib64/libc.so.6 (0x00007fdf4aeb8000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fdf4b286000)
[Bjarne@VM-8-8-centos cTest]$ 

在测试完成之后,建议将头文件全部放在 include 目录,将静态库放在 lib 目录,按这样的格式进行打包。之后将打包好的库压缩后,别人解压到自己的目录即可使用。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ tree mymath_lib
mymath_lib
|-- include
|   |-- add.h
|   `-- sub.h
`-- lib
    `-- libmymath.a

2 directories, 3 files
[Bjarne@VM-8-8-centos cTest]$ 

其他人如何使用静态库工具包是自己的选择,他可以将静态库工具包拷贝到系统的目录 下,像使用库文件一样使用 <> 展开后直接使用;也可以拷贝到任意目录 ,然后使用 gcc 编译时添加选项 -I ,让编译器在编译时也搜索这个目录。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ ls
a.c  makefile  mymath_lib # mymath库已经完成打包
[Bjarne@VM-8-8-centos cTest]$ gcc a.c -o a.exe -I mymath_lib/include -lmymath -L mymath_lib/lib
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
3+4=7
3-4=-1
[Bjarne@VM-8-8-centos cTest]$ 

指令组成

gcc a.c -o a.exe ⏟ gcc指令的正常使用 -I mymath_lib/include ⏟ 新增头文件索索路径 -lmymath ⏟ 指明链接的库的名称 -L mymath_lib/lib ⏟ 新增库文件的搜索路径 \underbrace{\text{gcc a.c -o a.exe}}{\text{gcc}指令的正常使用}\ \ \underbrace{\text{-I mymath\lib/include}}{新增头文件索索路径}\ \ \underbrace{ \text{-lmymath}}{指明链接的库的名称}\ \ \underbrace{\text{-L mymath\lib/lib}}{新增库文件的搜索路径} gcc指令的正常使用 gcc a.c -o a.exe 新增头文件索索路径 -I mymath_lib/include 指明链接的库的名称 -lmymath 新增库文件的搜索路径 -L mymath_lib/lib

静态库拷贝到/usr

gcc 在使用的时候默认处理 C 语言,所以 C 标准库不用指明要链接哪个库,在什么路径下,gcc 自身就会找到,所以不用显式指明部分选项。例如头文件 stdio.h,直接使用 <> 展开即可,也不需要在编译阶段使用 -l-L 指定 stdio.h 绑定的库文件所在。

进程的库搜索路径:

  • 从左到右搜索 -L 指定的目录。
  • 由环境变量指定的目录(LIBRARY_PATH)。
  • 由系统指定的目录。
    • /usr/lib
    • /usr/lib64
    • /usr/local/lib

例如,这里将自己创造的 mymath 库的头文件拷贝到系统目录 /usr/include(需要 root 权限):

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ ls
a.c  makefile  mymath_lib
[Bjarne@VM-8-8-centos cTest]$ sudo cp mymath_lib/include/*.h /usr/include
[sudo] password for Bjarne:# 使用sudo指令后,输入自己的密码即可 
[Bjarne@VM-8-8-centos cTest]$ sudo cp mymath_lib/lib/libmymath.a /usr/lib64  # 可做可不做,因为静态库依旧需要指定位置
[Bjarne@VM-8-8-centos cTest]$ ls /usr/include/add.h
/usr/include/add.h
[Bjarne@VM-8-8-centos cTest]$ ls /usr/include/sub.h
/usr/include/sub.h
[Bjarne@VM-8-8-centos cTest]$ ls /usr/lib64/libmymath.a
/usr/lib64/libmymath.a
[Bjarne@VM-8-8-centos cTest]$ 

然后再次修改代码:

c 复制代码
#include<stdio.h>
//这里将自己的add.h和sub.h像stdio.h一样使用
#include<add.h>
#include<sub.h>
int main(){
    printf("3+4=%d\n3-4=%d\n",add(3,4),sub(3,4));
    return 0;
}

因为静态库是作为可执行程序的一部分加入编译的,所以 gcc 编译器无法找到静态库的位置,依旧要指定静态库位置,才能正常编译。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ gcc a.c -o a.exe
/tmp/cc9aqic8.o: In function `main':
a.c:(.text+0x14): undefined reference to `sub'
a.c:(.text+0x25): undefined reference to `add'
collect2: error: ld returned 1 exit status
[Bjarne@VM-8-8-centos cTest]$ gcc a.c -o a.exe -lmymath -L/usr/lib64
[Bjarne@VM-8-8-centos cTest]$ ./a.exe # 可以不用指定头文件位置了
3+4=7
3-4=-1
[Bjarne@VM-8-8-centos cTest]$ 

这里因为将 add.h 和 sub.h 移动到 /usr/include 中,所以使用 #include<> 可以将两个头文件的内容正常展开,不需要指定头文件的搜索路径。

生成动态库

动态库在形成时和静态库是一样的**,**它也是要把所有的不含 main 函数的目标 .c 源文件编译生成 .o 文件 ,然后把所有的点 .o 文件进行打包就能形成动态库。这里测试生成动态库。

依旧沿用生成静态库时的代码。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ ls
a.c  add.c  add.h  makefile  sub.c  sub.h
[Bjarne@VM-8-8-centos cTest]$ cat *.h
#pragma once 
int add(int a, int b);

#pragma once 
int sub(int a, int b);

[Bjarne@VM-8-8-centos cTest]$ cat *.c
#include<stdio.h>
#include"add.h"
#include"sub.h"
int main(){
    printf("3+4=%d\n3-4=%d\n",add(3,4),sub(3,4));
    return 0;
}
#include "add.h"
int add(int a, int b) {
	return a + b;
}
#include "sub.h"
int sub(int a, int b) {
	return a - b;
}
[Bjarne@VM-8-8-centos cTest]$ 

和静态库不同的是,静态库使用专门的工具 ar 进行打包,动态库使用 gcc 编译器就能打包 ,但需要使用一些选项

  • -shared:表示生成共享库格式
  • -fPIC:产生位置无关码(position independent code)。在生成 .o 文件时使用。

先用 -fPIC 将目标 .c 文件生成 .o 文件,再用 -shared 生成动态库。生成动态库同样需要遵守命名规则。

交互界面:

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ ls
a.c  add.c  add.h  makefile  sub.c  sub.h
[Bjarne@VM-8-8-centos cTest]$ gcc -fPIC -c sub.c
[Bjarne@VM-8-8-centos cTest]$ gcc -fPIC -c add.c
# 也可以直接指定所有.c:gcc -fPIC -c sub.c add.c
[Bjarne@VM-8-8-centos cTest]$ ls | grep .o
add.o
sub.o
[Bjarne@VM-8-8-centos cTest]$ gcc *.o -shared -o libmymath.so
[Bjarne@VM-8-8-centos cTest]$ ls libmymath.so -l
-rwxrwxr-x 1 Bjarne Bjarne 7936 Dec 21 01:19 libmymath.so
[Bjarne@VM-8-8-centos cTest]$ 

如此,便生成了动态库 libmymath.so。再按照之前的格式对动态库进行打包。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ ls
a.c  add.c  add.h  add.o  libmymath.so  makefile  sub.c  sub.h  sub.o
[Bjarne@VM-8-8-centos cTest]$ mkdir mymath_lib # 创建目录
[Bjarne@VM-8-8-centos cTest]$ mkdir mymath_lib/include
[Bjarne@VM-8-8-centos cTest]$ mkdir mymath_lib/lib
[Bjarne@VM-8-8-centos cTest]$ mv *.h mymath_lib/include # 搬运
[Bjarne@VM-8-8-centos cTest]$ mv libmymath.so mymath_lib/lib
[Bjarne@VM-8-8-centos cTest]$ rm add.* sub.* # 清理多余的文件
[Bjarne@VM-8-8-centos cTest]$ ls
a.c  makefile  mymath_lib
[Bjarne@VM-8-8-centos cTest]$ tree mymath_lib
mymath_lib
|-- include
|   |-- add.h
|   `-- sub.h
`-- lib
    `-- libmymath.so

2 directories, 3 files
[Bjarne@VM-8-8-centos cTest]$ 

位置无关码fPIC

使用 gcc/g++ 形成的可执行程序,在 Linux 下格式为 ELF 格式,在这个格式下代码形成的汇编指令会按照 Linux 可理解的方式进行安排,使 Linux 可根据这个可执行程序创建进程执行任务。具体表现为可执行程序的内容中包含函数、变量等资源的地址和偏移量。

若将这个可执行程序移植到其他操作系统例如 Windows,则会发现其他操作系统很可能完全解读不了或错误解读这个可执行程序。所以在其他操作系统运行同一个功能的 C/C++ 程序 ,很有可能还需要重新在本地进行编译

对于这种代码移植问题(代码放到其他设备运行), Java 和 Python 等推出统一的虚拟机和工具包,只要设备安装了这些,一个代码在哪里都能通行,但代价是虚拟机和工具包往往非常大,一些嵌入式设备装不下。

在动态库中会有一张表将所有函数的地址全部保存。可执行程序运行时会找到动态库,将库文件中的代码段和数据段映射到进程的虚拟地址空间,再将函数和变量的具体地址 "填写" 到进程中的特定表格里。所以可执行程序里没有动态库的函数实现只有地址和偏移量程序要真正运行还需要把所有依赖的库的内容添加到进程中

这个过程中会产生一个现象:函数名等经过编译之后变成地址加偏移量,地址本质就是一个表示相对于计算机内存的位置的数字。可执行程序即使没有运行内部也有很多被安排好的地址,这些地址基本遵守虚拟地址空间的排布规则,这样将可执行程序从磁盘上拷贝到内存中时才能进行指令和数据上的映射。

这其中有一个问题,可执行程序在生成进程时,进程内容中的各个机器指令集会被操作系统映射在内存中的什么地方?

假设内存被分割成若干个小块,每个小块用 1 个十六进制数作为编号来描述,这样从 0 到若干个 F 组成的编号就代表了整个内存。有的指令集会直接指定目标在整个内存空间中的绝对地址 ,这个编址方式叫绝对编址 ,而有的指令集则会指定目标相对于当前指令位置的偏移量 ,这个编址方式叫相对编址

若是相对编址 ,则进程的位置本身就不固定 ,而进程还需要加载在磁盘中的固定位置的动态库 ,这就要求进程无论被安排在什么位置它链接的动态库的代码都能添加到进程中 ,使得动态库的函数能被找到。这就需要用到位置无关码 这种代码生成方式 。所以动态库采用 的是相对编址 的方式,所以可以做到动态库中的地址无关性。

访问动态库的资源是通过地址加偏移量来实现,而动态库中几乎只有偏移量,这使得动态库无论添加到哪个进程,都会将地址固定下来,然后通过偏移量就能找到函数、变量等资源的位置并进行访问。

所以位置无关码就是使 .o 文件几乎只包含偏移量,且这个偏移量在动态库形成后将不会发生改变。这样在 .o 文件组后续打包成动态库时,动态库才能添加到任何需要使用该库的进程中。

使用动态库

在交互界面使用动态库编译和使用静态库编译无任何区别。区别在于执行可执行程序。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ ls
a.c  makefile  mymath_lib
[Bjarne@VM-8-8-centos cTest]$ gcc a.c -o a.exe -I mymath_lib/include -lmymath -L mymath_lib/lib # 到这里和静态库无区别
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
./a.exe: error while loading shared libraries: libmymath.so: cannot open shared object file: No such file or directory
[Bjarne@VM-8-8-centos cTest]$ ldd a.exe
	linux-vdso.so.1 =>  (0x00007fffd5b28000)
	libmymath.so => not found # ldd找不到动态库mymath的位置
	libc.so.6 => /lib64/libc.so.6 (0x00007f0265f60000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f026632e000)
[Bjarne@VM-8-8-centos cTest]$ 

动态库只有在程序运行的时候才去链接动态库的代码,多个程序共享使用库的代码。

一个与动态库链接的可执行程序文件,它只包含它用到的函数入口地址组成的一个表,而不是外部函数所在目标文件的整个机器码。

在可执行文件开始运行之前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接 (dynamic linking),或者说加载动态库。静态库没有加载的说法,因为在编译阶段,静态库的内容就成为了可执行程序的一部分,不存在加载的过程。

正因为如此,这里无法直接运行 a.exe,使用 ldd 也查不到 mymath 的位置。

动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。

操作系统采用虚拟内存机制,允许物理内存中的一份动态库被要用到该库的所有进程通过写时拷贝的方式共用。所以动态库在内存中开始时只会打开一份,每个进程都会共用这个动态库,当某个进程需要修改动态库中的公共资源例如全局变量时,会触发写时拷贝的机制为有修改需求的进程拷贝一份独立的动态库。通过这种方式加载动态库,节省了内存和磁盘空间

想要使用动态库,必须同时找到可执行程序和程序所需要的动态库。在编译阶段只是告诉 gcc 动态库在哪里,但通过可执行程序创建进程和编译器知道动态库在哪没有任何关系运行程序时还是要指定动态库在哪

常见的方法:

  1. 拷贝 .so 文件本身或软链接到系统共享库路径下, 一般指 /usr/lib/usr/lib64。相当于安装到系统里。
  2. 更改环境变量 LD_LIBRARY_PATH。
  3. 添加或修改目录 ld.so.conf.d 中的配置文件,再使用 ldconfig更新。

动态库安装到系统

首先是拷贝 .so 文件本身或软链接到系统共享库路径下, 一般指 /usr/lib/usr/lib64

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ ls
a.c  makefile  mymath_lib
[Bjarne@VM-8-8-centos cTest]$ sudo cp mymath_lib/lib/libmymath.so /usr/lib64 # 使用root权限拷贝动态库到共享库路径
[Bjarne@VM-8-8-centos cTest]$ ls /usr/lib64/libmymath.so -l
-rwxr-xr-x 1 root root 7936 Dec 21 01:51 /usr/lib64/libmymath.so

在将动态库拷贝到共享库后,使用 gcc 编译时告诉 gcc 使用哪一个库就可以。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ gcc a.c -o a.exe -I mymath_lib/include -lmymath # 这里就不用-L指定动态库的位置了,gcc会在共享库寻找,但要指定库名
[Bjarne@VM-8-8-centos cTest]$ ldd a.exe
	linux-vdso.so.1 =>  (0x00007ffd48456000) # 刚拷贝的mymath,新鲜的
	libmymath.so => /lib64/libmymath.so (0x00007ff6838b5000)
	libc.so.6 => /lib64/libc.so.6 (0x00007ff6834e7000)
	/lib64/ld-linux-x86-64.so.2 (0x00007ff683ab7000)
[Bjarne@VM-8-8-centos cTest]$ ./a.exe # 正常运行
3+4=7
3-4=-1
[Bjarne@VM-8-8-centos cTest]$ sudo rm /usr/lib64/libmymath.so
[Bjarne@VM-8-8-centos cTest]$ ldd a.exe
	linux-vdso.so.1 =>  (0x00007ffd1e2d3000)
	libmymath.so => not found # 删了动态库后又找不到了,a.exe又无法运行
	libc.so.6 => /lib64/libc.so.6 (0x00007fb415a68000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fb415e36000)
[Bjarne@VM-8-8-centos cTest]$ 

这个方法适用于别人制作的成熟的动态库。

在本地创建动态库软链接

静态库不会在当前路径找,但如果动态库在当前路径下,程序是可以直接找到的。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ cp mymath_lib/lib/libmymath.so .
[Bjarne@VM-8-8-centos cTest]$ ls # 直接将静态库拷贝到当前目录
a.c  libmymath.so  makefile  mymath_lib
[Bjarne@VM-8-8-centos cTest]$ gcc a.c -o a.exe -I mymath_lib/include -lmymath -L.# 指定程序在当前目录找
[Bjarne@VM-8-8-centos cTest]$ ldd a.exe
	linux-vdso.so.1 =>  (0x00007ffcd4910000)
	libmymath.so (0x00007fb4d5695000)
	libc.so.6 => /lib64/libc.so.6 (0x00007fb4d52c7000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fb4d5897000)
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
3+4=7
3-4=-1
[Bjarne@VM-8-8-centos cTest]$ 

不是所有时候都适合直接拷贝动态库到当前目录,所以可以通过建立软链接的方式来代替。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ ls
a.c  makefile  mymath_lib
[Bjarne@VM-8-8-centos cTest]$ ln -s mymath_lib/lib/libmymath.so libmymath.so # 创建动态库软链接
[Bjarne@VM-8-8-centos cTest]$ ls
a.c  libmymath.so  makefile  mymath_lib
[Bjarne@VM-8-8-centos cTest]$ gcc a.c -o a.exe -I mymath_lib/include -lmymath -L.
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
3+4=7
3-4=-1
[Bjarne@VM-8-8-centos cTest]$ ls -l libmymath.so
lrwxrwxrwx 1 Bjarne Bjarne 27 Dec 21 02:20 libmymath.so -> mymath_lib/lib/libmymath.so
[Bjarne@VM-8-8-centos cTest]$ 

也可以将动态库的软链接拷贝到共享库目录,但需要注意名称不与原有的库冲突。

修改环境变量LD_LIBRARY_PATH

环境变量 LD_LIBRARY_PATH 叫做加载库路径。操作系统在加载运行可执行程序时,除了在系统的默认路径下去加载可执行程序外,它还会去该环境变量里搜索各种各样的动、静态库。这个环境变量默认情况下不存在,需要额外创建。

这里假设这个环境变量创建过,直接在原来的 LD_LIBRARY_PATH 的基础上添加地址。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ gcc a.c -o a.exe -I mymath_lib/include -lmymath -Lmymath_lib/lib
[Bjarne@VM-8-8-centos cTest]$ ldd a.exe
	linux-vdso.so.1 =>  (0x00007ffe68bdf000)
	libmymath.so => not found # 刚编译完的可执行程序找不到动态库所在
	libc.so.6 => /lib64/libc.so.6 (0x00007f6776d55000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f6777123000)
[Bjarne@VM-8-8-centos cTest]$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD:mymath_lib/lib # 修改环境变量
[Bjarne@VM-8-8-centos cTest]$ ldd a.exe
	linux-vdso.so.1 =>  (0x00007ffd904b5000) # 修改环境变量后可找到
	libmymath.so => mymath_lib/lib/libmymath.so (0x00007f3273e6e000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f3273aa0000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f3274070000)
[Bjarne@VM-8-8-centos cTest]$ ./a.exe
3+4=7
3-4=-1
[Bjarne@VM-8-8-centos cTest]$ 

LD_LIBRARY_PATH 在使用过程中是有风险的:

  1. 部分恶意软件 可以利用 LD_LIBRARY_PATH 来劫持库加载 。例如,在 ~/lib 即家目录的 lib 共享库中放置一个恶意的 libc.so 或电脑病毒,简单来说可以修改 LD_LIBRARY_PATH 使程序不去执行原有的动态库,而是去执行和原有动态库同名但不同 inode 编号的病毒。
  2. LD_LIBRARY_PATH 会影响当前 shell 及所有子进程中启动的程序,可能导致其他不相关的程序行为异常或崩溃。
  3. 加载器需要遍历该变量中的所有路径,设置过长的路径会轻微拖慢每个程序的启动速度。

因此,在许多生产环境和高安全要求的系统中,明确禁止使用 LD_LIBRARY_PATH。最多在测试时方便一下。

更改系统配置文件

使可执行程序找到动态库还有第 4 种方法:添加或修改目录 ld.so.conf.d 中的配置文件。

从目录名来看,ld 就表示的相当于 load 的意思,ld.so 就是动态库的意思,conf 就是配置文件,d 就是目录。这个目录里所有的以 .conf 结尾的文件,都是系统的配置文件。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ ls -l /etc/ld.so.conf.d
total 16 # 查看这个目录下的文件
-rw-r--r-- 1 root root 26 Jun 11  2024 bind-export-x86_64.conf
-rw-r--r-- 1 root root 19 Aug  9  2019 dyninst-x86_64.conf
-r--r--r-- 1 root root 63 Jun  4  2024 kernel-3.10.0-1160.119.1.el7.x86_64.conf
-rw-r--r-- 1 root root 17 Oct  2  2020 mariadb-x86_64.conf
[Bjarne@VM-8-8-centos cTest]$ ls -l ../cTest
total 24 # 查看当前目录下的文件
-rw-rw-r-- 1 Bjarne Bjarne  128 Dec 22 23:47 a.c
-rwxrwxr-x 1 Bjarne Bjarne 8424 Dec 22 23:51 a.exe
-rw-rw-r-- 1 Bjarne Bjarne  269 Dec 12 00:58 makefile
drwxrwxr-x 4 Bjarne Bjarne 4096 Dec 21 01:25 mymath_lib
[Bjarne@VM-8-8-centos cTest]$ 

在 ld.so.conf.d 中添加自己的 .conf 文件,将路径拷贝进去,然后输入指令 ldconfig 加载配置,在系统层面做一下刷新即可。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ ldd a.exe
	linux-vdso.so.1 =>  (0x00007ffe90ab2000)
	libmymath.so => not found # 同样是开始时并没有找到
	libc.so.6 => /lib64/libc.so.6 (0x00007f7fdd6bf000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f7fdda8d000)
[Bjarne@VM-8-8-centos cTest]$ ls
a.c  a.exe  makefile  mymath_lib
[Bjarne@VM-8-8-centos cTest]$ echo $PWD/mymath_lib/lib>mymath.conf
[Bjarne@VM-8-8-centos cTest]$ cat mymath.conf # 生成.conf文件
/home/Bjarne/work/cTest/mymath_lib/lib
[Bjarne@VM-8-8-centos cTest]$ sudo mv mymath.conf /etc/ld.so.conf.d
[Bjarne@VM-8-8-centos cTest]$ ldd a.exe # 拷贝到ld.so.conf.d
	linux-vdso.so.1 =>  (0x00007ffec21d2000)
	libmymath.so => not found # 还是找不到,因为没做刷新
	libc.so.6 => /lib64/libc.so.6 (0x00007f98bb563000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f98bb931000)
[Bjarne@VM-8-8-centos cTest]$ sudo ldconfig # 刷新需要root权限
[Bjarne@VM-8-8-centos cTest]$ ldd a.exe
	linux-vdso.so.1 =>  (0x00007ffc117da000)
	libmymath.so =>  /home/Bjarne/work/cTest/mymath_lib/lib/libmymath.so (0x00007f0fb7e4c000) # 刷新后就能找到了
	libc.so.6 => /lib64/libc.so.6 (0x00007f0fb7a7e000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f0fb804e000)
[Bjarne@VM-8-8-centos cTest]$ 

.conf 配置文件移除短时间内不会影响 ,但若是再次使用 ldconfig 进行刷新则又会找不到

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ sudo rm /etc/ld.so.conf.d/mymath.conf
[Bjarne@VM-8-8-centos cTest]$ ldd a.exe
	linux-vdso.so.1 =>  (0x00007ffde299c000)
	libmymath.so => /home/Bjarne/work/cTest/mymath_lib/lib/libmymath.so (0x00007f4dd2d23000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f4dd2955000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f4dd2f25000)
[Bjarne@VM-8-8-centos cTest]$ sudo ldconfig # 刷新后开始出现异常
[Bjarne@VM-8-8-centos cTest]$ ldd a.exe
	linux-vdso.so.1 =>  (0x00007ffe49f4c000)
	libmymath.so => not found # 找不到mymath.conf文件
	libc.so.6 => /lib64/libc.so.6 (0x00007f40cfd81000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f40d014f000)
[Bjarne@VM-8-8-centos cTest]$ 

目录同时存在动、静态库

假设其他人给自己的库里面,动态库和静态库都放在同一目录下,这个目录既包含动态库**,又包含静态库。这种情况是库的 开发者提供给用户的选择策略**,使用静态库是为了能独立部署性能机制优化或环境可控使用动态库则是为了节省磁盘或内存资源方便库升级或作为系统共享组件

若同时提供同一组方法的动静态库,使用 gcc 命令编译时,默认使用动态库,-l库名 默认选择动态库。

具体看环境,云服务器环境下测试是动态库,其他嵌入式开发环境或交叉编译可能会默认选择静态库。在 gcc系编译器、调试器的使用和c/c++编译原理-CSDN博客 中提到,gcc 有一个 -static 的选项,可以使生成可执行程序时只用静态链接。

但很多系统的库例如 libc.so 是动态链接,且没有安装同名的静态库,使用 -static 时会编译不通过,这时可以使用系统自带的工具 yum ,通过指令 sudo yum install glibc-static 进行安装(CentOS,Ubuntu 可用 sudo apt install glibc-static)。

这里演示使用 -static 的效果。使用的操作系统已经安装过 glibc-static,并提前准备好了 mymath 库的静态库版本和动态库版本放在本地目录。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ ls # 进行了格式调整
add.h sub.h 
libmymath.a  libmymath.so  # .a是静态库,.o是动态库
a.c  makefile  
[Bjarne@VM-8-8-centos cTest]$ gcc a.c -o a.exe -lmymath -L. -static
[Bjarne@VM-8-8-centos cTest]$ gcc a.c -o a2.exe -lmymath -L.
[Bjarne@VM-8-8-centos cTest]$ ls -al|grep .exe
-rwxrwxr-x 1 Bjarne Bjarne   8424 Dec 23 01:04 a2.exe
-rwxrwxr-x 1 Bjarne Bjarne 861328 Dec 23 01:03 a.exe # 可以看到大小区别
[Bjarne@VM-8-8-centos cTest]$ ldd a.exe
	not a dynamic executable # a.exe采用静态编译,所以不存在动态链接
[Bjarne@VM-8-8-centos cTest]$ ldd a2.exe
	linux-vdso.so.1 =>  (0x00007fffd7b26000)
	libmymath.so (0x00007fd15b232000)
	libc.so.6 => /lib64/libc.so.6 (0x00007fd15ae64000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fd15b434000)
[Bjarne@VM-8-8-centos cTest]$ mv libmymath.a .. # 证明a2.exe用动态库用
[Bjarne@VM-8-8-centos cTest]$ gcc a.c -o a3.exe -lmymath -L.
[Bjarne@VM-8-8-centos cTest]$ ls -al|grep a3.exe
-rwxrwxr-x 1 Bjarne Bjarne   8424 Dec 23 01:09 a3.exe 
# a3.exe大小和a2.exe一致,说明它们用的都是动态库进行编译
[Bjarne@VM-8-8-centos cTest]$ 

gcc 的 -l 选项的用途是指定库名,可通过 -l:file 来指定编译时用哪个库文件,若使用 -l库名 ,当动、静态库同时存在且库名相同时,默认选择动态库。

bash 复制代码
[Bjarne@VM-8-8-centos cTest]$ ls # 动静态库同时存在于本地
a.c  add.h  libmymath.a  libmymath.so  makefile  sub.h
[Bjarne@VM-8-8-centos cTest]$ gcc a.c -o a.exe -l:libmymath.a -L.
[Bjarne@VM-8-8-centos cTest]$ gcc a.c -o a2.exe -lmymath -L.
[Bjarne@VM-8-8-centos cTest]$ ldd a.exe # a.exe没有链接mymath库
	linux-vdso.so.1 =>  (0x00007fff97bd7000)
	libc.so.6 => /lib64/libc.so.6 (0x00007efdcde15000)
	/lib64/ld-linux-x86-64.so.2 (0x00007efdce1e3000)
[Bjarne@VM-8-8-centos cTest]$ ldd a2.exe
	linux-vdso.so.1 =>  (0x00007ffe2d517000)
	libmymath.so (0x00007f57e852c000) # a2.exe链接了mymath 库
	libc.so.6 => /lib64/libc.so.6 (0x00007f57e815e000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f57e872e000)
[Bjarne@VM-8-8-centos cTest]$ gcc a.c -o a3.exe -l:libmymath.so -L.
[Bjarne@VM-8-8-centos cTest]$ ldd a3.exe
	linux-vdso.so.1 =>  (0x00007ffec77fb000)
	libmymath.so (0x00007f6254065000) # a3.exe链接了mymath 库
	libc.so.6 => /lib64/libc.so.6 (0x00007f6253c97000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f6254267000)
[Bjarne@VM-8-8-centos cTest]$ ls -al|grep .exe
-rwxrwxr-x 1 Bjarne Bjarne 8424 Dec 23 02:59 a2.exe
-rwxrwxr-x 1 Bjarne Bjarne 8424 Dec 23 03:00 a3.exe
-rwxrwxr-x 1 Bjarne Bjarne 8472 Dec 23 02:59 a.exe # 文件大小也能反映情况
[Bjarne@VM-8-8-centos cTest]$ 

使用外部库

一句话总结:只要不是自己写的库和官方准备的库,就都是别人的库。使用外部库的方法就是使用动、静态库的方法。

比如 Linux 的黑框交互界面下的图形化界面 ncurses 库,可参考 【Linux拓展】ncurses库的安装和使用-CSDN博客

基于动静态库的思维拓展

自己结合各种资料总结的。很多描述不一定正确,还需结合严谨的计算机组成原理和操作系统原理进行理解。

结合进程地址空间理解库的加载

库中所有的函数都有入口地址,所谓的动态链接,其实就是要连接的库中的函数,这些函数的地址拷贝到可执行程序在进程地址空间中的共享区 (位于栈区和堆区之间,图见进程地址空间),因此动态库又可以称之为共享库

在可执行程序中会记录用到的动态库的位置,和用到的库中的数据例如函数的偏移量,而不是记录动态库的代码内容。

可执行程序被启动变成进程 时,动态库 中的内容会先在虚拟地址空间中建立与动态库内容映射的区域 。当进程首次访问库中的某个资源 时,会触发缺页异常 ,操作系统内核捕获到异常后,才会按需从磁盘读取 动态库的内容加载到物理内存 ,并映射到进程的虚拟地址空间 中。其他进程 需要使用相同的动态库 时,会优先使用已加载 到物理内存的那部分 ,若涉及修改 ,还会触发写时拷贝的机制再拷贝一份。

有关名词例如缺页中断需要深入理解操作系统原理才能了解,这里暂时省略。

进程需要执行动态库中的函数 的话,就会根据共享库中记录的库函数的起始位置 (基址)和偏移量找到库函数 的实际位置并跳转过去执行库函数的内容,执行完成之后再返回原来的语句。

所以将动态库映射到进程 的物理内存并通过共享区进行管理后,进程调用库函数本质 就是在地址空间内根据地址加偏移量找到库函数在进程中的位置进行调用。

总结:可执行程序在启动生成进程时:

  1. 先找到动态库的位置,详细就是使用动态库的 4 种方法。
  2. 动态库的内容按需加载到物理内存 中,并通过虚拟地址进行映射。被加载的动态库可能会被多个进程使用,因此会通过写时拷贝机制进行管理。
  3. 通过虚拟地址和物理地址建立键值关系
  4. 虚拟地址表示的区域是进程地址空间的共享区的某个位置 。且不能保证库在共享区域中会被安排到指定的某个位置,每次启动进程时库在共享区中的绝对起始位置(基址)都会发生变化。

解释库还原为源代码的过程

无论是动态库 还是静态库几乎不可能从库文件还原 成原始的、可读可编译的源代码 。因为很多用户定义的函数名变量名 最后都变成了某个地址加偏移量循环结构 变成了跳转指令

尽管这些信息可通过解读二进制内容为汇编代码得到,但对这些地址加偏移量进行命名,还要结合上下文推测它的功能极其困难,除非在生成库的同时保留有调试符号。

但也不是不能还原,将库还原为源代码的过程可以按照如下思路进行理解:

  1. 将库的二进制内容还原成汇编代码
  2. 选择 汇编代码要还原的高级语言例如 C++ 。
  3. 对每个地址加偏移量进行命名 ,对每个跳转指令翻译成原来的循环,并结合上下文理解该函数或变量的功能。
  4. 某些经常绑定在一起的地址加偏移量划为一个结构体对象
  5. 若两个结构体的前若干个字节的布局完全相同且后续有不同的字段 ,则可推测两个结构体是继承关系
  6. 若某些函数总是通过地址加偏移量访问另一个函数指针 ,则可猜测这个函数是结构体的虚函数表 ,结合继承关系可推导出多态关系

但需要注意的是,访问限定符是编译时的概念 ,在代码变成汇编指令 之后所有的访问限定关系会完全消失不留任何痕迹,还原访问限定符几乎不可能。若一定要还原访问限定符,需要结合库的原作者的创作时心理和习惯进行分析,这已经超过了几乎所有程序员的能力范畴,即还原别人的代码,还要了解别人的习惯和创作时心理,本质就是和对方进行各种博弈的过程。

综上,动、静态库还原为源码几乎不可能,还原时需要借助二进制转汇编的工具,还要对原作者的编码习惯和创作时的心理活动进行推理分析。在计算机行业有专门的逆向工程师负责进行类似的还原操作,这里不做过多描述,但知识点基本相同。若有读者认真学到这里,再学习一些软件的使用可尝试选择这个岗位。

总结

对于 C/C++ 来说,动、静态库需要分成编译时启动进程时两个阶段来理解。

编译 时,静态库所有内容 都会毫无保留地加入最终文件 也就是可执行程序中,动态库只保留了动态符号表 (即地址)这种查询手段

运行可执行程序 时,静态库不影响可执行程序的执行 ,但动态库会造成影响。这也是为什么将动态库从系统中剔除或篡改环境变量、配置文件时,可执行程序会因为找不到对应的动态库而无法运行。

同时将库文件还原为源码几乎不可能,涉及汇编和源码的二次编写,逆向工程师就负责做这个。

动、静态库的优、缺点

动态库的优点 :形成的可执行程序体积比较小,比较节省空间。

动态库的缺点速度 对比静态链接可能会比较 。而且对外置动态库有很强的依赖,一旦外置动态库没了,则整个程序将无法使用。

静态库的优点无视库,可以独立运行。

静态库的缺点体积大 ,浪费资源。同一个代码生成的动态库,是同一个代码生成的静态库的约 1 70 \frac{1}{70} 701,具体取决于静态库在编写时的大小。

目前的操作系统大都使用动态库。

写代码并编译、运行的开发环境,都要做这些事:

  1. 下载开发环境,以及提供 #include 的头文件和 .lib。其他语言也是如此,但不一定用#include,例如 Py 用 import
  2. 设置合理的查找路径。
  3. 规定好形成可执行程序的链接方式。

因为很多语言例如 Java、Python 等都大量甚至全部使用动态库,部分使用静态库,所以为了不重复写这些功能,在使用某个代码开发时都需要配置与之关联的开发环境。

相关推荐
文祐7 小时前
C++类之虚函数表没有虚继承的菱形继承
开发语言·c++·算法
sycmancia8 小时前
Qt——另一种创建线程的方式
开发语言·qt
你怎么知道我是队长8 小时前
JavaScript 函数 this 全解
开发语言·javascript·ecmascript
L-影8 小时前
单体项目结构
java·开发语言
2401_858286119 小时前
OS79.【Linux】POSIX信号量
linux·运维·服务器
孬甭_9 小时前
C++ string类
开发语言·c++
Python图像识别-19 小时前
基于yolov8的钢铁缺陷检测系统-2027毕业版(UI界面+Python项目源码+模型+标注好的数据集)
开发语言·python·yolo
sycmancia9 小时前
Qt——多线程与界面组件的通信
开发语言·qt·算法
你驴我9 小时前
WhatsApp 多账号消息路由中的去重与顺序保证实践
开发语言·php