如何使用 gcc 构建 c/c++ 项目,大家都很熟悉了,甚至对链接器、静态库、共享库等概念,大家也略知一二。然而,对于 ld 链接器、linux 操作系统(OS)及应用程序(exec)之间的详细交互流程,估计就有点懵了。接下来,我将从单个源文件编译、编译期链接、程序运行期这三个阶段入手,揭开应用程序运行背后的奥秘。
单个 c/cpp 文件可以被 gcc 编译成目标文件(.o 文件),这部分就不过多赘述,大家应该都很熟悉了。二进制目标文件中的 section 有很多,详细内容可以打开汇编代码详细研究下,下图列出了其中比较常见的段。
这里的目标文件包括 .o 文件及后面提到的库文件
符号表的作用是什么?
Func 是源文件中引用的外部符号,a 是源文件中定义的全局变量
.rela.* 的作用是什么?
全称 relocation(重定位),记录编译器在编译时不确定的符号地址——针对引用的外部符号。
dynamic 段中保存了可执行文件依赖哪些动态库。
GOT 段记录了需要引用的外部符号的地址。
多个 .o 文件可以通过链接器(ld)被打包在一起,组合成库文件。
库文件又分为静态库(.a 文件)和共享库(.so 文件)。
什么是 ld 呢?它本身也是可执行文件,属于 GNU 的一部分,将一堆目标文件通过符号表链接成最终的目标文件、库文件和可执行文件。
.a 文件如何生成?
ld 直接将涉及的所有目标文件打包进静态库文件。
.so 文件如何生成?
在链接生成共享库文件的过程中,并不拷贝目标文件中涉及的代码段,只记录它需要引用的外部符号位置(在哪些目标文件中)。
所有的目标文件、库文件和可执行文件都有统一的格式,即 ELF,Executable and Linking Format(可执行链接格式)。
libstdc++.so 是标准库文件
上图中,多个 .o 文件链接在一起形成 .a 文件,多个 .o 和 .so 文件也可以链接形成 .so 文件,可执行文件也可以由 .a 文件、.so、.o 文件链接而成。
如果可执行文件没有使用共享库,那么该程序就可以独立运行,因为它内部所有的符号都有对应的二进制机器码。这种情况比较简单,我们这里主要讨论下面这种程序运行方式。
如果可执行文件要使用共享库,那么该程序就不能独立运行,它在运行时需要使用共享库的代码,且对应的两种使用方式,分别是运行时动态链接和运行时动态加载。
可执行文件的组成
ld-linux.so:不是一个可执行程序,只是一个 shell 脚本。作为解释器,写在 elf 文件(可执行文件)中,ld-linux.so 先于 main 函数工作,用于查找主程序所依赖的共享库,实际上可以直接执行 ld-linux.so. 还有另外一种比较常见的是 ld.so,它是个符号链接,指向 ld-linux.so.(通过命令 ln -s ld.so ld-linux.so 创建)。
为什么这里使用解释器呢?
解释器的特点是动态特性和可移植性,比如在解释器执行时可以动态改变变量的类型、对程序进行修改以及在程序中插入良好的调试诊断信息等。而将解释器移植到不同的系统上,则程序不用改动就可以在移植了解释器的系统上运行。
同时解释器也有很大的缺点,比如执行效率低,占用空间大,因为不仅要给用户程序分配空间,解释器本身也占用了宝贵的系统资源。
动态链接和动态加载的区别
动态加载和动态链接都是在程序运行时发生,并将所需代码拷贝到内存,这点很重要!
关键区别是:动态链接的流程是 OS 直接把共享库的代码拷贝到内存,而动态加载由人工指定(代码中的 dlopen() 接口)。
动态链接需要 OS 的特殊支持,通过动态链接方式拷贝到内存的库代码可以在各个进程之间共享。而对动态加载而言,可以在各自进程中打开共享库代码。
其他概念
ldconfig:这是个可执行程序,隶属于 GNU,作用是在默认搜寻目录(/lib和/usr/lib)以及共享库配置文件 /etc/ld.so.conf 内所列的目录下,搜索出共享库文件(lib*.so*),进而创建出 ld-linux.so 所需要的链接和缓存文件。缓存文件默认为 /etc/ld.so.cache,此文件保存已排好序的共享库名字列表。更新缓存使新添加的库生效,当然系统启动时会自动运行 ldconfig。
ldd:这是 Linux 内核中自带的脚本,可以用来查看可执行文件链接了哪些共享库
strip <可执行文件名> 去除符号表,可以给可执行文件瘦身
使用 objdump、readelf、nm 等命令可以查询目标文件的详细内容。
gcc -print-search-dirs 可以查看 gcc 在编译、链接过程中的共享库搜索路径。