铺垫
1.动态库与静态库本质上都是文件。
2.关于动静态链接
动态链接:将库中需要的函数地址,填入可执行程序,建立关联。
优点:节省资源,不会出现太多重复代码。
缺点:对库的依赖性比较强,一旦库丢失,所有使用这个库的程序将无法运行。
静态链接:将库中的方法的实现拷贝到我们可执行程序中。
优点:不依赖库,同类型平台中可以直接使用。
缺点:可执行程序的体积比较大,比较浪费资源。
3.默认编译程序,用的是动态链接的,如果要静态链接 需要加上 -static
4.库的命名一般是libXXX.so(动态库) libXXX.a(静态库),真实的库名称是XXX的部分。
动静态库的制作和使用
为什么要有库?
1.提高开发效率
2.隐藏源代码
首先写几个简单的.c .h代码
myprint1.h1 #pragma once 2 #include<stdio.h>3 4 void print1();
myprint1.c1 #include"myprint1.h"2 3 void print1()4 {5 printf("hello 111\n"); 6 }
myprint2.h1 #pragma once 2 #include<stdio.h>3 4 void print2();
myprint2.c1 #include"myprint2.h"2 3 void print2()4 {5 printf("hello 222\n"); 6 }
main.c1 #include"myprint1.h" 2 #include"myprint2.h"3 4 int main()5 {6 print1();7 print2();8 return 0;9 }
以往都是直接进行编译
gcc -c myprint1.c
gcc -c myprint2.c
gcc -c main.c
生成同名.o
最后链接形成可执行文件
gcc -o test.exe main.o myprint1.o myprint2.o
总结如下图
静态库文件的制作
首先编译成.o的文件
gcc -c myprint1.c
gcc -c myprint2.c
打包.o的所有文件成为libmyc.a
ar -rc libmyc.a myprint1.o myprint2.o
-c(creat)如果没有libmyc.a则自动创建
-r(replace)如果已经有libmyc.a,且myprint1.o myprint2.o内容发生改动,则重新创建libmyc.a替换原文件
最后生成libmyc.a静态库文件。
静态库文件的使用
进行编译链接main.c文件
gcc main.c -lmyc -L .gcc 要编译的文件 -l库的名字 -L 库所在的路径
这里编译器会在指定的目录下寻找库,不会在当前目录下寻找,所以加上-L选项指定路径让编译器寻找。
生成a.out可执行文件。
动态库文件的制作
gcc -c -fPIC myprint1.c
gcc -c -fPIC myprint2.c-fPIC:产生位置无关码
形成.o文件。
gcc -shared -o libmyc.so myprint1.o myprint2.o
生成libmyc.so动态库文件。
动态库文件的使用
gcc main.c -L. -lmyc
-L要链接的库在哪个路径下
-l要链接的库
最后产生a.out可执行文件。
注意:
如果头文件不在当前目录,编译时需要使用 -I 选项。
总结来说:
gcc -o mytest main.c -L链接库所在路径 -l链接库名字 -I头文件的路径
一个小指令:
ldd a.out 查看可执行文件链接的库文件
编写库的人:未来要给别人(用库的人),交付的是:头文件+库文件,不给源代码。
总结
文件在编译成可执行文件后,
1.如果链接的是静态库,运行时就不需要静态库了。
原因是:静态链接是将库中的方法的实现拷贝到我们可执行程序中。
2.如果链接的是动态库,运行时还需要动态库。
对于动态库:
编译时的库搜索路径 ---- 编译器gcc
运行时的库搜索路径 ---- 操作系统OS
二者可以相同,也可以不同,操作系统通常是去系统默认的搜索路径(Linux中默认为/lib64 )去搜索库文件,编译器也默认去/lib64中搜索(可以通过指令更改 gcc -o mytest main.c -L链接库所在路径 -l链接库名字 )
3.在编译的预处理阶段已经将头文件展开在目标文件里了,所以运行时不需要头文件。
解决运行时动态库找不到问题的四种方法
1.最普通的方法(库比较官方时推荐)
把库拷贝到操作系统默认的搜索路径(Linux中默认为/lib64 ),既可以支持编译,又可以支持运行。
2.环境变量法
查看LD_LIBRARY_PATH环境变量
echo $LD_LIBRARY_PATH
把运行时需要查找库的目录添加
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:运行时需要查找库的路径
缺点:重启机器后,环境变量会恢复。
解决方法:需要更改配置文件,这样机器每次启动时读取配置文件,都会把目录加上。
在初始目录下,有2个配置文件 .bash_profile .bashrc
打开 .bashrc文件
看到最后一行,就是每次重启时配置的环境变量,在最后追加上库的路径即可(:冒号作为分隔符)。
3.软链接方法(库不是官方,个人写的,推荐这种方法)
sudo ln -s /动态库的绝对路径 /lib64/同动态库名
这时在/lib64目录下有一个动态库的同名文件指向原路径下的动态库。
4.配置文件法
在 /etc/ld.so.conf.d 路径下建立一个文件 文件名.conf 在该文件下放置动态链接库的路径即可。
/etc/ld.so.conf.d 的含义:ld:load加载 .so:动态库 .conf:配置文件 .d:目录
最后要让刚才的配置文件生效使用如下命令。
sudo ldconfig
关于同时存在动静态库时链接问题
1.如果同时提供动态库和静态库,gcc默认使用动态库
2.如果非要使用静态库,就必须使用static选项
3.如果只提供静态库,则只能对该库进行静态链接,但是程序不一定整体是静态链接的。
4.如果只提供动态库,默认是只能动态链接,若强行静态链接,会报错!
理解动态库加载
1.系统角度理解动态库加载
动态库加载后会被映射到进程的共享区中。
当进程1的代码mytest1需要用到myc动态库时,OS将该库加载到物理空间,进而通过页表映射到进程1的虚拟地址空间的共享区中,以使用。
添加一个进程,当进程2的代码mytest2再次需要用到 myc动态库时,直接将物理空间中该动态库的代码和数据,通过页表映射到进程2的虚拟地址空间的共享区中即可。
库只需要被加载一次,然后在不同的进程地址空间中进行映射,就可以使用该库,这就是为什么叫做动态库,共享库。
一些问题:
1.谁来决定,哪些库加载了,哪些库没加载?OS会自动完成。
2.系统中可不可以同时存在非常多已经加载的库?肯定的。
操作系统要对这些库进行管理,通过一个结构体struct loadlib进行描述已经加载的库,再通过链表进行增删查改管理。
struct loadlib
{char* libname;void* addr;……struct loadlib* next;
}
2.关于编址
可执行程序本身是有自己的格式信息的。
1.如果我们的可执行程序,没有加载到内存中,我们的可执行程序有没有地址(每行代码生成的指令的地址) ?有,程序通过编译后就有了地址。我们来验证。
objdump -S a.out > test.s在 Linux 中,使用 objdump -S a.out > test.s 命令可以将可执行文件 a.out 的反汇编代码(-S选项:混合源代码和汇编代码)输出到 test.s 文件中。
生成的 test.s 文件包含:
-
机器指令的汇编表示(如
mov
,call
)。 -
虚拟地址(逻辑地址):每条指令在进程虚拟地址空间中的位置。
可以看到可执行程序在没有加载进内存时就已经有了地址(每行代码生成的指令的地址)
其实我们的可执行程序,在没有加载之前,也已经基本上被按照类别(比如权限,访问属性等)已经将可执行程序划分成为各个区域了。如下:
size a.out
2.我们进程地址空间里面的很多地址数据属性数据,是从可执行程序来的。
程序执行的起始地址,存储在可执行文件头(如ELF的e_entry字段)。操作系统加载程序后,CPU从此地址开始执行。
编址的两种方式:绝对编址,相对编址
虚拟地址(线性地址):通常指分页机制中的地址(虚拟地址空间中的地址),即虚拟地址通过页表转换后的地址称为物理地址。
平坦模式:Linux将代码段、数据段等所有段的基址(Base Address)设为0,段限长(Limit)设为最大值。
绝对编址:是指在编译和链接中确定所有代码和数据的固定地址,这意味着程序中的每个指令和数据项都使用固定的、绝对的内存地址进行引用。这种编址方式也称为平坦模式。
绝对编址的范围:32位平台下 [0,4GB]
对于代码和数据的每一个地址都是绝对确定的,这就是未来的虚拟地址,可以对应到虚拟地址空间,填充到页表。
虚拟地址空间本身不仅是OS要遵守,编译器编译程序时也要遵守。
相对编址(逻辑地址):是指地址是相对于某个段的偏移量,通常用于表示程序中的变量、函数等的位置。逻辑地址在编译时生成,并且与具体的内存布局无关。通俗来说,如果我们将数据段与代码段分开,代码段从0开始,数据段从0开始,通过偏移量来描述地址的方式,称为相对编址。
Linux通过平坦模式(段基址为0),使逻辑地址的偏移量直接等于线性地址(虚拟地址),后续仅需分页机制转换为物理地址,从而简化内存管理。
总结:Linux逻辑地址 = 虚拟地址(线性地址)
3.理解动态库动态链接和加载的问题
a.一般程序加载
几个问题:
1.地址空间既然是一个数据结构的对象,那么谁来用什么数据初始化?
可执行程序加载时,会用可执行程序头部相关字段,来把地址空间这个结构体对象进行初始化。
所以不同程序就会有不同的正文区域,初始化区域,未初始化区域的大小。
2.代码是数据吗?是的。
所谓的地址空间,本质是由操作系统+编译器+CPU,三者共同配合完成的!
b.动态库的加载
静态库没有这一说法,一旦静态库被使用,那么静态库文件以绝对编址的方式写入代码中,随代码加载进物理地址。
对于动态库加载:
库的数据和方法的访问,都是通过库所在地址空间的起始地址+程序内部的偏移量即可。