LV03-01-链接库-01-基本概念

本文主要是C语言——链接库基本概念的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.6的64位版本
VMware® Workstation 17 Pro 17.0.0 build-20800274
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
点击查看本文参考资料
  • 通用
参考资料 相关链接
------
点击查看相关文件下载
------

一、链接库

1. 什么是链接库?

计算机中,有些文件专门用于存储可以重复使用的代码块,例如功能实用的函数或者类,这些通常被称为库文件,简称库( Library )。

链接库就是将开源的库文件进行编译、打包操作后得到的二进制文件。包含的代码可被程序调用,常见的有标准 C 库、数学库、线程库……等等。虽然链接库是二进制文件,但无法独立运行,必须等待其它程序调用,才会被载入内存。

在 linux 下,库文件大多存在于 /usr/lib 目录和 /lib 目录下,另外要注意的就是 Windows 和 Linux 下库文件的格式不兼容。

2. 两种库

库有两种:静态库(.a、.lib)动态库(.so、.dll)。所谓静态、动态是指链接。后面再详细学习链接。

2.1 静态库

静态库(Static Library)是一种包含多个目标文件(.o文件)的归档文件,通常具有.a或.lib的扩展名。静态库在编译时与程序一起链接,成为程序二进制文件的一部分。因此,使用静态库编译的程序在运行时不需要额外的动态链接库支持。

  • 静态库的优点:

(1)简单易用:静态库与程序一起编译链接,无需考虑运行时依赖问题。

(2)兼容性好:由于静态库与程序一起编译成单个二进制文件,因此不存在版本兼容性问题。

(3)性能优化:编译器可以进行更多的优化,如内联函数扩展、常量折叠等。

  • 静态库的缺点:

(1)占用空间:静态库的内容会全部嵌入到最终的程序中,导致程序体积增大。

(2)更新困难:如果需要更新静态库中的某个函数,必须重新编译链接整个程序。

(3)资源浪费:如果多个程序使用了同一个静态库,那么每个程序中都会包含一份静态库的代码,造成内存和存储空间的浪费。

2.2 动态库

动态库(Dynamic Library)是一种在程序运行时加载的库,它包含了可被多个程序共享的代码和数据。动态库在Windows系统中通常具有.dll(Dynamic Link Library)的扩展名,在类Unix系统中则通常具有.so(Shared Object)的扩展名。与静态库不同,动态库在编译时不会被完全链接到程序中,而是在程序运行时动态加载。

  • 动态库的优点:

(1)节省空间:多个程序可以同时使用同一个动态库,减少了存储空间和内存占用。

(2)易于更新:动态库可以在不重新编译链接整个程序的情况下进行更新。

(3)插件支持:动态库可以作为插件来扩展程序的功能。

  • 动态库的缺点:

(1)运行时依赖:程序在运行时需要找到并加载动态库,否则无法正常运行。这可能导致“DLL Hell”问题,即因为动态库版本或路径问题导致的程序崩溃或错误。

(2)兼容性问题:不同版本的动态库可能不兼容,需要确保程序与动态库的版本匹配。

(3)性能开销:动态库的加载和链接过程可能带来一定的性能开销。

2.3 静态库与动态库的比较

(1)链接时间:静态库在编译时与程序一起链接,而动态库在运行时动态加载。

(2)空间占用:静态库会增加程序的体积,而动态库可以实现多个程序之间的代码共享,节省空间。

(3)更新难度:静态库更新需要重新编译链接整个程序,而动态库可以在不修改程序的情况下进行更新。

(4)兼容性:静态库与程序一起编译成单个二进制文件,不存在版本兼容性问题;而动态库可能因版本不匹配导致兼容性问题。

(5)性能:静态库在编译时可以进行更多的优化,而动态库在运行时加载链接可能带来一定的性能开销。但在实际应用中,这种性能差异通常可以忽略不计。

二、两种链接方式

上面都是一些基本概念,接下来我们结合C语言的编译过程来深入了解一下动态库和静态库。

1. 回顾一下

我们来回顾一下程序编译成可执行程序的过程。

img

这一部分的知识重点与链接器相关,每个 C 语言源文件被编译后生成目标文件,这些目标文件最终要被链接在一起生成可执行文件。 链接器的主要作用是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确的衔接

img

2. 两种链接方式

我们知道一个完整的 C 语言项目可能包含多个 .c 源文件,项目的运行需要经过“编译”和“链接”两个过程:

  • 编译:由编译器逐个对源文件做词法分析、语法分析、语义分析等操作,最终生成多个目标文件。每个目标文件都是二进制文件,但由于它们会相互调用对方的函数或变量,还可能会调用某些链接库文件中的函数或变量,编译器无法跨文件找到它们确切的存储地址,所以这些目标文件无法单独执行。
  • 链接:对于各个目标文件中缺失的函数和变量的存储地址(后续简称“缺失的地址”),由链接器负责修复,并最终将所有的目标文件和链接库组织成一个可执行文件。

一个目标文件中使用的函数或变量,可能定义在其他的目标文件中,也可能定义在某个链接库文件中。链接器完成完成链接工作的方式有两种:静态链接动态链接

静态链接的过程由静态链接器负责完成,动态链接的过程由动态链接器负责完成。链接器的实现机制和操作系统有关,例如 Linux 平台上,动态链接器本质就是一个动态链接库。

2.1 静态链接

