前言:
库这个东西,我们在以前就经常用,只是以前在使用库的时候,有一些问题是无法在学习语言的时候解决的,所以我们今天站在系统的层面来学习一下库;
首先,我们先聊聊什么是库;
然后,我们就要尝试自己编写一个库;
之后,我们来讲一讲如何使用我们的库;
在此之后,我们会引出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 |
| 链接时机 | 编译链接阶段,把库代码拷贝进可执行程序 | 程序运行阶段,程序启动后才加载库 |
| 生成方式 | ar打包.o目标文件,编译目标文件不需要-fPIC | gcc 加-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 目标文件、静态库代码,直接复制合并到最终可执行文件里。
过程:
优点:
运行时不需要依赖外部库,移植方便。
缺点:
多个程序如果都用同一个库,内存里会加载多份库代码,浪费内存;程序体积更大。
总结:
编译链接阶段,库代码直接打包进 exe;运行时不再找库。

尾:
怎么从快八点多写到现在了,好累…….好累…..
中午吃啥?






