动静态库的制作与原理
- 一.什么是库
- 二.静态库
- 1.静态库生成
- 2.静态库使用
- 三.动态库
- 1.动态库生成
- 2.动态库使用
- 3.运行时找不到动态库问题
- 4.动静态库同时存在时:使用动态库
- 四.目标文件
- 五.ELF文件
- 1.ELF文件格式
- 2.目标文件链接过程
- 3.可执行程序加载过程
- 1.虚拟地址/逻辑地址
- 2.CPU如何执行程序
- 4.动态库的加载过程
- 七.静态链接
- 八.动态链接
- 1.程序真实的入口
- 2.动态库中的相对地址
- 3.程序如何与动态库映射
- 4.程序任何进行库函数调用
- 5.全局偏移量表GOT(global offset table)
- 6.总结
- 十.原理上理解动态库
本节重点:
- 动静态库的制作。
- 动静态库的使用。
- 动态库的查找。
- 可执行程序ELF格式。
- 可执行程序的加载过程。
- 虚拟地址空间和动态库加载的过程。
一.什么是库
库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。本质上来说库是一种可执行代码的⼆进制形式,可以被操作系统载入内存执行。
是包含了一组预编译好的目标文件(.o 文件)。
库有两种:
- 静态库 .a[Linux]、.lib[windows]
- 动态库 .so[Linux]、.dll[windows]
# ubuntu C动静态库
xzy@hcss-ecs-b3aa:~$ ls -l /lib/x86_64-linux-gnu/libc.so
-rw-r--r-- 1 root root 283 May 7 2024 /lib/x86_64-linux-gnu/libc.soxzy@hcss-ecs-b3aa:~$ ls -l /lib/x86_64-linux-gnu/libc.a
-rw-r--r-- 1 root root 6027922 May 7 2024 /lib/x86_64-linux-gnu/libc.a
预备工作,准备好历史封装的libc代码,在任意新增 “库文件”。
// my_stdio.h
#pragma once#define SIZE 1024#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2struct IO_FILE
{int flag; // 刷新方式 int fileno; // 文件描述符 char outbuffer[SIZE];int cap;int size;// TODO
};typedef struct IO_FILE mFILE;mFILE *mfopen(const char *filename, const char *mode);
int mfwrite(const void *ptr, int num, mFILE *stream);
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);// my_stdio.c
#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>mFILE *mfopen(const char *filename, const char *mode)
{int fd = -1;if(strcmp(mode, "r") == 0){fd = open(filename, O_RDONLY);}else if(strcmp(mode, "w")== 0){fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);}else if(strcmp(mode, "a") == 0){fd = open(filename, O_CREAT|O_WRONLY|O_APPEND, 0666);}if(fd < 0) return NULL;mFILE *mf = (mFILE*)malloc(sizeof(mFILE));if(!mf) {close(fd);return NULL;}mf->fileno = fd;mf->flag = FLUSH_LINE;mf->size = 0;mf->cap = SIZE;return mf;
}void mfflush(mFILE *stream)
{if(stream->size > 0){// 写到内核文件的文件缓冲区中!write(stream->fileno, stream->outbuffer, stream->size);// 刷新到外设fsync(stream->fileno);stream->size = 0;}
}int mfwrite(const void *ptr, int num, mFILE *stream)
{// 1. 拷贝memcpy(stream->outbuffer+stream->size, ptr, num);stream->size += num;// 2. 检测是否要刷新if(stream->flag == FLUSH_LINE && stream->size > 0 && stream->outbuffer[stream->size-1]== '\n'){mfflush(stream);}return num;
}void mfclose(mFILE *stream)
{if(stream->size > 0){mfflush(stream);}close(stream->fileno);
}// my_string.h
#pragma onceint my_strlen(const char *s);// my_string.c
#include "my_string.h"int my_strlen(const char *s)
{const char *end = s;while(*end != '\0')end++;return end - s;
}// 任意目录下,新建 main.c, 引入库头文件
#include "my_stdio.h"
#include "my_string.h"
#include <stdio.h>int main()
{const char *s = "abcdefg";printf("%s: %d\n", s, my_strlen(s));mFILE *fp = mfopen("./log.txt", "a");if(fp == NULL) return 1;mfwrite(s, my_strlen(s), fp);mfwrite(s, my_strlen(s), fp);mfwrite(s, my_strlen(s), fp);mfclose(fp);return 0;
}
二.静态库
- 静态库(.a):程序在编译链接的时候把库的代码拷贝(用到什么拷贝什么)到可执行文件中,程序运行的时候将不再需要静态库。
- 一个可执行程序可能用到许多的库,这些库运行有的是静态库,有的是动态库,而我们的编译默认为动态链接库,只有在该库下找不到动态.so的时候才会采用同名静态库。我们也可以使用 gcc 的 -static 强转设置链接静态库。
1.静态库生成
#编译生成目标.o文件
gcc -c my_stdio.c
gcc -c my_string.c#打包生成静态库libmystdio.a
ar -rc libmystdio.a my_stdio.o my_string.o#列出该静态库包含什么.o文件
ar -tv libmystdio.arw-r--r-- 0/0 2872 Jan 1 08:00 1970 my_stdio.o
rw-r--r-- 0/0 1264 Jan 1 08:00 1970 my_string.o
- ar 是 gnu 归档工具, rc 表示(replace and create:若存在就替换,不存在就创建)
- t:列出静态库中的文件。
- v:详细信息。
// Makefile文件// 创建库
libmystdio.a:my_stdio.o my_string.o@ar -rc $@ $^@echo "build $^ to $@ ... done"
%.o:%.c@gcc -c $<@echo "compling $< to $@ ... done"// 清除
.PHONY:clean
clean:@rm -rf *.a *.o stdc*@echo "clean ... done"// 发布
.PHONY:output
output:@mkdir -p stdc/include@mkdir -p stdc/lib@cp -f *.h stdc/include@cp -f *.o stdc/lib@tar -czf stdc.tgz stdc@echo "output stdc ... done"
2.静态库使用
场景1:头文件和库文件安装到系统路径下:cp *.h /usr/include/
、cp libmystdio.a /lib/
#即便安装到系统路径下, 也要-l指明第三方库名(否则报链接错误: 找不到库)
gcc main.c -o main -lmystdio
场景2:头文件和库文件和我们自己的源文件在同一个路径下
#默认只在系统路径下找库, 需要-L指定库路径(否则报链接错误: 找不到库)
gcc main.c -o main -L. -lmystdio
场景3:头文件和库文件有自己的独立路径
tree .
.
├── main.c
└── stdc├── include│ ├── my_stdio.h│ └── my_string.h└── lib└── libmystdio.a#默认只在系统路径和当前路径下找头文件, 需要-I指定头文件路径(否则报编译错误: 找不到头文件)
gcc main.c -o main -Istdc/include -Lstdc/lib -lmystdio
总结:
- gcc在查静态库时,只会到系统路径下查找,而在查头文件时若<.h>在系统中查找,若".h"在当前路径中查找。
- -L:指定库的搜索路径。
- -I:指定头文件的搜索路径。
- -l:指定库名。
- 可执行文件生成后,静态库删掉,程序照样可以运行(静态库在编译链接阶段会被拷贝(用到什么拷贝什么)到可执行文件中)
- 关于 -static 选项,稍后介绍。
- 库文件名称和引入库的名称:去掉前缀 lib,去掉后缀 .so/.a,如:libmystdio.a -> mystdio
三.动态库
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接。
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
1.动态库生成
#编译生成目标.o文件
gcc -c my_stdio.c -fPIC
gcc -c my_string.c -fPIC#打包生成静态库libmystdio.so
gcc -o libmystdio.so my_stdio.o my_string.o -shared
- -fPIC:产生位置无关码(position independent code)
- -shared:表示生成共享库格式。
// Makefile文件// 创建库
libmystdio.so:my_stdio.o my_string.o@gcc -o $@ $^ -shared@echo "build $^ to $@ ... done"
%.o:%.c@gcc -c $< -fPIC@echo "compling $< to $@ ... done"// 清除
.PHONY:clean
clean:@rm -rf *.so *.o stdc*@echo "clean ... done"// 发布
.PHONY:output
output:@mkdir -p stdc/include@mkdir -p stdc/lib@cp -f *.h stdc/include@cp -f *.so stdc/lib@tar -czf stdc.tgz stdc@echo "output stdc ... done"
2.动态库使用
场景1:头文件和库文件安装到系统路径下:cp *.h /usr/include/
、cp libmystdio.so /lib/
#即便安装到系统路径下, 也要-l指明第三方库名
gcc main.c -o main -lmystdio
场景2:头文件和库文件和我们自己的源文件在同一个路径下
#默认只在系统路径下找库, 需要-L指定库路径
gcc main.c -o main -L. -lmystdio
场景3:头文件和库文件有自己的独立路径
tree .
.
├── main.c
└── stdc├── include│ ├── my_stdio.h│ └── my_string.h└── lib└── libmystdio.so#默认只在系统路径和当前路径下找头文件, 需要-I指定头文件路径
gcc main.c -o main -Istdc/include -Lstdc/lib -lmystdio
3.运行时找不到动态库问题
问题:编译链接形成可执行文件没有问题,但是执行时找不到该动态库,可以使用ldd命令查看所依赖的动态库。
原因:生成可执行文件时,给gcc编译器指定了动态库路径,但是运行时就与gcc编译器无关了,操作系统需要加载程序,但是系统找不到动态库。
xzy@hcss-ecs-b3aa:~$ ./main
./main: error while loading shared libraries: libmystdio.so: cannot open shared object file: No such file or directoryxzy@hcss-ecs-b3aa:~$ ldd mainlinux-vdso.so.1 (0x00007ffd697c6000)libmystdio.so => not foundlibc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5429b43000)/lib64/ld-linux-x86-64.so.2 (0x00007f5429d79000)
解决方案:
- 拷贝 .so 文件到系统共享库路径下,一般指 /usr/lib、/usr/local/lib、/lib64 等。
- 在系统共享库路径下,建立同名软连接。
xzy@hcss-ecs-b3aa:~$ ln -s /home/xzy/stdio/stdc/lib/libmystdio.so /usr/lib/libmystdio.soxzy@hcss-ecs-b3aa:~$ ls -l /usr/lib/libmystdio.so
lrwxrwxrwx 1 root root 38 Feb 16 22:08 /usr/lib/libmystdio.so -> /home/xzy/stdio/stdc/lib/libmystdio.so
- 更改环境变量:LD_LIBRARY_PATH。Linux系统中查找动态库依靠该环境变量。
xzy@hcss-ecs-b3aa:~$ export LD_LIBRARY_PATH=/home/xzy/stdio/stdc/lib
#问题: 重启Linux环境变量就没了, 可以将其添加到家目录的.bashrc配置文件中
- ldconfig方案:在 /etc/ld.so.conf.d/ 目录下,创建配置文件(.conf),写入动态库所在的路径,ldconfig更新。
root@hcss-ecs-b3aa:~$ touch /etc/ld.so.conf.d/my.conf
root@hcss-ecs-b3aa:~$ echo /home/xzy/stdio/stdc/lib > /etc/ld.so.conf.d/my.conf
结果:动态库可以找到
root@hcss-ecs-b3aa:~$ ldd mainlinux-vdso.so.1 (0x00007ffc7eab8000)libmystdio.so => /home/xzy/stdio/stdc/lib/libmystdio.so (0x00007ff35e32e000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff35e105000)/lib64/ld-linux-x86-64.so.2 (0x00007ff35e340000)
4.动静态库同时存在时:使用动态库
xzy@hcss-ecs-b3aa:~$ tree .
.
├── main.c
└── stdc├── include│ ├── my_stdio.h│ └── my_string.h└── lib├── libmystdio.a└── libmystdio.soxzy@hcss-ecs-b3aa:~$ gcc main.c -o main -Istdc/include -Lstdc/lib -lmystdioxzy@hcss-ecs-b3aa:~$ ldd mainlinux-vdso.so.1 (0x00007ffe2d9eb000)libmystdio.so => /home/xzy/stdio/stdc/lib/libmystdio.so (0x00007fa28b296000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa28b067000)/lib64/ld-linux-x86-64.so.2 (0x00007fa28b2a2000)xzy@hcss-ecs-b3aa:~$ file main
main: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4276353543a51588ffab04e037ef2e0a17c049f8, for GNU/Linux 3.2.0, not strippedxzy@hcss-ecs-b3aa:~$ gcc main.c -o main -Istdc/include -Lstdc/lib -lmystdio -staticxzy@hcss-ecs-b3aa:~$ ldd mainnot a dynamic executablexzy@hcss-ecs-b3aa:~$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=fb03395552a503e727c154b19374534b6efb0585, for GNU/Linux 3.2.0, not stripped
结论:
- 如果同时提供动态库和静态库,gcc/g++ 默认使用动态库。
- 如果强制静态链接,必须提供相应的静态库。
- 如果只提供静态库,但是链接方式是动态链接的,gcc/g++ 没得选,只能采用静态链接。
四.目标文件
编译和链接这两个步骤,在Windows下被我们的IDE封装的很完美,我们一般都是一键构建非常方便,但一旦遇到错误的时候呢,尤其是链接相关的错误,很多人就束手无策了。在Linux下,我们通过gcc编译器来完成这一系列操作。
接下来我们深入探讨一下编译和链接的整个过程,来更好的理解动静态库的使用原理。
先来回顾下什么是编译呢?编译的过程其实就是将我们程序的源代码翻译成CPU能够直接运行的机器
代码。比如:在一个源文件 hello.c 里便简单输出"hello world!",并且调用一个run函数,而这个函数被定义在一个原文件 code.c 中。这里我们就可以调用 gcc -c 来分别编译这两个原文件。
# 编译两个源⽂件
xzy@hcss-ecs-b3aa:~$ gcc -c hello.c
xzy@hcss-ecs-b3aa:~$ gcc -c code.cxzy@hcss-ecs-b3aa:~$ ls
code.c code.o hello.c hello.o
可以看到,在编译之后会生成两个扩展名为 .o 的文件,它们被称作目标文件。要注意的是如果我们修改了一个原文件,那么只需要单独编译它这一个,而不需要浪费时间重新编译整个工程。目标文件是一个二进制的文件,文件的格式是 ELF,是对二进制代码的一种封装。
# file命令用于辨识文件类型
xzy@hcss-ecs-b3aa:~$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
五.ELF文件
要理解编译链链接的细节,我们不得不了解一下ELF(Executable and Linkable Format)文件。其实有以下四种文件其实都是ELF文件:
- 可重定位文件(Relocatable File):即 xxx.o 文件。包含适合于与其它目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
- 可执行文件(Executable File):即可执行程序。
- 共享目标文件(Shared Object File):即 xxx.so 文件。
- 内核转储(core dumps) ,存放当前进程的执行上下文,用于dump信号触发。
1.ELF文件格式
ELF文件由以下四部分组成:
- ELF头(ELF header):描述文件的主要特性。其位于文件的开始位置,它的主要目的是定位文件的其他部分。
- 程序头表(Program header table):列举了所有有效的段(segments)和它们的属性。表里记着每个段的开始的位置和偏移量(offset) + 长度,毕竟这些段,都是紧密的放在二进制文件中,需要段表的描述信息,才能把它们每个段分割开。
- 节头表(Section header table):包含对节的描述。
- 节(Section):ELF文件中的基本组成单位,包含了特定类型的数据。ELF文件的各种信息和数据都存储在不同的节中,如代码节存储了可执行代码,数据节存储了全局变量和静态数据等。
# 读取 ELF header 信息
readelf -h main# 读取 Program header table 信息
readelf -l main# 读取 Section header table 信息
readelf -S main
最常见的节:
- 代码节 (.text):用于保存机器指令,是程序的主要执行部分。
- 数据节 (.data):保存已初始化的全局变量和静态变量。
- 数据节 (.bss):保存未初始化的全局变量和静态变量。
2.目标文件链接过程
链接:就是将 ELF 文件一个个相同属性的 Section 进行合并!
静态库也是一堆.o文件需要合并,但是动态库是在程序运行时加载到内存中,不需要合并。
3.可执行程序加载过程
- 一个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成segment。
- 合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等。
- 这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到一起。
- 很显然,这个合并工作也已经在形成ELF的时候,合并方式已经确定了,具体合并原则被记录在了ELF的 程序头表(Program header table) 中。
# 查看可执行程序的 Section
xzy@hcss-ecs-b3aa:~$ readelf -S main# 查看Section合并后的segment
xzy@hcss-ecs-b3aa:~$ readelf -l main
1.虚拟地址/逻辑地址
问题:
- 一个ELF程序,在没有被加载到内存的时候,有没有地址呢?
- 进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪里来的?
答案:一个ELF程序,在没有被加载到内存的时候,本来就有地址,当代计算机工作的时候,都采用 “平坦模式” 进行工作。所以也要求ELF对自己的代码和数据进行统一编址,下面是 objdump -S main 反汇编之后,得到的一些汇编代码。
- 最左侧的就是ELF的虚拟地址,其实,严格意义上应该叫做逻辑地址(起始地址+偏移量),但是我们认为起始地址是0,也就是说,其实虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执行程序进行统一编址了。
- 进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪里来的?从ELF各个segment来,每个segment有自己的起始地址和自己的长度,用来初始化内核结构中的[start,end]等范围数据,另外在用详细地址,填充页表。
- ELF在被编译好之后,会把自己未来程序的入口地址记录在ELF header的Entry字段中,CPU 就从该地址开始依次往后执行。
所以:虚拟地址机制,不光光OS要支持,编译器也要支持。
2.CPU如何执行程序
- pc 指针:存放当前CPU执行指令的虚拟地址。
- EIP 指令寄存器:存放当前CPU执行的指令。
- MMU 硬件:查页表。
- CR3 寄存器:存放页表的物理地址。
根据当前指令的虚拟地址和指令的长度,计算下一个指令的虚拟地址,查找页表获得下一条要执行的指令…
虚拟地址空间:操作系统,CPU,编译器共同协作下的产物。
为什么要有虚拟地址:不用考虑物理地址,对代码进行统一连续编址。
4.动态库的加载过程
进程任何看到动态库?
进程间是如何共享动态库的?
七.静态链接
静态链接其实就是将编译之后的所有目标文件连同用到的一些静态库运行时库组合,拼装成一个独立的可执行文件。其中就包括地址修正,当所有模块组合在一起之后,链接器会根据我们的.o文件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从而修正它们的地址。
八.动态链接
动态链接其实远比静态链接要常用得多。
- 那为什么编译器默认不使用静态链接呢?静态链接会将编译产生的所有目标文件,连同用到的各种库,合并形成一个独里的可执行文件,它不需要额外的依赖就可以运行。照理来说应该更加方便才对是吧?
- 静态链接最大的问题在于生成的文件体积大,并且相当耗费内存资源。随着软件复杂度的提升,我们的操作系统也越来越臃肿,不同的软件就有可能都包含了相同的功能和代码,显然会浪费大量的硬盘空间。
- 这个时候,动态链接的优势就体现出来了,我们可以将需要共享的代码单独提取出来,保存成一个独里的动态链接库,等到程序运行的时候再将它们加载到内存,这样不但可以节省空间,因为同一个模块在内存中只需要保留一份副本,可以被不同的进程所共享。
动态链接到底是如何工作的??
首先要交代一个结论,动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序,操作系统会首先将程序的数据代码连同它用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使用情况为它们动态分配一段内存。
当动态库被加载到内存以后,一旦它的内存地址被确定,我们就可以去修正动态库中的那些函数跳转地址了。
1.程序真实的入口
在C/C++程序中,当程序开始执行时,它首先并不会直接跳转到 main 函数。实际上,程序的入口是 _start,这是一个由C运行时库(通常是glibc)或链接器(如ld)提供的特殊函数。在 _start 函数中,会执行一系列初始化操作,这些操作包括:
- 设置堆栈:为程序创建一个初始的堆栈环境。
- 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段。
- 动态链接:_start 函数会调用动态链接器的代码来解析和加载程序所依赖的动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确地映射到动态库中的实际地址。
- 调用 __libc_start_main:一旦动态链接完成,_start 函数会调用 __libc_start_main(这是glibc提供的一个函数)。__libc_start_main 函数负责执行一些额外的初始化工作,比如设置信号处理函数、初始化线程库(如果使用了线程)等。
- 调用 main 函数:__libc_start_main 函数会调用程序的 main 函数,此时程序的执行控制权才正式交给用户编写的代码。
- 处理 main 函数的返回值:当 main 函数返回时,__libc_start_main 会负责处理这个返回值,并最终调用 _exit 函数来终止程序。
上述过程描述了C/C++程序在 main 函数之前执行的一系列操作,但这些操作对于大多数程序员来说是透明的。程序员通常只需要关注 main 函数中的代码,而不需要关心底层的初始化过程。然而,了解这些底层细节有助于更好地理解程序的执行流程和调试问题。
2.动态库中的相对地址
动态库为了随时进行加载,为了支持并映射到任意进程的任意位置,对动态库中的方法,统一编址,采用相对编址的方案进行编制的(其实可执行程序也一样,都要遵守平坦模式,只不过exe是直接加载的)
3.程序如何与动态库映射
4.程序任何进行库函数调用
问题:代码区的数据只读,不能被修改的,真实的情况是什么呢?
5.全局偏移量表GOT(global offset table)
所以:动态链接采用的做法是在 .data(可执行程序或者库自己)中专门预留一片区域用来存放函数的跳转地址,它也被叫做全局偏移表GOT,表中每一项都是本运行模块要引用的一个全局变量或函数的地址。因为.data区域是可读写的,所以可以支持动态进行修改。
xzy@hcss-ecs-b3aa:~$ readelf -S a.out
...[24] .got PROGBITS 0000000000003fb8 00002fb80000000000000048 0000000000000008 WA 0 0 8
...# .got在加载的时, 会和.data合并成为一个segment, 然后加载在一起
xzy@hcss-ecs-b3aa:~$ readelf -l a.out
...05 .init_array .fini_array .dynamic .got .data .bss
...
- 由于代码段只读,我们不能直接修改代码段。但有了GOT表,代码便可以被所有进程共享。但在不同进程的地址空间中,各动态库的绝对地址、相对位置都不同。反映到GOT表上,就是每个进程的每个动态库都有独立的GOT表,所以进程间不能共享GOT表。
- 在单个.so下,由于GOT表与 .text 的相对位置是固定的,我们完全可以利用CPU的相对寻址来找到GOT表。
- 在调用函数的时候会首先查GOT表,然后根据表中的地址来进行跳转,这些地址在动态库加载的时候会被修改为真正的地址。
- 这种方式实现的动态链接就被叫做 PIC 地址无关代码 。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运行,并且能够被所有进程共享,这也是为什么之前我们给编译器指定 -fPIC 参数的原因,PIC = 相对编址 + GOT。
6.总结
- 静态链接的出现,提高了程序的模块化水平。对于一个大的项目,不同的人可以独立地测试和开发自己的模块。通过静态链接,生成最终的可执行文件。
- 我们知道静态链接会将编译产生的所有目标文件,和用到的各种库合并成一个独里的可执行文件,其中我们会去修正模块间函数的跳转地址,也被叫做编译重定位(也叫做静态重定位)
- 动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序,操作系统会首先将程序的数据代码连同它用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,但是无论加载到什么地方,都要映射到进程对应的地址空间,然后通过.GOT方式进行调用(运行重定位,也叫做动态地址重定位)
十.原理上理解动态库
静态库只在链接生成可执行文件时有效,运行可执行文件时无效(可以删除静态库),但是动态库在运行可执行文件时有效(不可以删除动态库,需要将动态库加载到内存中)
对于 程序头表(Program header table) 和 节头表(Section header table) 又有什么用呢?其实 ELF 文件提供 2 个不同的视角来让我们理解这两个部分:
- 链接视图(Linking view):对应节头表 Section header table
文件结构的粒度更细,将文件按功能模块的差异进行划分,静态链接分析的时候一般关注的是链接视图,能够理解 ELF 文件中包含的各个部分的信息。
为了空间布局上的效率,将来在链接目标文件时,链接器会把很多节(section)合并,规整成可执行的段(segment)、可读写的段、只读段等。合并了后,空间利用率就高了,否则,很小的很小的一段,未来物理内存页浪费太大(物理内存页分配一般都是整数倍一块给你,比如4KB),所以,链接器趁着链接就把小块们都合并了。 - 执行视图(execution view):对应程序头表 Program header table
告诉操作系统,如何加载可执行文件,完成进程内存的初始化。⼀个可执⾏程序的格式中,
⼀定有 program header table 。
说⽩了就是:⼀个在链接时作⽤,⼀个在运⾏加载时作⽤。
从 链接视图 来看:
• 命令 readelf -S hello.o 可以帮助查看ELF⽂件的节头表。
• .text节 :是保存了程序代码指令的代码节。
• .data节 :保存了初始化的全局变量和局部静态变量等数据。• .rodata节 :保存了只读的数据,如⼀⾏C语⾔代码中的字符串。由于.rodata节是只读的,所
以只能存在于⼀个可执⾏⽂件的只读段中。因此,只能是在text段(不是data段)中找到.rodata
节。
• .BSS节 :为未初始化的全局变量和局部静态变量预留位置
• .symtab节 :SymbolTable符号表,就是源码⾥⾯那些函数名、变量名和代码的对应关系。
• .got.plt节 (全局偏移表-过程链接表):.got节保存了全局偏移表。.got节和.plt节⼀起提供
了对导⼊的共享库函数的访问⼊⼝,由动态链接器在运⾏时进⾏修改。对于GOT的理解,我们后
⾯会说。
◦ 使⽤ readelf 命令查看.so⽂件可以看到该节。
从 执⾏视图 来看:
• 告诉操作系统哪些模块可以被加载进内存。
• 加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执⾏的。
我们可以在 ELF头 中找到⽂件的基本信息,以及可以看到ELF头是如何定位程序头表和节头表的。例
如我们查看下hello.o这个可重定位⽂件的主要信息: