0x01 前言

这篇文章主要是讲,在c语言和rust语言中,有关静态链接及动态链接的方方面面。

为了便于后续的讲解和测试,我们会创建有以下依赖关系的项目组:

其中,x是最终的可执行文件,绿色的是静态库,红色的是动态库,或者也可以叫做共享库,它们的依赖关系从图中也可以看出来。

下面是c语言对应的项目组:

下面是rust语言对应的项目组:

接下来,我们就逐步测试并讲解,c语言和rust语言中的静态链接及动态链接。

0x02 c语言中静态库的创建及链接

这次测试只使用x和y这两个项目,我们看一下y这个静态库是怎么创建的,以及它是怎么链接到x的。

因为我已经提前编写好了对应的Makefile,所以可以直接执行make命令,来构建x和y项目:

由上图可以看到,这次构建过程是先进入y目录,然后将y.c编译为y.o,然后再使用ar命令,将y.o打包为liby.a,这样y项目的静态库就创建好了。

静态库在linux下的命名规则,就是以lib作为前缀,以.a作为扩展名,并且它本质上就是对一堆.o文件的打包。

我们可以使用下面的命令,来查看一个静态库里包含的文件:

在创建好y的静态库以后,我们又使用cc命令,将x.c编译成x.o,然后将编译好的x.o文件,和liby.a静态库链接到一起,最终生成x这个可执行文件。

在linux下,cc其实就是gcc,我们也可以将其换成clang,但结果都是一样的。

注意,在生成x的命令里,我是直接使用y/liby.a这个全路径的方式,来指定要链接的静态库。

其实,我们还可以使用-L和-l参数的方式,来指定这个y静态库:

这两个参数就是在告诉链接器,到y目录里找liby.a这个库,并把它和x.o链接起来。

有关这两个参数更详细的介绍,可以查看 gcc 的相关文档。

在生成了x可执行文件后,我们来运行一下:

这两个项目的代码逻辑非常简单,输出结果也没有什么问题。

因为y是以静态库的方式链接到x的,也就是说,y的相关代码,都被拷贝到了x里,所以我们在运行x的时候,其实是不需要y的静态库存在的。

我们可以用以下方式来验证下,y的相关代码,已经被拷贝到了x里:

上图中,我使用objdump命令,查看x里的main函数,以及y函数的汇编代码。

在main函数的汇编代码里面,它使用call汇编指令来调用y函数,它预期的y函数的地址为1161。

我们再看一下y函数的汇编代码,它的起始地址正好是1161。

由此我们可以确定,y函数和main函数的相关代码,都在x里,并且都在x的.text这个section里。

这其实就证明了,y静态库里的代码,在链接过程中,被拷贝到了x里。

另外,上图中还展示了一些其他信息,比如y这个字符串的存放位置。

在c代码里,y函数是直接返回了一个常量字符串y,而这个常量字符串y,在编译后,是被存放到了x的.rodata这个section的2004位置。

这些信息,可以在y函数的汇编代码里看到。

在y函数的汇编代码里,它使用了lea这个汇编指令,把y字符串的存放位置2004,赋值到了rax寄存器里,这样后面在main函数里,就可以从rax寄存器里,获取y字符串的存放地址了。

上图的最后,我又使用了readelf命令,查看了.rodata这个section里的内容,我们可以看到,在这个section的2004位置,存放的正是y这个字符。

以上这种方式就验证了,静态库liby.a在链接到x时,它的代码被拷贝到了x,所以我们在运行x的过程中,是不需要liby.a的存在的。

其实还有一种更简单的方式,来验证这个结论,即,使用ldd命令:

ldd命令的作用,就是输出目标文件在运行时的依赖库,由上图可见,在x的这些依赖库里面,并没有liby.a这个库。

ldd命令虽然进一步证明了,x在运行时并不需要y静态库,但该命令的输出同时也带来了一些疑问,就是x在运行时依赖的这些库是什么,以及它为什么要依赖这些库?

这些库其实是linux下程序的通用依赖,比如libc.so,它就是c语言的标准库,我们在代码里调用的printf函数,就是在这个库里实现的。

那是在哪里指定了x要依赖这些库呢?其实就是在gcc构建x的时候。

构建x的过程,并不是靠gcc独立完成的,gcc内部也是通过调用各种其他命令,来完成各个步骤。

如果我们想看一下,在各个步骤中,gcc分别调用了什么命令,以及传递了什么参数,我们可以在执行gcc时,加上-v这个参数:

