什么是库
库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:
- 静态库 .a[Linux]、.lib[windows]
- 动态库 .so[Linux]、.dll[windows]
cpp
// ubuntu 动静态库
// C
$ ls -l /lib/x86_64-linux-gnu/libc-2.31.so
-rwxr-xr-x 1 root root 2029592 May 1 02:20 /lib/x86_64-linux-gnu/libc-2.31.so
$ ls -l /lib/x86_64-linux-gnu/libc.a
-rw-r--r-- 1 root root 5747594 May 1 02:20 /lib/x86_64-linux-gnu/libc.a
//C++
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so -l
lrwxrwxrwx 1 root root 40 Oct 24 2022 /usr/lib/gcc/x86_64-linux-
gnu/9/libstdc++.so -> ../../../x86_64-linux-gnu/libstdc++.so.6
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
/usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
// Centos 动静态库
// C
$ ls /lib64/libc-2.17.so -l
-rwxr-xr-x 1 root root 2156592 Jun 4 23:05 /lib64/libc-2.17.so
[whb@bite-alicloud ~]$ ls /lib64/libc.a -l
-rw-r--r-- 1 root root 5105516 Jun 4 23:05 /lib64/libc.a
// C++
$ ls /lib64/libstdc++.so.6 -l
lrwxrwxrwx 1 root root 19 Sep 18 20:59 /lib64/libstdc++.so.6 ->
libstdc++.so.6.0.19
$ ls /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a -l
-rw-r--r-- 1 root root 2932366 Sep 30 2020 /usr/lib/gcc/x86_64-redhat-
linux/4.8.2/libstdc++.a
预备工作,准备好代码,在任意新增"库文件"

cpp
//file.c
#include<stdio.h>
int main()
{
Myprint();
return 0;
}
//hello.c
#include<stdio.h>
void Myprint()
{
printf("hello\n ");
}
//hello.h
void Myprint();
bash
//Makefile
libmyc.a:hello.o file.o
ar -rc $@ $^
hello.o:hello.c
gcc -c $<
file.o:file.c
gcc -c $<
.PHONY:output
output:
mkdir -p lib/include
mkdir -p lib/mylib
cp -f *.h lib/include
cp -f *.a lib/mylib
.PHONY:clean
clean:
rm -f *.o libmyc.a

上述代码实现是静态库的原理,原来只要我把.o文件,打包链接起来就可以达到库的效果!头文件包含了方法的声明,库包含了方法的实现,所有的库(无论是动还是静),本质是源文件对应的.o!
静态库
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再
需要静态库,本质就是.o打了一个包。
- 一个可执行程序可能用到许多的库,这些库运行有的是静态库,有的是动态库,而我们的编译默认为动态链接库,只有在该库下找不到动态.so的时候才会采用同名静态库。我们也可以使用 gcc的 -static 强转设置链接静态库。
ar 是 gnu 归档工具, rc 表示 (replace and create),.a静态库,本质是一种归档文件,不需要使用者解包,而用gcc/g++直接进行链接即可!
- t: 列出静态库中的文件
- v:verbose 详细信息
库的命名一般一lib开头,.a结尾,去首区尾就是库的名字

库的使用
1.指定参数
cpp
//user.c
#include<stdio.h>
int main()
{
Myprint();
return 0;
}
如果我们要连接任何非C/C++标准库(包括其他外部或者我们自己写的) 都要指明 -l 或 -L

去哪里找(-L)找什么库(-l)
- -L: 指定库路径
- -I: 指定头文件搜索路径
- -l: 指定库名
- 测试目标文件生成后,静态库删掉,程序照样可以运行

或着在需要链接库的.c文件里面导包链接绝对路径
2.将库拷贝至Linux的默认路径下



因为是第三方库,还得带-l
所以,对库的安装,其实就是将对应的头文件,库文件拷贝到系统的指定文件下,将来编译时只要指定库名称,gcc自动找头文件和库文件
动态库
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
动态库生成
- shared: 表示生成共享库格式
- fPIC:产生位置无关码(position independent code)
- 库名规则:libxxx.so
与静态库不同的是,静态库使用ar归档类打包,动态库使用gcc打包(gcc既可以形成二进制文件,也可以形成动态库)
bash
libmyc.so:hello.o file.o
gcc -shared -o $@ $^
hello.o:hello.c
gcc -fPIC -c $<
file.o:file.c
gcc -fPIC -c $<
.PHONY:output
output:
mkdir -p lib/include
mkdir -p lib/mylib
cp -f *.h lib/include
cp -f *.so lib/mylib
.PHONY:clean
clean:
rm -f *.o libmyc.so
动态库的使用
cpp
//user.c
#include<stdio.h>
#include"hello.h"
int main()
{
Myprint();
return 0;
}
在同级目录下直接链接

库运行搜索路径

1.拷贝 .so 文件到系统共享库路径下
一般指 /usr/lib、/usr/local/lib、/lib64 或者开篇指明的库路径等入,如静态库处理一样
2.向系统共享库路径下建立同名软连接
3.更改环境变量: LD_LIBRARY_PATH

