本文将以清晰的逻辑脉络,带领读者从基础概念入手,逐步掌握动静态库的制作、生成、发布、使用及安装全流程。通过具体的代码示例与 Makefile 配置解析,结合编译链接原理与操作系统内存管理机制,深入理解静态库的 “空间换时间” 特性与动态库的 “运行时加载” 优势。同时,结合地址空间与程序执行原理,剖析库函数在内存中的定位与调用机制,帮助读者构建从理论到实践的完整知识体系。
静态库是将多个目标文件(.o 文件)打包成一个单一的库文件(.a 文件),然后使用者可以将这个库与他们的程序链接。主要步骤包括:
首先定义一组函数(例如 add
、sub
、mul
和 div
),然后将它们编译成目标文件(.c 文件)。
1.mymath.h
#pragma once
#include <stdio.h>
extern int myerrno ;
int add(int x, int y);
int sub(int x, int y);
int mul(int x, int y);
int div(int x, int y);
2.mymath.c
#include "mymath.h"
int myerrno = 0;
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
int mul(int x, int y)
{
return x * y;
}
int div(int x, int y)
{
if (y == 0)
{
myerrno = 1;
return -1;
}
return x / y;
}
使用 gcc
或 ar
编译工具,先编译 .c
文件为 .o
文件,再通过 ar -rc
命令打包生成 .a
静态库文件。
Makefile
lib = libmymath.a
$(lib): mymath.o
ar -rc $@ $^
mymath.o: mymath.c
gcc -c $^
.PHONY: clean
clean:
rm -f *.o *.a lib
解释:
lib = libmymath.a
:定义了一个名为 lib
的变量,它表示静态库文件的名称。clean
目标: rm -f *.o *.a
:删除生成的目标文件。静态库生成示意图
将静态库及其头文件打包发布。发布时的头文件包含了库函数的声明,是供其他程序使用的文档。
在之前的Makefile中加上以下代码就是发布库啦
.PHONY: output
output:
mkdir -p lib/include
mkdir -p lib/mymathlib
cp *.h lib/include
cp *.a lib/mymathlib
output
目标:
lib/include
和 lib/mymathlib
,然后将头文件和静态库文件复制到相应的目录中。在其他程序中使用静态库时,必须确保编译器能够找到库的头文件和静态库文件。可以通过添加 -I
和 -L
选项来指定头文件路径和库文件路径,使用 -l
选项指定库的名称。
先创建一个 test
目录,将上面的 lib
目录拷贝到其目录中,再创建一个 main.c
文件进行测试:
#include "mymath.h"
int main()
{
printf("1+1=%d\n", add(1, 1));
return 0;
}
使用 gcc 编译生成可执行程序:
可以发现系统表示没有找到 mymath.h
文件,那么是什么引起的报错呢?
在 main.c
中,使用 #include
语句来引用头文件时,编译器的查找顺序会受到影响。具体来说:
<>
时,编译器会在系统指定的目录中查找头文件。""
时,编译器首先在当前源文件的同级目录中查找头文件,如果没有找到,才会去系统指定的目录查找。在代码中,#include "mymath.h"
位于 lib/include
目录下,但由于编译器默认在当前目录查找头文件,因此会报错找不到头文件。
解决方法:
#include "/lib/include/mymath.h"
。-I
选项,指定额外的头文件搜索路径。例:gcc -I ./lib/include main.c
.这里我们选择方法三来解决问题:
可以看到,即使头文件正确找到,链接静态库时也可能出现错误,接下来我们继续解决问题。
GCC 默认只会在标准路径下查找库文件,因此会找不到自定义的静态库。
解决方法:
libmymath.a
)复制到系统默认的库文件路径。(库的安装)-L
来指定库的搜索路径,使用 -l
来指定链接的库。例如:
gcc main.c -I /path/to/include -L /path/to/lib -lmymath
这里 -L /path/to/lib
指定了静态库的路径,-lmymath
指定了库的名称。(mymath
的原名叫做 libmymath.a
,不过库名称要去掉前缀 lib
和后缀 .a
)要点:
静态库的安装本质上是将库文件和头文件放到系统的指定目录下。可以通过创建软链接的方式来实现安装,如将库文件链接到 /usr/lib
或者 /lib
目录下,这样系统和其他程序就能识别到这些库。
通过在指定目录下创建软链接的方式,如下图所示:
sudo ln -s /home/xny/linux/lesson23/test/lib/include /usr/include/myinc
sudo ln -s /home/xny/linux/lesson23/test/lib/mymathlib/libmymath.a /lib64/libmymath.a
此时包含头文件前面应该加上软链接的名字,如:#include <myinc/mymath.h>
这种形式。
// myprintf.h
#pragma once
#include <stdio.h>
void Print();
// myprintf.c
#include "myprintf.h"
void Print()
{
printf("Hello Linux\n");
}
// mylog.h
#pragma once
#include <stdio.h>
void Log(const char* info);
// mylog.c
#include "mylog.h"
void Log(const char* info)
{
printf("log: %s\n", info);
}
编译选项:使用 -fPIC
选项编译源文件,生成位置无关的代码,这对于动态库至关重要。然后,使用 -shared
选项将目标文件 .o
链接成动态库 .so
文件。例如:
gcc -fPIC -c myprintf.c
gcc -fPIC -c mylog.c
gcc -shared -o libmymethod.so *.o
ar -rc
命令打包,动态库通过 gcc -shared
生成。cp
命令将头文件、静态库和动态库复制到适当的目录(如 mylib/include
和 mylib/lib
)中,以便其他程序可以访问这些文件。dy-lib = libmymethod.so
static-lib = libmymath.a
.PHONY: all
all: $(dy-lib) $(static-lib)
$(static-lib): mymath.o
ar -rc $@ $^
mymath.o: mymath.c
gcc -c $^
$(dy-lib): mylog.o myprint.o
gcc -shared -o $@ $^
mylog.o: mylog.c
gcc -fPIC -c $^
myprint.o: myprint.c
gcc -fPIC -c $^
.PHONY: clean
clean:
rm -rf *.o *.a *.so mylib
.PHONY: output
output:
mkdir -p mylib/include
mkdir -p mylib/lib
cp *.h mylib/include
cp *.a mylib/lib
cp *.so mylib/lib
使用动态库:可以将动态库中的函数像使用静态库一样在程序中引用。需要包含相应的头文件,并在编译时指定动态库的路径和库名。例如:
#include "myprintf.h"
#include "mylog.h"
编译时链接动态库:通过 -L
选项指定动态库的位置,使用 -l
选项指定库名。例如:
gcc -L/path/to/lib -lmymethod main.c -o main
运行时问题:虽然在编译时指定了动态库的路径,程序运行时仍然可能找不到库。这是因为动态库需要通过加载器来加载。解决方法有以下几种:
将库文件拷贝到系统默认路径(如 /lib64
或 /usr/lib64
)。
在系统默认路径下建立软链接。
因为 我们仅仅是告诉了编译器所需的动态库在哪里,而可执行程序运行靠的是加载器,上面的 not found
表示加载器不知道动态库在哪里。
解决方案:在系统默认的库路径(/lib64
、/usr/lib64
、/usr/lib
)下建立软链接
设置 LD_LIBRARY_PATH
环境变量,指定动态库路径。
export LD_LIBRARY_PATH=yourpath:$LD_LIBRARY_PATH
在 /etc/ld.so.conf.d/
中添加路径,并运行 ldconfig
更新系统库缓存。
加载与共享:动态库在进程运行时被加载到内存。操作系统管理着所有动态库的加载情况,确保同一库在多个进程间共享。例如,当 A.exe
和 B.exe
都使用同一个动态库时,该库只会被加载一次,多个进程通过映射共享这份内存。
动态库的共享与写时拷贝:由于动态库被多个进程共享,全局变量(如 errno
)可能会产生问题。操作系统通过 写时拷贝(Copy-on-Write)机制来解决这个问题:当某个进程修改共享的全局变量时,会为该进程创建该变量的副本,避免其他进程受到影响。
.exe
文件)时,会为每个代码段、数据段等指定地址。这些地址被称为逻辑地址。逻辑地址是程序在虚拟内存中的地址,而不是真正的物理地址。printf
)时,程序内部有一个逻辑地址(例如 0x11223344
)对应于该函数。操作系统并不会把动态库加载到固定的内存地址,而是通过相对地址来加载和执行函数。-fPIC
选项告诉编译器采用位置无关代码(PIC)。这样,生成的库会使用相对地址而不是绝对地址,以便操作系统可以在加载时将库加载到任何位置。ld.so
)在运行时根据需求加载。动静态库技术的背后,折射出软件开发中 “分治” 与 “抽象” 的核心思想。通过将复杂功能封装为库,开发者得以在更高抽象层构建系统,这不仅降低了开发复杂度,更奠定了现代软件开发生态的基础。希望读者能以本文为起点,进一步探索 Linux 系统编程的更多奥秘,在代码的世界中不断积累与成长。