这个命令输出的内容非常多,但我们此时只用关心最终的链接过程就好,也就是只用看上图中的选中行部分。

由上图中的选中行,我们可以获取很多信息,比如,x的dynamic linker被指定为了/lib64/ld-linux-x86-64.so.2,比如,x.c是先被编译成 /tmp/ccy2DBQ2.o,然后再参与链接过程,又比如,我们指定的liby.a这个静态库,也参与了此链接过程。

不过我们最关心的是-lc这个参数,这个参数就是在告诉链接器,在链接x的过程中,也要把libc.so这个库链接进来。

这也就是为什么上面ldd命令的输出中,会显示x依赖libc.so的原因。

细心的同学到这里可能又会有个疑问,就是链接过程不应该是使用ld命令吗,为什么上图中使用的是collect2呢?

其实collect2又是gcc体系的一层封装,它的底层还是调用ld命令的,我们可以通过以下方式来确认下:

虽然我们自己编写的y库,是被静态链接到了x里,但其他的一些通用依赖,比如libc.so,还是被动态链接到x里的。

这就要求,在运行x时,/usr/lib/libc.so.6 这个库文件是必须要存在的,且版本信息也要对应上,这样才能保证在运行x前,libc的相关代码能被加载到内存,然后在运行x时,x里的代码才可以调用libc里的函数。

动态链接虽然带来了很多灵活性,但也带来了一些限制,比如说,我们把这个x程序,拷贝到另外一台机器,如果那台机器上没有libc.so库,或者该库的版本不对,都会导致在那台机器上无法运行x程序。

因为这些限制,有时候我们想把x程序的所有依赖,包括libc库等,都静态链接到x里,也就是说,把所有依赖库的代码,都拷贝到x里,这样我们就可以把x拷贝到任意的一台机器,随意执行了。

那这个怎么实现呢?

其实也很简单,就是使用gcc的-static参数:

由上图可见,此次构建的x程序,就没有任何外部依赖了,且file命令也显示其是statically linked。

0x03 rust语言中静态库的创建及链接

接下来我们对比看下,在rust语言中,如何创建静态库,以及如何把它链接到目标程序。

我们还是使用x和y这两个项目测试,且代码逻辑和c语言一样。

这两个项目的代码及配置如下:

在x项目的Cargo.toml文件里,我们添加了其对y项目的依赖,在y项目的Cargo.toml文件里,我们将这个库的crate-type设置为rlib。

rlib在rust里表示的就是静态库,且它是rust里的默认库类型,也就是说,当我们创建一个库项目,且没有指定crate-type,那它的crate-type就是rlib。

我们这里显示设置这个值,是为了让大家更明确这个信息。

接下来我们构建并运行x:

构建过程和运行结果都没有什么问题,且都和c语言类似。

然后我们看下x的运行时依赖:

这个输出也和c语言类似。

这里的类似包含两个方面,一个是x没有对y静态库的依赖,也就是说,y静态库的代码在链接过程中也被拷贝到了x里,另一个是rust生成的x和c生成的x依赖的动态库都差不多,比如说,都依赖c标准库libc.so。

我们也可以通过objdump命令,确认y的相关代码,确实被拷贝到了x里:

我们看一下生成的y的静态库:

上图中的选中行,就是生成的y的静态库,注意它的扩展名是.rlib,而不是c语言中的.a。

虽然它的扩展名是.rlib,但它本质上和.a一样,也是一个打包文件,我们依旧可以使用ar命令,查看它打包的内容:

和c不同的是,rlib里额外包含了一个lib.rmeta文件,该文件里存放了该库的描述信息,比如它的依赖是什么。

而 y-8c1df728706eba4f.14rngywsbdxyw63mrl24bqzfr.rcgu.o 文件里存放的则是 y/src/lib.rs 编译后的代码:

看上图可发现,它的内容,和我们之前从x里解析出来的y函数汇编基本类似。

在rust中,x的构建过程,是rust的编译器rustc先解析并处理rust源码,然后再调用llvm,让其帮忙生成对应的.o文件,之后,再调用系统链接器,将这些.o文件,以及相关的依赖库链接起来,这样最终就生成了x。

也就是说,将x的所有构成项链接起来这一步,是和c语言一样的,也是使用的系统链接器。

我们可以使用rustc的 --print link-args 参数,输出rustc最终调用的链接器命令是什么:

上图中的选中行就是rustc最终调用的链接器命令。

该命令非常长,但重点信息其实就只有几个,一是它使用的是cc,也就是gcc,来完成链接步骤的,二是链接项中包含y的静态库 liby-8c1df728706eba4f.rlib,三是它和c一样,也链接了一些通过库,比如ibc.so等。

