Skip to content

LV010-打包和使用

一、测试文件

首先,我们需要准备三个文件,文件名分别为 main.c 、 example.c 和 example.h 文件,其中 example.c 中为我们的主程序,example.c 和 example.h 中为我们定义的一些函数和变量。

1. main.c

c
#include <stdio.h>
#include "example.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 main.c example:global=%d\n", global);
	global = 30;
	myTest();
	
    return 0;
}

2. example.c

c
#include <stdio.h>
#include "example.h"
int global = 10;

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

void myTest(void)
{
	printf("This is example.c!global=%d\n", global);
}

3. example.h

c
#ifndef __EXAMPLE_H__
#define __EXAMPLE_H__

extern int global;

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

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

4. Makefile

这里就不放统一的Makefile了,动态库和静态库打包和使用的方式不一样,具体可以看后面演示不同链接库用法的示例。

makefile
lib_static:
	gcc -c example.c -o example.o
	ar -rsv libexample.a example.o

lib_dynamic:
	gcc -shared -fPIC example.c -o libexample.so

# 两个库文件同时存在时,默认使用动态库,指定使用静态库可以加上 -static 选项
app_demo:
	gcc -c main.c -o main.o
	gcc main.o -o main -L. -lexample

.PHONY: clean

clean:
	rm -rf *.a *.o *.out *.so main

二、静态链接库

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

1. 怎么创建?

1.1 编译源文件

  • (1)编译静态链接库所有的相关的 .c 源码文件,基本命令如下
shell
gcc -c example.c -o example.o

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

1.2 打包静态库

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

我们将会在终端看到如下提示:

shell
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 部分可以自定义。

1.3 查看符号

1.3.1 nm命令

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

shell
nm libxxx.a

nm 命令参数如下:

shell
-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             仅显示定义的符号
1.3.2 使用实例

我们可以使用上面的 libexample.a 试一下:

shell
sumu@virtual-machine:~/workspace$ nm libexample.a 

example.o:
0000000000000000 D global
                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 T mySum
0000000000000014 T myTest
                 U printf

对应的源文件为 example.c,前面也都了解过,这里的符号说明如下:

text
D 该符号位于初始话数据段中。一般来说,分配到data section中。例如定义全局int baud_table[5] = {9600, 19200},则会分配于初始化数据段中。
U 在库中被调用,但并没有在库中定义(表明需要其他库支持)
T 库中定义的函数
W 所谓的"弱态"符号,它们虽然在库中被定义,但是可能被其他库中的同名符号覆盖

2. 使用静态库

2.1 -L-l

2.1.1 命令格式

可以通过 -L指定库的路径,-l指定库的名称:

shell
gcc [源文件] [编译选项] -L[库路径] -l[库名] -o [输出文件] # 库名要去掉前缀lib和后缀.a
参数说明
-L/path/to/dir指定静态库搜索目录
-lname链接名为 libname.a 的静态库
-o output指定输出可执行文件名

传统的-lxxx这种写法中,GCC会自动查找对应的libxxx.a,当然我们的-l参数还可以写成-l:libxxx.a,这样直接指定具体的库文件名,绕过自动搜索机制,精确控制用哪个文件。

【例】

shell
# 示例1:使用当前目录下的静态库
gcc main.c -L. -lmylib -o app

# 示例2:使用绝对路径
gcc main.c -L/usr/local/lib -lssl -lcrypto -o secure_app

# 示例3:直接指定静态库文件
gcc main.c /usr/lib/libz.a -o compressor

# 示例4:多个静态库
gcc main.c -L./lib -lmath -lutils -o program
2.1.2 使用实例
  • (1)直接编译目标文件——将会报错

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

shell
gcc main.c -Wall

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

