C/C++Unix/Linux应用层编程基本原理,系统编程基础

库、链接与执行

本文将介绍三个部分:可执行文件的编译与运行,静态库的创建以及动态库的创建, 在每个部分中将阐述静态链接器、动态链接器以及Linux内核在三个部分中各自发挥的作用。**


一、可执行文件的编译与运行

概括为:
一个有依赖的程序/动态库 = 嵌入静态库代码 + 嵌入动态库名字 + 可选择的嵌入动态库路径(倘若动态库不在标准路径)

1. 可执行文件的诞生:编译阶段

调用 gcc 可直接生成可执行文件:

gcc -o program.out main.c utils.c (动态库文件) -l库名 -L库路径以通知静态链接器如何找到该库

动态库可无需-l选项,直接与源文件放在一起。而静态库不行。
在此期间,gcc 完成了以下工作:

  1. 编译源码:将每个 .c 源文件编译成 .o 目标文件(实际由 cc1 编译器完成)。其中编译器找头文件的路径为本地/标准路径(/include,usr/include,usr/local/include...)
  2. 调用静态链接器 ld:将 所有.o 目标文件与静态库中main需要的目标文件链接成最终的可执行文件,并在可执行文件写下其依赖的动态库(如 libc.so)。其中ld要么在标准路径下找库文件,即/lib、/usr/lib等,可通过以下命令查看:
$ ld --verbose | grep SEARCH_DIR
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");

,但若指定了-L选项,则优先在该选项指定的路径下找。
如需要将来运行时提示动态链接器所依赖的动态库的地址,可以让ld在程序中写下运行时需要的动态库的地址,供动态链接器查找,即指定选项-Wl,-rpath,/动态库地址/。一些开箱即用的程序(程序与动态库打包在一起的应用)就是利用了这一特性,将动态库地址指定为'$ORIGIN'/lib,通知动态链接器该程序需要的共享库在此程序所在目录下的lib目录下。

最终生成的 program.out 中:

  • 包含所有 .o 目标文件的代码和静态链接的符号
  • 记录动态库依赖信息(如 libc.so.6),但不会嵌入动态库的代码(即只是记录动态库的名字)

2. 可执行文件的运行

通过 ./program.out 运行程序时:

  1. 内核加载可执行文件

    • 检查文件头是否为 ELF 格式
    • 若需要动态链接,内核加载动态链接器(如 /lib64/ld-linux-x86-64.so.2
  2. 动态链接器工作流程
    动态链接器会:

    • 解析可执行文件的动态段(.dynamic),获取依赖的共享库列表
    • 按优先级搜索共享库:

      RPATH → LD_LIBRARY_PATH → /etc/ld.so.cache → /lib → /usr/lib

      请注意:ld.so.cache由ldconfig程序所维护,ldconfig是引导动态链接器并维护更新三方库的强力工具。
      让ldconfig监视除了标准路径外的其他路径的方法:
      在/etc/ld.so.conf.d目录下,新建一个.conf配置文件在其中输入你想要让其监视的目录的绝对路径即可,每条路径间换行分隔。
      让ldconfig根据其监视的目录下的变化,如新增了一个指定了soname的库,更新ld.so.cache以让动态链接器找到该库,则可以调用以下命令:

      ldconfig -v | grep 你的库名
    • 加载共享库到内存并完成符号重定位
    • 执行共享库的初始化代码(如构造函数)
    • 将控制权交给程序的 main 函数

二、静态库的创建

  1. 编译源文件生成目标文件

    gcc -c utils1.c utils2.c -o utils1.o utils2.c
  2. 打包静态库

    ar r libutils.a utils1.o utils2.o

    生成 libutils.a,本质是 .o 文件的集合


三、动态库的创建

  1. 编译位置无关代码

    gcc -c -fPIC utils.c -o utils.o

    -fPIC 生成地址无关代码(该代码会根据进程的不同在不同内存偏移量处产生变量),确保库可被多个进程共享

  2. 生成共享库

    gcc -shared -o libutils.so utils.o

与静态库一样,还是打包目标文件放在一起。但静态库使用的打包工具是ar,而动态库的打包直接可以用gcc编译器。
动态库的版本必须命名如下:libxxx.so.a.b.c,这里不解释。
如希望默认使用版本a下的最新版本,需指定其soname,即使用gcc的选项-Wl,-soname,/别名/,soname去掉b.c,且会被ldconfig用来创建一个符号链接,指向版本a下的最新版库。此时便可使用-l:libxxx.so.a链接版本a下的最新次要版本库。
如希望默认使用该库所有版本的最新版,则需要手动创建一个名为libxxx.so符号链接指向最新版的共享库。此时该库名称被称为链接器名称


四、核心组件职责总结

组件作用阶段核心功能
静态链接器 ld编译阶段合并目标文件、解析静态符号、生成可执行文件或动态库
动态链接器 ld-linux.so运行时阶段加载共享库、动态符号解析、地址重定位、管理延迟绑定
Linux内核加载阶段验证ELF格式、加载可执行文件到内存、初始化用户态栈、调用动态链接器

关键区别总结

特性静态链接动态链接
库代码存储嵌入可执行文件独立存储在 .so 文件
内存占用每个进程独立加载多个进程共享同一份库内存
更新维护需重新编译程序替换 .so 文件即可生效
启动速度较快(无运行时加载开销)较慢(需动态链接)
磁盘空间较大较小
发生阶段可执行文件生成阶段运行阶段
任务完成者静态链接器ld(被gcc调用)动态链接器ld-linux.so

动静态库即打包的目标文件,通过编译器调用的静态链接器与用户源代码静态链接(合并代码,嵌入共享库名字)即可得到可执行文件。可执行文件需要运行时再动态链接共享库。
可执行文件 = 编译 + 静态链接
库 = 编译 + 打包
通过理解这些机制,开发者可以更好地优化程序架构,在模块化、性能和部署灵活性之间做出平衡。

回复

This is just a placeholder img.