TLPI 第41章 读书笔记:Fundamentals of Shared Libraries

笔记和练习博客总目录见:开始读TLPI。

共享库是一种将库函数放入单一单元的技术,多个进程在运行时可以共享这个单元。这种技术可以节省磁盘空间和内存。本章讲解共享库的基本知识。下一章将介绍共享库的一些高级功能。

41.1 Object Libraries

构建程序的一种方式很简单:将每一个源文件编译,生成对应的目标文件,再把所有目标文件链接在一起,得到可执行程序,示例如下:

bash 复制代码
$ cc -g -c prog.c mod1.c mod2.c mod3.c
$ cc -g -o prog_nolib prog.o mod1.o mod2.o mod3.o

链接操作实际上由独立的链接器程序ld完成。当我们使用cc(或gcc)命令来链接程序时,编译器会在后台调用ld。在 Linux 平台上,应当始终通过gcc间接调用链接器,因为gcc会保证传给ld正确的选项,并让程序链接到正确的库文件。

然而在很多场景下,有些源文件会被多个程序共用。为减少重复工作量,一种初步办法是:只编译一次这类源文件,再根据需要将它们链接到不同的可执行文件中。该方法虽然能节省编译时间,但仍存在缺陷:链接阶段必须列出全部目标文件名;此外,大量目标文件还会把目录弄得杂乱不堪,不便管理。

为解决这些问题,我们可以把一组目标文件打包成一个独立单元,这就是目标库(object library) 。目标库分为两类:静态库 与共享库。共享库是更现代的目标库类型,相比静态库具备多项优势,我们将在 41.3 节中介绍。

An aside: including debugger information when compiling a program

在上面展示的cc命令中,我们使用了-g选项,用于在编译出的程序中加入调试信息。一般而言,编译程序和库文件时保留调试信息是个好习惯。(早年有时会省略调试信息,以减小最终可执行文件占用的磁盘与内存空间;但如今磁盘和内存的成本已经很低。)

另外,在部分架构(例如 x86-32)上,不应指定-fomit-frame-pointer选项 ,因为该选项会导致无法调试。(而在另一些架构,如 x86-64,该选项默认开启,原因是它不会妨碍调试。)出于同样的理由,不要使用strip(1)工具剔除可执行文件与库中的调试信息。

41.2 Static Libraries

在开始讨论共享库之前,我们先简要介绍静态库,以此厘清共享库的差异与优势。

静态库也叫归档库(archive),是 UNIX 系统上最早出现的库类型。它具备以下优点:

  • 我们可以把一组常用目标文件打包到单个库文件中,后续可使用该库构建多个可执行程序;构建各个应用时,无需重新编译原始源文件。
  • 链接命令变得更简洁。不再需要在链接命令行罗列一长串目标文件,只需指定静态库名称。链接器能够检索静态库,并提取出可执行程序所需要的目标文件。

Creating and maintaining a static library

实际上,静态库就是一个文件,里面保存着所有被加入其中的目标文件副本。归档文件还会记录每一个组成它的目标文件的各类属性,包括文件权限、用户 ID 与组 ID 数值,以及最后修改时间。按照惯例,静态库的命名格式为libname.a。

静态库通过ar(1)命令来创建和维护,该命令的通用格式如下:

bash 复制代码
$ ar options archive object-file...

options(选项)参数由一串字母组成,其中一个字母是操作码,其余字母为修饰符,用来改变操作的执行方式。下面是一些常用操作码:

  • r(replace,替换):将目标文件插入归档,若归档中已存在同名目标文件则予以替换。这是创建和更新归档文件的标准方式。我们可以用下面命令构建归档库:
bash 复制代码
$ cc -g -c mod1.c mod2.c mod3.c
$ ar r libdemo.a mod1.o mod2.o mod3.o
$ rm mod1.o mod2.o mod3.o

如上所示,构建完库之后,如果需要,可以删除原始目标文件,因为它们不再需要。

  • t(table of contents,目录) :显示归档的内容目录。默认只列出归档内目标文件的名称。加上v(verbose,详细)修饰符后,还能看到归档中记录的每个目标文件的全部其他属性,示例如下:
bash 复制代码
$ ar tv libdemo.a
rw-r--r-- 1000/100 1001016 Nov 15 12:26 2009 mod1.o
rw-r--r-- 1000/100 406668 Nov 15 12:21 2009 mod2.o
rw-r--r-- 1000/100 46672 Nov 15 12:21 2009 mod3.o

每个目标文件展示的附加属性从左到右依次为:该文件被加入归档时的权限、用户 ID 与组 ID、文件大小,以及最后修改的日期和时间。

  • d(delete,删除):从归档中删除指定模块,示例:
bash 复制代码
$ ar d libdemo.a mod3.o

Using a static library

将程序与静态库链接有两种方式。第一种是在链接命令中直接写出静态库文件名,示例如下:

复制代码
$ cc -g -c prog.c
$ cc -g -o prog prog.o libdemo.a

另一种方式:把库放到链接器默认搜索的标准目录(例如/usr/lib)下,再通过-l选项指定库名(即去掉文件名前缀lib与后缀.a后的名称):

bash 复制代码
$ cc -g -o prog prog.o -ldemo

如果库存放在链接器默认不会检索的目录中,可以用-L选项告诉链接器额外搜索该目录:

bash 复制代码
$ cc -g -o prog prog.o -Lmylibdir -ldemo

静态库虽然可以包含大量目标模块,但链接器只会把程序真正用到的模块复制进可执行文件。

链接完成程序后,就可以用常规方式运行它:

bash 复制代码
$ ./prog
Called mod1-x1
Called mod2-x2

41.3 Overview of Shared Libraries

当通过链接静态库构建程序(或者根本不使用任何库来构建程序)时,生成的可执行文件会包含所有被链接进该程序的目标文件副本。因此,如果多个不同可执行程序使用相同的目标模块,每个可执行文件都会拥有一份该目标模块的独立副本。这种代码冗余存在若干缺点:

  • 磁盘空间被浪费,用于存放同一份目标模块的多份副本,这类空间损耗可能相当可观。
  • 如果多个使用相同模块的程序同时运行,每个程序都会在虚拟内存中保留目标模块的独立副本,从而增大系统整体的虚拟内存占用。
  • 若静态库内某个目标模块需要修改(例如安全补丁或缺陷修复),所有用到该模块的可执行程序都必须重新链接,才能纳入这次改动。这个问题还会进一步恶化:系统管理员需要清楚哪些应用程序链接了这个库。

