1.静态库
我们可以自己做一个静态库 但是在此之前我们需要了解一些基本的指令
ar 是 Linux/Unix 下用于操作「归档文件(Archive File)」 的命令行工具,最核心的用途是创建、修改、查看静态库(.a 后缀的库文件)
-r(replace):替换 / 添加文件到归档中
若归档中已有同名成员文件,则替换它;若没有,则新增。
-c(create):创建归档文件
若指定的归档文件不存在,则自动创建(不会提示)。
下图中 ar -rc libmymath.a mymath.o 的含义:
创建(或修改)libmymath.a 归档文件,将 mymath.o 添加 / 替换到其中。
其他常用选项
-s(create index):生成归档的符号索引
静态库中包含符号索引时,链接器能更快找到符号(建议创建静态库时加上 -s,比如 ar -rcs libmymath.a mymath.o)。
-t(table):查看归档文件中的成员列表
示例:ar -t libmymath.a → 会输出 mymath.o(显示库中包含的目标文件)。
-x(extract):从归档中提取指定成员文件
示例:ar -x libmymath.a mymath.o → 从 libmymath.a 中提取出 mymath.o 文件。
静态库(.a)就是用 ar 打包 .o 目标文件得到的归档文件 ------ar 是生成静态库的 "打包工具"。
而动态库(.so)不使用 ar,而是用 gcc -shared 生成(你之前改动态库时替换了 ar 命令)。
bash
lib=libmymath.a
$(lib):mymath.o
ar -rc $@ $^
mymath.o:mymath.c
gcc -c $^
.PHONY:clean
clean:
rm -rf *.o *.a lib
.PHONY:output
output:
mkdir -p lib/include
mkdir -p lib/mymathlib
cp *.h lib/include
cp *.a lib/mymathlib
cpp
//test.c的内容
#include "mymath.h"
int main()
{
extern int myerrno;
printf("1+1=%d\n", add(1,1));
printf("10/0=%d, errno=%d\n", div(10, 0), myerrno);
return 0;
}
cpp
//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;
}
cpp
//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);

我们实习生成静态库的时候 我们发现我们的gcc -L -l -I这三个参数
那么这三个参数是干什么的呢???
- -I(大写字母 I):指定头文件的搜索路径
作用:告诉 GCC 在「编译阶段」去哪里找 #include 引用的头文件(比如 #include "mymath.h")。
默认逻辑:GCC 会自动搜索系统默认头文件路径(如 /usr/include、/usr/local/include),但自己写的头文件(非系统路径)需要用 -I 指定路径。
示例(对应你之前的 mymath.h):
若头文件在 ./lib/include 目录下,用 -I./lib/include 让 GCC 找到它:

