1. 介绍
使用GNU的工具我们如何在Linux下创建自己的程序函数库?一个“程序函数库”简单的说就是一个文件包含了一些编译好的代码和数据,这些编译好的代码和数据可以在事后供其他的程序使用。程序函数库可以使整个程序更加模块化,更容易重新编译,而且更方便升级。
程序函数库可分为3种类型:静态函数库(static libraries)、共享函数库(shared libraries)、动态加载函数库(dynamically loaded libraries)。静态函数库是在程序执行前就加入到目标程序中去了,动态函数库同共享函数库是一个东西(在linux上叫共享对象库, 文件后缀是.so ,windows上叫动态加载函数库, 文件后缀是.dll)。
2. 静态函数库
静态函数库实际上就是简单的一个普通的目标文件的集合,一般来说习惯用“.a”作为文件的后缀。可以用ar这个程序来产生静态函数库文件。Ar是archiver的缩写。静态函数库现在已经不在像以前用得那么多了,主要是共享函数库与之相比较有很多的优势的原因。慢慢地,大家都喜欢使用共享函数库了。不过,在一些场所静态函数库仍然在使用,一来是保持一些与以前某些程序的兼容,二来它描述起来也比较简单。
静态库函数允许程序员把程序link起来而不用重新编译代码,节省了重新编译代码的时间。不过,在今天这么快速的计算机面前,一般的程序的重新编译也花费不了多少时间,所以这个优势已经不是像它以前那么明显了。静态函数库对开发者来说还是很有用的,例如你想把自己提供的函数给别人使用,但是又想对函数的源代码进行保密,你就可以给别人提供一个静态函数库文件。理论上说,使用ELF格式的静态库函数生成的代码可以比使用共享函数库(或者动态函数库)的程序运行速度上快一些,大概1-5%。
创建一个静态函数库文件,或者往一个已经存在地静态函数库文件添加新的目标代码,可以用下面的命令:
ar rcs my_library.a file1.o file2.o
这个例子中是把目标代码file1.o和file2.o加入到my_library.a这个函数库文件中,如果my_library.a不存在则创建一个新的文件。在用ar命令创建静态库函数的时候,还有其他一些可以选择的参数,可以参加ar的使用帮助。这里不再赘述。
一旦你创建了一个静态函数库,你可以使用它了。你可以把它作为你编译和连接过程中的一部分用来生成你的可执行代码。如果你用gcc来编译产生可执行代码的话,你可以用“-l”参数来指定这个库函数。你也可以用ld来做,使用它的“-l”和“-L”参数选项。具体用法可以参考info:gcc。
3. 共享库
共享库是程序启动时加载的库。共享库安装正确后,所有启动的程序将自动使用新的共享库。它实际上比这更灵活和复杂,因为Linux使用的方法允许您:
更新库并且仍然支持希望使用这些库的旧版,非后向兼容版本的程序。
在执行特定程序时,重写特定库或甚至库中的特定函数。
在程序使用现有库运行时执行所有这些操作。
3.1 约定
对于共享库来支持所有这些所需的属性,必须遵循许多约定和准则。您需要了解图书馆名称之间的区别,特别是“soname”和“实名”(以及它们的相互作用)。您还需要了解它们应该放在文件系统中的位置。
每个共享库都有一个名为“soname”的特殊名称。soname具有前缀``lib'',库的名称,短语“.so”,后跟一个句点和一个版本号,每当界面改变时都会递增(作为一个特殊的例外,级别C库不以“lib”开头)。一个完全合格的soname包含作为前缀的目录; 在一个工作系统上,一个完全合格的soname只是一个与共享库的“真实姓名”的符号链接。
每个共享库还有一个“实名”,它是包含实际库代码的文件名。真正的名字增加了一个时期,次要号码,另一个时期和发行号码。最后一个期间和发行号码是可选的。次要号码和发行号码通过让您准确知道安装了哪些版本的库,来支持配置控制。请注意,这些数字可能与用于在文档中描述库的数字不同,尽管这样做更容易。
另外,编译器在请求库时使用的名称(我将其称为“链接器名称”),这只是没有任何版本号的soname。
管理共享库的关键是这些名称的分离。程序在内部列出他们需要的共享库时,应该只列出他们需要的soname。相反,创建共享库时,只能创建具有特定文件名的库(具有更详细的版本信息)。当您安装新版本的库时,将其安装在几个特殊目录之一中,然后运行程序ldconfig(8)。ldconfig检查现有文件,并将声名创建为真实名称的符号链接,以及设置缓存文件/etc/ld.so.cache(稍后描述)。
ldconfig不设置链接器名称; 通常这是在库安装期间完成的,链接器名称简单地创建为“最新”的soname或最新的真实名称的符号链接。我建议将链接器名称作为与soname的符号链接,因为在大多数情况下,如果您更新库,那么您希望在链接时自动使用它。我问HJ Lu为什么ldconfig不会自动设置链接器名称。他的解释基本上是你可能想使用最新版本的库来运行代码,但是可能需要 开发 链接到旧的(可能不兼容的)库。因此,ldconfig不会对您希望程序链接的任何假设,因此安装程序必须特别修改符号链接以更新链接器将用于库。
因此,/usr/lib/libreadline.so.3是一个完全限定的soname,其中ldconfig将被设置为与/usr/lib/libreadline.so.3.0之类的一些真实名称的符号链接 。还应该有一个链接器名称 /usr/lib/libreadline.so ,它可以是引用/usr/lib/libreadline.so.3的符号链接 。
共享库必须位于文件系统的某个位置。大多数开源软件往往遵循GNU标准; 有关更多信息,请参阅info:standards#Directory_Variables上的信息文件文档 。GNU标准建议默认安装/usr/local/lib中的所有库,当分发源代码(所有命令都应该进入/usr/local/bin)时。它们还定义了覆盖这些默认值和调用安装例程的约定。
文件系统层次标准(FHS)讨论了在分发中应该去哪里(请参阅 http://www.pathname.com/fhs)。根据FHS,大多数库应该安装在/usr/lib中,但启动所需的库应该在/lib中,不属于系统的库应该在/usr/local/lib中。
这两个文件之间没有真正的冲突; GNU标准建议开发人员使用默认的源代码,而FHS则建议分销商使用默认值(通常通过系统的软件包管理系统来选择覆盖源代码默认值)。在实践中,这很好地工作:您下载的“最新”(可能是buggy!)源代码自动安装在“本地”目录(/usr/local),一旦该代码已经成熟,软件包管理器可以轻松地覆盖默认值,以将代码放置在标准的发行版中。请注意,如果您的库调用只能通过库调用的程序,则应将这些程序放在/usr/local/libexec(在/usr/libexec中)。一个复杂的情况是,Red Hat派生的系统在搜索库时默认不包括/usr/local/lib; 请参阅下面关于/etc/ld.so.conf的讨论。其他标准库位置包括用于X-windows的/usr/X11R6/lib。请注意,/lib/security用于PAM模块,但通常会作为DL库加载(下面也将讨论)。
3.2 如何使用共享库
在基于GNU glibc的系统(包括所有Linux系统)上,启动ELF二进制可执行文件会自动导致程序加载器被加载并运行。在Linux系统上,此加载程序名为/lib/ld-linux.so.X(其中X是版本号)。反过来,这个装载器可以找到并加载程序使用的所有其他共享库。
要搜索的目录列表存储在文件/etc/ld.so.conf中。许多Red Hat派生的发行版通常不会在/etc/ld.so.conf文件中包含/usr/local/lib。我认为这是一个错误,并在/etc/ld.so.conf中添加/usr/local/lib是在Red Hat派生系统上运行许多程序所需的常见“修复”。
如果您只想覆盖库中的一些函数,但保留库的其余部分,则可以在/etc/ld.so.preload中输入覆盖库(.o文件)的名称。这些“预加载”库将优先于标准集。此预加载文件通常用于紧急补丁; 分发通常不会在交付时包含这样的文件。
在程序启动时搜索所有这些目录将是非常低效的,因此实际使用了缓存安排。程序ldconfig(8)默认读入/etc/ld.so.conf文件,在动态链接目录中设置适当的符号链接(因此它们将遵循标准约定),然后将缓存写入/ etc / ld.so.cache,然后被其他程序使用。这极大地加快了访问图书馆的速度。这意味着,每当添加一个DLL,当一个DLL被删除或一组DLL目录发生变化时,ldconfig必须运行; 运行ldconfig通常是软件包管理器在安装库时执行的步骤之一。在启动时,动态加载器实际上使用文件/etc/ld.so.cache,然后加载它需要的库。
顺便说一句,FreeBSD对这个缓存使用稍微不同的文件名。在FreeBSD中,ELF缓存为/var/run/ld-elf.so.hints,a.out缓存为/var/run/ld.so.hints。这些仍然由ldconfig(8)更新,所以这个位置的差异只能在几个异乎寻常的情况下重要。
3.3 环境变量
各种环境变量可以控制此过程,并且有一些环境变量允许您覆盖此过程。
LD_LIBRARY_PATH
您可以临时替换不同的库进行此特定执行。在Linux中,环境变量LD_LIBRARY_PATH是一个冒号分隔的目录库,首先要在库文件的标准目录集之前进行搜索; 当调试新库或为特殊目的使用非标准库时,这非常有用。环境变量LD_PRELOAD列出了覆盖标准集的函数的共享库,就像/etc/ld.so.preload一样。这些由加载器/lib/ld-linux.so实现。我应该注意,虽然LD_LIBRARY_PATH适用于许多类Unix系统,但它并不适用; 例如,此功能在HP-UX上可用,但作为环境变量SHLIB_PATH,在AIX上,此功能是通过变量LIBPATH(具有相同的语法,
LD_LIBRARY_PATH适用于开发和测试,但不应由正常用户正常使用的安装过程进行修改; 请参阅http://www.visi.com/~barr/ldpath.html上的“为什么LD_LIBRARY_PATH为坏”,以 了解为什么。但它仍然可用于开发或测试,以及解决不能解决的问题。如果您不想设置LD_LIBRARY_PATH环境变量,那么在Linux上,您甚至可以直接调用程序加载器并传递参数。例如,以下将使用给定的PATH而不是环境变量LD_LIBRARY_PATH的内容,并运行给定的可执行文件:
/lib/ld-linux.so.2 - 文件路径路径可执行
只需执行ld-linux.so而不使用参数即可提供更多的使用帮助,但是再一次不要使用它来进行正常使用 - 这些都是用于调试的。
LD_DEBUG
GNU C加载器中的另一个有用的环境变量是LD_DEBUG。这会触发dl *函数,以便他们提供关于他们正在做什么的相当详细的信息。例如:
导出LD_DEBUG =文件 command_to_run
在处理库时显示文件和库的处理,告诉您哪些依赖关系被检测到,哪些SO以什么顺序加载。将LD_DEBUG设置为“bindings”显示有关符号绑定的信息,将其设置为“libs”,显示库搜索路径,并将ti设置为“`versions”显示版本依赖。
将LD_DEBUG设置为“帮助”,然后尝试运行程序将列出可能的选项。再次,LD_DEBUG不适用于正常使用,但在调试和测试时可以方便。
其他环境变量
实际上还有一些控制加载过程的其他环境变量; 他们的名字以LD_或RTLD_开头。大多数其他的是用于低级别的加载程序调试或用于实现专门的功能。他们大多没有文件证明; 如果您需要了解它们,了解它们的最佳方式是读取装载器的源代码(gcc的一部分)。
如果不采取特殊措施,允许用户控制动态链接的库对于setuid/setgid程序将是灾难性的。因此,在GNU加载程序(程序启动时加载程序的其余部分)中,如果程序为setuid或setgid,那么这些变量(和其他类似的变量)将被忽略或受到很大的限制。加载程序通过检查程序的凭据来确定程序是否被setuid或setgid; 如果uid和euid不同,或者gid和egid不同,那么加载器会假定程序是setuid/setgid(或者从一个下降的),因此极大地限制了其控制链接的能力。如果您阅读GNU glibc库源代码,可以看到这一点; 特别看到文件elf/ rtld.c和sysdeps/generic/dl-sysdep.c。这意味着如果你使uid和gid等于euid和egid,然后调用一个程序,这些变量就会有效果。其他类Unix系统处理不同的情况,但出于同样的原因:setuid/setgid程序不应该受到环境变量集的不当影响。
3.4 创建共享库
创建共享库很容易。首先,使用gcc -fPIC或-fpic标志创建将进入共享库的对象文件。-fPIC和-fpic选项可以实现“位置独立代码”生成,这是共享库的一个要求; 见下文的差异。您使用-Wl gcc选项传递soname。-Wl选项将选项传递给链接器(在这种情况下为-soname链接器选项) - -Wl之后的逗号不是打字错误,并且您不能在选项中包含未转义的空格。然后使用以下格式创建共享库:
gcc -shared -Wl,-soname,your_soname \ -o library_name file_list library_list
这是一个例子,它创建两个对象文件(ao和bo),然后创建一个包含它们的共享库。请注意,此编译包括调试信息(-g),并将生成警告(-Wall),这些共享库不是必需的,但建议使用。编译生成对象文件(使用-c),并包含所需的-fPIC选项:
gcc -fPIC -g -c -Wall acgcc -fPIC -g -c -Wall bcgcc -shared -Wl,-soname, libmystuff.so.1 \ -o libmystuff.so.1.0.1 ao bo -lc
这里有几点值得注意:
不要剥离生成的库,并且不要使用编译器选项-fomit-frame-pointer,除非你真的必须。生成的库将工作,但这些操作使调试器大多没有用。
使用-fPIC或-fpic生成代码。是否使用-fPIC或-fpic生成代码是依赖于目标的。-fPIC选项始终有效,但是可能产生比-fpic更大的代码(请记住,这是PIC在更大的情况下,因此可能产生更大量的代码)。使用-fpic选项通常会生成更小更快的代码,但会有平台相关的限制,例如全局可见符号的数量或代码的大小。链接器将告诉您,创建共享库时是否适合。如果有疑问,我选择-fPIC,因为它总是有效。
在某些情况下,调用gcc来创建对象文件也需要包含“-Wl,-export-dynamic”选项。通常,动态符号表仅包含动态对象使用的符号。此选项(创建ELF文件时)将所有符号添加到动态符号表(有关详细信息,请参阅ld(1))。当有“反向相关性”时,您需要使用此选项,即,DL库具有未解决的符号,按照惯例,必须在要加载这些库的程序中定义它们。对于“反向相关性”工作,主程序必须使其符号动态可用。请注意,如果您只使用Linux系统,则可以使用“-rdynamic”而不是“-Wl,export-dynamic”,但根据ELF文档,“-rdynamic”
在开发过程中,修改也被许多其他程序使用的库的潜在问题 - 您不希望其他程序使用“开发”库,只是您正在测试的特定应用程序。您可能使用的一个链接选项是ld的“rpath”选项,它指定正在编译的特定程序的运行时库搜索路径。从gcc,您可以通过这样指定来调用rpath选项:
-Wl,-rpath,$(DEFAULT_LIB_INSTALL_PATH)
如果您在构建库客户机程序时使用此选项,则不需要再打扰LD_LIBRARY_PATH(下文将介绍),除了确保它不冲突,或者使用其他技术来隐藏库。
3.5 安装和使用共享库
创建共享库后,您需要安装它。简单的方法是将库复制到标准目录(例如/usr/lib)中,并运行ldconfig(8)。
首先,您需要在某个地方创建共享库。然后,您将需要设置必要的符号链接,特别是从soname到真实名称的链接(以及从无版本的soname,即以“.so”结尾的soname)为用户谁没有指定版本)。最简单的方法是运行:
ldconfig -n directory_with_shared_libraries
最后,当你编译你的程序时,你需要告诉链接器你正在使用的任何静态和共享库。使用-l和-L选项。
如果您不能或不想在标准位置安装库(例如,您没有权限修改/usr/lib),则需要更改方法。在这种情况下,您需要将其安装在某个地方,然后为您的程序提供足够的信息,以便程序可以找到库...并且有几种方法可以做到这一点。您可以在简单的情况下使用gcc的-L标志。您可以使用“rpath”方法(如上所述),特别是如果您只有一个特定的程序将库放置在“非标准”位置。您也可以使用环境变量来控制事物。特别是,您可以设置LD_LIBRARY_PATH,这是一个冒号分隔的目录列表,用于在通常的位置之前搜索共享库。如果你使用bash,
LD_LIBRARY_PATH =。:$ LD_LIBRARY_PATH my_program
如果要仅覆盖几个选定的函数,可以通过创建一个覆盖目标的文件并设置LD_PRELOAD来实现; 此对象文件中的函数将仅覆盖这些函数(留下其他函数)。
通常你可以不需要更新库; 如果有API更改,则库创建者应该更改soname。这样,多个库可以在单个系统上,并为每个程序选择正确的库。但是,如果一个程序中断更新到保持相同soname的库,您可以强制它使用旧的库版本通过将旧的库复制到某个地方,重命名该程序(比如说旧的名称加上“.orig ''),然后创建一个小的“包装器”脚本,该脚本重置库以使用并调用真实(重命名)程序。您可以将旧图书馆放在自己的特殊区域,如果您愿意,尽管编号约定允许多个版本生活在同一目录中。包装脚本可能看起来像这样:
#!/ bin / sh的 导出LD_LIBRARY_PATH = /usr/local/my_lib:$ LD_LIBRARY_PATH exec /usr/bin/my_program.orig $ *
编写自己的程序时请不要依赖这个; 尝试确保您的库向后兼容,或者您每次进行不兼容的更改时都会在soname中增加版本号。这只是处理最坏情况问题的“紧急”方法。
您可以使用ldd(1)查看程序使用的共享库列表。所以,例如,您可以通过键入以下方式查看ls使用的共享库:
ldd /bin/ ls
一般来说,您将看到依赖的声名的列表,以及这些名称解析的目录。在几乎所有情况下,您至少有两个依赖关系:
/lib/ld-linux.so.N(其中N为1或更多,通常至少为2)。这是加载所有其他库的库。
libc.so.N(N为6以上)。这是C库。即使是其他语言也倾向于使用C库(至少要实现自己的库),所以大多数程序至少包括这个库。
请注意:千万不能对你不信任的程序运行LDD。如ldd(1)手册中明确指出的,ldd通过设置特殊环境变量(对于ELF对象,LD_TRACE_LOADED_OBJECTS),然后执行程序(在某些情况下)工作。不可信程序可能强制ldd用户运行任意代码(而不是简单地显示ldd信息)。所以,为了安全起见,不要在不信任的程序上使用ldd来执行。
4. 动态加载的函数库 Dynamically Loaded (DL) Libraries
动态加载的函数库Dynamically loaded (DL) libraries是一类函数库,它可以在程序运行过程中的任何时间加载。它们特别适合在函数中加载一些模块和plugin扩展模块的场合,因为它可以在当程序需要某个plugin模块时才动态的加载。例如,Pluggable Authentication Modules(PAM)系统就是用动态加载函数库来使得管理员可以配置和重新配置身份验证信息。
Linux系统下,DL函数库与其他函数库在格式上没有特殊的区别,我们前面提到过,它们创建的时候是标准的object格式。主要的区别就是这些函数库不是在程序链接的时候或者启动的时候加载,而是通过一个API来打开一个函数库,寻找符号表,处理错误和关闭函数库。通常C语言环境下,需要包含这个头文件。
Linux中使用的函数和Solaris中一样,都是dlpoen() API。当然不是所有的平台都使用同样的接口,例如HP-UX使用shl_load()机制,而Windows平台用另外的其他的调用接口。如果你的目的是使得你的代码有很强的移植性,你应该使用一些wrapping函数库,这样的wrapping函数库隐藏不同的平台的接口区别。一种方法是使用glibc函数库中的对动态加载模块的支持,它使用一些潜在的动态加载函数库界面使得它们可以夸平台使用。具体可以参考https://developer.gnome.org/。另外一个方法是使用libltdl,是GNU libtool的一部分,可以进一步参考CORBA相关资料。