linux(9) 库

发布时间:2026/11/1 1:44:03
linux(9) 库

前言:

库这个东西,我们在以前就经常用,只是以前在使用库的时候,有一些问题是无法在学习语言的时候解决的,所以我们今天站在系统的层面来学习一下库;

首先,我们先聊聊什么是库;

然后,我们就要尝试自己编写一个库;

之后,我们来讲一讲如何使用我们的库;

在此之后,我们会引出ELF文件,从而对虚拟地址有一个全新的理解;

最后,我们来讲讲动态链接和静态链接;

那么,做好咯,我们现在发车~

正文:

静态库是什么:

我们先来一个例子:

比如今天有两个人,一个叫梅(我),一个叫张三,我们的老师布置了一个作业,让我们实现一个.......的功能,要自己来写函数;

我比张三厉害一些,所以我很快就写完了这个作业:

[tsx@VM-0-10-centos mei]$ tree . |-- func.c |-- func.h `-- main.c

内部如下:

[tsx@VM-0-10-centos mei]$ cat main.c #include "func.h" int main() { print(); return 0; } [tsx@VM-0-10-centos mei]$ cat func.c #include "func.h" void print() { printf("今天是九月十七日\n"); } [tsx@VM-0-10-centos mei]$ cat func.h #include<stdio.h> void print();

运行结果如下:

[tsx@VM-0-10-centos mei]$ gcc -o main main.c func.c [tsx@VM-0-10-centos mei]$ ./main 今天是九月十七日

所以呀,我很轻松的就完成了这个作业,这时候呢;

张三有点急了,这小子跑过来对我说,“那个,小梅啊,我这作业不会写,你发一份给我呗”

我想了想,直接发过去的话......这老师检查作业非常严格,要是发现我俩写的一模一样,那最后说是谁抄谁的?容易得罪人.......因此我根本不想把源文件给他,这时候,我就想着:

咦?我们不是还有一种编译链接的方式吗?就是把所有.c文件都编译成.o文件,再进行链接

[tsx@VM-0-10-centos mei]$ gcc -c *.c [tsx@VM-0-10-centos mei]$ tree . |-- func.c |-- func.h |-- func.o |-- main |-- main.c |-- main.o `-- makefile

就像这样,我们就得到了两个.o文件,那么我们是不是可以只把.o文件发给张三,然后让他去链接啊,而且.o里面存的是二进制机械码(ELF文件,我们稍后讲),这老师应该也看不懂,所以啊,我对张三说:

“张三啊,我把这个.o文件发给你,你自己链接吧!”

张三挠了挠头:

“也行吧!”

于是呢,我做了如下操作:

[tsx@VM-0-10-centos 917]$ tree . |-- mei | |-- func.c | |-- func.h | |-- func.o | |-- main | |-- main.c | |-- main.o | `-- makefile `-- zhangsan 2 directories, 7 files [tsx@VM-0-10-centos 917]$ mv mei/func.o zhangsan [tsx@VM-0-10-centos 917]$ tree . |-- mei | |-- func.c | |-- func.h | |-- main | |-- main.c | |-- main.o | `-- makefile `-- zhangsan `-- func.o

这时候啊,张三就得到了我的func.o,但是张三这时候挠了挠头,这二进制机械码......张三看不懂啊?!所以张三就来问我:

“你这写的啥啊,我都看不懂你写了什么函数”

这时候我拍了拍脑袋:

“哦~~~我该把头文件发给你的,你等等,我把头文件发给你”

[tsx@VM-0-10-centos 917]$ mv mei/func.h zhangsan [tsx@VM-0-10-centos 917]$ tree . |-- mei | |-- func.c | |-- main | |-- main.c | |-- main.o | `-- makefile `-- zhangsan |-- func.h `-- func.o

这时候呢,张三有了我的头文件和函数,那他是不是就可以自己编main.c函数,然后链接一下就好了呀?