共享库正是为了解决上述这些缺陷而设计。共享库的核心思想:所有需要这些目标模块的程序共用目标模块的同一份副本。目标模块不会被复制到链接生成的可执行文件中;取而代之,当第一个需要该共享库中模块的程序启动时,库的单份副本会在运行时载入内存。后续执行其他使用同一个共享库的程序时,它们直接复用这份已经加载到内存中的库。使用共享库意味着,可执行文件占用更少的磁盘空间,运行时也占用更少的虚拟内存。

尽管共享库的代码可以在多个进程间共享,但库中的变量不能共享。每一个使用该库的进程,都会拥有库内定义的全局变量与静态变量的独立副本。

共享库还有下述额外优势:

  • 由于程序整体体积更小,在某些场景下,程序载入内存并启动的速度会更快。该优势仅在大型共享库已经被别的程序加载时成立。第一个加载该共享库的程序,启动时间反而更长,因为系统需要查找并把这个共享库载入内存。
  • 目标模块不会复制进各个可执行文件,而是统一维护在共享库内。因此(受 41.8 节所述限制约束),我们可以修改目标模块,无需重新链接程序就能让程序感知改动。甚至正在运行的程序还在使用旧版本共享库时,就可以完成这类更新。

新增这项功能带来的主要代价如下:

  • 无论从原理层面,还是在创建共享库、编译使用共享库的程序这类实操层面,共享库都比静态库更加复杂。
  • 共享库必须编译为位置无关代码(详见 41.4.2 节)。在大多数处理器架构上,位置无关代码会带来性能损耗,因为它需要占用一个额外寄存器(Hubicka, 2003)。
  • 符号重定位必须在运行时执行。符号重定位过程中,程序对共享库内每一个符号(变量或函数)的引用,都需要被修改,使其匹配该符号在虚拟内存中的实际运行地址。受这个重定位过程影响,使用共享库的程序,执行速度相比静态链接的同等程序会略慢一些。

共享库的另一个用途是作为 Java 本地接口(JNI)的构建模块,它允许 Java 代码通过调用共享库中的 C 函数直接访问底层操作系统的功能。更多信息,请参见 Liang, 1999 和 Rochkind, 2004。

41.4 Creating and using Shared Libraries-A First Pass

为理解共享库的工作原理,我们先来查看构建并使用共享库所需的最简步骤序列。暂时先不考虑共享库文件的常规命名规范;该规范将在 41.6 节介绍,它能让程序自动加载所需库的最新版本,同时还可以让同一个库的多个不兼容版本(即所谓主版本)和平共存。

本章仅讨论**可执行与可链接格式(ELF)**的共享库。现代 Linux 以及众多其他 UNIX 实现中,可执行文件与共享库均采用 ELF 格式。ELF 取代了更早的 a.out 和 COFF 格式。

41.4.1 Creating a Shared Library

我们可以按下面步骤,把前面创建的静态库改建成共享库版本:

bash 复制代码
$ gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
$ gcc -g -shared -o libfoo.so mod1.o mod2.o mod3.o

第一条命令生成将要放入库中的三个目标模块。(-fPIC选项将在下一节讲解。)-shared选项用于创建包含这三个目标模块的共享库。

按照惯例,共享库以lib作为前缀,后缀为.so,即共享目标文件(shared object)。

在本节示例中,我们使用gcc而非等价的cc命令,意在强调:创建共享库所用的命令行选项和编译器相关。在其他 UNIX 平台上使用别的 C 编译器,大概率需要更换选项。

注意:也可以只用一条命令完成源码编译并生成共享库:

bash 复制代码
$ gcc -g -fPIC -Wall mod1.c mod2.c mod3.c -shared -o libfoo.so

但为了清晰区分编译 和构建库这两个步骤,本章示例会把这两步拆成独立命令书写。

共享库和静态库不同:无法从已生成的共享库中单独新增或删除某个目标模块。和普通可执行文件一样,共享库内部的各个目标文件不再保持独立身份。

41.4.2 Position-Independent Code

cc -fPIC选项用于指定编译器生成位置无关代码(PIC)。该选项会改变编译器生成代码的方式,例如访问全局变量、静态变量、外部变量,访问字符串常量,以及获取函数地址这类操作。经过这种改动后,代码在运行时可以被加载到任意虚拟地址。这对于共享库是必需的,因为在链接阶段无法预知共享库代码最终会放在内存的哪个位置。(共享库运行时的内存地址取决于多种因素,比如加载该库的程序已经占用的内存大小、以及该程序已经加载了哪些其他共享库。)

在 Linux/x86-32 平台上,可以使用未加 -fPIC 编译 的模块来创建共享库。但这么做会丧失共享库的部分优势:包含位置相关内存引用的程序文本页无法在多个进程之间共享。在部分处理器架构上,不使用 -fPIC 根本无法构建共享库。

想要判断一个已存在的目标文件是否通过 -fPIC 编译,可以在目标文件的符号表中查找 _GLOBAL_OFFSET_TABLE_ 这个符号,使用下面任一命令:

bash 复制代码
$ nm mod1.o | grep _GLOBAL_OFFSET_TABLE_
$ readelf -s mod1.o | grep _GLOBAL_OFFSET_TABLE_

反过来,如果下面两条等价命令任意一条有输出,则说明指定的共享库中至少包含一个没有用 -fPIC 编译的目标模块:

bash 复制代码
$ objdump --all-headers libfoo.so | grep TEXTREL
$ readelf -d libfoo.so | grep TEXTREL

字符串TEXTREL代表存在这样的目标模块:它的代码段(text segment)含有需要在运行时执行重定位的引用。

我们将在 41.5 节进一步介绍nm、readelf与objdump这几条命令。

41.4.3 Using a Shared Library

使用共享库时,需要执行两个步骤,而使用静态库的程序不需要这两步:

  • 由于可执行文件不再包含所需目标文件的副本,它必须有一种机制,用来在运行时识别自身需要的共享库。实现方式是在链接阶段把共享库名称嵌入到可执行文件内部 。(用 ELF 术语来说,库依赖关系记录在可执行文件的DT_NEEDED标签中。)一个程序全部的共享库依赖列表,称为它的动态依赖列表。
  • 在运行时,必须有一种机制来解析这个嵌入的库名:也就是找到可执行文件内指定名称对应的共享库文件;如果该库尚未载入内存,就把库加载进内存。

当我们把程序和共享库进行链接时,库名会自动嵌入可执行文件:

bash 复制代码
$ gcc -g -Wall -o prog prog.c libfoo.so

