欢迎光临
我们一直在努力

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目标文件,编译目标文件不需要-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 目标文件、静态库代码,直接复制合并到最终可执行文件里。

过程:

  • 多个.o文件,把各自的代码段、数据段合并拼接;
  • 做符号重定位:把所有函数调用、全局变量的引用,填成真实的内存地址;
  • 最终生成的可执行文件,包含了全部需要的代码,运行时不再依赖外部库。
  • 优点:

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

    缺点:

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

    总结:

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

    尾:

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

    中午吃啥?

    赞(0)
    未经允许不得转载:171主机测评 » linux(9) 库
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址