[tsx@VM-0-10-centos zhangsan]$ tree . |-- func.h `-- func.o 0 directories, 2 files [tsx@VM-0-10-centos zhangsan]$ touch zhang_main.c [tsx@VM-0-10-centos zhangsan]$ vim zhang_main.c [tsx@VM-0-10-centos zhangsan]$ gcc -c zhang_main.c [tsx@VM-0-10-centos zhangsan]$ gcc -o zhang_main *.o [tsx@VM-0-10-centos zhangsan]$ tree . |-- func.h |-- func.o |-- zhang_main |-- zhang_main.c `-- zhang_main.o [tsx@VM-0-10-centos zhangsan]$ ./zhang_main 今天是九月十七日

我们发现,事实果然如此啊,只要我把.o文件和头文件给了张三,那我就可以让张三在不看到我源代码的同时,可以进行编译!

那么,后来我反思了一下,我后来发现,这样一个个的把.o文件和头文件给过去,好臃肿,好挫啊,所以我就想了一个办法,把它们捆绑成库:

[tsx@VM-0-10-centos 917]$ tree . |-- mei | |-- func2.c | |-- func2.h | |-- func2.o | |-- func.c | |-- func.h | |-- func.o | |-- main | |-- main.c | |-- main.o | `-- makefile `-- zhangsan

我先让一切都回到没有给张三.o文件和头文件的时候,为了方便我们演示,我又多加了一个func2.c,内容如下:

[tsx@VM-0-10-centos mei]$ cat func2.c #include "func2.h" void print2() { printf("今天是农历八月初七\n"); }

接下来,我们来进行静态库的打包

我们要用到的命令是ar rc 库名称 库用到的.o文件