此时如果尝试直接运行程序,会得到下面的报错信息:

bash 复制代码
$ ./prog
./prog: error in loading shared libraries: libfoo.so: cannot
open shared object file: No such file or directory

这就引出第二个必要步骤:动态链接 ,也就是在运行时解析嵌入在程序中的库名。这项工作由动态链接器 (也叫动态链接加载器、运行时链接器)完成。动态链接器本身就是一个共享库,文件名为/lib/ld-linux.so.2,所有使用共享库的 ELF 可执行程序都会调用它。

路径名/lib/ld-linux.so.2通常是一个符号链接,指向动态链接器的实体文件。实体文件命名格式为ld-version.so,其中version是系统上安装的 glibc 版本,例如ld-2.11.so。在部分处理器架构上,动态链接器的路径名有所不同。例如在 IA-64 架构上,动态链接器的符号链接名为/lib/ld-linux-ia64.so.2。

动态链接器读取程序所需共享库的列表,并依据一套预定义规则在文件系统中查找库文件。其中一部分规则规定了共享库通常存放的标准目录集合。例如,很多共享库存放在/lib和/usr/lib目录下。

上面出现报错的原因是:我们的库放在当前工作目录,而当前目录不在动态链接器的标准搜索目录列表内。

部分架构(例如 zSeries、PowerPC64、x86-64)支持同时运行 32 位与 64 位程序。在这类系统中,32 位库存放于*/lib子目录,64 位库存放于*/lib64子目录。

The LD_LIBRARY_PATH environment variable

告诉动态链接器某个共享库存放在非标准目录的一种方法:把该目录加入用冒号分隔 的目录列表,设置到环境变量LD_LIBRARY_PATH中。(也可以用分号分隔目录,但此时整个列表必须加引号,防止 shell 解析分号。)如果定义了LD_LIBRARY_PATH,动态链接器会优先 在该变量列出的目录里查找共享库,之后才去标准库目录搜索。(后文会讲到,生产环境的应用绝不应该依赖LD_LIBRARY_PATH;但现阶段,这个变量为我们入门共享库提供了简便手段。)

于是,我们可以用下面这条命令运行程序:

bash 复制代码
$ LD_LIBRARY_PATH=. ./prog
Called mod1-x1
Called mod2-x2

这条命令使用(bash、Korn、Bourne)shell 的语法:在执行 prog 的进程内临时创建环境变量 。这个设置指示动态链接器在.,也就是当前工作目录中搜索共享库。

LD_LIBRARY_PATH目录列表里如果出现空目录项(例如dirx::diry中间的空白项),等价于.(当前工作目录)。(但要注意:把LD_LIBRARY_PATH设为空字符串并不会得到同样效果。)我们应当避免这种写法(SUSv3 标准同样不鼓励在PATH环境变量里使用这类写法)。

Static linking and dynamic linking contrasted

通常,链接 一词用来描述调用链接器ld,将一个或多个编译好的目标文件合并为单个可执行文件的过程。有时会使用静态链接 这个术语,用来把该步骤和动态链接 区分开;动态链接指可执行程序在运行时加载其所依赖共享库的过程。(静态链接有时也被称作链接编辑 ,而像ld这类静态链接器,有时被叫做链接编辑器。)

所有程序(包括使用共享库的程序)都会经历静态链接阶段。而在运行时,使用共享库的程序还会额外执行动态链接。

41.4.4 The Shared Library Soname

在前面的示例中,嵌入到可执行文件内、并由动态链接器在运行时查找的名称,就是共享库文件的实际文件名。这个名称称为库的真实名称(real name) 。不过我们可以(而且通常都会)为共享库创建一个别名,叫做soname (在 ELF 术语中对应DT_SONAME标签)。

如果一个共享库设置了 soname,那么在静态链接阶段,写入可执行文件的将是 soname 而非真实文件名;后续运行时,动态链接器就会依据这个 soname 去查找库。soname 的作用是引入一层间接引用,让可执行程序在运行时可以加载和链接阶段所用库版本不同但 ABI 兼容的共享库。

我们会在 41.6 节介绍共享库真实名称与 soname 的命名规范。这里先用一个简化示例说明原理。

使用 soname 的第一步,是在创建共享库时指定它:

bash 复制代码
$ gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
$ gcc -g -shared -Wl,-soname,libbar.so -o libfoo.so mod1.o mod2.o mod3.o

-Wl,-soname,libbar.so 是传给链接器的指令,作用是给共享库libfoo.so打上 soname 标记,soname 值为libbar.so。

想要查看已有共享库的 soname,可以使用下面任意一条命令:

bash 复制代码
$ objdump -p libfoo.so | grep SONAME
SONAME libbar.so
$ readelf -d libfoo.so | grep SONAME
0x0000000e (SONAME) Library soname: [libbar.so]

创建带 soname 的共享库之后,照常编译生成可执行文件:

复制代码
$ gcc -g -Wall -o prog prog.c libfoo.so

但这一次,链接器检测到库libfoo.so内含有 sonamelibbar.so,于是把这个 soname嵌入到可执行文件中。

此时尝试运行程序,会看到如下结果:

bash 复制代码
$ LD_LIBRARY_PATH=. ./prog
prog: error in loading shared libraries: libbar.so: cannot open
shared object file: No such file or directory

问题根源:动态链接器找不到名为libbar.so的文件。

使用 soname 时,还需要额外一步:创建一条符号链接,将 soname 映射到库的真实文件名。该符号链接必须放在动态链接器会检索的目录之中。于是我们可以按下面方式运行程序:

bash 复制代码
$ ln -s libfoo.so libbar.so  # 在当前目录创建soname对应的符号链接
$ LD_LIBRARY_PATH=. ./prog
Called mod1-x1
Called mod2-x2

图 41-1 展示完整流程:生成带有内置 soname 的共享库、将程序与该共享库链接,以及创建运行程序所必需的 soname 符号链接。

图略

Figure 41-1: Creating a shared library and linking a program against it

图 41-2 展示了将图 41-1 中生成的程序载入内存、准备执行时所发生的一系列步骤。

想要查看某个进程当前正在使用哪些共享库,可以查看 Linux 特有的文件/proc/PID/maps的内容(参见 48.5 节)。

图略

Figure 41-2: Execution of a program that loads a shared library

41.5 Useful Tools for Working with Shared Libraries

本节简要介绍几个工具,它们可用于分析共享库、可执行文件以及编译生成的目标文件(.o)。

ldd 命令

