详解 gcc 编译、链接原理—揭开应用程序运行背后的奥秘

如何使用 gcc 构建 c/c++ 项目,大家都很熟悉了,甚至对链接器、静态库、共享库等概念,大家也略知一二 。然而,对于 ld 链接器、linux 操作系统(OS)及应用程序(exec)之间的详细交互流程,估计就有点懵了 。接下来,我将从单个源文件编译、编译期链接、程序运行期这三个阶段入手,揭开应用程序运行背后的奥秘 。
单个源文件编译单个 c/cpp 文件可以被 gcc 编译成目标文件(.o 文件),这部分就不过多赘述,大家应该都很熟悉了 。二进制目标文件中的 section 有很多,详细内容可以打开汇编代码详细研究下,下图列出了其中比较常见的段 。

详解 gcc 编译、链接原理—揭开应用程序运行背后的奥秘

文章插图
这里的目标文件包括 .o 文件及后面提到的库文件
符号表的作用是什么?
  1. 记录该目标文件中定义的全局变量及函数;
  2. 记录该目标文件中引用的全局变量及函数;

详解 gcc 编译、链接原理—揭开应用程序运行背后的奥秘

文章插图
Func 是源文件中引用的外部符号,a 是源文件中定义的全局变量
.rela.* 的作用是什么?
全称 relocation(重定位),记录编译器在编译时不确定的符号地址——针对引用的外部符号 。
dynamic 段中保存了可执行文件依赖哪些动态库 。
GOT 段记录了需要引用的外部符号的地址 。
编译期链接多个 .o 文件可以通过链接器(ld)被打包在一起,组合成库文件 。
库文件又分为静态库(.a 文件)和共享库(.so 文件) 。
什么是 ld 呢?它本身也是可执行文件,属于 GNU 的一部分,将一堆目标文件通过符号表链接成最终的目标文件、库文件和可执行文件 。
.a 文件如何生成?
ld 直接将涉及的所有目标文件打包进静态库文件 。
.so 文件如何生成?
【详解 gcc 编译、链接原理—揭开应用程序运行背后的奥秘】在链接生成共享库文件的过程中,并不拷贝目标文件中涉及的代码段,只记录它需要引用的外部符号位置(在哪些目标文件中) 。
所有的目标文件、库文件和可执行文件都有统一的格式,即 ELF,Executable and Linking Format(可执行链接格式) 。
详解 gcc 编译、链接原理—揭开应用程序运行背后的奥秘

文章插图
libstdc++.so 是标准库文件
上图中,多个 .o 文件链接在一起形成 .a 文件,多个 .o 和 .so 文件也可以链接形成 .so 文件,可执行文件也可以由 .a 文件、.so、.o 文件链接而成 。
程序运行期如果可执行文件没有使用共享库,那么该程序就可以独立运行,因为它内部所有的符号都有对应的二进制机器码 。这种情况比较简单,我们这里主要讨论下面这种程序运行方式 。
如果可执行文件要使用共享库,那么该程序就不能独立运行,它在运行时需要使用共享库的代码,且对应的两种使用方式,分别是运行时动态链接和运行时动态加载 。
可执行文件的组成
详解 gcc 编译、链接原理—揭开应用程序运行背后的奥秘

文章插图
 
ld-linux.so:不是一个可执行程序,只是一个 shell 脚本 。作为解释器,写在 elf 文件(可执行文件)中,ld-linux.so 先于 main 函数工作,用于查找主程序所依赖的共享库,实际上可以直接执行 ld-linux.so. 还有另外一种比较常见的是 ld.so,它是个符号链接,指向 ld-linux.so.(通过命令 ln -s ld.so ld-linux.so 创建) 。
详解 gcc 编译、链接原理—揭开应用程序运行背后的奥秘

文章插图
 

详解 gcc 编译、链接原理—揭开应用程序运行背后的奥秘

文章插图
 
为什么这里使用解释器呢?
解释器的特点是动态特性和可移植性,比如在解释器执行时可以动态改变变量的类型、对程序进行修改以及在程序中插入良好的调试诊断信息等 。而将解释器移植到不同的系统上,则程序不用改动就可以在移植了解释器的系统上运行 。
同时解释器也有很大的缺点,比如执行效率低,占用空间大,因为不仅要给用户程序分配空间,解释器本身也占用了宝贵的系统资源 。
动态链接和动态加载的区别
动态加载和动态链接都是在程序运行时发生,并将所需代码拷贝到内存,这点很重要!
关键区别是:动态链接的流程是 OS 直接把共享库的代码拷贝到内存,而动态加载由人工指定(代码中的 dlopen() 接口) 。
动态链接需要 OS 的特殊支持,通过动态链接方式拷贝到内存的库代码可以在各个进程之间共享 。而对动态加载而言,可以在各自进程中打开共享库代码 。


推荐阅读