[tsx@VM-0-10-centos mei]$ ar rc libmei.a func.o func2.o [tsx@VM-0-10-centos mei]$ tree . |-- func2.c |-- func2.h |-- func2.o |-- func.c |-- func.h |-- func.o |-- libmei.a |-- main |-- main.c |-- main.o `-- makefile

这样,我们就形成了我的静态库了;

同时,我们也可以通过 ar -t 来看我的库中包含了哪些.o文件

[tsx@VM-0-10-centos mei]$ ar -t libmei.a func.o func2.o

好,现在我们有库了,我们把库和头文件打包一下:

[tsx@VM-0-10-centos mei]$ tree . |-- func2.c |-- func2.h |-- func2.o |-- func.c |-- func.h |-- func.o |-- libmei.a |-- main |-- main.c |-- main.o |-- makefile `-- yanmei |-- ku `-- tou 3 directories, 11 files [tsx@VM-0-10-centos mei]$ mv libmei.a yanmei/ku [tsx@VM-0-10-centos mei]$ mv func.h func2.h yanmei/tou [tsx@VM-0-10-centos mei]$ tree . |-- func2.c |-- func2.o |-- func.c |-- func.o |-- main |-- main.c |-- main.o |-- makefile `-- yanmei |-- ku | `-- libmei.a `-- tou |-- func2.h `-- func.h

那yanmei这个目录,就是我们最终要给张三的咯:

[tsx@VM-0-10-centos 917]$ tree . |-- mei | |-- func2.c | |-- func2.o | |-- func.c | |-- func.o | |-- main | |-- main.c | |-- main.o | `-- makefile `-- zhangsan `-- yanmei |-- ku | `-- libmei.a `-- tou |-- func2.h `-- func.h

那现在,张三就获得了我精心为他准备的静态库了,那么怎么用呢?

静态库的使用:

静态库的使用有四种,不过嘛,我们今天只讲最常用的两种:

①:用命令指明路径与库名称

[tsx@VM-0-10-centos 917]$ cd zhangsan [tsx@VM-0-10-centos zhangsan]$ touch main.c [tsx@VM-0-10-centos zhangsan]$ vim main.c [tsx@VM-0-10-centos zhangsan]$ gcc -o main main.c -L yanmei/ku -I yanmei/tou -l mei [tsx@VM-0-10-centos zhangsan]$ ./main 今天是九月十七日 今天是农历八月初七 [tsx@VM-0-10-centos zhangsan]$ tree . |-- main |-- main.c `-- yanmei |-- ku | `-- libmei.a `-- tou |-- func2.h `-- func.h

这种方法写起来比较长,不过我觉得比较简单,毕竟相对于把文件安装到系统目录下,实在是方便太多了

②.把头文件,静态库安装到系统目录下:

[tsx@VM-0-10-centos zhangsan]$ sudo cp yanmei/tou/func.h /usr/include [tsx@VM-0-10-centos zhangsan]$ sudo cp yanmei/tou/func2.h /usr/include [tsx@VM-0-10-centos zhangsan]$ sudo cp yanmei/ku/libmei.a /usr/lib [tsx@VM-0-10-centos zhangsan]$ rm main [tsx@VM-0-10-centos zhangsan]$ tree . |-- main.c `-- yanmei |-- ku | `-- libmei.a `-- tou |-- func2.h `-- func.h 3 directories, 4 files [tsx@VM-0-10-centos zhangsan]$ gcc -o main main.c -l mei [tsx@VM-0-10-centos zhangsan]$ ./main 今天是九月十七日 今天是农历八月初七

核心是:

把头文件放到 /usr/include 目录下

把库放到 /usr/lib 目录下

对库的理解:

那我链接的本质不就是,把一堆.o文件给连接到一起吗?而我的库在经过我们上面的探索之后,我们知道了,库本质不也就是一堆.o文件的集合体吗!!!!

此外,注意一下,库必须以lib开头,以.a或.so 结尾,掐头去尾,即为库的真名

动态库:

如何打包动态库:

结论:

gcc -shared (.o文件) -o (动态库名字)

实际演示:

[tsx@VM-0-10-centos mei]$ tree . |-- func2.c |-- func.c |-- main |-- main.c |-- main.o `-- makefile 0 directories, 6 files [tsx@VM-0-10-centos mei]$ gcc -c -fPIC func.c [tsx@VM-0-10-centos mei]$ gcc -c -fPIC func2.c [tsx@VM-0-10-centos mei]$ tree . |-- func2.c |-- func2.o |-- func.c |-- func.o |-- main |-- main.c |-- main.o `-- makefile
fPIC:产⽣位置⽆关码(position independent code)

这个是必须加上的;

[tsx@VM-0-10-centos mei]$ gcc -shared func.o func2.o -o libqingzhu.so [tsx@VM-0-10-centos mei]$ tree . |-- func2.c |-- func2.h |-- func2.o |-- func.c |-- func.h |-- func.o |-- libqingzhu.so |-- main |-- main.c |-- main.o `-- makefile 0 directories, 11 files [tsx@VM-0-10-centos mei]$ mkdir -p qingzhu/tou [tsx@VM-0-10-centos mei]$ cd qingzhu [tsx@VM-0-10-centos qingzhu]$ mkdir ku [tsx@VM-0-10-centos qingzhu]$ cd .. [tsx@VM-0-10-centos mei]$ mv func.h func2.h qingzhu/tou [tsx@VM-0-10-centos mei]$ mv libqingzhu.so qingzhu/ku [tsx@VM-0-10-centos mei]$ tree . |-- func2.c |-- func2.o |-- func.c |-- func.o |-- main |-- main.c |-- main.o |-- makefile `-- qingzhu |-- ku | `-- libqingzhu.so `-- tou |-- func2.h `-- func.h

这样,我们在小梅的目录中,就建好了qingzhu这个目录啦,里面就包含我们的动态库以及头文件;

之后,我们把它移到张三目录中

