静态库
基本概念
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
优点
(1)程序独立,不依赖外部库文件 (2)执行速度快(库代码已在内存中)
缺点
(1)文件体积大(多个程序包含相同库代码)(2)更新库需要重新编译程序
创建示例
# 1. 编译为对象文件
gcc -c mylib.c -o mylib.o
# 2. 打包为静态库
ar rcs libmylib.a mylib.o
# 3. 使用静态库
gcc main.c -L. -lmylib -o myprogram
完整makefile代码示例
lib := libmymath.a
$(lib): mymath.o
ar -rc $@ $^
mymath.o: mymath.c
gcc -c $^
.PHONY: clean
clean:
rm -f *.o *.a
.PHONY: output
output:
mkdir -p lib/include
mkdir -p lib/mymathlib
cp *.h lib/include
cp *.a lib/mymathlib
动态库
基本概念
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
动态库在进程运行的时候是要被加载的而静态库没有,常见动态库被所有的可执行程序(动态链接的),都要使用,动态库也被称为共享库。所以动态库在系统加载之后,会被所有进程共享。
优点
(1)节省磁盘和内存空间(2)便于库的更新(ABI兼容时可热更新)
缺点
(1)需要确保目标系统有正确版本的库(2)稍慢的启动速度(需要加载库)
创建示例
# 1. 编译为位置无关代码
gcc -c -fPIC mylib.c -o mylib.o
# 2. 创建共享库
gcc -shared -o libmylib.so mylib.o
# 3. 使用动态库
gcc main.c -L. -lmylib -o myprogram
# 4. 运行时指定库路径(可选)
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./myprogram
动态库加载需要让系统加载器知道这个动态库在哪里,这样才能找到动态库加载出这个可执行文件。
如何让可执行程序找到动态库:
(1)拷贝到系统默认库路径
# 将动态库复制到系统库目录
sudo cp /path/to/your/lib.so /lib64/
适合系统级库,需要管理员权限
(2)建立软链接到系统路径
# 创建软链接
sudo ln -s /home/sanye/mylib/lib/libmymethod.so /lib64/libmymethod.so
# 验证软链接
ll /lib64/libmymethod.so
推荐方式,不占用额外空间,便于管理
(3)配置环境变量
# 临时生效(当前终端会话)
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/sanye/mylib/lib
# 永久生效(添加到~/.bashrc)
echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/sanye/mylib/lib' >> ~/.bashrc
source ~/.bashrc
适合开发调试,临时修改
(4)创建配置文件(永久有效)
# 创建配置文件
sudo touch /etc/ld.so.conf.d/bit_108.conf
# 编辑配置文件,添加库路径
echo "/home/sanye/mylib/lib" | sudo tee /etc/ld.so.conf.d/bit_108.conf
# 重新加载配置
sudo ldconfig
永久生效,适合生产环境部署
库文件名称和引入库的名称
如:libc.so -> c库,去掉前缀lib,去掉后缀.so,.a
当我们的项目目录如下,想要包含头文件进行编译时:
├── main.c
└── lib/
├── include/
│ └── mymath.h
└── mymathlib/
└── libmymath.a
#include "lib/include/mymath.h"
int main()
{
printf("1+1=%d\n", add(1,1));
return 0;
}
当我们直接包含mymath.h时,我们会发现编译不通过,因为在系统预处理时,要进行头文件展开,头文件展开的第一件事情就是要找到我们的头文件的位置,一般系统会在默认的/user/include/路径下去寻找,但是系统中并没有添加我们的mymyth的头文件,所以找不到。
所以我们的解决方法有:
(1)
gcc main.c -I./lib/include -L./lib/mymathlib -lmymath
-I 选项告诉编译器:"请额外去 ./lib/include 这个目录里找头文件"。这样,它就能找到mymath.h了。-L 选项告诉链接器:"请额外去 ./lib/mymathlib 这个目录里找库文件"。-l 选项告诉链接器:"请链接名为 libmymath.a (或 libmymath.so )的库"。
(2)将自己或者第三方库和头文件拷贝到系统的默认路径中
(3)将头文件里面包含软链接,把软链接放到系统的默认路径下
关键工具和命令
查看库依赖
# 查看程序的库依赖
ldd ./myprogram
# 查看库的依赖
ldd libmylib.so
# 查看符号表
nm libmylib.a
nm -D libmylib.so # 动态库用-D
进程地址空间
虚拟地址与物理地址
虚拟地址
本质:进程视角的逻辑地址
范围:每个进程独立4GB/128TB空间
连续性:总是连续的虚拟空间
管理:操作系统/CPU MMU管理
可见性:进程可见
虚拟地址的作用
(1)内存保护
// 进程A看到的地址空间
0x400000: 自己的代码
0x600000: 自己的数据
0x700000: 自己的堆
// 进程B看到的地址空间
0x400000: 自己的代码 // 相同虚拟地址,不同物理地址!
0x600000: 自己的数据
每个进程有独立的地址空间,无法访问其他进程的内存。
(2)简化编程
// 程序员无需关心物理内存布局
int *p = malloc(1024 * 1024); // 总是返回有效的虚拟地址
(3)内存扩展
// 使用交换文件/分页技术
// 即使只有4GB物理内存,每个进程仍可使用4GB虚拟空间
int huge_array[1024 * 1024 * 1024]; // 4GB数组(在64位系统)
物理地址
本质:硬件视角的实际内存地址
范围:实际物理内存大小(如16GB)
连续性:可能物理不连续
管理:内存控制器管理
可见性:进程不可见,OS内核可见
虚拟地址与物理地址的转换过程
虚拟地址
↓
MMU(内存管理单元)
↓
页表查找(TLB缓存)
↓
物理地址
↓
内存控制器
↓
物理内存/磁盘
关于地址:
1.程序没有加载前的地址(程序)
当编译形成一个可执行程序时,还没有运行这个程序,这个程序是有地址的。
2.程序加载后的地址(进程)
当可执行程序加载到内存当中时,需要占据物理内存,所以每一条指令都有物理地址。重要的指令一般有两套地址,一个是自己指令采用的加载到内存之前的逻辑地址,以及加载到物理内存时的物理地址。
CPU要加载我们的代码时,程序已经加载到内存了,进程数据结构也创建好了,那么CPU该如何执行第一条指令呢?
在进程替换中,进程有一个表头,这个表头会知道代码区和其他区的开始到结束的位置,这个表头包含了一个entry地址,这个entry地址的地址不是物理地址,而是逻辑地址。CPU内有一个寄存器,叫做pc指针或者EIP。一个进程它自己要记录自己的cwd(工作目录)和exe(能找到自己的可执行程序),所以当一个可执行程序被加载到内存时,可执行程序要运行时要拿到表头的entry地址加载到CPU的寄存器当中。因为entry本身就是虚拟地址,所以CPU开始执行,将entry地址跑到正文代码段开始执行,因为是虚拟地址,所以直接读页表。我们可以先不加载物理内存,此时页表当中没有建立物理地址那么这个时候执行会发生缺页中断,然后程序就会被加载进来,被加载进来后,你的程序就具备了物理地址,所以加载之后页表就可以立即填上左侧的虚拟地址和右侧的物理地址。
然后就可以按顺序去执行代码。CPU读取到的地址,内部可能有数据也可能有地址,这个地址是虚拟地址,再通过页表转成物理地址去运行。
3.动态库的地址
相对地址也叫做逻辑地址
绝对地址
库可以在虚拟内存中任意位置加载。库在形成时,让自己内部函数不要采用绝对编址,只表示每个函数在库中的偏移量即可。