库制作与原理

1502 字
8 分钟
库制作与原理

1. 什么是库#

库是写好的、现有的、成熟的、可以复用的代码。每个程序都依赖很多底层库,库的存在使得开发不必从零开始。

本质:库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。

库的两种类型:

  • 静态库:.a(Linux)、.lib(Windows)

  • 动态库:.so(Linux)、.dll(Windows)

2. 静态库#

2.1 特点#

  • 静态库(.a)在程序编译链接时把库的代码直接拷贝到可执行文件中。

  • 程序运行时不再需要静态库。

  • 可执行文件体积较大,但部署简单(不依赖外部库)。

2.2 静态库的本质#

库的本质:无论是静态库还是动态库,其基础都是源文件编译后生成的 .o 目标文件。静态库(.a)就是多个 .o 文件打包成一个归档文件。使用 gcc/g++ 链接时,链接器会从归档中提取需要的模块合并到最终可执行文件中。

静态库中不包含 main 函数,库只提供可复用的功能模块,不包含程序入口。

2.3 制作静态库#

使用 ar(GNU归档工具)将多个 .o 文件打包成 .a 文件。

示例:将 my_stdio.c 和 my_string.c 封装成静态库 libmystdio.a。

Makefile 片段:

libmystdio.a: my_stdio.o my_string.o
ar -rc $@ $^
%.o: %.c
gcc -c $<
  • ar -rc:r 表示插入或替换文件,c 表示创建归档文件。

  • 查看静态库内容:ar -tv libmystdio.a(t 列出文件,v 显示详细信息)。

2.4 使用静态库#

编译时需指定头文件路径、库路径和库名。

  • 头文件搜索路径:-I 选项

  • 库文件搜索路径:-L 选项

  • 库名:-l 选项(去掉前缀 lib 和后缀 .a/.so)

使用场景示例:

Terminal window
# 场景1:头文件和库文件已安装到系统路径(如 /usr/lib, /usr/include)
$ gcc main.c -lmystdio
# 场景2:头文件和库文件在当前目录
$ gcc main.c -L. -lmystdio
# 场景3:头文件和库文件在独立路径
$ gcc main.c -I头文件路径 -L库文件路径 -lmystdio

注意:静态库链接后,可执行文件不再依赖该库,即使删除静态库,程序仍可运行。

3. 动态库#

3.1 特点#

  • 动态库(.so)在程序运行时才被加载到内存,多个程序可以共享内存中的同一份库代码。

  • 可执行文件只包含函数入口地址的引用,体积小。

  • 节省磁盘和内存空间,但部署时需要确保运行时能找到动态库。

库<应用程序> = 1(一份物理内存中的动态库可被多个进程共享)。

3.2 制作动态库#

生成位置无关码(-fPIC)并链接为共享库(-shared)。

Makefile 片段:

libmystdio.so: my_stdio.o my_string.o
gcc -o $@ $^ -shared
%.o: %.c
gcc -fPIC -c $<
  • -fPIC:生成位置无关代码,使得动态库可以被加载到任意内存地址。

  • -shared:生成共享库。

3.3 使用动态库#

编译命令与静态库类似:

Terminal window
$ gcc main.c -L. -lmystdio

生成的可执行文件 a.out 在运行时需要找到 libmystdio.so。

3.4 运行时动态库查找问题#

使用 ldd a.out 查看可执行文件依赖的共享库:

Terminal window
$ ldd a.out
linux-vdso.so.1 => (0x...)
libmystdio.so => not found
libc.so.6 => /lib64/libc.so.6
/lib64/ld-linux-x86-64.so.2

出现 not found 表示运行时找不到动态库。

关键理解:编译时用 -L 和 -l 只告诉了编译器(gcc)库的位置,但操作系统(动态链接器)并不知道。系统 != gcc,运行时需要另外告诉系统库的路径。

3.5 解决方案#

  1. 拷贝库到系统库路径 如 /usr/lib、/usr/local/lib、/lib64 等(需 root 权限)。

  2. 创建软链接 在系统库路径下创建同名软链接指向实际库文件。

  3. 设置环境变量 LD_LIBRARY_PATH

Terminal window
export LD_LIBRARY_PATH=库所在目录:$LD_LIBRARY_PATH
  1. 配置 /etc/ld.so.conf.d/ 并运行 ldconfig 在 /etc/ld.so.conf.d/ 下创建一个 .conf 文件,写入库路径,然后运行 ldconfig 更新缓存。
Terminal window
$ sudo cat /etc/ld.so.conf.d/bit.conf
/root/tools/linux
$ sudo ldconfig

4. 编译链接的默认行为#

  • 结论1:gcc/g++ 默认使用动态库。如果同时存在同名静态库和动态库,优先链接动态库。

  • 结论2:若想强制静态链接,需加 -static 选项,此时必须存在对应的静态库。如果只存在静态库,则只能静态链接。

  • 结论3:Linux 系统默认安装的库通常优先提供动态库(.so),静态库(.a)可能需要单独安装(如 -devel 包)。

5. 可执行程序的加载过程(简述)#

  1. 操作系统读取可执行文件的头部信息,获取程序入口地址。

  2. 根据文件中的段信息,将代码段、数据段等映射到虚拟内存的合适位置。

  3. 对于动态链接的可执行文件,还需加载动态链接器(如 /lib64/ld-linux-x86-64.so.2)。

  4. 动态链接器负责加载所需的共享库(.so),完成符号重定位(将库函数调用地址修正为实际内存地址)。

  5. 最后跳转到程序入口开始执行。

6. 虚拟地址空间与动态库加载#

  • 每个进程拥有独立的虚拟地址空间(例如 32 位系统为 4GB,其中内核占用一部分)。

  • 动态库被加载到虚拟地址空间的共享区域,通过页表映射到物理内存。

  • 由于使用了位置无关代码(-fPIC),动态库可以被加载到任意地址而不需修改代码,从而实现多个进程共享同一份物理内存中的库。

7. 总结#

  • 静态库:链接时合并到可执行文件,运行时独立,生成方式 ar -rc。

  • 动态库:运行时加载,节省资源,生成方式 gcc -shared -fPIC。

  • 库的查找:编译时用 -L 和 -l,运行时依赖动态链接器搜索路径(系统目录、LD_LIBRARY_PATH、ldconfig 配置)。

  • 加载过程:操作系统将程序映射到虚拟内存,动态链接器完成共享库的加载和符号重定位。

  • 虚拟内存:使动态库能在物理内存中只保留一份副本,被多个进程共享。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Profile Image of the Author
HandlerOne
おかえり
公告
本站已整理 138 篇技术笔记,并按 8 条主线排成学习路线(141 站);站内搜索与桌宠对话均已上线。
分类
标签
最新动态
站点统计
文章
138
分类
12
标签
42
总字数
597,044
站点信息
构建平台
腾讯云 · Nginx
博客版本
v6.16.8
文章目录