将动态库的路径填进LD_LIBRARY_PATH环境变量里,系统也会在这里面找,像这种更改环境变量的操作,系统重启后就恢复了

4. ldconfig方案:配置/ etc/ld.so.conf.d/ ,ldconfig更新

创建一个.conf文件,将库的路径填入,如果不行就用nano,vim可能被配置过


细节:动静态库g++/gcc默认使用动态库,非静态链接只能-static,一旦-static,就必须得存在对应的静态库,只存在静态库,可执行程序对于该库,只能静态链接了

在Linux系统下,默认安装大部分库,默认优先安装动态库,库:应用程序=1:n,vs不仅可以形成可执行程序,也能形成动静态库
目标文件
编译和链接这两个步骤,在Windows下被我们的IDE封装的很完美,我们一般都是一键构建非常⽅便,但一旦遇到错误的时候呢,尤其是链接相关的错误,很多人就束手无策了。在Linux下,我们之前也学习过如何通过gcc编译器来完成这一系列操作。

在编译之后会生成两个扩展名为 .o 的文件,它们被称作目标⽂件。要注意的是如果我们
修改了一个原文件,那么只需要单独编译它这一个,而不需要浪费时间重新编译整个工程。目标文件是一个二进制的文件,文件的格式是 ELF ,是对二进制代码的一种封装。
bash
$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
## file命令⽤于辨识⽂件类型。
ELF文件
要理解编译链链接的细节,我们不得不了解⼀下ELF文件。其实有以下四种文件其实都是ELF文件:
- 可重定位文件(Relocatable File) :即 xxx.o 文件。包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
- 可执行文件(Executable File) :即可执行程序。
- 共享目标文件(Shared Object File) :即 xxx.so文件。
- 内核转储(core dumps) ,存放当前进程的执行上下文,用于dump信号触发。一个ELF文件由以下四部分组成:
- ELF头(ELF header) :描述文件的主要特性。其位于文件的开始位置,它的主要目的是定位文件的其他部分。
- 程序头表(Program header table) :列举了所有有效的段(segments)和他们的属性。表里记着每个段的开始的位置和位移(offset)、长度,毕竟这些段,都是紧密的放在二进制文件中,需要段表的描述信息,才能把他们每个段分割开。
- 节头表(Section header table) :包含对节(sections)的描述。
- 节(Section ):ELF文件中的基本组成单位,包含了特定类型的数据。ELF文件的各种信息和数据都存储在不同的节中,如代码节存储了可执行代码,数据节存储了全局变量和静态数据等。最常见的节:代码节(.text):用于保存机器指令,是程序的主要执行部分。数据节(.data):保存已初始化的全局变量和局部静态变量。

ELF从形成到加载轮廓
ELF形成可执行:
- step-1:将多份 C/C++ 源代码,翻译成为目标 .o 文件
- step-2:将多份 .o 文件section进行合并