无论 各个目标文件中缺失的函数和变量的存储地址 位于其它目标文件还是链接库,链接库都会逐个找到各目标文件中这些缺失的地址。采用此链接方式生成的可执行文件,可以独立载入内存运行,这种链接方式称为静态链接,用到的链接库称为静态链接库

img

这种在生成可执行文件之前完成所有链接操作,所使用的静态链接库有如下特点:

  • 编译(链接)时把静态库中相关代码复制到可执行文件中,这也意味着文件本身的体积会很大。

clip_image021[4]

  • 程序中已包含代码,运行时不再需要静态库。
  • 程序运行时无需加载库,运行速度更快。
  • 当系统中存在多个链接同一个静态库的可执行文件时,每个可执行文件中都存有一份静态库的指令和数据,这会占用更多磁盘和内存空间。
  • 静态库升级后,整个程序就必须重新链接后才能运行,。假设一个程序有 10 个模块构成,每个模块的大小为 1 MB,那么每次更新任何一个模块,用户就必须重新获取 10 MB 的程序。

静态库与汇编生成的目标文件一起链接为可执行文件,生成的可执行文件可以独立运行。那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。

2.2 动态链接

链接器先从所有目标文件中找到部分缺失的地址,然后将所有目标文件组织成一个可执行文件。如此生成的可执行文件,仍缺失部分函数和变量的地址,待文件执行时,需连同所有的链接库文件一起载入内存,再由链接器完成剩余的地址修复工作,才能正常执行。

在这种链接方式中,链接所有目标文件的方法仍属静态链接,而载入内存后进行的链接操作称为动态链接,用到的链接库称为动态链接库。动态链接库是 Windows 平台上对动态链接过程所用库文件的称呼, Linux 平台上更习惯称为共享库或者共享对象文件

总的来说动态链接,指的就是将链接的时机推迟到程序运行时再进行。具体来讲,对于一个以动态链接方式运行的项目,首先由静态链接器将所有的目标文件组织成一个可执行文件,但是在程序编译时并不会被连接到目标代码中,而会在运行时将所需的动态链接库全部载入内存,由动态链接器完成可执行文件和动态库文件的链接工作。动态链接库有如下特点:

  • 编译(链接)时仅记录用到哪个共享库中的哪个符号,不复制共享库中相关代码,这样程序不包含库中代码,文件本身的体积就没那大了。

  • 不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。

img
  • 动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新

  • 库升级方便,无需重新编译程序。

  • 使用更加广泛。

采用动态链接的方式,每次程序运行时都需要重新链接,这样确实会损失一部分程序性能,但实际情况是,动态链接库和静态链接相比,性能损失大约在 5% 以下,由此换取程序在空间上的节省以及更新时的便利,是相当值得的。

动态链接库可以随可执行文件一同载入内存,也可以在可执行文件运行过程中载入,即可执行文件什么时候需要,动态链接库才会载入内存。

三、命名规范

1. 静态库

Linux静态库命名规范,必须是”lib[your_library_name].a”:lib为前缀,中间是静态库名,扩展名为.a。

2. 动态库

Linux动态链接库的名字形式为 libxxx.so,前缀是lib,后缀名为”.so”。

  • 针对于实际库文件,每个共享库都有个特殊的名字”soname”。在程序启动后,程序通过这个名字来告诉动态加载器该载入哪个共享库。
  • 在文件系统中,soname仅是一个链接到实际动态库的链接。对于动态库而言,每个库实际上都有另一个名字给编译器来用。它是一个指向实际库镜像文件的链接文件(lib+soname+.so)。

四、两个命令

1. nm

1.1 使用格式

有时候可能需要查看一个库中到底有哪些函数,nm命令可以打印出库中的涉及到的所有符号。库既可以是静态的也可以是动态的。基本格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
NAME
nm - list symbols from object files

SYNOPSIS
nm [-A|-o|--print-file-name] [-a|--debug-syms]
[-B|--format=bsd] [-C|--demangle[=style]]
[-D|--dynamic] [-fformat|--format=format]
[-g|--extern-only] [-h|--help]
[-l|--line-numbers] [--inlines]
[-n|-v|--numeric-sort]
[-P|--portability] [-p|--no-sort]
[-r|--reverse-sort] [-S|--print-size]
[-s|--print-armap] [-t radix|--radix=radix]
[-u|--undefined-only] [-V|--version]
[-X 32_64] [--defined-only] [--no-demangle]
[--plugin name]
[--no-recurse-limit|--recurse-limit]]
[--size-sort] [--special-syms]
[--synthetic] [--with-symbol-versions] [--target=bfdname]
[objfile...]

nm列出的符号有很多,常见的有三种:

1
2
3
U 在库中被调用,但并没有在库中定义(表明需要其他库支持)
T 库中定义的函数
W 所谓的"弱态"符号,它们虽然在库中被定义,但是可能被其他库中的同名符号覆盖

1.2 使用实例

我们用后面编译的zlib库先看一眼:lib/zlib-1.2.10/lib · 苏木/imx6ull-app-demo - 码云 - 开源中国 (gitee.com)

image-20241027095002961

2. ldd

2.1 使用格式

ldd命令可以查看一个可执行程序依赖的共享库,基本命令格式如下:

1
ldd [option]... file...

2.2 使用实例

我们这里以ls命令为例(man手册也是这各实例):

1
ldd /bin/ls
image-20241027095329391