nm(1) 命令可以报告给定库中的符号列表。它适用于静态库和共享库。对于给定的库,nm(1) 可以列出定义的符号名称、每个符号的值和符号的类型。如果库中提供了该信息(请参阅 -l 选项),它还可以识别符号在源代码中定义的位置(按文件名和行号)。
符号类型需要稍作解释。类型以字母显示;小写字母表示符号是本地的,而大写字母表示符号是全局的(外部的)。典型的符号类型包括 T(代码段中的正常定义)、D(已初始化的数据段)、B(未初始化的数据段)、U(未定义;符号被库使用但未被库定义)和 W(弱;如果另一个库也定义了这个符号,则该定义将覆盖此定义)。
如果您知道函数的名称,但您真的不记得它是在哪个库中定义的,您可以使用 nm 的 ``-o'' 选项(它在每行中添加文件名前缀)以及 grep 来查找库名称。从 Bourne shell 中,您可以搜索 /lib、/usr/lib、/usr/lib 的直接子目录和 /usr/local/lib 中的所有库以查找 ``cos'',如下所示
nm -o /lib/* /usr/lib/* /usr/lib/*/* \ /usr/local/lib/* 2> /dev/null | grep 'cos$' |
关于 nm 的更多信息可以在本地安装的 nm ``info'' 文档中找到,链接为 info:binutils#nm。
库应使用 gcc __attribute__((constructor)) 和 __attribute__((destructor)) 函数属性导出初始化和清理例程。有关这些属性的信息,请参阅 gcc info 页面。构造函数例程在 dlopen 返回之前执行(或者如果在加载时加载库,则在 main() 启动之前执行)。析构函数例程在 dlclose 返回之前执行(或者如果在加载时加载库,则在 exit() 或 main() 完成之后执行)。这些函数的 C 原型是
void __attribute__ ((constructor)) my_init(void); void __attribute__ ((destructor)) my_fini(void); |
共享库不能使用 gcc 参数 ``-nostartfiles'' 或 ``-nostdlib'' 编译。如果使用这些参数,构造函数/析构函数例程将不会被执行(除非采取特殊措施)。
历史上,有两个特殊函数 _init 和 _fini 可以用于控制构造函数和析构函数。但是,它们已经过时,并且它们的使用可能会导致不可预测的结果。您的库不应使用这些;请改用上面的函数属性 constructor 和 destructor。
如果您必须使用使用 _init 或 _fini 的旧系统或代码,以下是它们的工作方式。定义了两个特殊函数用于初始化和完成模块:_init 和 _fini。如果在库中导出了函数 ``_init'',则在首次打开库时调用它(通过 dlopen() 或仅作为共享库)。在 C 程序中,这仅意味着您定义了一个名为 _init 的函数。有一个对应的函数称为 _fini,每当客户端完成使用库时(通过调用 dlclose() 将其引用计数降至零,或在程序正常退出时)调用它。这些函数的 C 原型是
void _init(void); void _fini(void); |
在这种情况下,当在 gcc 中将文件编译为 ``.o'' 文件时,请务必添加 gcc 选项 ``-nostartfiles''。这可以防止 C 编译器将系统启动库链接到 .so 文件。否则,您将收到 ``multiple-definition'' 错误。请注意,这与使用推荐的函数属性编译模块完全不同。感谢 Jim Mischel 和 Tim Gentry 建议添加关于 _init 和 _fini 的讨论,并帮助创建它。
值得注意的是,GNU 加载器允许共享库是文本文件,使用专门的脚本语言而不是通常的库格式。这对于间接组合其他库非常有用。例如,这是/usr/lib/libc.so在我的一个系统上
/* GNU ld script Use the shared library, but some functions are only in the static library, so try that secondarily. */ GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a ) |
有关此的更多信息,请参阅关于 ld 链接器脚本(ld 命令语言)的 texinfo 文档。一般信息位于 info:ld#Options 和 info:ld#Commands,可能的命令在 info:ld#Option Commands 中讨论。
通常,对外部函数的引用是按需绑定的,而不是在应用程序启动时全部绑定。如果共享库已过时,则可能缺少必需的接口;当应用程序尝试使用该接口时,可能会突然且意外地失败。
解决此问题的方法是符号版本控制与版本脚本相结合。通过符号版本控制,用户可以在启动程序时收到警告,如果应用程序使用的库太旧。您可以从 ld 手册中关于版本脚本的讨论中了解更多信息,链接为 https://gnu.ac.cn/manual/ld-2.9.1/html_node/ld_25.html。
如果您正在构建一个应移植到多个系统的应用程序,您可以考虑使用 GNU libtool 来构建和安装库。GNU libtool 是一个通用的库支持脚本。Libtool 将使用共享库的复杂性隐藏在一个一致的、可移植的接口之后。Libtool 提供了可移植的接口来创建目标文件、链接库(静态和共享)、链接可执行文件、调试可执行文件、安装库、安装可执行文件。它还包括 libltdl,一个用于动态加载程序的可移植性包装器。有关更多信息,请参阅其文档,链接为 https://gnu.ac.cn/software/libtool/manual.html
生成文件中包含的所有符号对于调试都很有用,但会占用空间。如果您需要空间,可以消除其中一些。
最好的方法是首先正常生成目标文件,并首先进行所有调试和测试(使用它们进行调试和测试要容易得多)。之后,一旦您彻底测试了程序,请使用 strip(1) 删除符号。strip(1) 命令使您可以很好地控制要消除哪些符号;有关详细信息,请参阅其文档。
另一种方法是使用 GNU ld 选项 ``-S'' 和 ``-s'';``-S'' 从输出文件中省略调试器符号信息(但不是所有符号),而 ``-s'' 从输出文件中省略所有符号信息。您可以通过 gcc 将这些选项作为 ``-Wl,-S'' 和 ``-Wl,-s'' 调用。如果您总是剥离符号并且这些选项足够,请随意使用,但这是一种不太灵活的方法。
您可能会发现论文 Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux 很有用。它描述了如何创建一个真正小的程序可执行文件。坦率地说,在正常情况下您不应该使用大多数这些技巧,但它们在展示 ELF 的真正工作原理方面非常有启发性。
值得注意的是,如果您正在编写 C++ 程序,并且您正在调用 C 库函数,则在您的 C++ 代码中,您需要将 C 函数定义为 extern "C"。否则,链接器将无法找到 C 函数。在内部,C++ 编译器会 ``mangle'' C++ 函数的名称(例如,出于类型目的),并且需要告知它们给定的函数应作为 C 函数调用(因此,不要对其名称进行 mangle)。
如果您正在编写一个可以从 C 或 C++ 调用的程序库,建议您在头文件中包含 'extern "C"' 命令,以便为您的用户自动执行此操作。当与文件顶部的常用 #ifndef 结合使用以跳过重新执行头文件时,这意味着 C 或 C++ 可用于某些头文件 foobar.h 的典型头文件将如下所示
/* Explain here what foobar does */ #ifndef FOOBAR_H #define FOOBAR_H #ifdef __cplusplus extern "C" { #endif ... header code for foobar goes here ... #ifdef __cplusplus } #endif #endif |
KDE 开发人员注意到,大型 GUI C++ 应用程序可能需要很长时间才能启动,部分原因是它需要进行许多重定位。对此有几种解决方案。有关更多信息,请参阅 Making C++ ready for the desktop (by Waldo Bastian)。
Linux 标准库 (LSB) 项目的目标是开发和推广一套标准,以提高 Linux 发行版之间的兼容性,并使软件应用程序能够在任何兼容的 Linux 系统上运行。该项目的主页位于 http://www.linuxbase.org。
一篇总结如何开发符合 LSB 应用程序的好文章于 2002 年 10 月发表,Developing LSB-certified applications: Five steps to binary-compatible Linux applications,作者为 George Kraft IV(IBM Linux 技术中心高级软件工程师)。当然,如果您希望您的代码是可移植的,则需要编写仅访问标准化可移植性层的代码。此外,LSB 提供了一些工具,以便 C/C++ 程序的应用程序编写者可以检查 LSB 合规性;这些工具使用链接器和特殊库的一些功能来执行这些检查。显然,您需要安装这些工具才能进行这些检查;您可以从 LSB 网站获取它们。然后,只需使用 “lsbcc” 编译器作为您的 C/C++ 编译器(lsbcc 在内部创建一个链接环境,如果未遵循某些 LSB 规则,它将发出警告)
$ CC=lsbcc make myapplication (or) $ CC=lsbcc ./configure; make myapplication |
$ lsbappchk myapplication |