[tsx@VM-0-10-centos mei]$ mv qingzhu ../zhangsan [tsx@VM-0-10-centos mei]$ cd ../zhangsan [tsx@VM-0-10-centos zhangsan]$ tree . |-- main.c |-- qingzhu | |-- ku | | `-- libqingzhu.so | `-- tou | |-- func2.h | `-- func.h `-- yanmei |-- ku | `-- libmei.a `-- tou |-- func2.h `-- func.h 6 directories, 7 files

我们在演示的时候,不是把静态库和头文件都安装到系统目录了嘛,我们现在删掉,防止对我们产生影响;

[tsx@VM-0-10-centos zhangsan]$ sudo rm /usr/include/func.h [sudo] password for tsx: [tsx@VM-0-10-centos zhangsan]$ sudo rm /usr/include/func2.h [tsx@VM-0-10-centos zhangsan]$ sudo rm /usr/lib/libmei.a

动态库的使用:

给张三安装好后,我们来使用这个动态库啦,

编译时:

①:把库文件和头文件安装到系统指定路径下:

1. 把头文件复制到系统头文件目录 sudo cp qingzhu/tou/*.h /usr/include 2. 把动态库复制到系统库目录 sudo cp qingzhu/ku/libqingzhu.so /usr/lib

②:本地路径指定编译

就和我们在静态库使用做的一样:

[tsx@VM-0-10-centos zhangsan]$ gcc main.c -o main -Iqingzhu/tou -Lqingzhu/ku -lqingzhu [tsx@VM-0-10-centos zhangsan]$ tree . |-- main |-- main.c |-- qingzhu | |-- ku | | `-- libqingzhu.so | `-- tou | |-- func2.h | `-- func.h `-- yanmei |-- ku | `-- libmei.a `-- tou |-- func2.h `-- func.h
问题:

咦?那我们是不是就好啦?那动态库真的太简单啦~~当然不是!!!!!

我们这时候如果运行的话:

[tsx@VM-0-10-centos zhangsan]$ ./main ./main: error while loading shared libraries: libqingzhu.so: cannot open shared object file: No such file or directory

这什么意思呀?

找不到动态库......

怎么会这样呢?

因为!在编译的时候,我们指明的路径,是给编译器指明了路径,而我们在运行的时候,是谁来干活啊?加载器!现在我又没有指明路径,当然找不到咯

那我们这里也给出两种解决方案:

运行时:

①:把库和头文件安装到系统目录