如果我们对rustc的源码比较了解的话,我们还可以通过控制其日志输出,来获取各种信息。

比如说,我还是想知道rustc最终调用的链接命令是什么,我就可以使用如下命令:

我们再看上图中的选中行,它和上面我们用rustc的 --print link-args 参数输出的链接命令是一样的。

这种控制rustc日志输出的方式,更加灵活,也能获取更多有关rustc的运行时信息。

我们在上述两种方式执行的命令中,cargo build命令部分都加了一个-v参数,这个参数的作用,就是告诉cargo,让其输出其调用的完整的rustc命令是什么,这个输出对于理解rustc的行为,是非常有帮助的。

目前我们构建的x,还是有一些动态依赖的,那在rust中,如何构建一个全静态的x呢?

我们可以使用rustc的 -C target-feature=+crt-static 参数:

有关rust中的静态编译,可以查看rust reference的 这部分 内容。

0x04 c语言中动态库的创建及链接

这次参与测试的,除了x和y这两个项目之外,还要加上z项目,z是一个动态库,这一节我们主要看z这个动态库是如何创建的,以及它是如何被链接到x的。

以下是x和z的相关代码:

构建x并运行:

上图展示了构建z动态库,以及把它链接到x,使用的命令。

现在我们看下x的运行时依赖:

看上图中的选中行,此时x在运行时,就会依赖z/libz.so这个动态库。

我们再使用objdump命令,查看下x相关函数的汇编代码:

我们看到,在main函数中,它调用的并不是z函数,而是z@plt,z@plt函数的地址为1040。

在z@plt函数中,它又跳转到了4008这个地址对应内存里,存放的目标函数地址,而这个目标函数,就是我们编写的z函数。

在运行时加载libz.so,将它里面的z函数地址,赋值到4008地址对应的内存里,这些操作,都是由dynamic linker完成的,dynamic linker就是上面我们看到过的 /lib64/ld-linux-x86-64.so.2。

目前x在运行时有一些通用依赖,比如libc.so,以及有对我们自己编写的libz.so库的依赖,那我们可以像之前一样,把这些所有的运行时依赖,都静态链接到x里吗?

我们可以试一下:

由上图我们可以看到,是不行的,它也告诉我们原因了,就是动态库libz.so,是不能作为静态链接使用的。

那为什么我们之前就可以把libc.so等动态库,静态链接到x呢?

因为gcc链接到x里的,根本就不是libc.so等动态库,而是其对应的静态库:

有关gcc什么时候会选择动态库,什么时候会选择静态库,可以查看下gcc以及ld命令中对-static参数的说明。

0x05 rust语言中动态库的创建及链接

我们再对比看一下rust语言,这次参与测试的项目还是x, y, z,且代码逻辑和c语言也一样:

我们在x项目的Cargo.toml文件里添加了对z的依赖,在z项目的Cargo.toml文件里将crate-type设置为dylib,这一点是非常重要的。

dylib是在告诉rustc,要生成动态库,而非静态库。

接下来我们开始构建x:

上面的输出其实包含很多有用的信息,不过现在我们只看上图中的选中部分,它告诉rustc,将静态库liby.rlib,以及动态库libz.so,链接到x里。

我们再看下生成的liby.rlib和libz.so:

下面我们运行下x:

这次运行x报错了,说找不到libstd.so这个动态库。

我们再使用cargo run命令运行x看下:

cargo run运行x是成功了的。

在执行cargo run时我添加了-vv参数,这样就能输出cargo底层,是如何执行x命令的,其中最主要的就是LD_LIBRARY_PATH环境变量的设置,这个环境变量就是在告诉dynamic linker,到这些目录里去找x依赖的动态库。

刚才我们看到,直接运行x是失败的,说libstd.so这个动态库找不到,这个动态库其实位于这个目录:

而这个目录因为不是dynamic linker要寻找的库目录,所以上面我们直接执行x,会提示libstd.so找不到。

当我们把这个目录,以及libz.so所在目录target/debug/deps,添加到LD_LIBRARY_PATH环境变量之后,dynamic linker就可以在运行时,找到x依赖的这些动态库了,所以x就可以正常运行了,这个在上面我们也演示过了。

我们再看一下在运行时,x依赖的动态库有哪些:

上图中第一次运行ldd命令有两个问题,一是把libz.so解析成了/usr/lib/libz.so这个系统库,而不是我们自己的z项目,二是libstd.so这个库还是找不到。