ldd(1)(list dynamic dependencies,列出动态依赖)命令,用于展示一个程序(或共享库)运行所需要的共享库。示例如下:

bash 复制代码
$ ldd /usr/bin/gcc
        linux-vdso.so.1 (0x00007fff51d3f000)
        libm.so.6 => /lib64/libm.so.6 (0x00007ffb5f633000)
        libc.so.6 => /lib64/libc.so.6 (0x00007ffb5f42a000)
        /lib64/ld-linux-x86-64.so.2 (0x00007ffb5f823000)

ldd命令会解析每一个库引用(采用与动态链接器完全相同的搜索规则),并按如下格式展示结果:

复制代码
库名 => 解析得到的文件路径

对于大多数 ELF 可执行文件,ldd至少会列出两项:动态链接器ld-linux.so.2,以及标准 C 库libc.so.6。

在部分处理器架构上,C 库的名称有所不同。例如,在 IA-64 和 Alpha 架构中,该库名为libc.so.6.1。

The objdump and readelf commands

objdump命令可以从可执行文件、编译后的目标文件或共享库中提取多种信息,包括反汇编后的二进制机器码。它也能展示这些文件中各个 ELF 段头部的信息;在该用法下,它的功能与readelf相近 ------readelf也输出同类信息,只是展示格式不同。本章末尾列出了更多关于objdump与readelf的参考资料。

The nm command

nm命令列出目标库或可执行程序内部定义的全部符号。该命令的一个用途:在多个库中,查找某个符号是在哪一个库里面定义的。例如,想要找出哪个库定义了crypt()函数,可以执行下面命令:

bash 复制代码
$ nm -A /usr/lib64/lib*.so 2> /dev/null | grep ' crypt$'
/usr/lib/libcrypt.so:00007080 W crypt

💡 我的输出与上不同,一是libcrypt.so在目录/usr/lib64中,二是-A需要换为-D才有输出信息。

nm 的 -A 选项指定:在每一行符号信息的开头都列出库文件名 。这一选项是必要的,因为默认情况下,nm只会打印一次库名,然后在后续各行列出该库包含的所有符号;这种输出形式不适合上面示例中的过滤操作。除此之外,我们丢弃标准错误输出,用来屏蔽 nm 无法识别文件格式时产生的报错信息。从上面的输出可以看出,crypt() 函数是在 libcrypt 库中定义的。

41.6 Shared Library Versions and Naming Conventions

我们来探讨共享库版本管理需要处理的问题。通常,共享库的后续版本之间是互相兼容的:也就是各个模块中的函数保持相同的调用接口,并且语义等价(即执行结果完全一致)。这类版本有差异但相互兼容的库版本,称为共享库的次版本(minor versions) 。但偶尔需要创建库的新主版本(major version)------ 该版本和旧版本不兼容。(41.8 节会更精确地说明哪些情况会造成这类不兼容。)与此同时,系统还必须能够继续运行依赖旧版本库的程序。

为满足上述版本管理需求,共享库的真实文件名(real names)与soname采用一套标准命名规范。

Real names, sonames, and linker names

共享库的每一个不兼容版本,都用唯一的主版本标识 加以区分,该标识是库真实文件名的组成部分。按照惯例,主版本标识使用数字;每当库发布一次不兼容版本,这个数字就顺序加一。除主版本标识之外,真实文件名还包含次版本标识 ,用来区分同一主版本下互相兼容的各个次版本。真实文件名遵循命名格式:libname.so.major-id.minor-id。

和主版本标识一样,次版本标识可以是任意字符串,但惯例上一般是单个数字,或是由小数点分隔的两个数字:第一个数字代表次版本号,第二个数字代表该次版本内部的补丁级别或修订号。下面是一些共享库真实文件名示例:

  • libdemo.so.1.0.1
  • libdemo.so.1.0.2 次版本更新,与 1.0.1 版本兼容
  • libdemo.so.2.0.0 新主版本,和所有 1.* 版本不兼容
  • libreadline.so.5.0

共享库的 soname 包含和真实文件名相同的主版本标识,但不带次版本标识 。因此 soname 的格式为:libname.so.major-id。

soname 通常作为相对符号链接创建在存放库实体文件的目录下。下面是若干 soname 示例,以及它们所指向的真实文件名:

  • libdemo.so.1 -> libdemo.so.1.0.2
  • libdemo.so.2 -> libdemo.so.2.0.0
  • libreadline.so.5 -> libreadline.so.5.0

对于共享库的某一个主版本,可能存在多个由不同次版本标识区分的库文件。通常,每个主版本对应的 soname 软链接,指向该主版本内最新的 次版本(上面libdemo.so的例子即是如此)。这套机制保证共享库在运行时拥有正确的版本语义。

因为静态链接阶段,可执行文件中嵌入的是与次版本无关的 soname;后续可以修改 soname 软链接,使其指向更新的次版本共享库。这样就能保证程序运行时加载该库最新的次版本。

此外,由于库的不同主版本拥有各自不同的 soname,它们可以和平共存,供依赖对应版本的程序调用。

除真实文件名和 soname 之外,每个共享库一般还有第三个名称:链接名(linker name) ,用于编译链接可执行文件时指定该共享库。链接名是一个符号链接,仅保留库名,不带任何主、次版本号,格式为 libname.so。借助链接名,我们可以编写版本无关的链接命令,自动使用正确(最新)版本的共享库。

链接名通常创建在它所指向文件的同一目录下。它既可以链接到库最新主版本的真实文件名 ,也可以链接到对应的soname 。一般情况下,推荐将链接名指向 soname;这样一来,soname 的变更会自动体现在链接名上。(41.7 节会介绍,ldconfig程序可以自动维护 soname 保持为最新版本;如果遵循上面这套规范,ldconfig也会间接维护链接名。)

如果想要将程序链接到共享库的旧主版本,就不能使用链接名。此时需要在链接命令里,直接指定对应的真实文件名或者 soname,以此标明所需要的(主)版本。

💡 技术上虽然可以让符号链接指向旧的版本,但强烈不建议这么做。因为这破坏了linker name的设计意图和系统约定,后续也容易遗忘。

下面是几个链接名示例:

复制代码
libdemo.so -> libdemo.so.2
libreadline.so -> libreadline.so.5

表 41-1 汇总了共享库真实文件名、soname 与链接名的相关信息,图 41-3 展示了这三类名称之间的关系。

Table 41-1: Summary of shared library names