[tsx@VM-0-10-centos zhangsan]$ sudo cp qingzhu/ku/libqingzhu.so /usr/lib [sudo] password for tsx: [tsx@VM-0-10-centos zhangsan]$ sudo cp qingzhu/tou/*.h /usr/include [tsx@VM-0-10-centos zhangsan]$ ./main ./main: error while loading shared libraries: libqingzhu.so: cannot open shared object file: No such file or directory [tsx@VM-0-10-centos zhangsan]$ sudo rm /usr/lib/libqingzhu.so [tsx@VM-0-10-centos zhangsan]$ sudo cp qingzhu/ku/libqingzhu.so /usr/lib64 [tsx@VM-0-10-centos zhangsan]$ ./main 今天是九月十七日 今天是农历八月初七

注意哦,这里把头文件安装到 /usr/lib 是不可以的;

正如我上面写的,就报错了,原因是:

64 位 CentOS,系统库目录不是/usr/lib,64 位动态库要放到/usr/lib64/usr/lib是给 32 位库用的,你把 64 位 so 放进去,动态链接器不会去这里找,所以依然报错。

那就有人要问了,那我静态库怎么就可以安装到 /usr/lib 目录下呢?

静态库只在编译链接阶段使用,不区分/usr/lib和/usr/lib64的运行时查找规则;动态库是程序运行时由动态链接器加载,会严格区分 64 位库目录。

②:设置临时变量:

[tsx@VM-0-10-centos zhangsan]$ sudo rm /usr/lib64/libqingzhu.so [tsx@VM-0-10-centos zhangsan]$ sudo rm /usr/include/func.h /usr/include/func2.h [tsx@VM-0-10-centos zhangsan]$ export LD_LIBRARY_PATH=qingzhu/ku [tsx@VM-0-10-centos zhangsan]$ ./main 今天是九月十七日 今天是农历八月初七

和我们在环境变量改PATH这个环境变量的时候差不多;

就是:PATH=$PATH:(路径)

总结一下:

这里依旧给一头表格来总结:

对比项静态库(.a)动态库(.so)
文件后缀.a.so
链接时机编译链接阶段,把库代码拷贝进可执行程序程序运行阶段,程序启动后才加载库
生成方式ar打包.o目标文件,编译目标文件不需要-fPICgcc 加-fPIC生成位置无关代码,-shared生成 so
程序体积可执行文件大,库代码被复制到程序内部可执行文件更小,库单独存放,多个程序可共享同一份 so
运行依赖编译完成后,不再依赖原静态库文件,删掉.a 照样运行运行时必须存在对应的.so,找不到直接报错
内存占用多程序同时运行,各自保存一份库代码,内存冗余内存中只加载一份库,多个进程共享,节省内存
系统目录差异64 位 CentOS,.a放/usr/lib链接器也能找到;仅链接阶段生效64 位 CentOS,64 位 so 必须放/usr/lib64;动态链接器运行时严格区分目录
更新方式库更新,必须重新编译整个程序才能生效替换新的 so 文件,程序不用重新编译,重启程序即可生效
链接参数-lxxx-lxxx
优点运行不依赖外部库,移植简单,运行速度略快程序体积小、节省内存,库升级方便
缺点可执行文件大,库更新需要重编译,内存浪费运行依赖 so 文件,部署时必须附带动态库,容易出现找不到库的问题

ELF文件:

是啥:

ELF(Executable and Linkable Format),Linux 下目标文件、可执行程序、静态库、动态库统一使用的文件格式。 四种文件都是 ELF:

①.o目标文件(编译后,链接前)

②.a静态库:多个.o打包而成的 ELF 集合

③.so动态库:共享对象 ELF

④. 可执行文件(./a.out,我们直接运行的程序)

结构:

一个ELF文件的组成部分:

①. ELF头(ELF header):描述⽂件的主要特性。其位于⽂件的开始位置,它的主要⽬的是定位⽂
件的其他部分
②. 程序头表(Program header table):列举了所有有效的段(segments)和他们的属性。表⾥
记着每个段的开始的位置和位移(offset)、⻓度,毕竟这些段,都是紧密的放在⼆进制⽂件中,
需要段表的描述信息,才能把他们每个段分割开。
③. 节头表(Section header table):包含对节(sections)的描述。
④. 节(Section):ELF⽂件中的基本组成单位,包含了特定类型的数据。ELF⽂件的各种信息和
数据都存储在不同的节中,如代码节存储了可执⾏代码,数据节存储了全局变量和静态数据等。

长啥样:

我这里给大家画了一下;

如何读:

# 查看ELF头部信息 readelf -h main # 查看程序头表PHT readelf -l main # 查看节头表SHT readelf -S main

使用演示:

[tsx@VM-0-10-centos zhangsan]$ readelf -h main ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x400560 Start of program headers: 64 (bytes into file) Start of section headers: 6464 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 9 Size of section headers: 64 (bytes) Number of section headers: 30 Section header string table index: 29 [tsx@VM-0-10-centos zhangsan]$ readelf -l main Elf file type is EXEC (Executable file) Entry point 0x400560 There are 9 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x00000000000001f8 0x00000000000001f8 R E 8 INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x000000000000082c 0x000000000000082c R E 200000 LOAD 0x0000000000000e00 0x0000000000600e00 0x0000000000600e00 0x000000000000023c 0x0000000000000240 RW 200000 DYNAMIC 0x0000000000000e18 0x0000000000600e18 0x0000000000600e18 0x00000000000001e0 0x00000000000001e0 RW 8 NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254 0x0000000000000044 0x0000000000000044 R 4 GNU_EH_FRAME 0x0000000000000700 0x0000000000400700 0x0000000000400700 0x0000000000000034 0x0000000000000034 R 4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 10 GNU_RELRO 0x0000000000000e00 0x0000000000600e00 0x0000000000600e00 0x0000000000000200 0x0000000000000200 R 1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag .note.gnu.build-id 06 .eh_frame_hdr 07 08 .init_array .fini_array .jcr .dynamic .got [tsx@VM-0-10-centos zhangsan]$ readelf -S main There are 30 section headers, starting at offset 0x1940: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 0000000000400238 00000238 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.ABI-tag NOTE 0000000000400254 00000254 0000000000000020 0000000000000000 A 0 0 4 [ 3] .note.gnu.build-i NOTE 0000000000400274 00000274 0000000000000024 0000000000000000 A 0 0 4 [ 4] .gnu.hash GNU_HASH 0000000000400298 00000298 0000000000000038 0000000000000000 A 5 0 8 [ 5] .dynsym DYNSYM 00000000004002d0 000002d0 00000000000000f0 0000000000000018 A 6 1 8 [ 6] .dynstr STRTAB 00000000004003c0 000003c0 0000000000000077 0000000000000000 A 0 0 1 [ 7] .gnu.version VERSYM 0000000000400438 00000438 0000000000000014 0000000000000002 A 5 0 2 [ 8] .gnu.version_r VERNEED 0000000000400450 00000450 0000000000000020 0000000000000000 A 6 1 8 [ 9] .rela.dyn RELA 0000000000400470 00000470 0000000000000018 0000000000000018 A 5 0 8 [10] .rela.plt RELA 0000000000400488 00000488 0000000000000060 0000000000000018 AI 5 23 8 [11] .init PROGBITS 00000000004004e8 000004e8 000000000000001a 0000000000000000 AX 0 0 4 [12] .plt PROGBITS 0000000000400510 00000510 0000000000000050 0000000000000010 AX 0 0 16 [13] .text PROGBITS 0000000000400560 00000560 0000000000000182 0000000000000000 AX 0 0 16 [14] .fini PROGBITS 00000000004006e4 000006e4 0000000000000009 0000000000000000 AX 0 0 4 [15] .rodata PROGBITS 00000000004006f0 000006f0 0000000000000010 0000000000000000 A 0 0 8 [16] .eh_frame_hdr PROGBITS 0000000000400700 00000700 0000000000000034 0000000000000000 A 0 0 4 [17] .eh_frame PROGBITS 0000000000400738 00000738 00000000000000f4 0000000000000000 A 0 0 8 [18] .init_array INIT_ARRAY 0000000000600e00 00000e00 0000000000000008 0000000000000008 WA 0 0 8 [19] .fini_array FINI_ARRAY 0000000000600e08 00000e08 0000000000000008 0000000000000008 WA 0 0 8 [20] .jcr PROGBITS 0000000000600e10 00000e10 0000000000000008 0000000000000000 WA 0 0 8 [21] .dynamic DYNAMIC 0000000000600e18 00000e18 00000000000001e0 0000000000000010 WA 6 0 8 [22] .got PROGBITS 0000000000600ff8 00000ff8 0000000000000008 0000000000000008 WA 0 0 8 [23] .got.plt PROGBITS 0000000000601000 00001000 0000000000000038 0000000000000008 WA 0 0 8 [24] .data PROGBITS 0000000000601038 00001038 0000000000000004 0000000000000000 WA 0 0 1 [25] .bss NOBITS 000000000060103c 0000103c 0000000000000004 0000000000000000 WA 0 0 1 [26] .comment PROGBITS 0000000000000000 0000103c 000000000000002d 0000000000000001 MS 0 0 1 [27] .symtab SYMTAB 0000000000000000 00001070 0000000000000600 0000000000000018 28 46 8 [28] .strtab STRTAB 0000000000000000 00001670 00000000000001c4 0000000000000000 0 0 1 [29] .shstrtab STRTAB 0000000000000000 00001834 0000000000000108 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), l (large), p (processor specific)

我们截一张图看看,哇,好多节,有三十个吧

我们再看这个,叫段,有九个

那么段和节什么关系?

我如果啊,要把一个文件加载到内存里,是按照内存页加载的,如果一个节呢,没有占满一个内存页,也占一整个内存页,就会造成很大的内存浪费;所以呀,我们把相同权限的节,放到一个段里,然后进行加载,这样的话,就可以给我们省下来内存;故,段是在加载过程中的概念,实际上在文件中是不存在的

内存页是啥?内存中的内存页,就相当于磁盘里的块,都是读取最小单位

ELF与虚拟地址:

首先呢,先问大家一个问题,我们的ELF文件在编好以后,,有没有“地址”呀?

答案是有的,只不过不是物理地址,而是逻辑地址(可以理解为虚拟地址),我们会记录下每一行代码的偏移量,来作为我们的逻辑地址。

我们的计算机在进行工作的时候,要求采用“平坦模式”进行工作,要求ELF文件对自己的代码和数据进行统一编址

也就是说,其实虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执行程序进⾏统⼀编址了

有什么用呢?那我再问大家一个问题:

进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪⾥来的?
从ELF各个 segment来,每个segment有⾃⼰的起始地址和⾃⼰的⻓度,⽤来初始化内核结构中的[start, end]等范围数据,另外在⽤详细地址,填充⻚表.

所以:虚拟地址机制,不仅os支持,编译器也支持!!!

所以,我们的流程是,我的可执行程序中,本来就有编好的虚拟地址,我要执行程序时,我用我ELF文件中的虚拟地址来给mm_struct赋值,将虚拟地址写入页表,然后在我分配到物理内存的时候,将物理地址填页表。

这一块与动态链接关系比较深,那我们就先讲讲动态链接;

动态链接:

那么我们的动态库啊,是出了名的省空间,不需要把动态库复制到每一个可执行文件中,那么我们来看看我们是如何调用动态库的;

首先我们要知道,动态库是ELF文件形式,而这个形式由于“平坦模式”,我们的ELF文件内部,是存着偏移量的,我们只要根据偏移量,加上虚拟起始地址,我们就可以得到它的虚拟地址,再通过页表,我们可以得知它的物理地址。

而动态链接的关键是,让我们在可执行文件中用到的函数,与库中对应的函数建立关系,然后对我们加载到内存中的程序的库函数调⽤进⾏地址修改,在内存中⼆次完成地址设置;

可是,这样我们修改的不就是代码区了嘛......代码区是只读的啊!!!不可修改

所以,我们就引入了GOT表(全局偏移量表)

动态链接采⽤的做法是在.data(可执⾏程序或者库⾃⼰)中专⻔预留⼀⽚区域⽤来存放函数
的跳转地址,它也被叫做全局偏移表GOT,表中每⼀项都是本运⾏模块要引⽤的⼀个全局变量或
函数的地址。
因为.data区域是可读写的,所以可以⽀持动态进⾏修改
注意:
①. 由于代码段只读,我们不能直接修改代码段。但有了GOT表,代码便可以被所有进程共享。但在不同进程的地址空间中,各动态库的绝对地址、相对位置都不同。反映到GOT表上,就是每个进程的每个动态库都有独⽴的GOT表,所以进程间不能共享GOT表。
②. 在单个.so下,由于GOT表与.text的相对位置是固定的,我们完全可以利⽤CPU的相对寻址来找到GOT表。
③. 在调⽤函数的时候会⾸先查表,然后根据表中的地址来进⾏跳转,这些地址在动态库加载的时候会被修改为真正的地址。
④. 这种⽅式实现的动态链接就被叫做PIC地址⽆关代码 。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运⾏,并且能够被所有进程共享,这也是为什么之前我们给编译器指定-fPIC参数的原因,PIC=相对编址+GOT。

静态链接:

静态链接,就是链接器把程序用到的所有.o目标文件、静态库代码,直接复制合并到最终可执行文件里。

过程:

此时我们有了许多ELF文件,我们会在符号表中,给我们的未定义符号,找到唯一的符号定义,然后把我们所需要的多个ELF文件,合并为一个ELF文件,之后再次进行编址,再然后将符号表中的未定义符号前的地址进行修改,改为我们最终编好的逻辑地址

(注意,在此处遍历符号表指的是,会遍历每一个ELF文件的符号表找我需要的,如果一个ELF文件中有我需要的符号,哪怕只有这一个符号我需要,后续我也会把这一个.o文件全部包含)

优点:

运行时不需要依赖外部库,移植方便。

缺点:

多个程序如果都用同一个库,内存里会加载多份库代码,浪费内存;程序体积更大。

总结:

编译链接阶段,库代码直接打包进 exe;运行时不再找库。

尾:

怎么从快八点多写到现在了,好累.......好累.....

中午吃啥?