LV04-05-文件操作-链接库的使用

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

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 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官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
--- ---

一、链接库

1. 什么是链接库?

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

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

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

2. 两种链接方式

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

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

一个目标文件中使用的函数或变量,可能定义在其他的目标文件中,也可能定义在某个链接库文件中。链接器完成完成链接工作的方式有两种:静态链接动态链接,静态链接的过程由静态链接器负责完成,动态链接的过程由动态链接器负责完成。链接器的实现机制和操作系统有关,例如 Linux 平台上,动态链接器本质就是一个动态链接库。

2.1 静态链接

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

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

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

2.2 动态链接

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

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

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

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

  • 多个程序可共享同一个库。

  • 程序运行时才需要加载库。

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

  • 使用更加广泛。

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

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

二、静态链接库

下边我们就来看一看如何在 Linux 下创建和使用静态链接库。首先,我们需要准备三个文件,文件名分别为 test.c 、 file.c 和 file.h 文件,其中test.c中为我们的主程序,file.c 和 file.h中为我们定义的一些函数和变量。文件内容如下:

点击查看三个文件内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include "file.h"

int main(int argc, char *argv[])
{
int a = 2;
int b = 3;
int sum = 0;

sum = mySum(a, b);
printf("sum = %d\n",sum);
printf("This is test.c test:global=%d\n", global);
global = 30;
myTest();

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include "file.h"
int global = 10;

int mySum(int a, int b)
{
return (a + b);
}

void myTest(void)
{
printf("This is file.c test:global=%d\n", global);
}
1
2
3
4
5
6
7
8
#ifndef __FILE_H__
#define __FILE_H__

extern int global;

int mySum(int a, int b);
void myTest(void);
#endif

正常来说,我们要在 test.c 中使用 file.c 中的变量和函数,我们是需要将test.c 和file.c一起编译,最终生成一个可执行文件,下边我们来尝试单独编译两个文件,然后使用静态链接库的方式使test.c可以正常的使用file.c中的变量和函数。

1. 创建静态链接库

  • (1)编译静态链接库所有的相关的 .c 源码文件
1
gcc -c file.c -o file.o

【注意】若要生成静态链接库中有多个 .c 源文件,则需要全部编译成 .o 文件。

  • (2)将生成的 .o 文件打包生成静态库
1
ar -rsv libfile.a file.o  # 注意库的命名,必须为 libxxx.a

不出意外,我们会在终端看到如下提示:

1
2
ar: 正在创建 libfile.a
a - file.o
点击查看命令说明

ar 命令常用于创建静态链接库,其中 r 、 c 、 s 是 ar 命令创建静态链接库所需要设定的参数。

c禁止在创建库时产生的正常消息
r如果指定的文件已经存在于库中,则替换它
s无论 ar 命令是否修改了库内容都强制重新生成库符号表
v将建立新库的详细的逐个文件的描述写至标准输出
q将指定的文件添加到库的末尾
t将库的目录写至标准输出

【注意】

(1)若有多个 .o 文件,则需要一起打包。

(2) Linux 平台上静态链接库的名称不是随意的,通常需要遵循 libxxx.a 格式, xxx 部分可以自定义。

(3)我们可以通过 nm 命令查看已经打包好的静态链接库中的符号信息,命令格式如下:

1
nm libxxx.a
点击查看 nm 命令参数
  • 参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-A 或-o或 --print-file-name 打印出每个符号属于的文件
-a或--debug-syms 打印出所有符号,包括debug符号
-B BSD码显示
-C或--demangle[=style] 对低级符号名称进行解码,C++文件需要添加
--no-demangle 不对低级符号名称进行解码,默认参数
-D 或--dynamic 显示动态符号而不显示普通符号,一般用于动态库
-f format或--format=format 显示的形式,默认为bsd,可选为sysv和posix
-g或--extern-only 仅显示外部符号
-h或--help 显示命令的帮助信息
-n或-v或--numeric-sort 显示的符号以地址排序,而不是名称排序
-p或--no-sort 不对显示内容进行排序
-P或--portability 使用POSIX.2标准
-V或--version 查看版本
--defined-only 仅显示定义的符号

2. 使用静态链接库

  • (1)编译目标文件

按照之前的命令,编译 test.c 文件:

1
gcc test.c -Wall

不出意外的话,我们会收到如下提示:

1
2
3
4
5
6
7
8
/usr/bin/ld: /tmp/ccJXaMvx.o: warning: relocation against  global' in read-only section  .text'
/usr/bin/ld: /tmp/ccJXaMvx.o: in function main':
test.c:(.text+0x33): undefined reference to mySum'
/usr/bin/ld: test.c:(.text+0x52): undefined reference to global'
/usr/bin/ld: test.c:(.text+0x6b): undefined reference to global'
/usr/bin/ld: test.c:(.text+0x74): undefined reference to myTest'
/usr/bin/ld: warning: creating DT_TEXTREL in a PIE
collect2: error: ld returned 1 exit status

很明显,所有引用了其他文件的地方全部报错了,原因就在于我们并没有将这个文件与我们刚才创建的静态库建立联系。我们先生成目标文件,先不进行链接:

1
gcc -c test.c -o test.o
  • (2)进行链接,生成可执行文件

记得之前我们使用数学函数的时候,链接数学库的时候加上 -lmath 就可以了,那这里是不是也一样呢?

1
gcc test.o -o test -lfile

没啥意外的话,一定是事与愿违啊,我们会收到如下提示:

1
2
/usr/bin/ld: 找不到 -lfile
collect2: error: ld returned 1 exit status

这是因为链接器它找不到 libfile.a 文件的位置,系统那么大,我怎么知道库在哪嘞?找不到的话,我们直接告诉链接器静态链接库在哪不就好了吗。所以我们修改命令如下:

1
gcc test.o -o test -L/home/hk/6temp/test/ -lfile

这样便可以得到正确的可执行文件 test 了。

3.静态链接库总结

(1)一般来说,我们可以直接生成可执行文件:

1
2
3
gcc <source.c> -o <target_name> -L<lib_path> -l<xxx_name>
-L 表示静态链接库库所在的路径
-l 后面跟静态链接库的名称

或者就是简略一点:

1
gcc 源码.c  -Wall -L 路径  -lxxxx

这样生成的可执行文件名称为默认的 a.out 。

三、动态链接库

下边我们就来看一看如何在 Linux 下创建和使用动态链接库。和上边一样首先,我们需要准备三个文件,分别为 test.c 、 file.c 和 file.h 文件,文件内容如下:

点击查看三个文件内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include "file.h"

int main(int argc, char *argv[])
{
int a = 2;
int b = 3;
int sum = 0;

sum = mySum(a, b);
printf("sum = %d\n",sum);
printf("This is test.c test:global=%d\n", global);
global = 30;
myTest();

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 头文件 */
#include <stdio.h>
#include "file.h"
int global = 10;

int mySum(int a, int b)
{
return (a + b);
}

void myTest(void)
{
printf("This is file.c test:global=%d\n", global);
}
1
2
3
4
5
6
7
8
#ifndef __FILE_H__
#define __FILE_H__

extern int global;

int mySum(int a, int b);
void myTest(void);
#endif

Linux 平台上,我们更习惯将动态链接库称为共享库文件或者“共享对象文件,后缀名通常为 .so 。

1. 创建动态链接库

  • 生成动态链接库
1
gcc -shared -fPIC <file1.c file2.c ... > -o libxxx.so
点击查看命令说明
  • -shared :表示生成动态链接库;
  • -fPIC :也可以写成 -fpic ,功能是令 GCC 编译器生成动态链接库时,用相对地址表示库中各个函数和变量的存储位置。这样做的好处是,无论动态链接库被加载到内存的什么位置,都可以被多个程序(进程)同时调用;
  • -o libxxx.so : -o 选项用于指定生成文件的名称,此命令最终生成的动态链接库文件的文件名为 libxxx.so 。

在本例中就是:

1
gcc -shared -fPIC file.c -o libfile.so

【注意】

(1)在 Linux 中,动态链接库文件的命名格式为 libxxx.so ,其中 xxx 部分可以自定义。

(2)上边的命令也可以拆分开来:

1
2
3
4
5
# 1. 生成与位置无关的目标文件 (.o文件)
gcc -c -fPIC <file1.c file2.c ...>

# 2. 生成动态库
gcc -shared <file1.o file2.o ...> -o libxxx.so

2. 使用动态链接库

我们编译主程序的时候按下边的命令格式进行:

1
gcc <source.c> -o <target> -L<lib_path> -l<xxx_name>

在本例中就是:

1
gcc -o test test.c -L. -lfile

然后我们会发现,正常生成了 test 可执行文件,也就说明链接过程是可以通过的。那接下来我们执行一下这个可执行文件看看是否可行呢?:

1
./test

不出意外的话,会收到如下错误:

1
./test: error while loading shared libraries: libfile.so: cannot open shared object file: No such file or directory

执行结果提示,执行时无法找到 libfile.so 动态链接库。我们通过以下命令,可以查看可执行文件执行时需要调用的所有动态链接库,以及它们各自的存储位置:

1
ldd <可执行文件名称>

在本例中就是:

1
ldd test

执行此命令后,我们会得到如下提示:

1
2
3
4
linux-vdso.so.1 (0x00007ffe617f7000)
libfile.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6ad4f79000)
/lib64/ld-linux-x86-64.so.2 (0x00007f6ad517a000)

libfile.so 显示为 not found ,这就是导致可执行文件执行失败的直接原因。

点击查看常用处理办法

常用的处理方法有下边几种:

  • (1)将链接库文件移动到标准库目录下。
1
2
3
4
/usr/lib
/usr/lib64
/lib
/lib64
  • (2)在终端直接添加环境变量 LD_LIBRARY_PATH ,输入以下命令:
1
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxx

其中 xxx 为动态链接库文件的绝对存储路径(需要注意的是此方式仅在当前终端有效,关闭终端后无效)。

  • (3)修改 ~/.bashrc 或 ~/.bash_profile 文件。
1
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxx

其中 xxx 为动态库文件的绝对存储路径,保存之后,执行 source .bashrc 指令(此方式仅对当前登陆用户有效)。

  • (4)添加 /etc/ld.so.conf.d/*.conf 文件,并添加动态库路径,然后执行 ldconfig 刷新。
1
2
3
4
5
6
7
8
# 1. 新建自己的动态库搜索路径配置文件
sudo vim /etc/ld.so.conf.d/filename.conf

# 2. 添加自己的动态库路径(最好是绝对路径)然后保存并退出

# 3. 刷新配置文件
cd /etc/ld.so.conf.d/
sudo ldconfig

这里修改环境变量来添加动态库的路径:

1
2
3
4
5
6
7
8
# 打开相关文件
vim ~/.bashrc

# 添加以下内容
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/hk/2Sharedfiles/2Linux/01Study/04-LV4/05-day5/05Library

# 刷新相关文件
source .bashrc

之后我们再执行的时候就不会有问题啦。同时我们也会发现这样写出来的程序,只要找不到动态库的位置,就一定无法执行,但是有些动态库就算找不到,但是可能也并不影响其他功能的正常执行,这样的话,我们能否在找不到相应的动态库的时候,只是哪一部分功能无法使用,但是其他功能正常呢?当然可以啦,我们接着往下看。

3. 通过函数调用动态库

3.1 dlopen()

在 linux 下可以使用 man 3 dlopen 命令查看该函数的帮助手册。

1
2
3
4
5
6
7
/* 需包含的头文件 */
#include <dlfcn.h>

/* 函数声明 */
void *dlopen(const char *filename, int flags);

// Link with -ldl.

【函数说明】该函数用于打开指定名称的动态库,名称可以携带路径,然后返回一个句柄给调用进程。

【函数参数】

  • filename : char 类型指针变量,表示要打开的动态库的名称,可以包含路径,如果文件名包含斜杠(“/”),则它将被解释为(相对或绝对)路径名。如果filename指定的对象依赖于其他共享对象,那么动态链接器也会使用相同的规则自动加载这些共享对象。(如果这些对象有依赖关系,这个过程可能会递归发生。)
  • flags :int 类型,表示打开动态库的模式。
点击查看 flags 取值详情
  • 必须要有以下两个值之一
1
2
RTLD_LAZY : 执行惰性绑定。只在执行引用符号的代码时解析它们。如果符号从未被引用,那么它就永远不会被解析。(也就是说在 dlopen 返回前,对于动态库中存在的未定义的变量 (如外部变量 extern,也可以是函数) 不执行解析,就是不解析这个变量的地址。)
RTLD_NOW : 如果指定了该值,或者将环境变量LD_BIND_NOW设置为非空字符串,则在dlopen()返回之前解析共享对象中的所有未定义符号。如果不能这样做,则返回一个错误。换句话说就是需要在 dlopen 返回前,解析出每个未定义变量的地址,如果解析不出来,在 dlopen 会返回 NULL.

还有一些可选的值,目前还没用过,后边用到了再补充。

【返回值】void *类型,成功时,dlopen()为加载的库返回一个非NULL句柄。如果出现错误(找不到文件、不可读、格式错误或在加载过程中产生错误),将返回NULL。

【使用格式】none

【注意事项】 none

3.2 dlclose()

在 linux 下可以使用 man 3 dlclose 命令查看该函数的帮助手册。

1
2
3
4
5
6
7
/* 需包含的头文件 */
#include <dlfcn.h>

/* 函数声明 */
int dlclose(void *handle);

// Link with -ldl.

【函数说明】该函数用于关闭打开的动态库。

【函数参数】

  • handle : void 类型指针变量,表示已经打开的动态库的句柄。

【返回值】int 类型,成功返回0,失败返回非0值。

【使用格式】none

【注意事项】 none

3.3 dlsym()

在 linux 下可以使用 man 3 dlsym命令查看该函数的帮助手册。

1
2
3
4
5
6
7
/* 需包含的头文件 */
#include <dlfcn.h>

/* 函数声明 */
void *dlsym(void *handle, const char *symbol);

// Link with -ldl.

【函数说明】该函数用于根据动态链接库操作句柄 (handle) 与符号 (symbol),返回符号对应的地址。

【函数参数】

  • handle : void 类型指针变量,表示已经打开的动态库的句柄。
  • symbol :char 类型指针变量,表示要查找地址的符号的名称。

【返回值】void *类型,成功返回与符号关联的地址,失败返回NULL。错误的原因可以使用dlerror(3)进行诊断。

【使用格式】none

【注意事项】 none

3.4 使用实例

还是使用之前创建的动态链接库 libfile.so ,这里要先清除之前设置的环境变量LD_LIBRARY_PATH中关于该动态库的位置的相关语句,这样我们才能更清楚的看到使用dlopen会为我们带来哪些好处。我们修改主程序 test.c 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <dlfcn.h>

int main(int argc, char *argv[])
{
int a = 2;
int b = 3;
int sum = 0;
void *handler = dlopen("/home/hk/6temp/test/libfile.so", RTLD_LAZY);
int (*pFunc)(int, int) = dlsym(handler, "mySum");
int *pVar = dlsym(handler, "global");

sum = pFunc(a, b);
printf("sum = %d\n",sum);
printf("This is test.c test:global=%d\n", *pVar);
*pVar = 30;

void (*mytest)(void) = dlsym(handler, "myTest");
mytest();
dlclose(handler);
return 0;
}

然后我们编译程序:

1
gcc test.c -o test -ldl

这时候我们发现,我们并没有告诉链接器动态库的位置和名称,但是依然可以编译通过,我们执行程序,会得到以下输出:

1
2
3
sum = 5
This is test.c test:global=10
This is file.c test:global=30

这说明,我们的程序正常执行了。

3.5 总结

我们会发现,直接在编译主程序的时候链接动态库的话,我们需要做的事情有:

(1)告诉编译器动态库的位置(-L参数)和动态库的名称(-l参数)

(2)添加动态库位置的环境变量,否则可执行程序在执行的时候是找不到动态库的。

(3)万一找不到动态库,整个进程都会崩溃。

而我们后边使用dlopen函数打开动态库的情况,则避免了上边的问题,我们使用dlopen函数打开动态库,然后找到相应的符号地址进行引用,这样即便动态库找不到,我们也仅仅是这一部分代码无法执行,只要做好错误处理,其他功能是不受影响,而且整个进程也不会崩溃,所以其实还是后者会更加好一些。