LV03-01-链接库-02-如何创建和使用链接库

本文主要是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官方提供)
点击查看本文参考资料
  • 通用
参考资料 相关链接
------
点击查看相关文件下载
------

一、静态链接库

下边我们就来看一看如何在 Linux 下创建和使用静态链接库。

1. 文件准备

首先,我们需要准备三个文件,文件名分别为 test.c 、 file.c 和 file.h 文件,其中test.c中为我们的主程序,file.c 和 file.h中为我们定义的一些函数和变量。LV01_GCC_COMPILE/02_static_lib · 苏木/imx6ull-app-demo - 码云 - 开源中国 (gitee.com)

1.1 test.c

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 file.c

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.3 file.h

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中的变量和函数。

2. 创建静态链接库

2.1 编译静态链接库

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

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

2.2 打包静态库

  • (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 仅显示定义的符号

3. 使用静态链接库

3.1 生成可执行文件

  • (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./ -lfile

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

3.2 验证可执行程序

我们上面已经得到test可执行程序了,我们在ubuntu中执行一下:

image-20241027100623414

4. 静态链接库总结

(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 下创建和使用动态链接库。

1. 文件准备

和上边一样首先,我们需要准备三个文件,分别为 test.c 、 file.c 和 file.h 文件。LV01_GCC_COMPILE/03_dynamic_lib · 苏木/imx6ull-app-demo - 码云 - 开源中国 (gitee.com)

1.1 test.c

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 file.c

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.3 file.h

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

2. 创建动态链接库

2.1 命令格式

1
gcc -shared -fPIC <file1.c file2.c ... > -o libxxx.so
  • -shared :表示生成动态链接库;

  • -fPIC :也可以写成 -fpic ,功能是令 GCC 编译器生成动态链接库时,用相对地址表示库中各个函数和变量的存储位置。这样做的好处是,无论动态链接库被加载到内存的什么位置,都可以被多个程序(进程)同时调用;

  • -o libxxx.so : -o 选项用于指定生成文件的名称,此命令最终生成的动态链接库文件的文件名为 libxxx.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.2 使用实例

在本例中就是:

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

3 使用动态链接库

3.1 生成可执行程序

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

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

在本例中就是:

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

然后我们会发现,正常生成了 test 可执行文件,也就说明链接过程是可以通过的。

3.2 验证可执行程序

那接下来我们执行一下这个可执行文件看看是否可行呢?:

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

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

image-20241027101452410

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

4. 动态库位置找不到?

4.1 四种处理方法

上面我们已经知道执行失败的原因是动态链接过程中找不到动态库,常用的处理方法有下边几种:

  • (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

4.2 使用实例

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

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

# 添加以下内容
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/sumu/1sharedfiles/linux_develop/imx6ull-app-demo/LV01_GCC_COMPILE/02_dynamic_lib/libfile.so

# 刷新相关文件
source .bashrc

之后我们再执行的时候就不会有问题啦。我们可以再执行一下可执行程序试一下:

image-20241027101815717

会发现没有问题了。

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

4. 通过函数调用动态库

4.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

4.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

4.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

4.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
23
#include <stdio.h>
#include <dlfcn.h>

int main(int argc, const char *argv[])
{
int a = 2;
int b = 3;
int sum = 0;
char *file_so_path="/home/sumu/1sharedfiles/linux_develop/imx6ull-app-demo/LV01_GCC_COMPILE/02_dynamic_lib/libfile.so";
void *handler = dlopen(file_so_path, 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 -L. -ldl

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

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

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

5. 动态链接库总结

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

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

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

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

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