口碑好的郑州网站建设,设计师服务平台鱼巴士,二维码样式大全制作,泾川建设路网站一、什么是库1、动静态库概念# 库是写好的现有的#xff0c;成熟的#xff0c;可以复⽤的代码。现实中每个程序都要依赖很多基础的底层库#xff0c;不可能每个⼈的代码都从零开始#xff0c;因此库的存在意义⾮同寻常。# 本质上来说库是⼀种可执⾏代码的⼆进制形式#x…一、什么是库1、动静态库概念# 库是写好的现有的成熟的可以复⽤的代码。现实中每个程序都要依赖很多基础的底层库不可能每个⼈的代码都从零开始因此库的存在意义⾮同寻常。# 本质上来说库是⼀种可执⾏代码的⼆进制形式可以被操作系统载⼊内存执⾏。库有两种
在Linux当中以.so为后缀的是动态库以.a为后缀的是静态库。在Windows当中以.dll为后缀的是动态库以.lib为后缀的是静态库。
# 问题动静态库里面是否需要包含main函数? 答案是库函数中不要包含main函数我们链接过程会把库函数链接如果库函数还有main函数就会导致命名冲突导致链接失败所以不要2、动静态库优缺点# 静态库在程序编译时被链接到目标代码中。一旦链接完成静态库的代码就成为目标程序的一部分。这意味着如果多个程序都使用了同一个静态库那么每个程序都会包含一份该库的副本从而导致程序体积较大。
优点独立性强不依赖外部环境因为库代码已经被包含在程序中。运行时加载速度相对较快因为不需要在运行时进行库的加载操作。缺点生成的程序体积较大。如果静态库有更新需要重新编译链接所有使用该库的程序
# 动态库在程序运行时被加载。多个程序可以共享同一个动态库只有当程序运行时才会将动态库加载到内存中。这大大减小了程序的体积同时也方便了库的更新和维护。
优点生成的程序体积较小因为库代码没有被包含在程序中。库的更新不影响已编译的程序只需要更新动态库文件即可。缺点依赖外部环境运行时需要确保动态库存在且路径正确。加载动态库可能会带来一定的时间开销。
3、动静态库原理# 我们知道一个源文件变为一个可执行文件将经历四个步骤
预处理 完成头文件展开、去注释、宏替换、条件编译等最终形成xxx.i文件。编译 完成词法分析、语法分析、语义分析、符号汇总等检查无误后将代码翻译成汇编指令最终形成xxx.s文件。汇编 将汇编指令转换成二进制指令最终形成xxx.o文件。链接 将生成的各个xxx.o文件进行链接最终形成可执行程序。
# 比如我们现在有test1.ctest2.ctest3.c以及main1.c这四个.c文件经过预处理编译汇编之后分别生成test1.otest2.otest3.o以及main1.o这四个.o文件。最后经过生成a.out的可执行文件。# 但是此时我们的main2.c文件的生成同时也需要依赖test1.ctest2.ctest3.c这三个文件生成可执行程序的步骤都是一样的。此时我们就可以选择将test1.ctest2.ctest3.c这三个文件生成的test1.otest2.otest3.o进行打包之后再使用时只需要链接这个包即可这个包其实就是我们常说的库。# 所以动静态库的本质其实是一堆xxx.o文件的集合。对于库的使用只需要提供头文件让使用者了解具体功能的作用。在编译程序时通过链接指定的库来实现对库中功能的调用。
二、静态库1、静态库的打包# 接下来我们以使用之前写过的库函数缓冲区代码为例讲解一下我们如何将我们的文件打包成静态库:# 然后我们需要将mystdio.hmystdio.cmystring.hmystring.c这4个文件打包成静态库。1. 首先第一步将源文件生成对应.o文件。2. 第二步使用ar指令打包成对应的静态库。# 其中ar是gun归档工具ar指令用法为ar 选项 库名 打包文件名其中又两个关键选项
-r(replace)若静态库文件当中的目标文件有更新则用新的目标文件替换旧的目标文件-c(create)建立静态库文件
# 其中需要注意的是动静态库真实文件名需要去掉前缀lib再去掉后缀.so或者.a及其后面的版本号比如说libc-2.17.so就是C语言的标准库其名为: c。3. 将头文件和生成的静态库组织起来。# 当把自己的库提供给他人使用时通常需要给予两个文件夹
一个文件夹用于存放头文件集合。比如可以将mystdio.h和mystring.h这两个头文件放置在名为include的目录下。(头文件本质是对源文件方式的使用说明文档)另一个文件夹用于存放所有的库文件。例如把生成的静态库文件libmyc.a放到名为mylib的目录下。
# 最后将这两个目录include和mylib都放置在lib目录下此时就可以把lib提供给别人使用了。4. 将lib打包。# 此时我们的lib.tgz就相当于一个安装包了下载过去就可以使用。# 为了方便我们处理我们可以写一个Makefile:
libmyc.a:mystdio.o mystring.oar -rc $ $^%.o:%.c #展开所有.c文件生成对应的.o文件gcc -c $.PHONY:clean
clean:rm -rf ./*.o libmyc.a lib.tgz.PHONY:output #发表库
output:mkdir -p lib/includemkdir -p lib/mylibcp -f ./*.h lib/includecp -f ./*.a lib/mylibtar czf lib.tgz lib
2、静态库的使用# 首先我们将lib.tgz解压。# 我们如果使用我们打包的静态库在使用gcc编译时需要带有以下三个选项
-I指定头文件搜索路径。-L指定库文件搜索路径。-l指明需要链接库文件路径下的哪一个库。
# 由于在程序执行时编译器并不知晓我们所声明的头文件以及链接库的具体位置而且链接库中可能存在不同的库文件。因此我们需要在命令行中指定头文件的搜索路径库文件的搜索路径以及具体使用哪个库。# 比如我们需要执行main.c其中main.c中使用静态库中的函数。
#include mystdio.h
#include mystring.h#include stdio.hint main()
{const char *s abcdefg;printf(%s: %d\n, s, my_strlen(s));MyFile *fp mfopen(./log.txt, a);if(fp NULL) return 1;MyFwrite(s, my_strlen(s), fp);MyFwrite(s, my_strlen(s), fp);MyFwrite(s, my_strlen(s), fp);MyFclose(fp);return 0;
}# 其中需要注意的是-I-L-l这三个选项后面可以加空格也可以不加空格。# 那么我们就有个疑问那就是我们平时使用gcc编译文件时为什么没有带-I-L-l这三个选项呢# 其实很简单因为我们之前使用的库都默认在系统的路径下编译器能准确识别这些存在于配置文件中的路径系统搜索头文件的路径在/usr/include目录下搜索库文件在/lib.64目录下。其实如果为了方便我们也可以将头文件和库文件拷贝到系统路径/usr/include/lib.64下
sudo cp lib/include/* /usr/include/sudo cp lib/lib/* /lib.64/
# 这时再使用gcc编译时就只需要带-l选项指明链接库文件下具体哪个库。# 但是实际上我们并不推荐将自己写的头文件和库文件拷贝到系统路径下因为这样做可能会对系统文件造成污染。
三、动态库1、动态库的打包# 动态库的打包相对于静态库较为复杂但大致相同我们还是利用mystdio.hmystdio.cmystring.hmystring.c这4个文件进行打包演示1. 首先第一步将源文件生成对应.o文件。# 但是与静态库不同的是需要带-fPIC选项因为动态库运行时才会被加载。
font stylecolor:rgb(28, 31, 35);-fPICposition independent code/font即产生位置无关码作用于编译阶段其目的是告诉编译器生成与位置无关的代码。在这种情况下所产生的代码中不存在绝对地址全部采用相对地址起始位置加上偏移量。这使得动态库被加载器加载到内存的任意位置时都能够正确执行。倘若不添加该选项代码中使用的库函数在执行时会尝试调到对应位置执行但此时可能会因该位置被其他动态库所占用而找不到该函数。
2. 使用-shared选项将所有目标文件打包为动态库。# 生成对应的动态库并不需要使用ar指令还是使用gcc编译只不过需要带-shared选项。3. 将头文件和生成的动态态库组织起来。# 与静态库类似当把自己的库提供给他人使用时通常需要给予两个文件夹
一个文件夹用于存放头文件集合。比如可以将mystdio.h和mystring.h这两个头文件放置在名为include的目录下。另一个文件夹用于存放所有的库文件。例如把生成的静态库文件libmyc.so放到名为mylib的目录下。
# 最后将这两个目录include和mylib都放置在lib目录下此时就可以把lib提供给别人使用了。# 同样为了方便管理我们也可以定义一个makefile文件。
libmyc.so:mystdio.o mystring.ogcc -shared -o $ $^%.o:%.c #展开所有.c文件生成对应的.o文件gcc -fPIC -c $.PHONY:clean
clean:rm -rf ./*.o libmyc.so lib.tgz.PHONY:output #发表库
output:mkdir -p lib/includemkdir -p lib/mylibcp -f ./*.h lib/includecp -f ./*.so lib/mylibtar czf lib.tgz lib2、动态库的使用# 我们如果使用我们打包的动态库使用gcc编译时同样需要带有以下三个选项
-I指定头文件搜索路径。-L指定库文件搜索路径。-l指明需要链接库文件路径下的哪一个库。
# 因为在程序执行时编译器同样并不知晓我们所声明的头文件以及链接库的具体位置而且链接库中可能存在不同的库文件。因此我们需要在命令行中指定头文件的搜索路径库文件的搜索路径以及具体使用哪个库。# 比如我们需要执行main.c 其中main.c中使用动态库中的函数。# 但是与静态库不同的是我们并不能直接执行main这个可执行文件。# 为什么使用了-I-L-l这三个选项还是没有找到对应的动态库呢
这是由于我们使用-I-L-l这三个选项仅仅是在编译期间向编译器告知我们所使用的头文件和库文件的具体位置以及具体的库名。然而当可执行程序生成后它便与编译器不再有直接关系。所以该可执行程序运行起来时操作系统仍找不到该可执行程序所依赖的动态库。
# 那么静态库为什么没有这个问题
因为静态库是把库在链接时拷贝到可执行程序里面只要链接编译成功后就不需要依赖静态库。而动态库是需要加载程序的同时找到你所依赖的库
# 所以其实只需要让系统可以找到我们可执行程序需要的库即可因此这里我们有四种方法1. 第一种就是将库文件拷贝到系统共享的库路径下。
sudo cp lib/mylib/libmyc.so /lib64
# 但是这种方法可能会对系统文件造成污染所以我们一般不采取该方法。2. 第二种就是给我们的库路径建立一个软链接。
ln -s lib/mylib/libmyc.so /lib64/libmyc.so
3. 第三种就是更改环境变量LD_LIBRARY_PATH。
export LD_LIBRARY_PATH$LD_LIBRARY_PATH:/home/tata/lesson11/my_stdio/lib/mylib(对应动态库所在路径)
# LD_LIBRARY_PATH是程序运行动态查找库时所要搜索的路径我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH环境变量当中程序运行起来时就能找到对应的路径下的动态库。# 但是我们知道环境变量在重启时会自动恢复所以这种方法只在当前状态下有效具有临时性。4. 第四种就是配置.conf/文件。# 在系统中/etc/ld.so.conf.d/是用于搜索动态库的路径。此路径下存放的全是后缀为.conf的配置文件这些配置文件中所存放的内容都是动态库的路径。# 因此若将自己库文件的路径也放置在该路径下那么当可执行程序运行时系统就能够找到我们的库文件。并且这种行为是永久的并不会随重启而改变。# 首先我们将对应的库文件所在地址写入一个.conf文件中然后将其导入/etc/ld.so.conf.d/路径最后使用指令ldconfig更新一下配置文件最后我们就能执行我们的可执行文件了。四、动静态库的使用# 在Linux下我们可以通过ldd 文件名来查看一个可执行程序所依赖的库文件。这其中的libc.so.6就是该可执行程序所依赖的库文件我们通过ls命令可以发现libc.so.6实际上只是一个软链接。# 实际上该软链接的源文件libc-2.17.so和libc.so.6在同一个目录下为了进一步了解我们可以通过file 文件名命令来查看libc-2.17.so的文件类型。# 如果文件所链接的库中动静态库同时存在呢# 此时我们链接程序可以链接成功也可以正常运行程序然后我们ldd查看发现他使用的动态库链接是采用动态链接# 通过上图观察我们知道gcc/g编译器默认都是动态链接的。# 如果想使用静态链接需要在后面加一个-static。且一旦要静态链接就必须存在静态库。如果你并没有安装对应的静态库的话可以使用以下指令安装。
sudo yum install glibc-static sudo yum install libstdc-static五、目标文件# 编译和链接这两个步骤在Windows下被我们的IDE封装的很完美我们⼀般都是⼀键构建⾮常⽅便但⼀旦遇到错误的时候呢尤其是链接相关的错误很多⼈就束⼿⽆策了。在Linux下我们之前也学习过如何通过gcc编译器来完成这⼀系列操作。# 接下来我们深入探讨一下编译和链接的整个过程来更好的理解动态静态库的使用原理。# 先来回顾下什么是编译呢编译的过程其实就是将我们程序的源代码翻译成CPU能够直接运行的机器代码。# 比如在一个源文件 hello.c 里便简单输出hello world!并且调用一个run函数而这个函数被定义在另一个源文件 code.c 中。这里我们就可以调用 gcc -c 来分别编译这两个源文件。
// hello.c
#include stdio.hvoid run();
int main() {printf(hello world!\n);run();return 0;
}// code.c
#includestdio.h
void run {printf(running... n);
}// 编译两个源⽂件
$ gcc -c hello.c
$ gcc -c code.c
$ ls
code.c code.o hello.c hello.o# 可以看到在编译之后会生成两个扩展名为.o的文件它们被称作目标文件。要注意的是如果我们修改了一个原文件那么只需要单独编译它这一个而不需要浪费时间重新编译整个工程。目标文件是一个二进制的文件文件的格式是ELF是对二进制代码的一种封装。
$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
## file命令⽤于辨识⽂件类型六、ELF文件1、ELF格式# 要理解编译链接的细节我们不得不了解一下ELF文件。其实以下4种文件其实都是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的描述。节SectionELF文件中的基本组成单位包含了特定类型的数据。ELF文件的各种信息和数据都存储在不同的节中如代码节存储了可执行代码数据节存储了全局变量和静态数据等。# 最常见的节代码节text用于保存机器指令是程序的主要执行部分。数据节data保存已初始化的全局变量和局部静态变量。2、ELF形成可执行step-1将多份 C/C 源代码翻译成为目标 .o 文件 动静态库ELFstep-2将多份 .o 文件section进行合并
注意实际合并是在链接时进⾏的但是并不是这么简单的合并也会涉及对库合并此处不做过多追究。
3、ELF可执⾏⽂件加载3.1 Section Header Table# 一个ELF会有多种不同的Section在加载到内存的时候也会进行Section合并形成Segment。合并原则相同属性比如可读可写可执行需要加载时申请空间等。这样即便是不同的Section在加载到内存中可能会以Segment的形式加载到一起。很显然这个合并工作也已经在形成ELF的时候合并方式已经确定了具体合并原则被记录在了ELF的程序头表Program header table中。将来加载程序时哪些数据节是在一块加载由这个表指明。# 我们可以通过readelf命令读取可执行程序的ELF-S选项就可以读取ELF的Section Header Table从而读取所有的数据节。这里我们读取Is命令的ELF# 我们看到Section Header Table就是大小为30的数组数组里面存储每个section的信息而我们的ELF格式就是一个大文件而我们要定位一个section只需要知道section相对于文件开头的偏移量section长度即可。# 所以我们可以把二进制或文本文件想象为一个一维数组数组里面的元素就是一个一个的字节所以我们不管要定位section还是ELF Header或者是其他区域我们只要知道相对于文件开头的偏移量section长度即可定位每一个区域。# 上图这里的Address和Offset就是偏移量和长度.text就是我们的代码段.data就是我们的全局变量。因为我们的全局变量在加载的时候就要确定好所以在可执行程序里面就给我们形成了。3.2 Program Header Table# 我们可以通过readelf命令读取合并之后的segment加上-l选项就可以读取Program Header Table这里我们读取Is命令的# 我们可以看到一共有13个segment在文件偏移呈64的位置LOAD表示将来要加载到内存中的区域。也可以发现.data和.bss都是合并在5号segment中所以已初始化数据和未初始化数据都加载一块了其实我们的.rodata只读数据区和.text代码区其实也是加载到一块的但是这个机器没有那么做而已。# 所以我们把这些数据节和合并为数据段将来进行整体加载将来加载时操作系统读取Program Header Table表根据偏移量位置长度找到对应若干个数据节的segment然后进行加载到空间里此时就完成了加载的过程# 结论所以其实我们ELF已经在链接时把如何加载的问题确定。同时我们可以看到这里还有一个FLags他表示该分段是否可读写所以我们的程序如何知道代码段是只读的哪些段是可读可写的页表的权限位信息从哪里来其实都是操作系统读取Program Header Table的信息然后初始化页表的权限位信息即可。# 那么 程序头表 和 节头表 ⼜有什么⽤呢其实 ELF ⽂件提供 2 个不同的视图/视⻆来让我们理解这两个部分
所以程序头表Program Header Table的作用链接时把每个.o文件的.data和.text合并为segment然后更新Section Header Table表面可执行程序是如何形成的包含每个节的属性如是否可读写等是被链接器使用的。节头表Section Header Table的作用加载时也会形成Program Header Table他会告诉操作系统如何加载可执行程序如何完成内存等初始化是被加载器使用的。
# 我们链接时会把多个.o的.text和.data合并为segment而真正合并是在加载器加载时完成的3.3 ELF Header# ELF Header保存的是整个ELF的管理信息例如每个区域的开始和结束位置。# 我们可以通过readelf指令-h选项可以查看目标文件的ELF Header信息# Magic魔数文件开头的一组特定字节序列通常位于文件的开头不同的文件格式都有其特定的魔数通过检查文件的魔数系统可以快速判断文件的类型。
// 查看⽬标⽂件
$ readelf -h hello.o
ELF Header:Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00Class: ELF64 # ⽂件类型Data: 2s complement, little endian # 指定的编码⽅式Version: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: REL (Relocatable file) # 指出ELF⽂件的类型Machine: Advanced Micro Devices X86-64 # 该程序需要的体系结构Version: 0x1Entry point address: 0x0 # 系统第⼀个传输控制的虚拟地址在那启动进程。假如⽂件没有如何关联的⼊⼝点该成员就保持为0。Start of program headers: 0 (bytes into file)Start of section headers: 728 (bytes into file)Flags: 0x0Size of this header: 64 (bytes) # 保存着ELF头⼤⼩(以字节计数)Size of program headers: 0 (bytes) # 保存着在⽂件的程序头表program header table中⼀个⼊⼝的⼤⼩Number of program headers: 0 # 保存着在程序头表中⼊⼝的个数。因此e_phentsize和e_phnum的乘机就是表的⼤⼩(以字节计数).假如没有程序头表变量为0。Size of section headers: 64 (bytes) # 保存着section头的⼤⼩(以字节计数)。⼀个section头是在section头表的⼀个⼊⼝Number of section headers: 13 # 保存着在section header table中的⼊⼝数⽬。因此e_shentsize和e_shnum的乘积就是section头表的⼤⼩(以字节计数)。假如⽂件没有section头表值为0。Section header string table index: 12 # 保存着跟section名字字符表相关⼊⼝的section头表(section header table)索引。
七、链接与加载1、静态链接# ⽆论是⾃⼰的.o还是静态库中的.o本质都是把.o⽂件进⾏连接的过程。所以研究静态链接本质就是研究.o是如何链接的。# 这里我们通过一段代码来观察
// hello.c#includestdio.hvoid run();int main()
{printf(hello tata!\n);run();return 0;
}
// code.c#includestdio.hvoid run()
{printf(running...\n);
}# 这里我们在hello.c写了一个main函数里面使用了run函数而run函数的实现在code.c文件。然后把这两个文件编译成.o文件链接两个.o文件形成main.exe。他们都使用了printf因此两个.o文件都要和C标准库链接然后.o之间也要相互连接因为hello.c调用了run函数。# 因为这两个.o是合并形成了main.exe所以动态链接只有C标准库。# 然后我们可以使用objdump -d 目标文件指令对目标文件的代码段(.text)进行返回反汇编。这里我们对code.o和hello.o进行反汇编后写入.s文件中。# 通过反汇编我们可以知道call其实就是调用函数这里两个call就是调用printf和run。然后call汇编指令转化为机器码就是e8 xx xx xx xx 这个e8就是call命令的机器码xx xx xx xx就是调用的函数地址。而此时xx xx xx xx为全0是由于此时我们函数地址并没有填充因为我们只是对hello.o进行反汇编并没有链接因为我们printf和run要和C标准库和.o链接才可以知道要调用的函数实现才能得到函数地址所以我们的code.c的run调用printf的汇编地址也是0因为也没有链接C标准库所以只能暂时设为0。# 这里我们使用readelf -s 目标文件读取目标文件的符号表# 我们可以看到run和puts其实printf底层调用的就是puts我们都看到他们两个方法都是UND未定义的code.c调用printf也是一样但是code.c是run不是UND未定义的因为他已经实现了run函数所以将来链接的时候hello.c的run就会去code.c的符号表找到run方法但是他们的puts的都是未定义他们又会去C标准库查找此时所有调用的方法就都可以找到了就完成链接了。# 所以查看连接后的可执行程序main.exe的符号表就发现run的就不是未定义的了说明连接后就可以找到run方法了但是因为我们是动态链接所以puts还是未定义的。# 我们看到run对应的Ndx的值为16说明多个section合并后是处于第16个section的。# 后我们又发现main.exe的第16个section就是.text说明run和main都被合并了代码段。# 然后我们反汇编链接后的可执行文件main.exe发现我们的run和puts的地址已经被填充了run填充call的地址就是我们run函数的地址1149所以我们的链接时就会把我们call的0地址填充修改call地址。# 把所有的.oELF的section合并完成了编址此时就完成了静态链接。我们把连接过程对.o中外部符号call后面的地址修改叫做地址重定位。所以.o文件也叫做重定位目标文件因为链接时地址会被修改。# 静态链接就是把库中的.o进⾏合并和上述过程⼀样。所以链接其实就是将编译之后的所有⽬标⽂件连同⽤到的⼀些静态库运⾏时库组合拼装成⼀个独⽴的可执⾏⽂件。其中就包括我们之前提到的地址修正当所有模块组合在⼀起之后链接器会根据我们的.o⽂件或者静态库中的重定位表找到那些需要被重定位的函数全局变量从⽽修正它们的地址。这其实就是静态链接的过程。# 所以链接过程中会涉及到对.o中外部符号进⾏地址重定位。2、ELF加载与进程地址空间2.1 虚拟/逻辑地址(平坦模式编址)# 问题一个可执行程序没有加载到内存里此时他有没有地址?
其实我们前面看到汇编要定位一个函数或变量时其实不是拿着变量名或函数名而是以地址来定位所以其实我们的代码中的变量名编译好后都变成了地址同时我们的可执行程序没有加载到内存他也有地址。
# 问题我们的虚拟地址空间每个区域的开始和结束都记录在了mm_struct和vm_struct里面那他们里面的值从哪里来的?
从可执行程序ELF中每个sgment来每个sgment都有自己的起始地址和结束地址的逻辑地址用来初始化内核结构体中的start和end数据。也就是说加载时操作系统会读取Program Header Table中的相关字段然后用可执行程序的逻辑地址直接初始化内核数据结构的start和end。
# 所以虚拟地址空间机制不光光OS要⽀持编译器也要⽀持。2.2 重新理解进程虚拟地址空间# 我们的可执行程序的每行代码都有自己的地址当加载可执行程序时程序就会变为进程操作系统就会为他申请task_struct、mm_struct等内核数据结构而当我们的代码加载到内存的后每一行代码都要占据物理内存空间所以每一行代码也一定存在他的物理地址# mm_struct存在代码区的start和end而mm_struct的地址是虚拟地址所以mm_struct的地址加载到页表左侧物理地址加载到页表的右侧此时虚拟到物理地址的映射关系就有了。# 问题CPU怎么知道你的可执行程序的其实地址是什么也就是CPU怎么知道从哪里开始执行呢
我们的CPU有一个指令寄存器EIP用来存储当前执行指令的下一条地址。EIP是Program Header Table的一个字段这个字段他记录了一个地址就是程序的入口地址。
# 所以加载的过程中操作系统直接把我们Entry point address填充到CPU的EIP寄存器中加载后页表的虚拟和物理地址也有了此时CPU开始调度进程了。同时我们CPU还有CR3寄存器他会指向当前进程的页表根据EIP的地址通过CR3查表就可以找到地址对应代码的指令放入CPU中。如果该指令还call其他的虚拟地址此时CPU就会继续拿着该地址进行查表继续上述过程。所以进入CPU的地址都是虚拟地址此时CPU就不再关心物理地址。# 现在我们总体上来谈可执行程序是如何加载到内存的3、动态链接与库加载3.1 进程如何看到动态库# 静态库不涉及加载的问题因为静态库和.o链接合并形成可执行了所以静态库就是以ELF为载体加载的。# 而我们的可执行如果是动态链接此时我们的可执行程序和动态库是两个独立文件所以一旦我们的程序运行起来就需要对动态库进行查找因为库也是一个独立的文件所以就需要把动态库也加载到物理内存中。# 那么如何让我们的进程看到动态库呢(动态库是如何和我们的可执行程序关联的)
库也要建立页表的映射关系经过页表映射关系映射到一个进程地址空间上的共享区上此时程序一但调用库方法只需要从代码区跳转到共享区完成调用后再返回即可完成库函数调用而以前我们自己的调用就是从代码区内部跳转到代码区即可。
# 库函数调用步骤
被进程看到 -- 动态库映射到进程地址空间被进程调用 -- 在进程的地址空间中进行跳转
3.2 进程间如何共享库# 如果是两个进程调用库的话只需要让进程B也建立动态库和共享库的映射即可。# 动态库的本质就是在系统层面上把公共的部分抽取出来只保存一份而静态链接则会出现重复代码因为静态库是拷贝到.o文件中就会在内存中加载多份。所以动态库也叫共享库。3.3 动态链接3.3.1 动态链接如何工作# 动态链接其实远⽐静态链接要常⽤得多。⽐如我们查看下 main.exe 这个可执⾏程序依赖的动态库会发现它就⽤到了⼀个c动态链接库# 这⾥的 libc.so是C语⾔的运⾏时库⾥⾯提供了常⽤的标准输⼊输出⽂件字符串处理等等这些功能。那为什么编译器默认不使⽤静态链接呢静态链接会将编译产⽣的所有⽬标⽂件连同⽤到的各种库合并形成⼀个独⽴的可执⾏⽂件它不需要额外的依赖就可以运⾏。照理来说应该更加⽅便才对是吧# 静态链接最⼤的问题在于⽣成的⽂件体积⼤并且相当耗费内存资源。随着软件复杂度的提升我们的操作系统也越来越臃肿不同的软件就有可能都包含了相同的功能和代码显然会浪费⼤量的硬盘空间。# 这个时候动态链接的优势就体现出来了我们可以将需要共享的代码单独提取出来保存成⼀个独⽴的动态链接库等到程序运⾏的时候再将它们加载到内存这样不但可以节省空间因为同⼀个模块在内存中只需要保留⼀份副本可以被不同的进程所共享。# 动态链接到底是如何⼯作的
⾸先要交代⼀个结论动态链接实际上将链接的整个过程推迟到了程序加载的时候。⽐如我们去运⾏⼀个程序操作系统会⾸先将程序的数据代码连同它⽤到的⼀系列动态库先加载到内存其中每个动态库的加载地址都是不固定的操作系统会根据当前地址空间的使⽤情况为它们动态分配⼀段内存。当动态库被加载到内存以后⼀旦它的内存地址被确定我们就可以去修正动态库中的那些函数跳转地址了。
3.3.2 动态链接器# 我们还可以发现无论是我们的可执行程序还是系统的指令除了依赖C标准库还都依赖一个linux-x86的动态库其实所有的C程序都依赖这个库。这是为什么呢
也就是说链接时_start函数会帮我们加载程序所依赖的动态库而我们的上面依赖的linux.so就是动态链接器动态链接器负责加载动态库他通过搜索环境变量LD_LIBRARY_PATH和配置文件/etc/ld.so.conf及其子配置文件来找到动态库。
3.3.3 动态库中的相对地址# 动态库为了随时进⾏加载为了⽀持并映射到任意进程的任意位置对动态库中的⽅法统⼀编址采⽤相对编址的⽅案进⾏编制的(其实可执⾏程序也⼀样都要遵守平坦模式只不过exe是直接加载的)。3.3.4 程序与库的映射注意动态库也是⼀个⽂件要访问也是要被先加载要加载也是要被打开的让我们的进程找到动态库的本质也是⽂件操作不过我们访问库函数通过虚拟地址进⾏跳转访问的所以需要把动态库映射到进程的地址空间中
3.3.5 库函数调用原理 -- 加载地址重定位3.3.6 全局偏移量表GOT(global offset table)问题代码区不是只读的吗怎么可以修改呢
是的代码区(.text)是只读的可是我们还是想使用 起始地址偏移量 的方式完成库函数调用所以动态链接采用的做法是在.data可执行程序或者库自己中专门预留一片区域用来存放函数的跳转地址它也被叫做全局偏移量表GOT。表中每一项都是本运行模块要引用的一个全局变量或函数的地址因为.data区域是可读写的所以可以支持动态进行修改。
# GOT 表本质位于 .data 段的函数指针数组存储外部函数/变量的绝对地址。3.3.7 库间依赖 -- PLT机制
注意不仅仅有可执⾏程序调⽤库库也会调⽤其他库库之间是有依赖的如何做到库和库之间互相调⽤也是与地址⽆关呢库中也有.GOT,和可执⾏⼀样这也就是为什么⼤家为什么都是ELF的格式