因此我们也可以把我们的库文件头文件都拷贝到系统默认文件目录路径下
这样我们链接时候就只需要
cpp
gcc test.c -lmymath
当然也可以使用我们前面学习过的软链接
我们可以把我们当前库文件和头文件的路径存放在系统默认文件目录路径下
假设我们创建的软链接的名字是mylinc
那么头文件就需要是
cpp
#include"mylinc\mymath.h"
这样我们链接时候就只需要
cpp
gcc test.c -lmymath
- -L(大写字母 L):指定库文件的搜索路径
作用:告诉 GCC 在「链接阶段」去哪里找要链接的库文件(比如 libmymath.a 静态库)。
默认逻辑:GCC 会自动搜索系统默认库路径(如 /usr/lib、/usr/local/lib),但自己编译的库(非系统路径)需要用 -L 指定路径。
示例(对应你之前的 libmymath.a):
若库文件在 ./lib/mymathlib 目录下,用 -L./lib/mymathlib 让 GCC 找到它:
- -l(小写字母 l):指定要链接的库名称
作用:告诉 GCC 在「链接阶段」要链接哪个库(需配合 -L 或系统默认路径)。
规则:库名需去掉 lib 前缀 + .a/.so 后缀(比如库文件是 libxxx.a,则填 xxx)。
示例(对应你的 libmymath.a):
要链接这个库,用 -lmymath:
头文件就是mymath.h 库文件就是mymath.c
注意:如果你的mymath.c的文件中#include"绝对路径" 那么gcc参数-I就可以不用写了
直接这样就可以了
bash
gcc test.c -L./lib/mymathlib -lmymath
2.动态库
我们在原本makefile文件中含有静态库的前提下加入了动态库的指令
bash
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
-c:只编译 + 汇编,不链接(生成目标文件 .o)
核心作用
告诉 gcc 停止在「编译 + 汇编」步骤,不执行后续的「链接」操作,最终输出「目标文件(.o 后缀)」,而非可执行文件或库文件。
-fpic:生成「位置无关代码(PIC)」
核心作用
让编译生成的 .o 文件满足「位置无关性」------ 代码加载到内存时,不依赖固定的内存地址,可在任意地址运行。
关键背景(为什么需要 PIC?)
动态链接库(.so)的特点是「被多个程序共享」:
多个程序同时使用同一个 .so 时,系统会把 .so 加载到内存的「任意空闲地址」;
如果 .so 中的代码是「位置相关」的(依赖固定地址),加载到不同地址后会导致指令失效(比如函数调用地址错误);
「位置无关代码(PIC)」通过「相对地址跳转」「全局偏移表(GOT)」等机制,解决了这个问题,确保 .so 可在任意地址正常运行。
补充说明
等价参数 -fPIC:-fpic 对代码大小有轻微限制(适用于多数场景),-fPIC 无限制(兼容性更强,推荐优先使用);
仅用于编译共享库:静态库(.a)不需要 PIC(静态库会被直接嵌入可执行文件,地址在链接时就固定了),只有 .so 必须用 PIC。
-shared:生成动态链接库(.so 文件)
核心作用
告诉 gcc 在「链接阶段」生成「动态链接库(.so 后缀)」,而非可执行文件(默认输出)或静态库(.a)。
关键细节
-shared 是「链接参数」,需要接收 .o 文件(通常是 PIC 格式的 .o)作为输入,链接后生成 .so;
生成的 .so 文件包含完整的可执行代码,可被其他程序通过 dlopen() 或编译时链接(-lxxx)的方式调用;
与静态库(.a)的区别:.so 不嵌入程序,仅在程序运行时动态加载(节省磁盘 / 内存空间),.a 会被嵌入程序(程序体积变大,但运行时无需依赖外部库)。

我们gcc 的参数是告诉编译器的 但是一旦生成可执行文件就和编译器没用关系了
但是可执行文件运行时库需要被加载到内存里 但是这个时候找不到了
加载系统也会在系统默认路径下去搜索
1.拷贝到系统默认的库路径 /lib64/usr/lib64/
2.在系统默认的库路径 /lib64/usr/lib64 / 下建立软连接
3.将自己的库所在的路径,添加到系统的环境变量 LD_LIBRARY_PATH 中
4./etc/ld.so.conf.d 建立自己的动态库路径的配置文件,然后重新 ldconfig 即可
这里我们主要示范第三种和第四种方法
第三种方法

第四种方法


记得一定要ldconfig
那么思考一下 我们库里面有一些共享的变量 比如error
是怎么实现的 通过写实拷贝!!!
1.动态库的相对地址
一个代码变成一个可执行程序中间有这些步骤
源代码(.c/.cpp) → 预处理 → 预处理源码(.i) → 编译 → 汇编代码(.s) → 汇编 → 目标文件(.o/.obj) → 链接 → 可执行程序(.exe/ELF)
当我们编译后我们的代码就生成了逻辑地址
然后当代码被加载到内存中 会占用物理内存 所以会产生物理地址
操作系统会为其开辟虚拟地址空间
逻辑地址会被转化成虚拟地址
然后再通过页表实现虚拟地址和物理地址的映射
++现在大部分电脑逻辑地址和虚拟地址已经不做区分 所以没必要纠结++
动态库的加载是在运行的时候加载
在链接的时候只是确定了要加载哪些库 并没有真的加载
但是多个进程可能使用同一个动态库
如果动态库是绝对地址
意味着动态库加载到每个进程就必须占用不同进程虚拟地址空间的是同一个虚拟地址
但是每个进程的代码可能有不同的虚拟地址被逻辑地址转换的时候占用了
所以一般动态库的地址用的是相对地址 这样可以避免冲突
动态库加载的本质:通过页表,建立「动态库在当前进程内的虚拟地址」与「动态库所在物理内存地址」的映射关
单独的动态库只存在物理地址 不存在虚拟地址
这也是为什么我们之前生成动态库要加上-fPIC
动态库的 "相对地址" 特性,正是通过 -fPIC 编译选项实现的。