名称 格式 说明
real name(真实文件名) libname.so.maj.min 存放库代码的实体文件;库的每一组主版本+次版本对应一个真实文件。
soname libname.so.maj 库的每个主版本对应一个soname;链接阶段嵌入到可执行文件;运行时通过同名符号链接查找库,该软链接指向对应的(最新)真实文件名。
linker name(链接名) libname.so 指向最新真实文件名,或者(更常见)指向最新 soname的符号链接;仅存在一份;用于编写与版本无关的链接命令。

Figure 41-3: Conventional arrangement of shared library names

Creating a shared library using standard conventions

综合上面所有知识,下面演示如何按照这套标准规范构建我们的示例库。

首先,编译生成目标文件:

bash 复制代码
$ gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c

然后创建共享库,真实文件名 为libdemo.so.1.0.1,soname 为libdemo.so.1:

bash 复制代码
$ gcc -g -shared -Wl,-soname,libdemo.so.1 -o libdemo.so.1.0.1 \
mod1.o mod2.o mod3.o

接下来,为 soname 和链接名创建对应的符号链接:

bash 复制代码
$ ln -s libdemo.so.1.0.1 libdemo.so.1
$ ln -s libdemo.so.1 libdemo.so

我们可以用ls验证这套结构(awk 用来筛选我们关心的字段):

bash 复制代码
$ ls -l libdemo.so* | awk '{print $1, $9, $10, $11}'
lrwxrwxrwx libdemo.so -> libdemo.so.1
lrwxrwxrwx libdemo.so.1 -> libdemo.so.1.0.1
-rwxr-xr-x libdemo.so.1.0.1

之后,就可以使用链接名编译生成可执行程序(注意链接命令里完全不用写任何版本号),然后照常运行程序:

bash 复制代码
$ gcc -g -Wall -o prog prog.c -L. -ldemo
$ LD_LIBRARY_PATH=. ./prog
Called mod1-x1
Called mod2-x2

41.7 Installing Shared Libraries

在前面所有示例中,我们在用户私有目录下创建共享库,再借助环境变量LD_LIBRARY_PATH,让动态链接器去检索该目录。普通用户与特权用户都可以使用这种方式。但生产环境的应用不应当采用该方案。

更常规的做法是:将共享库及其配套符号链接安装到若干标准库目录之一,常见目录如下:

  • /usr/lib:大多数标准库的安装目录;
  • /lib:存放系统启动阶段所需库的目录(系统启动时,/usr/lib此时可能尚未挂载);
  • /usr/local/lib:用于存放非标准或实验性库。如果/usr/lib是多机共享的网络挂载目录,而我们只想让本机使用这个库,把库放在这个目录也很合适;
  • 或者存放在/etc/ld.so.conf中列出的任意目录(稍后介绍该文件)。

大多数情况下,将文件复制到上述任一目录都需要超级用户权限 。安装完成后,必须创建 soname 和链接名对应的符号链接,通常在库文件所在目录中建立相对符号链接。

因此,如果要把我们的示例库安装到/usr/lib(该目录权限仅允许root修改),操作步骤如下:

bash 复制代码
# 以下命令需root权限
mv libdemo.so.1.0.1 /usr/lib
cd /usr/lib
ln -s libdemo.so.1.0.1 libdemo.so.1
ln -s libdemo.so.1 libdemo.so

该shell会话的最后两行,分别创建了soname符号链接与链接名符号链接。

ldconfig

ldconfig(8) 程序用于解决共享库存在的两个潜在问题:

  • 共享库可以存放在多个不同目录。倘若动态链接器需要遍历全部这些目录去查找库,库加载的速度将会非常缓慢。
  • 当安装库的新版本或者移除旧版本时,soname 对应的符号链接可能会过时失效。

ldconfig 通过完成两项工作来解决上述问题:

  1. 它扫描一组标准目录,创建或更新缓存文件 /etc/ld.so.cache;该缓存文件保存这些目录下**各个库主版本(取每个主版本对应的最新次版本)**的清单。动态链接器在运行时解析库名,就使用这份缓存。
    为构建缓存,ldconfig 会先扫描 /etc/ld.so.conf 文件中列出的目录,再扫描 /lib 和 /usr/lib。/etc/ld.so.conf 文件由一系列目录路径构成(必须写绝对路径),路径之间可用换行符、空格、制表符、逗号或冒号分隔。部分发行版会把 /usr/local/lib 写入该配置文件(如果没有,就需要手动添加)。
    命令 ldconfig -p 可以查看 /etc/ld.so.cache 的当前内容。
  2. 它检查每一个库的各个主版本中最新次版本(次版本号最大的版本) ,读取库内部嵌入的 soname,然后在同目录下为每个 soname 创建(或更新)相对符号链接。

为了正确完成上述操作,ldconfig要求库文件遵循前面介绍的命名规范(即库真实文件名包含主、次版本标识,版本迭代时版本号按规则递增)。

默认情况下,ldconfig会同时执行上面两项任务。可以通过命令行选项选择性关闭其中一项:

  • -N:不重建缓存;
  • -X:不创建 / 更新 soname 符号链接。

另外,-v(verbose,详细输出)选项可以让ldconfig打印执行过程信息。

每当安装新库、更新或删除已有库,或是修改/etc/ld.so.conf里的目录列表时,都应当运行ldconfig。

下面举一个ldconfig的使用示例:假设我们需要安装同一个库的两个不同主版本,操作如下:

复制代码
$ su
Password:
# mv libdemo.so.1.0.1 libdemo.so.2.0.0 /usr/lib
# ldconfig -v | grep libdemo
libdemo.so.1 -> libdemo.so.1.0.1 (changed)
libdemo.so.2 -> libdemo.so.2.0.0 (changed)

我们仍然需要手动创建链接名对应的符号链接,如下面这条命令所示:

复制代码
# ln -s libdemo.so.2 libdemo.so

但是,如果我们安装该库一个新的 2.x 次版本,由于链接名指向最新的 soname,ldconfig 可以间接让链接名保持最新,示例如下:

复制代码
# mv libdemo.so.2.0.1 /usr/lib
# ldconfig -v | grep libdemo
libdemo.so.1 -> libdemo.so.1.0.1
libdemo.so.2 -> libdemo.so.2.0.1 (changed)

如果是开发、使用私有库 (也就是没有安装到标准系统库目录的库),可以使用 -n 选项,让 ldconfig 帮我们创建 soname 符号链接。该选项指定:ldconfig 仅处理命令行给出目录中的库,不更新缓存文件。

下面示例使用 ldconfig 处理当前工作目录中的库:

