C C++ Link
链接,将多个可重定位目标文件和标准库函数合并为可执行目标文件的过程,为了解决外部内存地址的依赖问题
在链接之前,各个程序模块都是相互独立的,模块A所使用到的模块B的内容,在模块A的视角下仅仅是一个符号,并不清楚其具体内容。链接过程可以理解为把模块B的内容结合到A中。整个过程类似搭积木最后的模块拼接过程。而这一拼接过程,采用专业术语来表达,即重定向。
- 静态链接 -> 静态地址重定位, 地址在链接时就已经确定
- 动态链接 -> 装载时地址重定位,地址在编译时是相对地址,具体的绝对地址在加载时由装载器(Loader)进行计算和修改
- 显示运行时链接 -> 运行时地址重定位
需要注意的是,Windows 下的链接过程涉及的知识和 Linux 下并不完全相同,本文仅面向 Linux 下的链接过程。同时,下文内容涉及到 ELF 这种文件格式,关于 ELF 的介绍,请见 ELF File Format Analysis。
Concept Ayalyze
Library Classification
- 静态链接库:static library, 一种文件归档(archive). relocatable Files + 索引(index) -> static library
静态链接库(libadd.a)的文件格式:
libadd.a: current ar archive
- 动态链接库:shared library(Linux) / dynamic link library(Windows)
动态链接库(libadd.so)的文件格式:
libadd.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=644e95c7d3f9bd18796622c3041e7653e402d179, not stripped
从本质上来说就是以上这两种,但是如果采用的链接方式是显示运行时链接,那么利用到的library又被称作dynamic loading library,使用的仍然是xxx.so文件,只是换了一种使用方式
Linking Mode
- 静态链接:使用
-static
选项,仅使用静态链接器,在编译链接过程中就将其他模块装入可执行文件中
静态链接生成的可执行目标文件的文件格式:
main: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=65422291d167d002123191a5f63d9a5503d6d670, not stripped
- 动态链接:默认的链接方式, 同时使用静态链接器和动态链接器,动态链接器在运行前将共享模块装载进内存并进行重定位操作
动态链接生成的可执行目标文件的文件格式:
main: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=3e226365a1d11bf52e8c7f5b4b9a72bbecbd7007, not stripped
- 显示运行时链接(Explicit Run-time Linking): 通过某些机制在运行时将共享模块装载进内存并进行重定位操作,让程序在运行时加载或卸载共享模块
关于library种类和链接方式之间的关系,容易产生的一个误解
静态链接库,动态链接库 和 静态链接,动态链接,这些名称很容易带给人一种误解: 采用静态链接库时采用的就是静态链接,使用动态链接库时采用的就是动态链接。
首先从链接方式的分类来说,静态还是动态链接是由编译选项-static
决定的,并不是采用了静态链接库还是动态链接库决定的。
但是它们之间并不是没有关系,从另一个角度来说,根据GNU文档Options for Linking - static对于static选项的描述
prevents linking with the shared libraries
可以看出,当我们使用static选项进行静态链接时,使用的只能是静态链接库。
所以,关于库和链接方式的对应关系,需要从两个方向进行讲述:
- 使用静态链接库时,进行的可以是静态链接 或 动态链接; 使用动态链接库,只能进行动态链接
- 进行静态链接时,只能使用静态链接库;进行动态链接时,可以使用静态链接库或动态链接库
i.e. 使用静态链接库是进行静态链接的必要不充分条件, 使用动态链接库是进行动态链接的充分不必要条件
注:后半句的表述并不是非常准确,因为在现行系统下,进行动态链接都会利用到一个特殊的动态链接库-动态链接器,同时C/C++程序无论是否调用一些函数,都会链接libc.so(这一点判断还没有寻找确切的证据,仅是根据实际测试结果),从这个角度来说,使用动态链接库是进行动态链接的充要条件,而上面的表述仅仅是为了表述库和链接方式的那2点对应关系,所以给出的是充分不必要条件
重新分析上述给出的那种误解,采用静态链接库时,如果没有给出-static
选项,那么这时候进行的是动态链接,这正验证了使用静态链接库对于进行静态链接的不充分性
除了上述提出的各类问题,还会涉及到一个比较隐含的问题。上面提到使用静态链接库可以进行静态链接,但是在注中又提到一个事实,即虽然我们人为仅仅指定了一些静态链接库,但是背后会隐含利用一些特殊的动态链接库。动态链接库是不可以进行静态链接的,但是为何在进行静态链接时,这些特殊的动态链接库并没有报错。
事实上,这些动态链接库,都存在着与之对应的静态链接库,在我们添加-static
选项后,链接使用的就是这些静态链接库。e.g. libc.so就存在一个libc.a的静态链接库(可通过命令find /usr/lib /usr/local/lib -name "libc.a"
查找到)
Static Library
静态链接是可执行目标文件在构建过程中完成的,使用链接器将多个.o可重定位目标文件结合(实际上也可以将.so动态链接库结合进来,在动态链接部分详细说明),生成可执行目标文件。
静态链接库:Windows平台.lib (library),Linux平台.a (archive)
假设编写一个包含加法运算的静态链接库,供main函数调用
|
|
|
|
使用ar
命令将汇编过程生成的.o可重定位目标文件生成静态链接库
|
|
ar -rsv
: 以输出较多信息的方式,完成下述任务:创建归档文件的同时,创建归档索引
使用file
命令可以查看生成的文件类型如下:
libadd.a: current ar archive
在链接环节链接该静态链接库(假设libadd.o和main.cpp在同一路径下)
|
|
以上就是一个创建及使用静态链接库的全流程。
值得注意的是Linux下静态链接库的本质,查询一下ar
命令会发现,其不过是一个创建归档文件的命令,和目前的tar
作用是类似的。
因此,所谓静态链接库不过是把一些.o可重定位目标文件集中起来放置到一个文件中,以便链接环节将各模块结合在一起。
不过tar
和ar
创建的归档格式并不相同,仅ar
才能用于创建静态链接库。
为何要用.a这种归档格式作为静态链接库? 把这些零散的目标文件直接提供给库的使用者,很大程度上会造成文件传输、管理和组织方面的不便,于是通常人们使用“ar”压缩程序将这些目标文件压缩到一起,并且对其进行编号和索引,以便于查找和检索,就形成了.a这种归档格式的静态库文件
tar
创建的归档格式如下:
libadd.a: POSIX tar archive (GNU)
静态链接库在构建过程中的参与情况示意图:
注:从图中也可以看出.a文件本身只是.o文件的一个容器,实际参与链接过程的仍然是.o可重定位目标文件
静态链接的缺点 静态链接是将所需的所有库文件内容进行整合,全部装入到可执行文件中,这种方式会带来以下两种问题:
- 内存和磁盘空间浪费严重:共用相同库文件的,不同的可执行程序中都存在着相同的库文件,如下图所示
- 程序更新不便:更新可执行程序所用的其中一个库文件需要重新下载整个可执行程序
How static linker find the library
Use
-L
command optionUse
LIBRARY_PATH
environment variableUse some default search path
Default search paths contain such as /usr/lib
and /usr/local/lib
.
Shared / Dynamic Library
动态链接的动态指的是哪个环节是动态的?/ 什么是动态链接库?
所谓动态链接,这里有两种不同的描述,在《程序员的自我修养-链接、装载与库》中的说法大概描述是:不在编译链接环节对那些组成程序的目标文件进行链接,而是等到程序运行时才进行链接“。但是根据下面的动态链接流程图,动态链接大致可以理解为:动态链接 = 编译链接环节的部分链接 + 程序运行时的完全链接。
我目前对这一点的理解是,编译链接环节的输出结果只是保存了程序需要使用到哪些库(也就是ldd
能够查询到的那些),然后在程序运行过程中由动态链接器来完成实际的链接过程。所以说以上两种说法其实都没什么问题,真实链接的过程确实是在运行阶段由动态链接器完成的,但是编译链接环节确实也使用到链接器经历了一次链接环节,所以说成部分链接倒是也合理
动态链接库:Windows平台动态链接库.dll (dynamic link library),Linux平台共享对象文件.so (shared object file),在 Linux 下可以通过 ldconfig -p
检查目前系统中识别到的动态链接库
仍然采用静态链接库的场景,构建动态链接库。
|
|
|
|
创建动态链接库: 与创建静态链接库不同,由于静态链接库本质上就是可执行重定位文件的一个归档,因此必须首先生成.o。 动态链接库似乎经历了完整的构建流程,所以是对add.cpp还是对add.o都是可以的
|
|
在链接环节链接该动态链接库(假设libadd.so和main.cpp在同一路径下)
|
|
Two linking processes
使用动态链接库和静态链接库的一个显著区别在于:使用静态链接库时,程序构建完成了就可以直接执行了,但是使用动态链接库,程序构建完成并不一定表示可以正常执行。 In other words, 程序构建和程序执行是两个显著分离的过程。
在静态链接库的链接过程中,使用的的命令是g++ main.cpp -L. -ladd。 同样的,在使用动态链接库时也一样可以使用这一条命令,程序可以正常构建。但是一旦执行程序,会报以下错误:
./main: error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory
通过ldd
命令(MacOS下可以使用otool -L
)检查可执行目标文件所需的动态链接库:
linux-vdso.so.1 (0x00007ffe113c2000)
libadd.so => not found
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f4aa5de5000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4aa59f4000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f4aa5656000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4aa6370000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f4aa543e000)
可以发现,程序找不到libadd.so这一动态链接库。
Why can’t dynamic linker find the library
We have specify the search path of library using the option -L.
when compiling, why does this error still occur?
We mentioned above that using dynamic libraries is divided into two stages: the first is using static linker, the second is using dynamic linker.
The compile command option -L. -ladd
helps the static linker to find the libraries file, but it doesn’t work for dynamic linker.
So, the first linking stage can complete successfully and the second linking stage will encounter “No such file or directory” error.
How dynamic linker find the library1
动态链接器搜索库文件的顺序如下:
1. 使用指定的路径名
对于最开始引入的动态链接库的应用示例,修改编译命令g++ main.cpp ./libadd.so -o main
,此时在和libadd.so相同路径下即可正常执行可执行目标文件。通过ldd
命令可以发现内容有所改变
linux-vdso.so.1 (0x00007fff8a570000)
./libadd_dynamic.so (0x00007f5421cea000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f5421961000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5421570000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f54211d2000)
/lib64/ld-linux-x86-64.so.2 (0x00007f54220ee000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f5420fba000)
关于这种方式的实现机理,通过 readelf -d main
命令可以查询到 ELF 文件的 .dynamic section 内容, 同 g++ main.cpp -o main
编译得到的文件的 readelf
命令的执行结果的关键差别如下所示:
|
|
2. 使用 DT_RPATH
中指定的目录
We can use compile option -Wl,-rpath,/custom/rpath/
to specify the path in the executable file for the dynamic linker.
It is similar to the first method, because it affects the dynamic linker also with the help of ELF file format’s .dynamic section. We can use the command readelf -d <binary_name> | grep 'R.*PATH'
to verify it.
3. 使用环境变量 LD_LIBRARY_PATH
修改环境变量LD_LIBRARY_PATH
(macOS下为DYLD_LIBRARY_PATH
),将libadd.so所在路径添加到环境变量中
有几点注意事项:
- 根据Comparison of Shell Script Execution Modes的知识,可执行文件的执行会在子进程中进行,因此此处的变量需要设置为Environment/Global Variables以便子进程可以访问到。
- 在 macOS 下,
DYLD_LIBRARY_PATH
无法被设置为环境变量,即执行env | grep DYLD_LIBRARY_PATH
结果为空。对于这个奇怪的现象,在man dyld
中有下面一则提示
Note: If System Integrity Protection is enabled, these environment variables are ignored when executing binaries protected by System Integrity Protection.
截止到更新此处内容时,还并没有尝试关闭 System Integrity Protection 来测试是否是这种安全机制导致的这种奇怪现象,目前暂且认为是如此。
4. 使用 DT_RUNPATH
中指定的目录
这种方式和滴2种方法有着一定联系,暂时没有遇到相关应用场景,暂时按下不表待后续完善
如果二进制文件中存在 DT_RUNPATH 动态段属性,则动态链接器会搜索这些目录。这些目录仅用于查找 DT_NEEDED(直接依赖项)条目所需的对象,不适用于这些对象的子对象,这些子对象必须自己有自己的 DT_RUNPATH 条目。这与 DT_RPATH 不同,后者适用于依赖树中所有子对象的搜索。
5. 从缓存文件 /etc/ld.so.cache
中搜索
按照目前理解,如果期望使用这种方式,需要首先将期望被搜索的路径添加到 /etc/ld.so.conf
文件中,然后通过 sudo ldconfig
构建出新的 /etc/ld.so.cache
,然后 dynamic linker 就会根据 ld.so.cache
来进行搜索
由于未涉及到相关应用常见,此方法还未经测试,等待后续测试完善
6. 在默认路径中搜索
默认路径包括 /lib 和 /usr/lib(某些 64 位架构中,默认路径为 /lib64 和 /usr/lib64)
在上述示例中,把生成的libadd.so移动到/usr/local/lib等默认搜索路径(根据现有理解,make install
所做的工作就是将相关文件复制到这些默认的搜索路径当中,但是个人并不是很推荐这种做法,因为直接采用默认搜索路径就类似黑盒,在不同设备中默认设置并不一定相同,显式给出各种信息会使得编译链接过程更加清晰明了)
Summary
注:从图上也可以验证上述的说法,相较于静态链接库,动态链接库在构建过程和执行过程中都会发挥作用
静态链接器和动态链接器都分别完成了哪些工作 / 从动态链接的流程图中可以看出,xxx.so文件同时参与静态链接库和动态链接库两个链接过程,在这两个过程中这个library分别起到了什么作用?
答:假设程序P.cpp使用到了一个其他库中定义的函数fun()。当程序P.cpp被编译为P.o之后,编译器是不知道fun()函数的地址的。如果fun()是static library中的函数,那么静态链接器会根据所用的static library,直接将P.o中的fun()函数的地址进行重定位。如果fun()是shared library中的,那么静态链接器会将其标记为动态链接的符号,等到装载时由动态链接器完成重定位。可见,xxx.so需要被用到两次
从 FLE Experiment 看 static linking
实验说明:这是南京大学蒋炎岩老师讲授的操作系统系统设计与实现课程的实验。
FLE: Funny (Fluffy) Linkable Executable
We analyse the project structure firstly,this project provides three .c source files and one .h header file.
minilib.h
: As if this is the C standard library headers.libc.c
: As if this is the implementation of libc.foo.c
main.c
With the supportion of simulated runtime library minilib.h
and libc.c
, using the gcc to compile the foo.c
and main.c
, we can get the ELF format relocatable file foo.o
and main.o
.
Then, we use the translation tool to translate the foo.o
and main.o
to FLE format file foo.fle
and main.fle
.
At last, linking the FLE file foo.fle
and main.fle
, we will get the hello.fle
, using the executing tool we can execute the hello.fle
which is equal to the ELF format executable file.
FLE_ld 实现
此函数的功能为链接多个 FLE 文件,解析符号和重定位,最终生成一个可执行的文件。
实现方式为通过两次遍历实现符号解析和重定位,并将结果以自定义的 JSON 格式存储在输出文件中