一个ELF存着各种数据,链接就是把每个ELF合并起来重新形成一个ELF,合并是在链接时进行的,但是并不是这么简单的合并,也会涉及对库合并,此处不做过多追究
ELF可执行文件加载
一个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成segment
合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等。
这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到一起很显然,这个合并工作也已经在形成ELF的时候,合并方式已经确定了,具体合并原则被记录在了ELF的**程序头表(Program header table)**中
查看可执行程序的section: readelf -S xxx
bash
[xm@hcss-ecs-68d7 study_06]$ readelf -S user
There are 30 section headers, starting at offset 0x1918:
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
000000000000003c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002d8 000002d8
00000000000000f0 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 00000000004003c8 000003c8
000000000000006e 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000400436 00000436
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
0000000000000048 0000000000000018 AI 5 23 8
[11] .init PROGBITS 00000000004004d0 000004d0
000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 00000000004004f0 000004f0
0000000000000040 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400530 00000530
0000000000000182 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 00000000004006b4 000006b4
0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 00000000004006c0 000006c0
0000000000000010 0000000000000000 A 0 0 8
[16] .eh_frame_hdr PROGBITS 00000000004006d0 000006d0
0000000000000034 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000400708 00000708
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
0000000000000030 0000000000000008 WA 0 0 8
[24] .data PROGBITS 0000000000601030 00001030
0000000000000004 0000000000000000 WA 0 0 1
[25] .bss NOBITS 0000000000601034 00001034
0000000000000004 0000000000000000 WA 0 0 1
[26] .comment PROGBITS 0000000000000000 00001034
000000000000002d 0000000000000001 MS 0 0 1
[27] .symtab SYMTAB 0000000000000000 00001068
00000000000005e8 0000000000000018 28 46 8
[28] .strtab STRTAB 0000000000000000 00001650
00000000000001bf 0000000000000000 0 0 1
[29] .shstrtab STRTAB 0000000000000000 0000180f
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)
NR编号就好比是一个数组存放不同的Section, 属性(Type),地址(Address),以及偏移量(Offset),通过程序头表(Program header table)存放的起始地址和偏移量(Offset),就能找到对于的Section ,也是通过这种方式划分不同的section节点,每个节点都存放着二进制文件的不同信息
读取可执行程序合并后的ELF:readelf -l
bash
[xm@hcss-ecs-68d7 study_06]$ readelf -l user
Elf file type is EXEC (Executable file)
Entry point 0x400530
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
0x00000000000007fc 0x00000000000007fc R E 200000
LOAD 0x0000000000000e00 0x0000000000600e00 0x0000000000600e00
0x0000000000000234 0x0000000000000238 RW 200000
DYNAMIC 0x0000000000000e18 0x0000000000600e18 0x0000000000600e18
0x00000000000001e0 0x00000000000001e0 RW 8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x00000000000006d0 0x00000000004006d0 0x00000000004006d0
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
将不同section合并成一个section,在内存上加载就在一块了
为什么要将section合并成为segment?
- Section合并的主要原因是为了减少页面碎片,一个section可能不足4KB操作单位和文件系统操作单位一样都是4KB,为提高内存使用效率。如果不进行合并,假设页面大小为4096字节(内存块基本大小,加载,管理的基本单位),如果.text部分为4097字节,.init部分为512字节,那么它们将占用3个页面,而合并后,它们只需2个页面。
- 此外,操作系统在加载程序时,会将具有相同属性的section合并成一个大的segment,这样就可以实现不同的访问权限,从而优化内存管理和权限访问控制。
对于 程序头表和节头表又有什么用呢,其实 ELF文件提供 2 个不同的视图/视角来让我们理解这
两个部分:
链接视图(Linking view) - 对应节头表 Section header table
- 文件结构的粒度更细,将文件按功能模块的差异进行划分,静态链接分析的时候一般关注的是链接视图,能够理解ELF文件中包含的各个部分的信息。
- 为了空间布局上的效率,将来在链接目标文件时,链接器会把很多节(section)合并,规整成可执行的段(segment)、可读写的段、只读段等。合并了后,空间利用率就高了,否则,很小的很小的一段,未来物理内存页浪费太大(物理内存页分配一般都是整数倍一块给你,比如4k),所以,链接器趁着链接就把小块们都合并了。
执行视图(execution view) - 对应程序头表 Program header table
- 告诉操作系统,如何加载可执行文件,完成进程内存的初始化。一个可执行程序的格式中,一定有 program header table 。
说白了就是:一个在链接时作用,一个在运行加载时作用

从链接视图来看:
- 命令 readelf -S hello.o 可以帮助查看ELF文件的节头表。
- .text节 :是保存了程序代码指令的代码节。
- .data节 :保存了初始化的全局变量和局部静态变量等数据。
- .rodata节 :保存了只读的数据,如一行C语言代码中的字符串。由于.rodata节是只读的,所
以只能存在于一个可执行文件的只读段中。因此,只能是在text段(不是data段)中到.rodata节。 - .BSS节 :为未初始化的全局变量和局部静态变量预留位置
- .symtab节 : Symbol Table 符号表,就是源码里面那些函数名、变量名和代码的对应关系。
- .got.plt节 (全局偏移表-过程链接表):.got节保存了全局偏移表。.got节和.plt节一起提供
了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。使用readelf 命令查看 .so文件可以看到该节。
从执行视图来看:
- 告诉操作系统哪些模块可以被加载进内存。
- 加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执行的。我们可以在ELF头 中找到文件的基本信息,以及可以看到ELF头是如何定位程序头表和节头表的。例如我们查看下hello.o这个可重定位文件的主要信息:
bash
[xm@hcss-ecs-68d7 study_06]$ readelf -h user
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: 0x400530
Start of program headers: 64 (bytes into file)
Start of section headers: 6424 (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
理解连接与加载
静态链接
无论是自己的.o, 还是静态库中的.o,本质都是把.o文件进行连接的过程,所以:研究静态链接,本质就是研究.o是如何链接的
链接其实就是将编译之后的所有目标文件连同用到的一些静态库运行时库组合,拼装成一个独立的可执行文件。其中就包括我们之前提到的地址修正,当所有模块组合在一起之后,链接器会根据我们的.o文件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从而修正它们的地址。这其实就是静态链接的过程

所以,链接过程中会涉及到对.o中外部符号进行地址重定位
ELF加载与进程地址空间
虚拟地址/逻辑地址
一个ELF程序,在没有被加载到内存的时候,有没有地址呢?进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪里来的?
一个ELF程序,在没有被加载到内存的时候,本来就有地址,当代计算机工作的时候,都采用"平坦
模式"进行工作。所以也要求ELF对自己的代码和数据进行统一编址,下面是 objdump -S 反汇编
之后的代码

最左侧的就是ELF的虚拟地址,其实,严格意义上应该叫做逻辑地址(起始地址+偏移量), 但是我们
认为起始地址是0.也就是说,其实虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执
行程序进行统一编址了.
进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪里来的?
从ELF各个segment来,每个segment有自己的起始地址和自己的长度,⽤来初始化内核结构中的[start, end]等范围数据,另外在用详细地址,填充页表.
所以:虚拟地址机制,不光光OS要支持,编译器也要支持.

进程如何看到动态库