复制代码
$ gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
$ gcc -g -shared -Wl,-soname,libdemo.so.1 -o libdemo.so.1.0.1 \
mod1.o mod2.o mod3.o
$ /sbin/ldconfig -nv .
.:
libdemo.so.1 -> libdemo.so.1.0.1
$ ls -l libdemo.so* | awk '{print $1, $9, $10, $11}'
lrwxrwxrwx libdemo.so.1 -> libdemo.so.1.0.1
-rwxr-xr-x libdemo.so.1.0.1

在上例中,执行ldconfig时写了完整路径,因为当前是普通用户账号,其PATH环境变量没有包含/sbin目录。

41.8 Compatible Versus Incompatible Libraries

随着迭代,我们可能需要修改共享库的代码。修改之后会产生库的新版本:如果新版本和旧版本兼容 ,那么只需要修改库真实文件名里的次版本号;如果不兼容,则必须定义库的新主版本。

当且仅当下面全部条件满足时,库的变更才被认定为与现有版本兼容:

  • 库中所有公开函数与变量的语义保持不变。换言之,每个函数的参数列表不变,对全局变量、输出参数的作用效果不变,返回值也不变。因此,性能优化、bug 修复(让行为更符合规范定义)这类修改属于兼容变更。
  • 库的公共 API 中,不能删除任何函数或变量。但向公共 API 新增函数、变量是允许的,属于兼容变更。
  • 各函数内部分配并返回的结构体保持不变;同样,库导出的公开结构体也不能改动。
    这条规则有一个例外:在特定场景下,可以在已有结构体末尾追加新成员 。但这种做法依然存在隐患,例如调用程序如果尝试分配该结构体类型的数组,就会出问题。库开发者有时会规避这个限制:在库初次发布时,就把导出结构体定义得比实际需要更大,预留一些填充字段,标注为保留供未来使用。

只要没有违反以上任意一条条件,就只需要修改现有库名称的次版本号。否则,就应当创建库的新主版本。

41.9 Upgrading Shared Libraries

共享库的优点之一:即便正在运行的程序还在使用旧版本库,我们依然可以安装库的新主版本或次版本 。

我们只需要编译生成新版库文件,放到合适目录,并按需更新 soname 和链接名的符号链接(更常见的做法是交由ldconfig自动完成)。

以共享库 /usr/lib/libdemo.so.1.0.1 为例,制作它的新次版本(也就是兼容升级),操作步骤如下:

复制代码
$ su
Password:
# gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
# gcc -g -shared -Wl,-soname,libdemo.so.1 -o libdemo.so.1.0.2 \
mod1.o mod2.o mod3.o
# mv libdemo.so.1.0.2 /usr/lib
# ldconfig -v | grep libdemo
libdemo.so.1 -> libdemo.so.1.0.2 (changed)

假设链接名已经配置正确(即指向库的 soname),那么就无需修改它。

已经在运行的程序会继续使用共享库此前的次版本;只有当这些程序退出并重新启动后,才会加载共享库的新次版本。

如果后续我们需要创建该共享库的新主版本(2.0.0),操作步骤如下:

复制代码
# gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
# gcc -g -shared -Wl,-soname,libdemo.so.2 -o libdemo.so.2.0.0 \
mod1.o mod2.o mod3.o
# mv libdemo.so.2.0.0 /usr/lib
# ldconfig -v | grep libdemo
libdemo.so.1 -> libdemo.so.1.0.2
libdemo.so.2 -> libdemo.so.2.0.0 (changed)
# cd /usr/lib
# ln -sf libdemo.so.2 libdemo.so

从上面的输出可以看到,ldconfig会自动为新主版本创建 soname 符号链接。但正如最后一条命令所示,我们必须手动更新链接名符号链接。

我们已经了解两种告知动态链接器共享库位置的方式:使用LD_LIBRARY_PATH环境变量,或是将共享库安装到标准库目录(/lib、/usr/lib,或/etc/ld.so.conf中列出的目录)。

还有第三种方式:在静态链接阶段,我们可以在可执行文件内嵌入一组目录列表,供程序运行时查找共享库。

如果库放在固定路径,但该路径不在动态链接器默认检索的标准目录中,这种方式就很有用。实现该功能,需要在编译生成可执行文件时使用链接器选项 -rpath:

复制代码
$ gcc -g -Wall -Wl,-rpath,/home/mtk/pdir -o prog prog.c libdemo.so

上面这条命令会把字符串/home/mtk/pdir写入可执行文件prog的运行时库路径(rpath)列表。这样,程序运行时,动态链接器解析共享库引用时,也会检索这个目录。

必要时,可以多次指定-rpath选项;所有目录会合并为一个有序的 rpath 列表,存入可执行文件。

也可以在单个-rpath内,用冒号分隔多个目录。运行时,动态链接器会按照-rpath中指定的顺序依次检索目录。

-rpath选项的替代方案是环境变量LD_RUN_PATH。该变量的值是一组用冒号分隔的目录,编译可执行文件时,这组目录会作为 rpath 列表。仅当编译时没有指定-rpath选项,才会使用LD_RUN_PATH。

Using the --rpath linker option when building a shared library

链接器选项 -rpath 在构建共享库时同样有用。假设存在共享库 libx1.so,它依赖另一个共享库 libx2.so,如图 41-4 所示。并且这两个库分别存放在非标准目录 d1 和 d2 中。下面我们一步步构建这两个库以及使用它们的应用程序。

Figure 41-4: A shared library that depends on another shared library

首先,在目录 pdir/d2 中编译构建 libx2.so。(为简化示例,我们省略库版本编号与显式 soname 设置。)

复制代码
$ cd /home/mtk/pdir/d2
$ gcc -g -c -fPIC -Wall modx2.c
$ gcc -g -shared -o libx2.so modx2.o

接下来在目录 pdir/d1 构建 libx1.so。由于 libx1.so 依赖 libx2.so,而后者不在标准目录,我们使用链接器选项 -rpath 指定它的运行时路径 。运行时路径可以和链接时路径(-L 指定)不一样,本例中二者恰好相同。

复制代码
$ cd /home/mtk/pdir/d1
$ gcc -g -c -Wall -fPIC modx1.c
$ gcc -g -shared -o libx1.so modx1.o -Wl,-rpath,/home/mtk/pdir/d2 \
-L/home/mtk/pdir/d2 -lx2

最后,在 pdir 目录编译主程序。主程序使用 libx1.so,该库位于非标准目录,因此再次使用 -rpath:

复制代码
$ cd /home/mtk/pdir
$ gcc -g -Wall -o prog prog.c -Wl,-rpath,/home/mtk/pdir/d1 \
-L/home/mtk/pdir/d1 -lx1

请注意:链接主程序时不需要提及 libx2.so 。链接器能够读取 libx1.so 内的 rpath 列表,找到 libx2.so,从而满足静态链接阶段所有符号都可解析的要求。

可以用下面命令查看 prog 和 libx1.so,检查它们内部的 rpath 内容:

复制代码
$ objdump -p prog | grep PATH
RPATH /home/mtk/pdir/d1   # 运行时在此目录查找 libx1.so
$ objdump -p d1/libx1.so | grep PATH
RPATH /home/mtk/pdir/d2   # 运行时在此目录查找 libx2.so

我们也可以用 readelf --dynamic(等价简写 readelf -d)命令,再搭配 grep 过滤,查看 rpath 列表。

可以使用 ldd 命令查看 prog 的全部动态依赖项:

复制代码
$ ldd prog
libx1.so => /home/mtk/pdir/d1/libx1.so (0x40017000)
libc.so.6 => /lib/tls/libc.so.6 (0x40024000)
libx2.so => /home/mtk/pdir/d2/libx2.so (0x4014c000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

The ELF DT_RPATH and DT_RUNPATH entries

在最初的 ELF 规范中,可执行文件或共享库内只能嵌入一种 rpath 列表,对应 ELF 文件中的DT_RPATH标记。

后续的 ELF 规范将DT_RPATH标记弃用 ,引入新标记DT_RUNPATH来存放 rpath 列表。

这两种 rpath 列表的区别在于:程序运行时动态链接器检索共享库,它们相对于环境变量LD_LIBRARY_PATH的查找优先级不同 :

DT_RPATH优先级更高;DT_RUNPATH优先级更低(参见 41.11 节)。

链接器默认会把 rpath 列表生成为DT_RPATH标记。如果想要让链接器改用DT_RUNPATH条目存放 rpath 列表,必须额外使用链接器选项 --enable-new-dtags(启用新动态标记)。

使用该选项重新编译程序,并用objdump检查生成的可执行文件,可以看到如下结果:

复制代码
$ gcc -g -Wall -o prog prog.c -Wl,--enable-new-dtags \
-Wl,-rpath,/home/mtk/pdir/d1 -L/home/mtk/pdir/d1 -lx1
$ objdump -p prog | grep PATH
RPATH /home/mtk/pdir/d1
RUNPATH /home/mtk/pdir/d1

可以看到,可执行文件同时包含DT_RPATH与DT_RUNPATH两个标记。链接器如此重复写入同一份 rpath 列表,是为了兼容老旧动态链接器(这类旧版本不识别DT_RUNPATH标记)。(glibc 2.2 版本才新增对DT_RUNPATH的支持。)

能够识别DT_RUNPATH的动态链接器会直接忽略DT_RPATH标记(参见 41.11 节)。

Using $ORIGIN in rpath

假设我们想要分发一个应用程序,该程序附带自研的共享库,但不想要求用户把这些库安装到标准目录。我们希望用户可以将应用解压到任意自选目录,解压完成后就能直接运行程序。

这里存在一个难题:应用程序本身无法获知共享库所在位置,除非让用户手动设置LD_LIBRARY_PATH,或者要求用户执行安装脚本来配置目录。这两种方案都不够理想。

为解决该问题,动态链接器支持在 rpath 配置中识别特殊字符串 $ORIGIN(等价写法 ${ORIGIN})。动态链接器将该字符串解释为**"存放当前应用可执行文件的目录"**。

举例来说,我们可以用下面命令编译程序:

复制代码
$ gcc -Wl,-rpath,'$ORIGIN'/lib ...

该写法假定:程序运行时,它依赖的共享库放在可执行文件所在目录下的 lib 子目录。

之后我们就可以给用户提供一个简易安装包,包里包含应用程序以及配套库。用户可以把这个包部署在任意位置,直接运行程序,也就是所谓的开箱即用应用(turn-key application)。

41.11 Finding Shared Libraries at Run Time

解析库依赖时,动态链接器会先检查每一条依赖字符串,判断其中是否包含斜杠/;当链接可执行文件时指定了显式库路径,就会出现这种情况。如果检测到斜杠,则该依赖字符串被视作路径名(绝对路径或相对路径),动态链接器直接按此路径加载库。如果没有斜杠,动态链接器将按照下面的规则依次检索共享库:

  1. 若可执行文件存在DT_RPATH运行时库路径列表(rpath),且不包含DT_RUNPATH列表,则按程序链接时给定的顺序检索这些目录。
  2. 如果环境变量LD_LIBRARY_PATH已定义,则按顺序遍历其值中用冒号分隔的各个目录。
    若该可执行文件是set-user-ID 或 set-group-ID 程序,动态链接器会忽略LD_LIBRARY_PATH。这是一项安全防护机制,防止用户诱导动态链接器加载同名的私有库,劫持程序所需库。
  3. 若可执行文件存在DT_RUNPATH运行时库路径列表,则按程序链接时给定的顺序检索这些目录。
  4. 检索/etc/ld.so.cache文件,查找是否存在该库对应的条目。
  5. 按顺序检索目录 /lib,然后是 /usr/lib。

41.12 Run-Time Symbol Resolution

假设一个全局符号(即函数或变量)在多处被定义,例如同时在可执行文件与共享库中定义,或是在多个共享库中定义。对该符号的引用会如何解析?

举个例子:我们有一个主程序和一个共享库,二者都定义了全局函数xyz();并且共享库内部还有另一个函数会调用xyz(),如图 41-5 所示。

Figure 41-5: Resolving a global symbol reference

编译构建共享库与可执行程序,然后运行程序,会看到如下结果:

bash 复制代码
$ gcc -g -c -fPIC -Wall foo.c
$ gcc -g -shared -o libfoo.so foo.o
$ gcc -g -o prog prog.c libfoo.so
$ LD_LIBRARY_PATH=. ./prog
main-xyz

从最后一行输出可以看出:主程序内定义的xyz()覆盖(介入)了共享库中的同名定义。

乍一看这个行为可能令人意外,但该设计有其历史渊源。最早的共享库实现采用这套机制,目的是让符号解析的默认语义,和链接对应静态库的行为完全保持一致。对应的规则如下:

  • 主程序中的全局符号定义,优先级高于库里面的同名定义。
  • 如果多个库中定义了同一个全局符号,则对该符号的引用会绑定到静态链接命令行从左向右扫描时找到的第一个定义。

这套语义虽然让程序从静态库迁移到共享库相对简单,但也会带来一些问题。最突出的问题是:它和 "共享库是独立自完备子系统" 的模型相冲突。

默认情况下,共享库无法保证对自身全局符号的引用,一定会绑定到库内部的该符号定义。

于是,当把多个组件整合为更大程序时,共享库的行为可能发生变化。这会导致应用出现难以预料的故障,也不利于分治调试(比如尝试用更少或不同的共享库复现问题)。

在上面的场景中,如果希望共享库内部调用xyz()时,调用库自身版本的函数 ,编译共享库时可以使用链接器选项 -Bsymbolic:

bash 复制代码
$ gcc -g -c -fPIC -Wall foo.c
$ gcc -g -shared -Wl,-Bsymbolic -o libfoo.so foo.o
$ gcc -g -o prog prog.c libfoo.so
$ LD_LIBRARY_PATH=. ./prog
foo-xyz

链接器选项 -Bsymbolic 指定:共享库内部对全局符号的引用,应优先绑定到本库内的定义(如果存在) 。(请注意:无论是否使用该选项,在主程序中调用xyz(),永远会执行主程序中定义的xyz()版本。)

41.13 Using a Static Library Instead of a Shared Library

尽管共享库几乎总是更优选择,但少数场景下静态库会更加合适。

静态链接程序的特点是:运行所需全部代码都打包在程序自身内部,这在某些场景下是优势。例如:用户无法或不愿在目标系统上安装共享库;或者程序要在没有共享库可用的环境中运行(比如 chroot 隔离环境)。

此外,即便共享库是兼容版本,升级后也可能意外引入 bug,导致应用程序异常。静态链接应用程序,可以让程序不受系统共享库变更的影响,自带全部运行所需代码;代价是程序体积更大,占用更多磁盘与内存。

默认情况下,如果链接器同时存在同名的共享库与静态库(例如使用-Lsomedir -ldemo链接,同时存在libdemo.so和libdemo.a),链接器会优先选用共享库版本 。

如果要强制使用静态库,可以采用下面任意一种方式:

  • 在 gcc 命令行直接指定静态库的完整路径(带.a后缀)。
  • 给 gcc 指定-static选项。
  • 使用 gcc 选项-Wl,-Bstatic和-Wl,-Bdynamic,手动切换链接器选择静态库 / 共享库。这些选项可以和-l参数混合写在 gcc 命令行;链接器按照参数书写顺序依次处理。

41.14 Summary

目标库是编译后的目标模块的集合,可供链接该库的程序使用。和其他 UNIX 实现一样,Linux 提供两类目标库:静态库 (早期 UNIX 系统唯一可用的库类型),以及更为现代的共享库。

共享库相比静态库具备多项优势,因此是现代 UNIX 系统上最主流的库类型。共享库的优势根源在于:程序链接该库时,程序所需的目标模块不会被复制进最终的可执行文件 。取而代之,静态链接器仅在可执行文件中写入运行时所需共享库的相关信息。当程序执行时,动态链接器利用这些信息加载需要的共享库。运行阶段,所有使用同一个共享库的进程,在内存中共享该库的同一份副本 。

由于共享库不会被拷贝进可执行文件,并且运行时所有程序共用内存里仅有的一份库镜像,共享库减少了系统所需的磁盘空间与内存占用。

共享库的soname(库别名)为运行时解析共享库引用增加了一层间接映射。如果一个共享库设置了 soname,静态链接器生成可执行文件时,记录到文件内的是这个 soname,而非库的真实文件名 。版本管理方案约定:共享库真实文件名格式为 libname.so.主版本号.次版本号,而 soname 格式为 libname.so.主版本号。该机制使得程序可以自动使用该主版本下最新的次版本库(无需重新链接程序),同时也支持创建不兼容的全新库主版本。

为在运行时定位共享库,动态链接器遵循一套标准检索规则,包括检索存放绝大多数共享库的目录(例如 /lib 和 /usr/lib)。

Further information

有关静态库与共享库的各类资料,可以查阅手册页:ar(1)、gcc(1)、ld(1)、ldconfig(8)、ld.so(8)、dlopen(3)、objdump(1);另外还可以阅读ld与readelf的 info 文档。

Drepper, 2004 (b) 详细讲解了在 Linux 平台编写共享库的诸多底层细节。更多实用资料可查阅 David Wheeler 所著的《Program Library HOWTO》,该文档发布于 Linux 文档项目(LDP)网站:http://www.tldp.org/。

GNU 的共享库机制与 Solaris 的实现有诸多相似之处,因此也值得阅读 Sun 的《链接器与库指南》(可访问 http://docs.sun.com/ 获取),里面包含更多说明与示例。Levine, 2000 一书对静态链接器与动态链接器的工作原理做了入门介绍。

GNU Libtool 可以帮开发者屏蔽构建共享库时各类平台相关的实现细节,相关资料可在线查阅 http://www.gnu.org/software/libtool,也可参考 Vaughan et al., 2000。

工具接口标准委员会发布的文档《可执行与可链接格式(Executable and Linking Format)》阐述了 ELF 的详细规范,在线地址:http://refspecs.freestandards.org/elf/elf.pdf。Lu, 1995 同样包含大量 ELF 的实用细节。

相关推荐
Apipi*1 小时前
30天速通Linux 第五章Linux 进程管理
linux·运维·服务器
杨云龙UP1 小时前
PostgreSQL 四种常见部署模式快速理解:单实例、流复制、repmgr、Patroni
linux·运维·数据库·postgresql·流复制·高可用ha
流浪0011 小时前
Linux系统篇46——线程(十一) 生产者消费者模型,三种关系和一个交易场所
linux·线程·并发·同步·互斥·阻塞队列·生产者消费者模型
Jason_zhao_MR2 小时前
新一代电能数据采集终端方案
linux·人工智能·嵌入式硬件·fpga开发·嵌入式
qetfw2 小时前
Debian SSH 安全配置:端口、来源限制与免密登录
linux·安全·debian·ssh
东鹏特饮2 小时前
15个实用Linux运维脚本
linux·运维·shell·脚本·redhat·常用生产脚本
远牧2 小时前
Ubuntu 26 升级踩坑实录:hermes-agent venv 重建全过程
linux·ubuntu·ai·agent
新时代牛马2 小时前
Linux PREEMPT_RT 详解(5.10.268-rt164)
linux·运维·服务器
眼不痛请看我3 小时前
Ubuntu_22.04_LTS虚拟机安装指南
linux·数据库·ubuntu