shell
sumu@virtual-machine:~/workspace$ gcc main.c -Wall
/tmp/cc6Uc7IW.o: In function `main':
main.c:(.text+0x2f): undefined reference to `mySum'
main.c:(.text+0x4e): undefined reference to `global'
main.c:(.text+0x67): undefined reference to `global'
main.c:(.text+0x70): undefined reference to `myTest'
collect2: error: ld returned 1 exit status

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

shell
gcc -c main.c -o main.o
  • (2)进行链接,生成可执行文件——链接报错

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

shell
gcc main.o -o main -lexample

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

shell
/usr/bin/ld: cannot find -lexample
collect2: error: ld returned 1 exit status

这是因为链接器它找不到 libexample.a 文件的位置。

  • (3)添加库的位置重新链接——正常生成可执行文件

系统那么大,我怎么知道库在哪嘞?找不到的话,我们直接告诉链接器静态链接库在哪不就好了吗。所以我们修改命令如下:

shell
gcc main.o -o main -L./ -lexample

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

  • (4)执行可执行文件。我们上面已经得到 main 可执行程序了,我们在 ubuntu 中执行一下:

image-20260310193014950

发现一切正常。

2.2 直接使用库的路径

2.2.1 命令格式

同样也可以直接使用库的路径:

shell
gcc [源文件] [编译选项] /path/libname.a -o [输出文件]

/path/libname.a就是库的路径,这样就要用完整的库名称。

【例】

shell
# 示例
gcc main.c /home/sumu/libexample.a -o app
2.2.2 使用实例

这里我们直接执行下面的命令:

shell
gcc main.c ./libexample.a -o main

然后可以直接执行:

image-20260324104644859

3. 小结

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

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

或者就是简略一点:

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

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

三、动态链接库

下边我们就来看一看如何在 Linux 下创建和使用动态链接库。这里还是使用一开始的几个测试文件。

1. 怎么创建?

1.1 命令格式

shell
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)上边的命令也可以拆分开来:

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

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

1.2 使用实例

在本例中就是:

shell
gcc -shared -fPIC example.c -o libexample.so

我们可以用 nm 命令看一下:

shell
sumu@virtual-machine:~/workspace$ nm libexample.so 
000000000020102c B __bss_start
000000000020102c b completed.7698
                 w __cxa_finalize@@GLIBC_2.2.5
0000000000000590 t deregister_tm_clones
0000000000000620 t __do_global_dtors_aux
0000000000200e10 t __do_global_dtors_aux_fini_array_entry
0000000000201020 d __dso_handle
0000000000200e18 d _DYNAMIC
000000000020102c D _edata
0000000000201030 B _end
00000000000006a4 T _fini
0000000000000660 t frame_dummy
0000000000200e08 t __frame_dummy_init_array_entry
0000000000000798 r __FRAME_END__
0000000000201028 D global
0000000000201000 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
00000000000006d0 r __GNU_EH_FRAME_HDR
0000000000000548 T _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
000000000000066a T mySum
000000000000067e T myTest
                 U printf@@GLIBC_2.2.5
00000000000005d0 t register_tm_clones
0000000000201030 d __TMC_END__

会发现,这里的动态库比上面的静态库中符号要多很多。

2. 动态库的链接

动态库的链接分为 编译时链接运行时链接 两种方式:

  • 编译时链接(静态链接动态库):在编译阶段就指定需要链接的动态库,链接器会将动态库的符号信息记录到可执行文件中。

  • 运行时链接(动态加载):程序运行时才加载动态库。

3. 编译时链接

3.1 -L-l

动态库即便是在运行的时候再加载符号,我们再编译程序的时候依然要进行链接,这里

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

和前面格式一样,-L用于指定动态库的路径,-l用于指定动态库的名称,这里名称也要去掉前缀lib和后缀.so。传统的-lxxx这种写法中,GCC会自动查找对应的libxxx.so,当然我们的-l参数还可以写成-l:libxxx.so,这样直接指定具体的库文件名,绕过自动搜索机制,精确控制使用哪个文件。

【例】

shell
gcc -o main main.c -L. -lexample

3.2 直接使用库的路径

和静态库一样,也可以直接使用库的路径:

shell
# 使用完整路径
gcc <source.c> /path/libname.so -o program

【例】

shell
gcc -o main main.c ./libexample.so

3.3 运行会找不到库?

在本例中我们使用动态库来编译主程序:

shell
gcc -o main main.c -L. -lexample   # 同目录下不要放对应的静态库,不加任何参数的时候默认会使用同名静态库,后面会提到这个问题
# 或者
gcc -o main main.c ./libexample.so # 这样的话要换个目录,不然库和可执行文件放在一起默认是可以找到的就不会出现下面找不到库的问题了

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

shell
./main

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

shell
./main: error while loading shared libraries: libexample.so: cannot open shared object file: No such file or directory

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

shell
ldd <可执行文件名>

在本例中就是:

shell
ldd main

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

image-20260310192704981

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

3.4 解决运行时库路径问题

3.4.1 五种处理方法

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

  • (1)将链接库文件移动到标准库目录下。但是这样会污染系统目录,多在嵌入式开发的时候这样做。
shell
/usr/lib
/usr/lib64
/lib
/lib64

Tips:不管是编译还是运行都会默认搜索lib和/usr/lib这两个路径。

  • (2)在终端直接添加环境变量 LD_LIBRARY_PATH ,输入以下命令:
shell
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxx

# 也可以这样做
LD_LIBRARY_PATH=/lib-path ./a.out

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

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

其中 xxx 为动态库文件的绝对存储路径,保存之后,执行 source .bashrc 指令(此方式仅对当前登陆用户有效,当然也可以房放到 /etc/profile这种系统级的环境变量文件中去)。

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

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

# 3. 刷新配置文件
cd /etc/ld.so.conf.d/
sudo ldconfig
  • (5)在编译目标代码时指定该程序运行时的动态库搜索路径,注意这里需要是绝对路径。这种方案最显著的问题就是硬编码路径,如果 -rpath 指定的是绝对路径(例如 /usr/local/lib/opt/myapp/lib),当二进制文件被移动到其他目录结构不同的机器上时,动态链接器仍然会去原来的路径寻找库,导致程序无法运行(error while loading shared libraries)。
shell
gcc main.c -o main -L. –lexample -Wl,-rpath=/root/workspace

例如,

shell
sumu@virtual-machine:~/workspace$ gcc main.c -o main -L. -lexample -Wl,-rpath=/home/sumu/workspace
sumu@virtual-machine:~/workspace$ ldd main
        linux-vdso.so.1 (0x00007ffefef62000)
        libexample.so (0x00007f471a7d0000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f471a3df000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f471abd4000)
sumu@virtual-machine:~/workspace$ ./main 
sum = 5
This is main.c example:global=10
This is example.c!global=30
3.4.2 一个使用实例

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

shell
# 打开相关文件
vim ~/.bashrc
# 添加以下内容
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/sumu/workspace/
# 刷新相关文件
source .bashrc

gcc -o main main.c -L. -lexample
./main

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

image-20260310192828015

会发现没有问题了。

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

3.4.3 动态库路径顺序

上面对应了5种方案,那么动态库搜索的顺序是怎样的?这里直接说结论:

  • (1)编译目标代码时指定的动态库搜索路径(rpath);
  • (2)环境变量LD_LIBRARY_PATH指定的动态库搜索路径
  • (3)配置文件/etc/ld.so.conf中指定的动态库搜索路径;
  • (4)默认的动态库搜索路径/lib;
  • (5)默认的动态库搜索路径/usr/lib。

这里我是看的资料,实际并没有抽时间去验证,有的时候若是出现更新了库后未生效的,可以考虑是不是在其他更高优先级的地方存在相同老版本的动态库。

4. 运行时链接

4.1 简介

4.1.1 什么是运行时链接

运行时链接是指程序在运行时(而非编译时)动态加载和链接共享库(Linux中为.so,Windows中为.dll)的机制。这种方式在编译程序的时候不需要链接动态库。

4.1.2 编译时链接和运行时链接的差异
text
┌─────────────────────────────────────────────────────────────────────────┐
│                    编译/链接阶段的差异                                     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   编译时链接(传统方式)                                                │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │  gcc main.c -o main -lmylib                                     │   │
│   │       │              │                                          │   │
│   │       │              └── 链接器需要知道 libmylib.so 的存在          │   │
│   │       │                                                         │   │
│   │       └── 编译时需要头文件                                         │   │
│   │                                                                 │   │
│   │  链接器会:                                                       │   │
│   │  - 在可执行文件的 .dynamic 节中记录依赖 libmylib.so                  │   │
│   │  - 在 .dynsym 中记录未解析的符号                                    │   │
│   │  - 程序启动时,加载器自动加载 libmylib.so                            │   │
│   └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
│   运行时链接(dlopen方式)                                                  │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │  gcc main.c -o main -ldl                                        │   │
│   │       │              │                                          │   │
│   │       │              └── 只需要链接 libdl(系统库)                 │   │
│   │       │                                                         │   │
│   │       └── 不需要头文件,不需要 -lmylib                              │   │
│   │                                                                 │   │
│   │  链接器:                                                         │   │
│   │  - 不知道我们会使用哪个库                                            │   │
│   │  - 不知道我们会调用哪个函数                                          │   │
│   │  - 编译后的可执行文件不包含任何业务库的依赖信息                          │   │
│   └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

4.2 相关接口

4.2.1 dlopen()

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

c
/* 需包含的头文件 */
#include <dlfcn.h>

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

// Link with -ldl.

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

函数参数

  • filename : char 类型指针变量,表示要打开的动态库的名称,可以包含路径,如果文件名包含斜杠(“/”),则它将被解释为(相对或绝对)路径名。如果 filename 指定的对象依赖于其他共享对象,那么动态链接器也会使用相同的规则自动加载这些共享对象。(如果这些对象有依赖关系,这个过程可能会递归发生。)
  • flags :int 类型,表示打开动态库的模式。

我们来看一下flags 取值详情,必须要有以下两个值之一:

text
RTLD_LAZY : 执行惰性绑定。只在执行引用符号的代码时解析它们。如果符号从未被引用,那么它就永远不会被解析。(也就是说在 dlopen 返回前,对于动态库中存在的未定义的变量 (如外部变量 extern,也可以是函数) 不执行解析,就是不解析这个变量的地址。)

RTLD_NOW  : 如果指定了该值,或者将环境变量LD_BIND_NOW设置为非空字符串,则在dlopen()返回之前解析共享对象中的所有未定义符号。如果不能这样做,则返回一个错误。换句话说就是需要在 dlopen 返回前,解析出每个未定义变量的地址,如果解析不出来,在 dlopen 会返回 NULL.

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

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

做了什么?

text
┌─────────────────────────────────────────────────────────────────────────┐
│   dlopen() 内部过程:                                                     │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │ 1. 查找库文件                                                     │   │
│      - 检查 LD_LIBRARY_PATH                                          │   │
│      - 检查 /etc/ld.so.cache                                         │   │
│      - 检查默认路径 (/lib, /usr/lib)                                  │   │
│   │                                                                 │   │
│   │ 2. 映射到内存                                                     │   │
│      - 调用 mmap() 将 .so 文件映射到进程地址空间                         │   │
│      - 加载代码段(可执行)                                             │   │
│      - 加载数据段(可读写)                                             │   │
│   │                                                                 │   │
│   │ 3. 符号解析(如果使用 RTLD_NOW)                                    │   │
│      - 遍历库的符号表                                                  │   │
│      - 绑定到主程序的符号表                                             │   │
│   └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
4.2.2 dlclose()

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

c
/* 需包含的头文件 */
#include <dlfcn.h>

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

// Link with -ldl.

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

函数参数

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

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

4.2.3 dlsym()

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

c
/* 需包含的头文件 */
#include <dlfcn.h>

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

// Link with -ldl.

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

函数参数

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

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

做了什么?

text
┌─────────────────────────────────────────────────────────────────────────┐
│   dlsym() 内部过程:                                                      │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │ 1. 在已加载的库中查找符号                                           │   │
│      - 搜索库的符号表 (.dynsym)                                       │   │
│      - 返回符号对应的内存地址                                           │   │
│   │                                                                 │   │
│   │ 2. 符号解析优先级                                                  │   │
│      - RTLD_NEXT: 查找下一个已加载的库                                  │   │
│      - RTLD_DEFAULT: 使用默认搜索顺序                                  │   │
│   └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

4.3 使用实例

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

c
#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/workspace/libexample.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 main.c example:global=%d\n", *pVar);
	*pVar = 30;

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

然后我们编译程序:

shell
gcc main.c -o main -L. -ldl

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

shell
sumu@virtual-machine:~/workspace$ gcc -o main main.c -ldl
sumu@virtual-machine:~/workspace$ ldd main
        linux-vdso.so.1 (0x00007fffe0109000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fa75075e000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa75036d000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fa750b64000)
sumu@virtual-machine:~/workspace$ ./main 
sum = 5
This is main.c example:global=10
This is example.c!global=30

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

5. 动态链接库总结

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

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

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

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

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

四、使用特定库

在实际项目中,经常需要同时链接静态库和动态库。链接顺序的规则同样适用,但还需要考虑 GCC 的默认行为。

1. GCC 的默认行为

前面我们知道了编译的时候使用静态库和动态库的时候都需要通过-L 指定库所在的目录,-l 指定库的名称,那么现在在一个文件夹内,生成了同名的静态库和动态库文件 libfile.a 和 libfile.so,执行 gcc -o app_demo main.c -L. -lexample,结果会是怎样的呢?

这里用前面的两个实例就可以验证,我们使用前面写的 makefile,做如下测试:

image-20260310193459347

当两个库都存在时,使用上面的命令可以正常编译程序,尽管程序未能正常执行,但是从反馈的错误信息上可以看出,这个错误是由于缺失.so 文件所导致的(未将 libfile.so 放入/usr/lib)。因此,在同时有同名的静态和动态库的情况下,gcc 默认是选择动态库的。

2. 强制使用特定类型的库

2.1 三种情况

2.1.1 强制静态库
shell
# 强制使用静态库
gcc -o app main.o -Wl,-Bstatic -lxxx -L.

# 或者使用 -static 选项(影响所有后续库)
gcc -o app main.o -static -lxxx -L.
  • -static:要求所有库(包括C库)都使用静态版本,如果任何一个库只有动态库(.so)没有静态库(.a),链接就会失败。
  • -Wl,-Bstatic:只对紧随其后的库生效,如果指定的库没有静态版本,链接失败。
2.1.2 强制动态库

这个其实也不用强制,因为前面已经说过了,GCC默认就是使用动态库,什么参数都不加就行了。当然,想加的话也是可以的,加上 Bdynamic 参数即可:

shell
# 强制使用动态库
gcc -o app main.o -Wl,-Bdynamic -lxxx -L.
2.1.3 混合链接?

前面有提到,-Bstatic将会导致后面的库都使用静态库,那后面要是有某个库要用动态库,可以这样:

shell
# 混合链接:foo 静态,bar 动态
gcc main.c -Wl,-Bstatic -lfoo -L/foo-path/ -Wl,-Bdynamic -lbar -L/bar-path/ -o program_mixed

2.2 使用示例

2.2.1 示例源码
c
/* libB.c */
#include <stdio.h>
void func_B(void) {
    printf("func_B called\n");
}
c
/* libA.c */
#include <stdio.h>
void func_A(void) {
    printf("func_A called\n");
}
c
/* main.c - 主程序 */
extern void func_A(void);
extern void func_B(void);
int main() {
    func_A();
    func_B();
    return 0;
}
2.2.2 Makefile
makefile
CC = gcc
AR = ar

.PHONY: all clean libs static dynamic mixed

# ==================== 库文件 ====================

# 静态库
libA.a: libA.o
	$(AR) rcs $@ $<

libB.a: libB.o
	$(AR) rcs $@ $<

libA.o: libA.c
	$(CC) -c $< -o $@

libB.o: libB.c
	$(CC) -c $< -o $@

# 动态库
libA.so: libA.c
	$(CC) -shared -fPIC -o $@ $<

libB.so: libB.c
	$(CC) -shared -fPIC -o $@ $<

# ==================== 测试目标 ====================

# 全部使用静态库
test-static: libA.a libB.a
	@echo "=== 测试: 全部使用静态库 ==="
	$(CC) -o app_static main.c -L. -l:libA.a -l:libB.a
	@echo "运行结果:"
	LD_LIBRARY_PATH=. ./app_static

# 全部使用动态库
test-dynamic: libA.so libB.so
	@echo "=== 测试: 全部使用动态库 ==="
	$(CC) -o app_dynamic main.c -L. -lA -lB -Wl,-rpath,.
	@echo "运行结果:"
	LD_LIBRARY_PATH=. ./app_dynamic

# 混合: libA 静态 + libB 动态
test-mixed-Astatic-Bdynamic: libA.a libB.so
	@echo "=== 测试: libA 静态 + libB 动态 ==="
	$(CC) -o app_mixed_AsBd main.c -L. -l:libA.a -lB -Wl,-rpath,.
	@echo "运行结果:"
	LD_LIBRARY_PATH=. ./app_mixed_AsBd

# 混合: libA 动态 + libB 静态
test-mixed-Adynamic-Bstatic: libA.so libB.a
	@echo "=== 测试: libA 动态 + libB 静态 ==="
	$(CC) -o app_mixed_AdBs main.c -L. -A -l:libB.a -Wl,-rpath,.
	@echo "运行结果:"
	LD_LIBRARY_PATH=. ./app_mixed_AdBs

# 默认目标: 构建所有库
all: libs

libs: libA.a libB.a libA.so libB.so

# 运行所有测试
test: test-static test-dynamic test-mixed-Astatic-Bdynamic test-mixed-Adynamic-Bstatic

# 清理
clean:
	rm -f *.o *.a *.so app_*

五、库大小问题

我们前面生成的静态库和动态库用的是同一个文件,这两个库的大小是不一样的:

shell
sumu@virtual-machine:~/workspace$ ls libexample* -alh
-rw-rw-r-- 1 sumu sumu 1.9K Mar 24 10:17 libexample.a
-rwxrwxr-x 1 sumu sumu 7.8K Mar 24 10:57 libexample.so

sumu@virtual-machine:~/workspace$ du -sh lib*
4.0K    libexample.a
8.0K    libexample.so

注意】这里要使用du命令和ls命令显示的大小怎么不一样?这是因为ls命令显示的是文件的逻辑大小,即文件实际包含的数据量,但是du显示的是文件占用磁盘块的大小,即文件在磁盘上实际占用的物理空间,文件系统是使用块来存储文件,通常块大小为4KB,即便文件只有1.9KB,也会占用4KB的1个完整块。

动态库包含额外信息,例如符号导出表、重定位表和动态链接信息等,另外还有位置无关代码 (PIC) 开销,动态库必须是 PIC(Position Independent Code),这会带来一些额外开销,例如

  • GOT (Global Offset Table):全局偏移表
  • PLT (Procedure Linkage Table):过程链接表
  • 每次访问全局变量/调用函数多一次内存访问
text
┌─────────────────────────────────────────────────────────┐
│  静态库 (.a)           vs        动态库 (.so)             │
├─────────────────────────────────────────────────────────┤
│  直接使用绝对地址                  使用 GOT/PLT 间接        │
│  编译时确定地址                    运行时确定地址            │
│  代码紧凑                         需要额外跳转层            │
└─────────────────────────────────────────────────────────┘

所以其实静态链接的可执行文件启动速度会更快,运行效率会更高。

参考资料:

Linux 指定编译时动态库路径和运行时动态库路径--解决报错symbol lookup error和cannot open shared object file_编译时动态库的路径和运行时动态库路径的关系-CSDN博客

GCC 中 -L、-rpath和-rpath-link的区别 - lsgxeva - 博客园

Qt .pro配置gcc相关命令(三):-W1、-L、-rpath和-rpath-link-CSDN博客