一、什么是库
(补充:库中不能出现 main 函数,因为库会和有 main 函数的程序链接在一起)
库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。 本质上来说库是一种,可以被操作系统载入内存执行。库有两种:
Linux 库分为静态库和动态库,本质是二进制代码集合。静态库在编译时合并进可执行文件,动态库在运行时加载。ELF 格式包含程序头表和节头表,分别对应操作系统段映射和编译器节管理。静态链接在编译期完成地址重定位,动态链接推迟至运行时,通过 GOT 表和 PLT 实现延迟绑定。进程通过虚拟地址空间映射共享动态库,节省内存。掌握库的制作、链接原理及 ELF 结构有助于深入理解程序加载与内存管理机制。

(补充:库中不能出现 main 函数,因为库会和有 main 函数的程序链接在一起)
库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。 本质上来说库是一种,可以被操作系统载入内存执行。库有两种:
我们看下面的库示例可以知道,库本质就是 linux 指定目录下的普通文件,所以我们就可以像对常规文件读写那样将库文件内容拷贝到我的可执行文件里,或者将库文件内容加载到内存里。
// 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
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
lrwxrwxrwx 1 root root 2156592 Jun 4 23:05 /lib64/libc-2.17.so
$ ls /lib64/libc.a
-rw-r--r-- 1 root root 5105516 Jun 4 23:05 /lib64/libc.a
// C++
$ ls /lib64/libstdc++.so.6
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
-rw-r--r-- 1 root root 2932366 Sep 30 2020 /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a
1、静态库本质是对静态库的源代码形成的所有.o 文件进行打包,静态库就是.o 文件的集合。 2、静态库会合并自己的代码到可执行程序,所以一旦链接静态库形成可执行程序后,可执行程序就不再依赖静态库了,我们使用 ldd 指令也查不到该可执行程序对静态库的依赖了。 3、库 = 头文件(方法手册)+ 库文件(.o 文件的集合),不论是动态库还是静态库都适用。 4、file 指令可以查看一个可执行程序文件的链接类型。
如果我们运行程序时引入的外部文件是以单个.o 文件的形式时,是可以直接链接所有目标文件(包括我自己程序的目标文件和外部引入的目标文件)形成可执行文件的。但是如果外部的文件是以静态库的形式引入当前程序时,我们直接链接程序是会报错的,因为要使用一个静态库必须要先找到,所以 gcc 编译时需要带 -l(库名) 选项,但是这还不够,因为库文件不同于头文件,系统一般不会去当前工作路径找静态库,而是由 gcc/g++编译器中的 /usr/bin/ld(加载器)去指定路径如 /lib64->/usr/lib64 这样的链接文件中找,所以需要指明到库在当前路径下。
下面是编译自己库文件的完整指令:
而 C 标准库却不用我们指定特定路径就能链接,因为 C 标准库本身就在编译器会去找的默认路径 /lib64->/usr/lib64 下,当我们把自己的库文件拷贝到默认路径下后,就可以不带-L 指定路径,只用带-l 指定文件名就可以完成链接了。 所以所谓库的安装,就是把库文件拷贝到系统默认路径下。
这里还有一个问题,那既然我们自己的文件和 C 标准库的文件都在默认路径下了,为什么 gcc 链接自己的库还需要 -l 指定文件名,而链接 C 标准库却不用?这是因为 gcc 本来就是用来编译 C 语言的,所以 gcc 默认认识 C 标准库,不用手动指定。 所以未来我们使用任何第三方库,至少需要带 -l(小写 l)指定文件名。
以前我们讲过带 "" 的头文件是告诉编译器先到当前路径查,若没找到再到系统默认路径中查。带 < > 的头文件是告诉编译器直接到系统默认路径中查。如果我们就想用 < > 包含当前文件下自己创建的头文件,可以用两种方法: 1、gcc test.c -I. ,用-I(大写 i) 选项指定一个头文件搜索路径。 2、将当前路径的头文件拷贝到系统指定头文件路径 /usr/include 路径下。
库 = 头文件 + 库文件。库使用需要搜索:1. 头文件 (-I (大 i))——预处理阶段选项 2. 库路径 (-L)——链接阶段选项 3. 库是谁 (-l (小 l)) ——链接阶段选项,这三种选项后面跟不跟空格都有效。库如果不想过多使用上面的选项,就需要将库安装到系统特定路径。库安装本质是把头文件和库文件拷贝到系统指定的,默认的,编译器能找到的路径下:头文件(/usr/include)和 库文件 (/lib64) 。
ar 是 gnu 归档工具,一种指令,类似 zip,它专门用来打包形成静态库。 rc 选项表示 (replace and create) ,已存在则替换,不存在则创建。
上面这段 makefile 的含义是: 1、自动查找 mymath.c 和 mystdio.c,通过 gcc -c 编译生成 mymath.o 和 mystdio.o。 2、用 ar 工具将这两个目标文件打包成静态库 libmyc.a。
makefile 中的顺序问题:即使 libmyc.a 的规则写在前面,%.o:%.c 写在后面,Make 也能正确找到依赖的编译规则,顺序不会导致逻辑错误。不过从可读性角度,通常会把'最终目标'(比如 libmyc.a)放在前面,依赖的规则(比如.o 的编译)放在后面。这样一眼就能看到最终要生成的结果,更符合阅读习惯。
形成库文件后还需要创建一个目录 mylibc,把头文件和库文件分别拷贝进目录的 include 和 lib 目录中,在把 mylibc 打包压缩,就可以把压缩包传给别人使用啦:
下面是 mylibc 的目录结构:
总结:给别人提供库就是以特定目录结构组织好的头文件和库文件。
由于动态库使用频率远远高于静态库,所以 gcc 天然支持将.o 文件打包形成动态库,不像静态库需要用 ar 指令打包。
shared: 表示生成共享库格式 fPIC:产生位置无关码 (position independent code)
动态库使用时和静态库有一些区别,当我们把自己形成的动态库链接形成可执行程序时带的 -L 选项是告诉了链接器库文件的路径在哪里,但当我们运行动态库形成的可执行程序时系统会报找不到库文件,这是因为我们没有告诉系统(加载器)库在哪里,因为动态库不像静态库在程序内部,运行可执行程序时动态库不会随着程序加载进内存,而是需要加载器去磁盘里找到动态库把它单独再加载进内存。 但是为什么在运行时不用指定 C 标准库的路径却能正常运行程序呢?这和其实和链接时一样,不仅链接时链接器会去默认路径 lib64 中找库文件,运行加载程序时加载器也会到默认路径 lib64 中找库文件。
这里解决运行找不到库文件报错有两种方法: 1、把库文件拷贝到系统默认路径 lib64 下,这样链接、运行时就都能找到了。 2、在系统默认路径 /lib64 下建立与当前路径下自己形成的动态库的软链接。
3、配置 LD_LIBRARY_PATH(加载库路径)环境变量,我们之前学过在命令行配置的环境变量是内存级的,所以只有该次 shell 会话中有效,如果我们想让该环境变量配置永久有效,就需要更改环境变量配置文件,下面只演示是临时配置,配置配置文件小编就不演示了。
4、更改系统配置文件,将动态库查找路径全局有效。
具体步骤是在 /etc/ld.so.conf.d 系统目录下添加一个以.conf 为后缀的动态库查找路径的配置文件,上面是该系统文件的示意图。然后在配置文件中写入自己库文件的绝对路径,然后使用 ldconfig 指令,该指令会将 /etc/ld.so.conf.d 系统目录下的文件全部热加载到内存中,这样我们的可执行程序就能依赖 libmyc.so 库文件了,运行可执行文件时就不会报错了。
虽然查找动态库我们介绍了 4 种方法,但是最佳实践是前两种,推荐大家优先使用前两种。
1、当我们 gcc/g++编译链接时不加-static,动静态库同时存在,默认优先使用动态库,进行动态链接。
2、当我们 gcc/g++编译链接时不加-static,只有静态库,只能使用提供的静态库,进行静态链接。 3、当我们 gcc/g++编译链接时加-static,-static 要求我们必须采用静态链接的方案,也就是说静态库必须存在,若不存在,则会报错。
我们在测试上面原理时会发生一个问题,只要加-static 就会报错,这是因为大部分 linux 系统只给我们安装了 C/C++的动态库,没有安装静态库。所以加-static 时就会因为找不到 C/C++的静态库而报错。所以需要给系统安装静态库,下面是安装指令:
# CentOS 7
sudo yum install glibc-static
# CentOS 8 及以上
sudo dnf install glibc-static
# 安装 C++ 静态库(如标准库 libstdc++的静态版本):
# CentOS 7
sudo yum install libstdc++-static
# CentOS 8 及以上
sudo dnf install libstdc++-static
# Ubuntu: 安装 C 语言静态库(如标准库 libc6 的静态版本):
sudo apt-get update
sudo apt-get install libc6-dev # 包含 C 标准库的静态文件和头文件
# 安装 C++ 静态库(如标准库 libstdc++的静态版本):
sudo apt-get update
sudo apt-get install libstdc++6-dev # 包含 C++ 标准库的静态文件和头文件
我们前面提到过,可执行程序也是文件,它们里面存放着程序的代码和数据,但是感觉这些概念都很虚,我们来落实落实,了解一下二进制可执行文件中的代码和数据在内存和磁盘中的组织形式。首先我们来认识一下 ELF 文件。
1、以 .a .so .o .exe 这些为后缀的二进制文件都是以 ELF 格式存在磁盘上。(类比音频是 mp3 格式) 2、最常见的节:代码节(.text):用于保存机器指令,是程序的主要执行部分。数据节(.data):保存已初始化的全局变量和局部静态变量。
查看 ELF Header: 指令:readelf -h 文件名 (ELF 头 (ELF header) :描述文件的主要特性。其位于文件的开始位置,它的主要目的是定位文件的其他部分。)
操作如下:
下面是 ELF header 内部结构:
ELF header 本质就是结构体,ELF header 在 32 位系统下大小为 52 字节,64 位系统下为 64 字节。
typedef struct elf32_hdr {
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry; /* Entry point */
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
typedef struct elf64_hdr {
unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */
Elf64_Half e_type;
Elf64_Half e_machine;
Elf64_Word e_version;
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags;
Elf64_Half e_ehsize;
Elf64_Half e_phentsize;
Elf64_Half e_phnum;
Elf64_Half e_shentsize;
Elf64_Half e_shnum;
Elf64_Half e_shstrndx;
} Elf64_Ehdr;
编译器和操作系统都要认识这个 ELF header: 编译器在编译源文件形成可执行程序时编译器会为我们创建 ELF header 结构体,所以编译器天然认识 ELF header。而操作系统未来要将可执行程序加载到内存里,就需要读取可执行程序的 ELF header,所以操作系统也认识 ELF header。
查看 Section Header Table: 指令:readelf -S 文件名 (节头表 (Section header table) :包含对节 (sections) 的描述,例如节的偏移量和大小,节头表中记录节的偏移量和大小可以说明每个节大小不是 4kb,否则就不用记录了。)
操作如下:
查看 Program Header Table: 指令:readelf -l 文件名 (列举了所有有效的段 (segments) 和他们的属性。表里记着每个段的开始的位置和位移(offset)、长度,毕竟这些段,都是紧密的放在二进制文件中,需要段表的描述信息,才能把他们每个段分割开。)
这里小编先介绍一下说明是数据段,它和数据节之间有什么关系: 前面我们介绍过了,在编译器角度,一个数据节不一定是 4kb,一般小于 4kb,具体大小由代码长度和数据大小决定,并且多个数据节可能会有相同的属性。在 OS 角度,磁盘和内存进行 IO 的时候,必须是 4kb,所以必然就需要将多个数据节合并对齐为 4kb,这个合并多个数据节大小为 4kb 的内容就是数据段,合并的时机是将磁盘数据加载到内存时。
至此我们明白了未来看待 ELF 文件有两种视角: 1、编译器:以 section 看待 2、操作系统:以 segment 看待
这里的 Program Header Table 就是一张合并方法表。
查看具体的 sections 信息: 指令:objdump -S 文件名
查看编译后的.o 目标文件: 指令:objdump -d 文件名 (查看反汇编)
step-1:将多份 C/C++ 源代码,翻译成为目标 .o 文件。 step-2:将多份 .o 文件的 section 进行合并,最后合并为一个可执行程序,该过程就是链接。
1、一个 ELF 会有多种不同的 Section,在加载到内存的时候,也会进行 Section 合并,形成 segment。 2、合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等。 3、这样,即便是不同的 Section,在加载到内存中,可能会以 segment 的形式,加载到一起。 4、很显然,这个合并工作在形成 ELF 的时候,合并方式已经确定了,具体合并原则被记录在了 ELF 的 程序头表 (Program header table) 中。
为什么要将 section 合并成为 segment: 1、Section 合并的主要原因是为了减少页面碎片,提高内存使用效率。如果不进行合并,假设页面大小为 4096 字节(内存块基本大小,加载,管理的基本单位),如果.text 部分为 4097 字节,.init 部分为 512 字节,那么它们将占用 3 个页面,而合并后,它们只需 2 个页面。 2、此外,操作系统在加载程序时,会将具有相同属性的 section 合并成一个大的 segment,这样就可以实现不同的访问权限,从而优化内存管理和权限访问控制,方便页表进行权限管理。
总结:section 在链接时作用,segment 在运行加载时作用。
无论是自己的.o, 还是静态库中的.o,本质都是把.o 文件进行连接的过程,所以研究静态链接,本质就是研究.o 文件是如何链接的。静态链接的行为如下:两个.o 的代码段合并到了一起,并进行了统一的编址。链接的时候,会修改.o 中没有确定的函数地址,在合并完成之后,进行 call 具体地址,完成代码调用。这个过程叫做链接时地址重定位。所以我们把.o/.obj 文件称为可重定位目标文件。
所以链接其实就是将编译之后的所有目标文件连同用到的一些静态库运行时库组合,拼装成一个独立的可执行文件。当所有模块组合在一起之后,链接器会根据我们的.o 文件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从而修正它们的地址。这其实就是静态链接的过程。所以,链接过程中会涉及到对.o 中外部符号进行地址重定位。
首先要明确动静态链接方式影响'虚拟地址的确定时机',不影响'虚实地址的转换机制',所以不论是动态链接还是静态链接都遵守下面要介绍的虚实地址的转换机制。
一个 ELF 可执行程序在没有被加载到内存的时候,其实它内部的每行代码和对应数据就已经有地址了,链接重定向可以间接证明这个结论,因为重定向需要拿到函数的地址(函数的本质就是相邻地址的集合)。
当代的计算机对 ELF 进行编址的时候,都是用一种线性的平坦模式进行编址,就是按照代码和数据出现的次序依次从低到高编址。
上面介绍的这种地址,其实是虚拟地址。在 linux 系统和平坦模式下,在磁盘/ELF 文件中称这种地址为逻辑地址,在内存中,这种地址称为虚拟地址。 (补充:ELF 的逻辑地址是编译链接时确定的'预期虚拟地址',进程的虚拟地址是运行时实际的地址空间;加载时通过映射和重定位,让逻辑地址适配到虚拟地址空间中(可能因地址空间随机化存在偏移))
所以在磁盘的数据被加载到磁盘之前,磁盘中的数据就已经被统一编址过了,只不过这时的地址是逻辑地址。在磁盘数据加载到内存后,这些数据就会有物理地址,物理地址、逻辑地址都有了,就可以对它们的地址做物理到虚拟的映射,就可以将映射地址填入到页表中。
现在物理、虚拟地址都有了,那么程序加载到内存后从哪里开始执行呢?这里就要引入之前介绍 ELF Header 时没有介绍的一个成员:Entry point address,它是当前可执行程序的入口虚拟地址。当程序加载到内存后,就会把 Entry point address 的值加载到 OS 的程序计数器 EIP 寄存器中。
除了 EIP 外,CPU 内还有一个 CR3 寄存器,它指向当前进程页表的首地址,还有一个 CPU 内部的硬件组件 MMU,它可以查页表完成从虚拟地址到物理地址的映射。有了上面这些,所以 MMU 就可以当着 EIP 的虚拟地址,拿着 CR3 指向的页表,就可以查页表将虚拟地址转换为物理地址、拿着物理地址开始访问物理地址指向的代码和数据了。不仅仅是程序入口虚拟地址是这样转换的,程序中遇到的所有虚拟地址比如程序内部的函数互相调用都是这样转换成物理地址的。所以以后进入 CPU 的是虚拟地址,出 CPU 的是物理地址(通过 MMU 进行转化),下面是转换示意图:
所以虚拟空间技术,需要操作系统(创建内核数据结构)、编译器(平坦模式编址)、CPU 硬件(寄存器、MMU)三者协同支持实现。
我们前面还说过,进程创建时要先创建 PCB、进程地址空间和页表,那进程地址空间进行划分时各个段的初始值从哪来呢?其实虚拟内存空间的段是从 ELF 文件中多个 section 合并成的 segment 的地址得来的,因为 ELF 文件中有多个有 section 合并成的 segment,我们可以用指令看一下:
并且每一个加载进内存的 segment 都对应一个虚拟内存中的 vm_area_struct:
首先小编先说明一点,为什么我们不讲进程如何看到静态库,因为静态库在链接时和可执行程序合并到一起了,进程天然就能看到。
那么动态库呢?首先程序要调用动态库,所以程序运行时动态库也需要加载到内存中,加载的内存中的动态库会提供页表映射到进程地址空间堆栈之间的共享区中,这样进程就能看到动态库了。
一个进程是可能同时映射多个库到共享区的。
1、当多个进程都要访问同一个动态库时,因为该动态库只会加载一次,所以每一个进程都会把库映射到自己的进程地址空间中。 2、所以动态库也被称为共享库,这样相比静态库能有效节省内存空间。 3、动态库的本质:通过地址空间映射,对公共代码进行去重。
动态链接其实远比静态链接要常用得多,因为静态链接最大的问题在于生成的文件体积大,并且相当耗费内存资源,这个时候,动态链接的优势就体现出来了,我们可以将需要共享的代码单独提取出来,保存成一个独立的动态链接库,等到程序运行的时候再将它们加载到内存,这样不但可以节省空间,因为同一个模块在内存中只需要保留一份副本,可以被不同的进程所共享。
那么动态链接到底是如何工作的? 首先要交代一个结论,动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序,操作系统会首先将程序的数据代码连同它用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使用情况为它们动态分配一段内存。 当动态库被加载到内存以后,一旦它的内存地址被确定,我们就可以去修正动态库中的那些函数跳转地址了。
首先明确_start 是 C 运行时库(如 glibc)提供的程序初始入口函数,它会调用动态链接器(ld-linux-x86-64.so)解析并加载程序依赖的动态库,ld-linux-x86-64.so 库如下图所示:
在 C/C++程序中,当程序开始执行时,它首先并不会直接跳转到 main 函数。实际上,程序的入口点是_start,这是一个由 C 运行时库(通常是 glibc)或链接器(如 ld)提供的特殊函数。在_start 函数中,会执行一系列初始化操作,这些操作包括:设置堆栈:为程序创建一个初始的堆栈环境。初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段。动态链接:这是关键的一步,_start 函数会调用动态链接器的代码来解析和加载程序所依赖的动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确地映射到动态库中的实际地址。
进程如何把动态库映射到自己的进程地址空间里: 首先要把动态库加载到内存中,其中动态库也是文件,所以我们要先打开库文件,因为打开文件后才能把文件加载到内存中。打开文件后 OS 会为该文件在内存中创建一个 struct file,struct file 初始化后会间接关联文件的 inode。然后 OS 会创建一个 vm_area_struct,并且 vm_area_struct 会关联 struct file。当我们拿到 inode 后就能拿到 inode 中的文件数据块信息,然后就能将文件的内容读取到物理内存中(这里把文件内容读取到物理内存中的空间就是文件内核缓冲区),这时动态库的物理地址就有了。接着 OS 还会在进程地址空间的共享区申请一段连续的空间来和物理地址做映射,然后我们就能拿到库在进程地址空间中的起始虚拟地址。
(提炼:先通过文件系统将库文件加载到内存里,加载到内存中后就能拿到该库对应的 inode 文件,然后就能通过 inode 拿到文件的数据块,就能把文件数据加载到内存里,这时物理地址就有了,然后再进程地址空间的共享区建立一段连续的虚拟空间,再将物理与虚拟地址建立映射)
下图是示意图,具体流程分 4 步走:
1、库已经被我们映射到了当前进程的地址空间中。 2、库的虚拟起始地址我们也已经知道了,物理地址映射到虚拟地址时确定。 3、库中每一个方法的偏移量地址我们也知道。因为在编译形成动态库时库中每个方法的偏移量就已经有了。 4、访问库中任意方法,只需要知道库的起始虚拟地址 + 方法偏移量即可定位库中的方法。该定位发生时机是在程序运行时,所以该过程被称为运行时地址重定向。 5、结论 1:整个调用过程,是从代码区跳转到共享区,调用完毕在返回到代码区,整个过程完全在进程地址空间中进行的。 6、结论 2:动态库被映射到进程地址空间(一般是共享区)的任意位置,进程都能调用。
但是这里还有一个问题,如上图所示,运行时地址重定向本质是将代码区原本的库文件名修改为库的起始虚拟地址,但是代码区不是只读的吗?为什么可以修改呢?这里就要引入一个概念:全局偏移量表,下面我们来详细介绍。
动态链接采用的做法是在进程地址空间的数据段(可执行程序或者库自己)中专门预留一片区域用来存放函数的跳转地址,它也被叫做全局偏移表 GOT,表中每一项都是本运行模块要引用的一个全局变量或函数的地址。因为.data 区域是可读写的,所以可以支持动态进行修改。
1、由于代码段只读,我们不能直接修改代码段。但有了 GOT 表,代码便可以被所有进程共享。但在不同进程的地址空间中,各动态库的绝对地址、相对位置都不同。反映到 GOT 表上,就是每个进程的每个动态库都有独立的 GOT 表,所以进程间不能共享 GOT 表。 2、在单个.so 下,由于 GOT 表与.text 的相对位置是固定的,我们完全可以利用 CPU 的相对寻址来找到 GOT 表。 3、在调用函数的时候会首先查表,然后根据表中的地址来进行跳转,这些地址在动态库加载的时候会被修改为真正的地址。 4、这种方式实现的动态链接就被叫做 PIC 地址无关代码。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运行,并且能够被所有进程共享,这也是为什么之前我们给编译器指定-fPIC 参数的原因,PIC=相对编址+GOT。
不仅仅有可执行程序调用库库也会调用其他库!库之间是有依赖的,如何做到库和库之间互相调用也是与地址无关的呢?库中也有.GOT,和可执行一样!这也就是为什么大家都是 ELF 的格式!
由于动态链接在程序加载的时候需要对大量函数进行重定位,这一步显然是非常耗时的。为了进一步降低开销,我们的操作系统还做了一些其他的优化,比如延迟绑定,或者也叫 PLT(过程连接表(Procedure Linkage Table))。与其在程序一开始就对所有函数进行重定位,不如将这个推迟到函数第一次被调用的时候,因为绝大多数动态库中的函数可能在程序运行期间一次都不会被使用到。 思路是:GOT 中的跳转地址默认会指向一段辅助代码,它也被叫做桩代码/stup。在我们第一次调用函数的时候,这段代码会负责查询真正函数的跳转地址,并且去更新 GOT 表。于是我们再次调用函数的时候,就会直接跳转到动态库中真正的函数实现。
静态链接的出现,提高了程序的模块化水平。对于一个大的项目,不同的人可以独立地测试和开发自己的模块。通过静态链接,生成最终的可执行文件。我们知道静态链接会将编译产生的所有目标文件,和用到的各种库合并成一个独立的可执行文件,其中我们会去修正模块间函数的跳转地址,也被叫做编译重定位 (也叫做静态重定位)。而动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序,操作系统会首先将程序的数据代码连同它用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,但是无论加载到什么地方,都要映射到进程对应的地址空间,然后通过.GOT 方式进行调用 (运行重定位,也叫做动态地址重定位)。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online