Linux:动静态库
一、库的由来
源文件(.c)经过编译(gcc -c 源文件)得到目标文件(.o),如果需要用到库中的内容,则再要经过链接这个过程,最终得到可执行程序。
假设现在,我们要在远端的某台机器运行这个可执行程序,那么对方也需要经过编译、链接,同时我们又不希望将源文件发送给对方,即闭源,于是我们可以把目标文件(.o)发送给对方,把所有的目标文件打包在一个文件里,就是库文件,对方在编译时,使用多个头文件(接口使用描述)+ 一个库文件就可以完成编译、链接了。
这也解释了为什么,我们平时写项目,会有多个头文件、多个源文件、头文件中不写具体实现方法。
二、制作库
我们使用自己的库,这种库是第三方库,使用gcc命令时要带 -l(小写的L)、-L选项,有时还需要-I(大写的i)。
-l 指定是哪个库
-L指定库在哪里
-I(大写的i)指定头文件在哪里
而我们以前不需要带这几个选项,就是因为默认情况下,gcc命令会在指定路径下找库和头文件。
大多数编译器
找头文件,默认有两个路径,一个是当前路径,一个是系统提供的路径
找库,一般是在系统提供的路径,去找对应语言的库。
Ubuntu下,
比如stdio.h,在/usr/include路径下
比如C语言的库,是在路径/usr/lib/x86_64-linux-gnu/libc.*
做库的角度:制作动静态库、下一步是发布动静态库,是一个打包的文件。
用库的角度:下载,安装(本质是把库、头文件拷贝到对应默认的路径),如果为了防止第三方库污染我们本来的库,编译时就得带很多选项来指定路径。
静态库,是一个大文件,需要用到ar工具。
动态库,是一个可执行程序,只需要用gcc 命令 即可完成制作。
三、第三方动态库的链接
背景:
某个用户为了防止第三库的污染,并没有安装库,而是带很多选项来编译,可以正常编译,但是可执行程序会报错。
原因就是动态库会有一个加载的过程,而静态库是把全部内容加到源文件中,编译结束,静态库就没有任何用处了,但是动态库不行,使用动态库编译的源文件,在运行可执行程序后,还再去访问动态库内容。
编译时,指定动态库是为了告诉编译器。
运行时,也要指定动态库,为了告诉操作系统。
解决方法一:
安装库,将库安装到系统指定的路径里,编译时编译器可以找到so库,运行时操作系统也可以找到so库。
解决方法二:
部分Linux机器可能存在一个全局环境变量,$LD_LIBRARY_PATH,这个环境变量就是辅助操作系统运行可执行程序时查找动态库的路径。
可以把第三方库的路径添加到这个环境变量中。
exprot LD_LIBRARY_PATH=$LD_LIBARRY_PATH:第三方库路径。
但是手动添加的环境变量在机器重启的时候会重新初始化,此时的这个环境变量的值是配置文件里面配置的,并没有这个第三方库路径。
因此,需要把这个第三方库路径配置到配置文件里面,一般是家目录的.bashrc
(推荐)解决方法三:
利用软链接!(软链接真正的用法!)
这个第三方库所在的目录还我们自己的目录下,但是可以创建软链接,这个快捷方式保存到系统指定目录里面。
一来不污染系统库,二来系统库里面只是多了一个快捷方法而已。
解决方法四:
/etc/ld.so.conf.d/这是一个目录,专门配置动态库路径。
这个目录下是各种配置文件。
在这个目录下新建一个自己的配置文件,把第三方库的路径添加进去,再执行命令ldconfig生效配置。即可运行。
动态库和静态库链接的几种情况。
- 如果同时提供动态库和静态库,gcc默认调用动态库。
- 如果想使用静态库,则的添加选项-static
- 如果只提供静态库,但是不采用静态链接的命令,可执行程序会被迫使用静态库来链接,但不一定全部代码都会被静态链接。
- 如果只提供动态库,非要静态链接,则报错。
四、动态库的加载
一个可执行程序运行起来后,变成了进程,拥有虚拟地址空间、PCB。
当执行到某个函数,比如Printf,需要使用动态库中的方法,此时动态库还在磁盘中,操作系统把动态库加载到物理内存中后,要在进程和动态库中建立连接,其实是把动态库的物理内存通过页表映射到了进程地址空间的共享区中。当cpu在代码区运行到printf后,转而去共享区(有对应函数的地址)运行。
由于操作系统管理多个进程,可能存在多个进程会共同访问某个动态库的方法,因此每一个进程都只是把动态库的物理地址通过页表映射到自己的进程地址空间,这就实现了:
所有进程中公共的代码和数据,把它封装成动态库后再运行,它们就在内存中只存在了一份,其他进程都做各自的映射即可。
因此,动态库也被称为共享库。
于是,有了新的问题。
- 进程如何知道它需要的这个库是否以及加载?
操作系统加载库,由操作系统管理,进程只在虚拟地址空间跳转地址即可。
- 内存中是否能同时存在多个已经加载的库?
存在,由操作系统管理,那么必然先描述、再组织,即必然定义了“已经加载了库”的结构体。
动态库的加载,其中方法的调用,比如printf语句的汇编是一条jmp语句,jmp的地址的相对地址,可执行程序中是库名+偏移量,加载到内存后,库也加载到内存后,(操作系统能够确定库的地址了)这个jmp语句向进程的地址空间映射的时候是一个库名地址+偏移量,这个库名地址是动态的,不是唯一确定的。
总结:动态库中的方法的地址通常表现为基地址 + 偏移量的形式,并且动态库的加载地址是可以变化的。
五、可执行程序的编址
可执行程序中是有地址的。
可执行程序把它的内容划分为代码部分、数据部分等等。
可执行程序中还有头字段,其中定义了main函数的地址,也包括了需要用到哪些库的指示信息,这些内容被称为可执行程序的格式,Linux下是ELF格式。
进程地址空间中,有很多地址字段,其中大部分都继承自可执行程序。
进程地址空间本质是由几个变量在维护,那么这些个变量是怎么初始化的?
- 区分绝对地址和相对地址、虚拟地址和物理地址,逻辑地址=虚拟地址(现今)逻辑地址=相对地址(早期),逻辑地址一般是磁盘上区分物理地址的叫法,虚拟地址一般是内存上区分物理地址的叫法。
绝对地址和相对地址。早期,区分绝对地址和相对地址(段地址+偏移量),这两种地址在计算机中都是存在的。
当今Linxu操作系统的计算机,其可执行程序中的地址是虚拟地址,也是绝对地址,只是把虚拟地址看成了起始地址为0+偏移量,等于绝对地址,不再区分相对地址,这种模式称为地址平坦模式,虚拟地址空间的编址实现由操作系统和编译器共同约定、共同遵守。
如今不再有段地址+偏移量这样的区分。
常常读作虚拟地址,本质上就是绝对地址!!!
- 或者说
早期有绝对地址和相对地址的区分,如今计算机发展足够成熟,不再有这个区别,或者说是为了兼容,把绝对地址看作了0+偏移量的相对地址。
而可执行程序中的地址编码(这个由编译器操作),就是虚拟地址,直接就是绝对地址,不再是某个段+偏移的计算结果,或者是0+偏移量,而这个虚拟地址也被拿来初始化了进程地址空间的地址(这个由操作系统完成),因此,进程地址空间中的地址不仅仅由操作系统主导,还有编译器参与。
更重要的是,cpu执行指令,指令中可能有地址,这个地址也是虚拟地址,于是可以说,虚拟地址空间是cpu、编译器、操作系统又或者说是整个计算机参与了维护。