本章重点:
动静态库的制作,使用和查找
可执行程序ELF格式
可执行程序的加载过程
虚拟地址空间和动态库加载的过程
动静态库的制作,使用和查找
1.在了解库的制作之前,我们首先需要知道什么是库。库是写好的现有的,成熟的,可以反复复用的代码。有了库我们只需要有头文件和知道对应的调用方法就可以使用库中的函数,不需要每次都造轮子手撕,我们平时使用的c/c++标准库都是这样封装的库,然后将头文件暴露出来给用户调用。
2.本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库分为静态库 .a[Linux]、.lib[windows],和动态库.so[Linux]、.dll[windows]。
3.静态库:静态库(.a):程序在编译链接的时候把库的代码链接到可执⾏⽂件中,程序运⾏的时候将不再需要静态库,静态库可以理解为多个.o文件的集合。⼀个可执⾏程序可能⽤到许多的库,这些库运⾏有的是静态库,有的是动态库,⽽我们的编译默认为动态链接库,只有在该库下找不到动态.so的时候才会采⽤同名静态库。我们也可以使⽤ gcc的 -static 强转设置链接静态库。
这里使用之前在基础IO文章中写的mylib.c和mylib.h来封装一个静态库。
需要先构造一个makefile文件帮助我们快速构造静态库,这里先用 ar
工具将 mylib.o
添加到libmylib.a
中,ar是gnu的一个归档工具,用于创建、修改、提取 .a
静态库文件。rc表示replace和create,表示如果这个库存在就替换,不存在就创建它。
此时我们并没有.o文件,所以还需要将.c文件编译生成.o文件,%.o:%.c,这是一个通配规则,意思是:让所有的 .c文件可以对应生成 .o
文件,此处只有一个.c文件,但是静态库可以由多个.o文件链接形成。
然后编写一个清理指令即可。
执行make指令我们就得到了一个静态链接库
那么如何使用静态链接库呢?我们只需要在需要调用静态库函数的文件包含对应的头文件,然后编译的时候带对应的库即可,这里又有三种方法链接库:
(1)库文件和头文件都安装在系统默认路径,比如:头文件在 /usr/include/
,库文件在 /usr/lib/
或 /usr/lib64/
(2)库文件和源文件在同一目录下,直接使用指令g++ -o main main.c. -L. -lmylib,-L告诉编译器在当前目录下寻找库,-l链接名为libmylib.a的静态库
(3)头文件和库文件分别在独立目录,此时mylib.h文件在Include文件中,libmylib.a在lib文件中,使用指令g++ -o main -main.c -Iinclude -Llib -lmylib,需要使用-I指定头文件路径,-L指定库的路径,-l指定连接哪个库
注意形成执行程序以后即使删除libmylib.a也可以正常运行可执行程序,因为静态库已经连接到可执行程序的内部了,所以我们可以看到的是此时的可执行程序会变得很大。
这里我们还可以在makefile内部使用指令将生成的静态库进行打包压缩然后发送给别人使用,此时只需要解压然后使用第三种方法也就是头文件和库文件分别在不同目录就可以链接库了。
所以我们完整的发布流程就是:先将.o文件链接成静态库,然后打包压缩头文件和静态库
4.动态库:程序在运⾏的时候才去链接动态库的代码,多个程序共享使⽤库的代码。 ⼀个与动态库链接的可执⾏⽂件仅仅包含它⽤到的函数⼊⼝地址的⼀个表,⽽不是外部函数所在⽬标⽂件的整个机器码。在可执⾏⽂件开始运⾏以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking) 。动态库可以在多个程序间共享,所以动态链接使得可执⾏⽂件更⼩,节省了磁盘空间。操作系统采⽤虚拟内存机制允许物理内存中的⼀份动态库被要⽤到该库的所有进程共⽤,节省了内存和磁盘空间。
动态库的生成过程与静态库类似,不一样的是生成库的指令以及生成.o文件的指令,shared: 表⽰⽣成共享库格式,fPIC:产⽣位置⽆关码(position independent code) ,库名规则:libxxx.s。
g++ -fPIC -c mylib.cpp -o mylib.o # 先编译为位置无关的目标文件
g++ -shared -o libmylib.so mylib.o # 再链接为共享库
动态库的使用方法也有三种:
(1)头⽂件和库⽂件安装到系统路径下,使用命令g++ main.c -lmylib即可链接动态库
(2)头文件与库文件与源文件同目录,使用命令g++ main.c -L. -lmylib
(3)头文件与库文件分别有独立的路径,使用命令g++ main.c -Iinclude -Llib -lmylib,注意当前lib路径下如果有同名动态库和静态库优先会链接动态库。
可以使用ldd 来查看链接了哪些库,此时发现确实是链接了我们的动态库
但是运行的时候发现报错了,这是因为程序在运行时找不到共享库 libmylib.so
,因为Linux 默认只在系统路径(如 /lib
, /usr/lib
)下查找共享库,不会自动查找 -L
指定的路径!
这里有三种方法可以解决,(1)拷贝.so文件到系统共享库的路径下 (2)向系统共享库路径下建立同名的软连接 (3)更改环境变量 这里使用方法三来演示:
这样就可以增加系统查找共享库的路径了。需要注意的是前面两种方法操作以后都需要使用sudo ldconfig来更新系统共享库的索引。
可执行程序ELF格式
在Linux之中有四种文件都是ELF格式的文件,有xxx.o文件,xxx.so文件,xxx可执行文件以及内核转储文件。一个ELF格式的文件由四个部分组成:
(1)ELF头:描述文件的主要特性,其文件的开始位置,它的最主要目的是定位文件的其他部分。
(2)程序表头: 列举了所有有效的段(segments)和他们的属性。表⾥记着每个段的开始的位置和位移(offset)、⻓度,毕竟这些段,都是紧密的放在⼆进制⽂件中,需要段表的描述信息,才能把他们每个段分割开。
(3)节头表(Section header table) :包含对节(sections)的描述。
(4)节(Section ):ELF⽂件中的基本组成单位,包含了特定类型的数据。ELF⽂件的各种信息和 数据都存储在不同的节中,如代码节存储了可执⾏代码,数据节存储了全局变量和静态数据等。
可执行程序的加载过程
ELF形成可执行程序分为以下两部:(1)将多份c/c++源代码翻译成为.o文件 (2)将多份.o文件的section进行合并。
所以链接其实就像将一个个的相同属性的section进行合并。
虚拟地址空间和动态库加载的过程
一个可执行程序在加载到内存之前有没有地址?进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪⾥来的⼀个ELF程序,在没有被加载到内存的时候,本来就有地址,当代计算机⼯作的时候,都采⽤"平坦模式"进⾏⼯作。所以也要求ELF对⾃⼰的代码和数据进⾏统⼀编址,下⾯是 objdump -S 反汇编之后的代码
左侧的就是ELF的虚拟地址,其实,严格意义上应该叫做逻辑地址(起始地址+偏移量), 但是我们认为起始地址是0.也就是说,其实虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执⾏程序进⾏统⼀编址了.
进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪⾥来的?从ELF各个 segment来,每个segment有⾃⼰的起始地址和⾃⼰的⻓度,⽤来初始化内核结构中的[start, end]等范围数据,另外在⽤详细地址,填充⻚表。
所以虚拟地址空间是cpu/编译器/OS协调操作下的共同产物。