Linux 编程提示
共享库之所以被广泛使用,很可能是因为它们允许创建共享可执行文件,从而减少磁盘空间占用。它们还允许将多个定义的全局变量压缩为所有程序模块共享的单个变量实例。另一种可能是创建现有共享库的兼容、即插即用替代品。然后,替换库中的改进或修复程序可以立即供与该库链接的可执行文件使用。最后一种可能性超出了本文的范围。
动态链接库 (DLL) 已成为 Linux 系统的重要组成部分。即使 ELF(为 Unix SVR4 设计的可执行文件和链接格式)使创建共享库变得轻而易举,即将到来,但当前的 a.out DLL 共享库可能还需要支持一段时间。在许多情况下,旧版本的 Linux 仍然需要支持,并且商业 a.out 库可能要求使用 a.out DLL 构建可执行文件,因为 a.out 库和 ELF 库不能在一个可执行文件中混合使用。在 ELF 从 Linux 的 alpha 版本进入生产环境所需的更稳定版本之前,甚至之后,a.out 共享库将继续被构建和使用。
如果提供了静态库的源代码,则可以通过完成五个明确定义的步骤来创建库的共享版本。本文将解释如何应用这些步骤来创建一个简单的共享库。其目的是帮助您了解共享库及其构建方式,以便您将来能够成功创建更复杂的共享库。
本文档假设使用 gcc 2.6.2 和 DLL tools 2.16 以及 libc 4.6.27。其他版本可能具有略微不同的语法或操作方式。所有这些项目都可以通过匿名 ftp 从 tsx-11.mit.edu 的 /pub/linux/packages/GCC/ 获取(tools-2.16.tar.gz 在 src 目录中)。请密切遵循发行说明中的所有安装说明,否则可能会导致不必要的问题。
共享库由两个基本部分组成:stub 和 image。stub 库的扩展名为 .sa。stub 是可执行文件将链接到的库。它提供将共享函数和变量重定向到内存中实际共享函数和变量所在位置的功能。库 image 的扩展名为 .so,后跟版本号。
库 image 包含二进制程序使用的实际可执行函数。Image 还包含两个特别值得注意的表:跳转表和全局偏移表 (GOT)。跳转表包含八字节条目,这些条目将对共享函数的调用从跳转表重定向到实际函数。跳转表的存在是为了提供创建兼容替换库的方法。由于每个函数在跳转表中都有固定大小的条目,因此跳转表可以为这些函数提供一个入口点,该入口点在库的修订之间保持不变。这允许以前链接的可执行文件继续运行,而无需重新编译。全局偏移表的功能与跳转表对库函数的功能相同,用于全局变量。
每个共享库都加载到 0x60000000 和 0xc0000000 之间的固定地址。如果可执行文件链接到两个或多个共享库,则这些库不得占用相同的地址范围。如果两个库应该重叠,则可执行文件重定向到的位置可能不包含预期的函数或变量。可以在 tools 2.16 发行版的 doc/table_description 目录中找到已注册共享库的列表。在定义新共享库的加载地址时,请检查此文件,以确保它与现有库的地址不冲突。此外,您可能应该注册新共享库使用的地址空间,以便将来的库不会与其冲突。如果库要分发,则注册尤为重要。
如前所述,此过程旨在创建简单共享库。虽然构建更复杂库的步骤相同,但修改多个或复杂的 makefile 的过程可能会变得有些令人困惑。对于您的第一次尝试,最好选择一个库,该库的所有库源都在单个目录中。一个不错的选择可能是 JPEG 库,可以通过匿名 FTP 从 ftp.funet.fi 获取,文件名为 /pub/gnu/ghostscript3/jpegsrc.v5.tar.gzi。或者,您可以创建几个简单的源代码模块和一个 makefile 来编译和构建静态库。这个测试库不需要做任何有用的事情,因为它仅用于教育目的。但是,由于您已经了解了构建过程的内部工作原理,因此您可以避免尝试理解另一个程序的 makefile 逻辑。此外,在进行共享库构建之前,请确保可以成功编译库的静态版本。
此处介绍的方法不是创建共享库的唯一方法,但事实证明它通常是成功的。它以包含在 makefile 中的文件的形式,提供了用于构建特定库的参数和方法的简单记录。首先,创建将包含在 makefile 中的文件;将其命名为 Shared.inc。该文件应如下所示:
SL_NAME=libxyz SL_PATH=/usr/local/lib SL_VERSION=1.0.0 SL_LOAD_ADDRESS=0x6a380000 SL_JUMP_TABLE_SIZE=1024 SL_GOT_SIZE=1024 SL_IMPORT=/usr/lib/libc.sa SL_EXTRA_LIBS=/usr/lib/gcc-lib/i486-linux\ /2.6.2/libgcc.a -lc SHPARMS=-l$(SL_PATH)/$(SL_NAME)\ -v$(SL_VERSION) \ -a$(SL_LOAD_ADDRESS) \ -j$(SL_JUMP_TABLE_SIZE) \ -g$(SL_GOT_SIZE) VERIFYPARMS=-l$(SL_NAME).so.$(SL_VERSION) -- \ $(SL_NAME).sa CC=gcc -B/usr/bin/jump pre-shlib: $(LIBOBJECTS) shlib-import: buildimport $(SL_IMPORT) shlib: $(LIBOBJECTS) mkimage $(SHPARMS) -- $(LIBOBJECTS) $(SL_EXTRA_LIBS) mkstubs $(SHPARMS) -- $(SL_NAME) verify-shlib $(VERIFYPARMS)
第一部分由一系列变量定义组成。这些变量具有以下含义:
- SL_NAME
正在构建的库的名称。
- SL_PATH
共享库将驻留的位置。
- SL_VERSION
库版本。
- SL_LOAD_ADDRESS
库将加载到的内存中的绝对地址。(检查 DLL 工具随附的 table_description 文件,以确保此地址不与另一个库重叠)。
- SL_JUMP_TABLE_SIZE
跳转表的大小。(暂时给它任意值;稍后将确定合适的值)。
- SL_GOT_SIZE
全局偏移表的大小。(暂时给它任意值;稍后将确定合适的值)。
- SL_EXTRA_LIBS
构建共享 image 所需的其他库。
SL_IMPORT 指示要从中导入符号的其他共享库。这些导入的符号用于帮助将全局变量引用定向到其他共享库中的正确位置。此处指定的库应该是构建目标库所需的任何共享库。目标 shlib-import 使用名为 buildimport 的 /bin/sh 脚本,该脚本使用 SL_IMPORT 作为参数调用。build import 脚本应包含以下命令:
#!/bin/sh echo -n > $JUMP_DIR/jump.import for lib in $*; do nm --no-cplus -o $lib | \ grep '__GOT__' | sed 's/__GOT__/_/'\ > $JUMP_DIR/jump.import done
此脚本使用 nm、grep 和 sed 从命令行上指定的每个 stub 库的全局偏移表中提取符号,以创建名为 jump.import 的文件(nm 命令序列摘自“在 Linux 中使用 DLL 工具”)。请务必 chmod u+x buildimport。SL_EXTRA_LIBS 是成功构建库所需的库。通常,可以通过检查使用此库构建可执行文件的 makefile 来确定这些库中的大多数(通常库的源代码中包含测试程序)。gcc 2.6.2 需要 libgcc.a;如果省略它,将存在对 _main 的未解析引用。通常需要使用 -lc 显式指定 libc。如果在制作库 image 时应该存在未解析的引用,则很可能是省略了必需的库。
将 CC 定义为 gcc -B/usr/bin/jump 是告诉编译器使用名为 /usr/bin/jumpas 的汇编器而不是默认汇编器。请务必检查原始 makefile 中指定的其他参数(以及 CC 是否定义为编译器变量),并根据需要进行添加和更改。CC 几乎总是被定义,因此已在此示例中使用。如果您使用的 DLL 工具版本早于 2.16 版本,则可能需要将 CC 指定为 gcc -B/usr/dll/jump/。
目标 pre-shlib 和 shlib 都以 LIBOBJECTS 作为依赖项。您可能会在原始 makefile 中静态库的目标中找到列表或包含库依赖项列表的变量。您应该将 LIBOBJECTS 定义为此依赖项列表,或者您应该将 Shared.inc 中的所有实例替换为为静态库指定的依赖项。为共享库构建依赖项列表时要小心;即使源代码模块不是最终库的一部分,也经常会被编译。在构建共享库期间应编译的唯一对象是最终将成为库一部分的对象。如果编译了其他对象,则这些模块中使用的符号和全局变量将最终出现在库的跳转配置文件中,并且可能出现在库本身中。这些不需要的函数和变量可能会导致库构建过程出现麻烦的行为或失败。
一般来说,请确保您了解库对象文件是如何构建的。此外,请确保使用与原始库相同的标志和选项构建共享库对象。现在编辑库 makefile(首先制作备份),并在 makefile 目标列表的末尾添加以下语句:
include Shared.inc
最后,从库的源目录中,执行以下操作:
mkdir jump JUMP_LIB=libxyz export JUMP_LIB JUMP_DIR=`pwd`/jump export JUMP_DIR
这些命令为 DLL 工具和汇编器创建一个工作目录,并设置成功构建共享库所需的必要环境变量。如果使用 csh 变体,则有必要使用 setenv。请记住将 libxyz 替换为目标库的名称(如 SL_NAME 中指定的那样)。
在每次编译之前,删除旧的 .o 文件以确保重新构建目标代码。执行 make clean 可能就足够了;但是,请小心 - 许多 makefile 会删除比 .o 文件更多的文件,您可能需要重新配置源代码。通常 rm *.o 可以更可靠地工作。
如果一切都已正确设置,现在应该可以通过输入以下内容开始首次编译:
make pre-shlib
此步骤使用带有 -B 开关前缀的汇编器编译库。这将从库源中提取必要的符号到名为 jump.log 的文件中。全局变量和函数将从 jump.log 中提取到必要的配置文件中,DLL 工具将在其中找到它们。一旦所有源代码都已编译,请更改为 JUMP_DIR 中指定的目录。Jump.log 应该在这个目录中。现在执行以下操作:
getvars getfuncs rm -f jump.log
这些命令将创建文件 jump.vars 和 jump.funcs。jump.vars 包含在编译期间找到的全局变量列表,而 jump.funcs 包含函数列表。如果出于某种原因,您不想导出在 jump.funcs 或 jump.vars 中找到的符号,请将条目移动到 JUMP_DIR 目录中名为 jump.ignore 的文件中。请务必从原始文件中删除添加到 jump.ignore 的任何条目。现在返回编译目录。
现在您应该创建 jump.imports 文件。由于之前在 Shared.inc 中定义了目标,因此只需输入:
make shlib-import
现在应该在 JUMP_DIR 目录中有一个名为 jump.imports 的文件。无需对此文件执行任何操作;它将用于确定哪些全局变量应位于导入的库之一中。
第二次编译是确定全局变量大小所必需的。必须知道全局变量的大小,以便可以正确设置 GOT 指针。删除上次编译的 .o 文件,然后执行以下操作:
make pre-shlib
现在更改为 JUMP_DIR 目录并执行:
getsize > jump.vars-new mv jump.vars jump.vars-old mv jump.vars-new jump.vars
在实际构建共享 image 和 stub 库之前,跳转表和 GOT 必须分配足够的存储空间,以容纳所有现有函数和全局变量,以及库修订中可能添加的函数或全局变量。要确定跳转表和 GOT 所需的字节数,请执行以下操作:
wc -l $JUMP_DIR/jump.funcs wc -l $JUMP_DIR/jump.vars
将结果行数乘以 8,以分别计算现有函数和全局变量所需字节数的下限。这些值应显着填充,以便将来可以扩展库。现在编辑 Shared.inc,并将 SL_JUMP_TABLE_SIZE 和 SL_GOT_SIZE 的设置替换为刚刚确定的值。如果在构建 image 时收到溢出消息,请增加这些值。请记住,这些大小应为 8 的倍数,并且计算的值是最小值,可能不足以构建库 image。
现在一切都应该准备就绪,可以实际构建共享 image 和 stub。在不删除 .o 文件的情况下,执行:
make shlib
这将首先构建 image,然后构建 stub 库。然后将验证 stub 和 image,以检查库是否已正确构建。如果一切顺利,则最后一条消息应类似于:
Used address range 0x6a37f020-0x6a395020 be aware! must be unique! The stub library and the sharable libraries have identical symbols.
第一行中指示的地址范围有些误导,因为指定的加载地址为 0x6a380000,而不是 0x6a37f020。这是正常的。但是,请注意最后一个地址,因为它指示库使用的最后一个地址。此地址通常会稍微填充,以确保为扩展留出空间。地址范围可能会记录为 0x6a380000-0x6a395fff 或 0x6a380000-0x6a39ffff,具体取决于将来可能需要的空间量。
第二行指示 image 和 stub 库已正确构建。如果验证过程应指示 stub 和 image 不同,则发生了错误。可能最常见的错误之一是 JUMP_LIB 环境变量和 SL_NAME 不匹配。如果出现问题,请仔细检查这两个变量是否匹配。如果一切顺利,现在应该有一个 stub 和 image 库。应将 image 复制到 SL_PATH 指定的目录,并将 stub 放置在编译器和链接器可以找到它的位置。将这些文件复制到最终目录后,输入:
ldconfig -v
应该有类似于以下的输出,指示 ldconfig 已为新库创建了一个符号链接,其中名称仅包含主版本号。这样做是因为仅使用主版本号对库进行查找。
libxyz.so.1 => libxyz.so.1.0.0 (changed)
如果 ldconfig 找不到库,请确保库所在的目录包含在 /etc/ld.so.conf 中的列表中。现在应该可以使用新库了。应保存 Shared.inc、jump.vars、jump.funcs、jump.import 和 jump.ignore。如果您需要重建库或创建兼容的替换库,这些文件将很有用。
Eric Kasten (tigger@petroglyph.cl.msu.edu) 自 1989 年以来一直是一名系统程序员。目前,他正在密歇根州立大学攻读计算机科学硕士学位,他的研究重点是网络和分布式系统。欢迎通过电子邮件向他发送经过深思熟虑的评论和问题。