产生这两个错误的原因是,ldd底层还是使用dynamic linker解析x的运行时依赖的,而我们上面说过,如果想要dynamic linker正确解析x的依赖,就要正确设置LD_LIBRARY_PATH这个环境变量。

在我们第二次运行ldd命令时,就设置好了LD_LIBRARY_PATH环境变量,所以x的依赖也就能正确输出了。

从ldd的输出我们可以看到,x在运行时依赖了很多动态库,比如我们自己编写的libz.so,rust的标准库libstd.so,以及c的标准库libc.so等。

这里要注意,rust的标准库libstd.so,和c的标准库libc.so,它们是不一样的,我们写rust代码时,一般是和rust标准库打交道,而在rust标准库里,会再去调用c标准库。

最后,我们再像之前一样,尝试构建出一个静态版的x:

它和上面c语言版本一样,也是因为libz.so是一个动态库,所以无法构建出一个静态版的x。

0x06 c语言中多级动静态库的创建及链接

文章最开始,我画了一张图,列出了n个用于后续测试的项目,并标明了它们的类型,以及它们之间的依赖关系。

然后,我又尽可能的使用最少的项目,逐步讲解了静态库和动态库的创建及链接。

现在,我们把这n个项目都用起来,综合看下多级静态库和动态库的创建及链接。

以下是相关项目的代码:

下面我们来构建x:

上图中输出了构建各个项目使用的命令。

有关y1,y2,z1,z2的构建过程,我们可以忽略,因为它们都是在创建基础的静态库和动态库,这个我们之前就看过了。

我们主要看下x,y,z的构建过程。

虽然y依赖静态库y1和动态库y2,但因为y本身也是个静态库,所以它还是由ar命令创建的,且在该命令里也并没有指出它的依赖项y1和y2。

看下y静态库里的内容:

还是和之前一样,只有一个y.o。

而z就不同了,虽然它和y一样,也依赖一个静态库z1和一个动态库z2,但它创建libz.so时是要指定其依赖项的,因为生成动态库的过程,本身就是个链接过程。

最终,静态库libz1.a的代码被拷贝到了libz.so里:

而libz2.so则作为libz.so的一项运行时依赖存在:

最后,我们再来看下上图中生成x的命令,它此时不仅要指定liby.a和libz.so,还要指定liby1.a和liby2.so,这两个是liby.a要用的,以及还要指定libz2.so,这个是libz.so要用的。

注意,在生成x的命令里,我们并没有指定libz1.a依赖,因为libz1.a的代码已经被拷贝到libz.so里了,所以在生成x时就没有必要再指定它了。

其实,我们是可以通过一些方式,让链接器自动帮我们寻找间接依赖,这样在生成x时,只用指定对y和z的依赖就好了,但如果这么做的话,又会引入很多复杂的知识点,所以这里就没那么干。

下面,我们再来看下x的完整运行时依赖:

由上图可见,x在运行时依赖于liby2.so, libz.so, 以及libz2.so,而liby.a和liby1.a这两个静态库,因为在生成x的时候,被静态链接到了x里,所以就没有出现在x的运行时依赖里,而libz1.a库,因为在生成libz.so时,被静态链接到了libz.so里,所以也没有出现在x的运行时依赖里。

0x07 rust语言中多级动静态库的创建及链接

作为对比,我们再来看下rust语言的,rust语言的项目结构及代码逻辑,和c语言一样。

x项目:

y,y1,y2项目:

z,z1,z2项目:

构建x:

上图中,我们可以看到各个项目的构建命令。

下面是各个项目的生成结果:

我们先来看下liby.rlib:

它里面有三个文件,lib.rmeta存放的是y库的描述信息,另外两个.o文件是y/src/lib.rs编译后的结果。

再来看下libz.so:

它运行时依赖libz2.so,而libz1.rlib则被静态链接到了libz.so里。

最后再来看下x:

它运行时依赖 liby2.so, libz.so, libz2.so,而 liby.rlib, liby1.rlib, libz1.rlib,因为被静态链接到了对应的x以及libz.so里,所以x没有对它们的运行时依赖,这个和c语言是一样的。

最后我们运行下x:

没有任何问题。

0x08 c语言和rust语言的交互

这个我们下篇文章再讲。

0x09 其他

如果有对linux及linux内核感兴趣的,可以扫描右侧二维码添加我的微信。

另外,我开设了一门 linux内核启动流程源码分析 课程,有对内核源码感兴趣的,欢迎报名。